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)
This commit is contained in:
1 parent
e44b9271d6
commit
30863e1f0b
25 files changed
+1278
-31
No files matched your search
@@ -293,6 +293,12 @@ mod imp {
|
|||||||
/// The global shortcut logic.
|
/// The global shortcut logic.
|
||||||
pub global_shortcuts: OnceCell<GlobalShortcuts>,
|
pub global_shortcuts: OnceCell<GlobalShortcuts>,
|
||||||
|
|
||||||
|
/// True once the shortcuts' `config` property is bound to ours
|
||||||
|
/// (Zig: `bindProperty` in `startupGlobalShortcuts`, after the
|
||||||
|
/// D-Bus connection was set; before that the config must not reach
|
||||||
|
/// the shortcuts, or the portal session is created twice).
|
||||||
|
pub global_shortcuts_bound: Cell<bool>,
|
||||||
|
|
||||||
/// Set to true so long as we request a window exactly once. This
|
/// Set to true so long as we request a window exactly once. This
|
||||||
/// prevents quitting the app before we've shown one window.
|
/// prevents quitting the app before we've shown one window.
|
||||||
pub requested_window: Cell<bool>,
|
pub requested_window: Cell<bool>,
|
||||||
@@ -827,7 +833,9 @@ impl Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Keep global shortcuts in sync with the config (property binding).
|
// Keep global shortcuts in sync with the config (property binding).
|
||||||
if let Some(gs) = self.imp().global_shortcuts.get() {
|
if self.imp().global_shortcuts_bound.get()
|
||||||
|
&& let Some(gs) = self.imp().global_shortcuts.get()
|
||||||
|
{
|
||||||
gs.set_config(self.imp().config.borrow().as_ref());
|
gs.set_config(self.imp().config.borrow().as_ref());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1075,6 +1083,7 @@ impl Application {
|
|||||||
gs.set_connection(self.dbus_connection().as_ref());
|
gs.set_connection(self.dbus_connection().as_ref());
|
||||||
|
|
||||||
// Keep the shortcut config in sync with the app.
|
// Keep the shortcut config in sync with the app.
|
||||||
|
self.imp().global_shortcuts_bound.set(true);
|
||||||
gs.set_config(self.imp().config.borrow().as_ref());
|
gs.set_config(self.imp().config.borrow().as_ref());
|
||||||
|
|
||||||
// Setup the signal handler for global shortcut triggers
|
// Setup the signal handler for global shortcut triggers
|
||||||
|
|||||||
@@ -157,6 +157,42 @@ fn vardict(entries: Vec<(&str, glib::Variant)>) -> glib::Variant {
|
|||||||
map.to_variant()
|
map.to_variant()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `(a{sv})`: the `CreateSession` arguments.
|
||||||
|
fn create_session_payload(handle_token: &str, session_handle_token: &str) -> glib::Variant {
|
||||||
|
let opts = vardict(vec![
|
||||||
|
("handle_token", handle_token.to_variant()),
|
||||||
|
("session_handle_token", session_handle_token.to_variant()),
|
||||||
|
]);
|
||||||
|
glib::Variant::tuple_from_iter([opts])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `(oa(sa{sv})sa{sv})`: the `BindShortcuts` arguments (session handle,
|
||||||
|
/// shortcuts, parent window, options).
|
||||||
|
fn bind_shortcuts_payload<'a>(
|
||||||
|
handle: &str,
|
||||||
|
shortcuts: impl Iterator<Item = (&'a str, String)>,
|
||||||
|
request_token: &str,
|
||||||
|
) -> Option<glib::Variant> {
|
||||||
|
let handle = ObjectPath::try_from(handle.to_owned()).ok()?;
|
||||||
|
let binds = glib::Variant::array_from_iter_with_type(
|
||||||
|
glib::VariantTy::new("(sa{sv})").ok()?,
|
||||||
|
shortcuts.map(|(trigger, description)| {
|
||||||
|
let opts = vardict(vec![
|
||||||
|
("description", description.to_variant()),
|
||||||
|
("preferred_trigger", trigger.to_variant()),
|
||||||
|
]);
|
||||||
|
glib::Variant::tuple_from_iter([trigger.to_variant(), opts])
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let opts = vardict(vec![("handle_token", request_token.to_variant())]);
|
||||||
|
Some(glib::Variant::tuple_from_iter([
|
||||||
|
handle.to_variant(),
|
||||||
|
binds,
|
||||||
|
String::new().to_variant(),
|
||||||
|
opts,
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
|
||||||
impl GlobalShortcuts {
|
impl GlobalShortcuts {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
glib::Object::new()
|
glib::Object::new()
|
||||||
@@ -279,39 +315,18 @@ impl GlobalShortcuts {
|
|||||||
fn make_payload(&self, method: Method, request_token: &str) -> Option<glib::Variant> {
|
fn make_payload(&self, method: Method, request_token: &str) -> Option<glib::Variant> {
|
||||||
match method {
|
match method {
|
||||||
// https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html#org-freedesktop-portal-globalshortcuts-createsession
|
// https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html#org-freedesktop-portal-globalshortcuts-createsession
|
||||||
Method::CreateSession => {
|
Method::CreateSession => Some(create_session_payload(request_token, &generate_token())),
|
||||||
let opts = vardict(vec![
|
|
||||||
("handle_token", request_token.to_variant().to_variant()),
|
|
||||||
(
|
|
||||||
"session_handle_token",
|
|
||||||
generate_token().to_variant().to_variant(),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
Some((opts,).to_variant())
|
|
||||||
}
|
|
||||||
// https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html#org-freedesktop-portal-globalshortcuts-bindshortcuts
|
// https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html#org-freedesktop-portal-globalshortcuts-bindshortcuts
|
||||||
Method::BindShortcuts => {
|
Method::BindShortcuts => {
|
||||||
let imp = self.imp();
|
let imp = self.imp();
|
||||||
let handle = imp.handle.borrow().clone()?;
|
let handle = imp.handle.borrow().clone()?;
|
||||||
let handle = ObjectPath::try_from(handle).ok()?;
|
let map = imp.map.borrow();
|
||||||
|
bind_shortcuts_payload(
|
||||||
let binds: Vec<(String, glib::Variant)> = imp
|
&handle,
|
||||||
.map
|
map.iter()
|
||||||
.borrow()
|
.map(|(trigger, action)| (trigger.as_str(), action.to_string())),
|
||||||
.iter()
|
request_token,
|
||||||
.map(|(trigger, action)| {
|
)
|
||||||
let opts = vardict(vec![
|
|
||||||
("description", action.to_string().to_variant().to_variant()),
|
|
||||||
("preferred_trigger", trigger.to_variant().to_variant()),
|
|
||||||
]);
|
|
||||||
(trigger.clone(), opts)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let opts = vardict(vec![(
|
|
||||||
"handle_token",
|
|
||||||
request_token.to_variant().to_variant(),
|
|
||||||
)]);
|
|
||||||
Some((handle, binds, String::new(), opts).to_variant())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -459,3 +474,40 @@ impl GlobalShortcuts {
|
|||||||
self.emit_by_name::<()>("trigger", &[&action.to_string()]);
|
self.emit_by_name::<()>("trigger", &[&action.to_string()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_session_payload_signature() {
|
||||||
|
let v = create_session_payload("tok", "stok");
|
||||||
|
assert_eq!(v.type_().as_str(), "(a{sv})");
|
||||||
|
let (m,): (HashMap<String, glib::Variant>,) = v.get().unwrap();
|
||||||
|
assert_eq!(m["handle_token"].get::<String>().unwrap(), "tok");
|
||||||
|
assert_eq!(m["session_handle_token"].get::<String>().unwrap(), "stok");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bind_shortcuts_payload_signature() {
|
||||||
|
let v = bind_shortcuts_payload(
|
||||||
|
"/org/freedesktop/portal/desktop/session/1_2/x",
|
||||||
|
[("CTRL+ALT+x", "new_window".to_owned())].into_iter(),
|
||||||
|
"tok",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(v.type_().as_str(), "(oa(sa{sv})sa{sv})");
|
||||||
|
assert_eq!(v.n_children(), 4);
|
||||||
|
let binds = v.child_value(1);
|
||||||
|
assert_eq!(binds.n_children(), 1);
|
||||||
|
let bind = binds.child_value(0);
|
||||||
|
assert_eq!(bind.child_value(0).str(), Some("CTRL+ALT+x"));
|
||||||
|
let opts: HashMap<String, glib::Variant> = bind.child_value(1).get().unwrap();
|
||||||
|
assert_eq!(opts["description"].get::<String>().unwrap(), "new_window");
|
||||||
|
assert_eq!(
|
||||||
|
opts["preferred_trigger"].get::<String>().unwrap(),
|
||||||
|
"CTRL+ALT+x"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ What remains is verification coverage that this Linux machine cannot provide:
|
|||||||
- Native Apple execution: Metal, IOSurface, CoreText, Apple input APIs, Mach handling and Apple packaging (compile-checked only, `just check-macos`).
|
- Native Apple execution: Metal, IOSurface, CoreText, Apple input APIs, Mach handling and Apple packaging (compile-checked only, `just check-macos`).
|
||||||
- Native Windows (real Windows console host, Windows-only APIs Wine stubs: `ResizePseudoConsole` is a Wine stub, POSIX-semantics rename over an existing file is unsupported by Wine). The Windows code paths are otherwise run under Wine (`just test-windows-wine`, see `notes/windows-wine.md`): ConPTY, `CreateProcessW`, pipes, the IO thread and `cmd.exe` children are runtime-tested; `just check-windows` stays the clippy gate.
|
- Native Windows (real Windows console host, Windows-only APIs Wine stubs: `ResizePseudoConsole` is a Wine stub, POSIX-semantics rename over an existing file is unsupported by Wine). The Windows code paths are otherwise run under Wine (`just test-windows-wine`, see `notes/windows-wine.md`): ConPTY, `CreateProcessW`, pipes, the IO thread and `cmd.exe` children are runtime-tested; `just check-windows` stays the clippy gate.
|
||||||
- Native FreeBSD execution (cross `cargo check` only, if the target could be added).
|
- Native FreeBSD execution (cross `cargo check` only, if the target could be added).
|
||||||
- Live portal / D-Bus integration.
|
- Real input methods (ibus/fcitx are not installed; the IME paths are covered with a mock `GtkIMContext`, see `notes/verification-VR.md`).
|
||||||
|
- Desktop-specific portal backends (GNOME/KDE GlobalShortcuts, a real notification daemon). The D-Bus/portal protocol Ghostty speaks (GTK single-instance and `+new-window`, desktop notifications, GlobalShortcuts, Settings colour scheme, systemd cgroup scope, xdg-open) *is* now covered at runtime against the original on a private bus, with mocks and the real xdg-desktop-portal + gtk backend (`just live-dbus`, `notes/live-dbus.md`).
|
||||||
- Real-browser rendering (wasm is checked with Node only).
|
- Real-browser rendering (wasm is checked with Node only).
|
||||||
- Native io_uring execution; Swift code was excluded.
|
- Native io_uring execution; Swift code was excluded.
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# Live D-Bus / portal integration (agent LIVE)
|
||||||
|
|
||||||
|
Harness: `scripts/live-dbus-test.sh` (`just live-dbus [scenario...]`), helpers in `scripts/live-dbus/`
|
||||||
|
(`scenario.sh`, `scenarios.sh`, `mock.py`, `normalize.py`, child scripts, `golden/`, `build-rust.sh`).
|
||||||
|
Automated gate: `tests/live_dbus.rs` (golden mode, see below).
|
||||||
|
|
||||||
|
## Method
|
||||||
|
- Every scenario runs once per binary (original `/usr/bin/ghostty` = oracle, Rust release-profile build) on a
|
||||||
|
**private session bus**: `dbus-run-session --config-file=...` with its own `servicedir` (only
|
||||||
|
`com.mitchellh.ghostty.service` pointing at the binary under test, so D-Bus activation starts the build under test, not
|
||||||
|
`/usr/bin/ghostty`), inside a private headless weston (as `visual-parity.sh`), private HOME/XDG dirs,
|
||||||
|
`GSETTINGS_BACKEND=memory`. The user's bus, portal and desktop are never touched. All spawned processes carry an
|
||||||
|
`LDB_TAG` env marker and are swept at the end.
|
||||||
|
- Mock services (`mock.py`, Gio via PyGObject) on the private bus: `org.freedesktop.Notifications`,
|
||||||
|
`org.freedesktop.systemd1` (Manager.StartTransientUnit, switchable to failing), `org.freedesktop.portal.Desktop`
|
||||||
|
(GlobalShortcuts incl. Request/Session objects, Settings, OpenURI). Control interface `test.ghostty.Mock`
|
||||||
|
(`Activate`, `SetColorScheme`, `Invoke`, `SetFail`). Every received call is logged as JSON.
|
||||||
|
- `busctl --user monitor --json=short` records all traffic; `normalize.py` keeps `org.gtk.*`, Notifications, systemd1 and
|
||||||
|
portal messages and normalizes pids, tokens, unique names, paths. Observable effects ("facts") come from the exported
|
||||||
|
GtkApplication window objects (`busctl tree`), process counts, child cwd/argv, and the pty (colour-scheme reports).
|
||||||
|
- `golden/<scenario>.txt` = the original's facts + received mock calls (recorded with `UPDATE_GOLDEN=1`). With the
|
||||||
|
original installed the harness compares original vs Rust directly **and** Rust vs golden; without it (CI,
|
||||||
|
`tests/live_dbus.rs`, `LABELS=rust`) only Rust vs golden. Debug builds use the app id `com.mitchellh.ghostty-debug`
|
||||||
|
(as Zig), so the Rust build must be the release profile (`scripts/live-dbus/build-rust.sh`).
|
||||||
|
- Real `xdg-desktop-portal` + `xdg-desktop-portal-gtk` do run on the private bus (scenario `real-portal`, keyfile
|
||||||
|
gsettings backend so `color-scheme` can be changed live). They cannot give Ghostty an app id without systemd/flatpak
|
||||||
|
("An app id is required"), so GlobalShortcuts against the real frontend only yields `NotAllowed`; the BindShortcuts
|
||||||
|
payload is therefore checked against the mock.
|
||||||
|
|
||||||
|
## Scenarios: original vs Rust
|
||||||
|
| Scenario | Covers | Result |
|
||||||
|
|---|---|---|
|
||||||
|
| single-instance | first launch owns `com.mitchellh.ghostty`; `+new-window` (plain, `-e`, `--working-directory`, both, wrong/bad `--class`), second GUI launch forwarded, `-e` launch is never single-instance, `--class=com.example.custom-app` own bus name/path and `+new-window --class`; exact `org.gtk.Actions.Activate new-window-command` payloads (the CLI prepends `--working-directory=<cwd>`), exported actions, window counts, child cwd/argv | identical |
|
||||||
|
| single-instance-detect | `gtk-single-instance=detect`: plain launch = single instance, `TERM_PROGRAM` set or any CLI argument = dedicated instance | identical |
|
||||||
|
| notifications | OSC 9, OSC 777 (title/body/empty parts/`;` in body/markup/non-ASCII), repeated body (replaces id), `ActionInvoked default` incl. unknown id, `desktop-notifications=false` after reload; exact `Notify(app, replaces_id, icon, summary, body, actions, hints, timeout)` | identical (app name `""`, summary `Ghostty` for OSC 9, actions `[default,""]`, hints `desktop-entry`, `image-path`, `urgency=1`) |
|
||||||
|
| shortcuts | `global:` keybinds -> `CreateSession`/`BindShortcuts` payload (ids, descriptions, order), `Activated` -> action, unknown id ignored, session `Close` + re-create on reload, none without global binds | **diverged, fixed** (see below) |
|
||||||
|
| settings | portal `org.freedesktop.appearance color-scheme` (Read/ReadAll/SettingChanged) -> CSI ? 997 reports (mode 2031), `theme = light:..,dark:..` OSC 11 colour, `window-theme=dark` | identical |
|
||||||
|
| cgroup | `linux-cgroup` default/always/never, memory/processes limits (`MemoryHigh`,`TasksMax`,`ManagedOOMMemoryPressure`,`PIDs`), failing StartTransientUnit (app keeps running) | identical |
|
||||||
|
| openuri | `open-config` -> `xdg-open <config>`; **no** portal OpenURI/OpenFile call (upstream TODO in `Application.openUrl`; the Rust port has the same TODO) | identical |
|
||||||
|
| openurl-click | ctrl+click on plain URL / OSC 8 link / `file://` / path -> `xdg-open` argv | Rust only (the original cannot be driven: no input injection, Xwayland pointer is not reachable in headless weston); golden derived from `Surface.zig`/`os/open.zig` |
|
||||||
|
| real-portal | real xdg-desktop-portal + gtk backend: Settings color-scheme live changes, GlobalShortcuts error path, Inhibit monitor | identical |
|
||||||
|
|
||||||
|
## Divergences found and fixed (Rust port bugs, not upstream bugs: normal commits)
|
||||||
|
1. **GlobalShortcuts payloads had the wrong D-Bus signature** (`crates/ghostty-apprt-gtk/src/class/global_shortcuts.rs`):
|
||||||
|
`CreateSession` was sent as `(v)` instead of `(a{sv})`, option values were double-boxed (`v` in `v`), and
|
||||||
|
`BindShortcuts` was `(oa(sv)sv)` instead of `(oa(sa{sv})sa{sv})`. A real portal rejects these, so global shortcuts could
|
||||||
|
never work. Now built with `tuple_from_iter`/typed arrays by `create_session_payload`/`bind_shortcuts_payload`;
|
||||||
|
unit tests assert the signatures.
|
||||||
|
2. **CreateSession was sent twice at startup** (`class/application.rs`): the app's `config` notify forwarded the config to
|
||||||
|
`GlobalShortcuts` before `startup_global_shortcuts`; Zig binds the property only after `setDbusConnection`
|
||||||
|
(`bindProperty(.., .sync_create)`), so the session is created once. New `global_shortcuts_bound` flag.
|
||||||
|
|
||||||
|
No upstream (deviant) bugs were found in this area; `DEVIANT_FIXES.md` is unchanged by this work.
|
||||||
|
|
||||||
|
## Not covered
|
||||||
|
- Input method (ibus/fcitx not installed) stays in "Not verifiable on this machine".
|
||||||
|
- The original cannot receive pointer input here, so link clicks are checked for the Rust build only.
|
||||||
|
- `notify-on-command-finished` never produced a notification for either binary in the headless setup (identical, no
|
||||||
|
focus change events), so only OSC 9/777 are compared.
|
||||||
|
- Real GlobalShortcuts backends (GNOME/KDE) are not installed; the real frontend answers `NotAllowed` for non-sandboxed
|
||||||
|
apps without systemd-assigned app ids.
|
||||||
@@ -299,3 +299,10 @@ check-all: clippy test check-wasm check-windows
|
|||||||
# Capture original Ghostty vs the Rust build in a headless weston and diff them.
|
# Capture original Ghostty vs the Rust build in a headless weston and diff them.
|
||||||
visual-parity *scenarios:
|
visual-parity *scenarios:
|
||||||
RUST_BIN="${RUST_BIN:-target/vp/production/ghostty}" scripts/visual-parity.sh {{scenarios}}
|
RUST_BIN="${RUST_BIN:-target/vp/production/ghostty}" scripts/visual-parity.sh {{scenarios}}
|
||||||
|
|
||||||
|
# Live D-Bus / portal comparison (private bus + nested weston): notifications,
|
||||||
|
# single-instance, global shortcuts, color-scheme, cgroup scope. Builds the
|
||||||
|
# iteration binary first. UPDATE_GOLDEN=1 re-records scripts/live-dbus/golden.
|
||||||
|
live-dbus *scenarios:
|
||||||
|
scripts/live-dbus/build-rust.sh
|
||||||
|
RUST_BIN="${RUST_BIN:-target/vp/production/ghostty}" scripts/live-dbus-test.sh {{scenarios}}
|
||||||
Executable
+120
@@ -0,0 +1,120 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Live D-Bus / portal comparison of original Ghostty and the Rust build.
|
||||||
|
#
|
||||||
|
# scripts/live-dbus-test.sh [scenario...]
|
||||||
|
#
|
||||||
|
# Scenarios: single-instance single-instance-detect notifications shortcuts settings cgroup openuri openurl-click real-portal
|
||||||
|
# (default: all). Each scenario runs once per binary on a *private* session
|
||||||
|
# bus (dbus-run-session with its own config: only our service dir, no user
|
||||||
|
# services) inside a private headless weston; mock services
|
||||||
|
# (scripts/live-dbus/mock.py) answer org.freedesktop.Notifications,
|
||||||
|
# org.freedesktop.systemd1 and the portal interfaces Ghostty uses. The
|
||||||
|
# user's session bus, portal and desktop are never touched.
|
||||||
|
#
|
||||||
|
# The original's normalized result (D-Bus traffic + observable effects) is the
|
||||||
|
# oracle; the Rust result must be identical. Exit status 1 on any difference,
|
||||||
|
# 0 when everything matches or the needed tools are missing (skip).
|
||||||
|
#
|
||||||
|
# env: RUST_BIN (default target/vp/production/ghostty: debug builds use a different app id), REF_BIN (default /usr/bin/ghostty),
|
||||||
|
# OUT (default target/live-dbus), KEEP=1 keeps the weston log.
|
||||||
|
set -u
|
||||||
|
ROOT=$(cd "$(dirname "$0")/.." && pwd)
|
||||||
|
RUST_BIN=${RUST_BIN:-$ROOT/target/vp/production/ghostty}
|
||||||
|
REF_BIN=${REF_BIN:-/usr/bin/ghostty}
|
||||||
|
OUT=${OUT:-$ROOT/target/live-dbus}
|
||||||
|
LD=$ROOT/scripts/live-dbus
|
||||||
|
SCEN=${*:-single-instance single-instance-detect notifications shortcuts settings cgroup openuri openurl-click real-portal}
|
||||||
|
|
||||||
|
for t in weston dbus-run-session busctl python3 xdotool; do
|
||||||
|
command -v $t >/dev/null || { echo "live-dbus: SKIP ($t missing)"; exit 0; }
|
||||||
|
done
|
||||||
|
python3 -c 'import gi; gi.require_version("Gio","2.0")' 2>/dev/null || { echo "live-dbus: SKIP (python gi missing)"; exit 0; }
|
||||||
|
[ -x "$RUST_BIN" ] || [ "${LABELS:-}" = ref ] || { echo "live-dbus: SKIP (no rust build at $RUST_BIN)"; exit 0; }
|
||||||
|
|
||||||
|
mkdir -p "$OUT"
|
||||||
|
export XDG_RUNTIME_DIR=${VP_RUNTIME_DIR:-/tmp/vp-runtime-$UID}
|
||||||
|
mkdir -p -m700 "$XDG_RUNTIME_DIR" /tmp/.X11-unix 2>/dev/null
|
||||||
|
WORK=$(mktemp -d "$OUT/work.XXXX")
|
||||||
|
TAG=ldb$$
|
||||||
|
SOCK=ldb$$
|
||||||
|
cat >"$WORK/weston.ini" <<INI
|
||||||
|
[core]
|
||||||
|
shell=desktop-shell.so
|
||||||
|
xwayland=true
|
||||||
|
[output]
|
||||||
|
name=headless
|
||||||
|
mode=1200x800
|
||||||
|
[shell]
|
||||||
|
panel-position=none
|
||||||
|
background-color=0xff202020
|
||||||
|
INI
|
||||||
|
__EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/50_mesa.json XWAYLAND_NO_GLAMOR=1 \
|
||||||
|
timeout 3000 weston --debug --backend=headless --renderer=gl --socket=$SOCK -c "$WORK/weston.ini" \
|
||||||
|
>"$WORK/weston.log" 2>&1 &
|
||||||
|
WPID=$!
|
||||||
|
cleanup() {
|
||||||
|
kill $WPID 2>/dev/null
|
||||||
|
for p in /proc/[0-9]*; do
|
||||||
|
grep -qa "LDB_TAG=$TAG" "$p/environ" 2>/dev/null && kill -9 "${p#/proc/}" 2>/dev/null
|
||||||
|
done
|
||||||
|
wait 2>/dev/null
|
||||||
|
[ -n "${KEEP:-}" ] || rm -rf "$WORK"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
for _ in $(seq 50); do [ -S "$XDG_RUNTIME_DIR/$SOCK" ] && break; sleep 0.1; done
|
||||||
|
[ -S "$XDG_RUNTIME_DIR/$SOCK" ] || { echo "live-dbus: SKIP (weston did not start)"; cat "$WORK/weston.log"; exit 0; }
|
||||||
|
sleep 1
|
||||||
|
XDISPLAY=$(grep -o "display :[0-9]*" "$WORK/weston.log" | tail -1 | sed 's/display //')
|
||||||
|
|
||||||
|
rc=0
|
||||||
|
RUST_ONLY=" openurl-click " # the original cannot be driven (no input injection): golden file only
|
||||||
|
cut_bus() { sed '/^== bus traffic/,$d' "$1"; }
|
||||||
|
run_label() { # scenario label bin
|
||||||
|
local s=$1 label=$2 bin=$3
|
||||||
|
local d=$OUT/$s/$label; rm -rf "$d"; mkdir -p "$d"
|
||||||
|
local run=$WORK/run-$s-$label; mkdir -p "$run/services"
|
||||||
|
# Private bus: only our service dir (activation must start the binary
|
||||||
|
# under test, not /usr/bin/ghostty), no user services.
|
||||||
|
cat >"$run/session.conf" <<CONF
|
||||||
|
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
|
||||||
|
<busconfig><type>session</type><keep_umask/><listen>unix:tmpdir=$XDG_RUNTIME_DIR</listen><auth>EXTERNAL</auth>
|
||||||
|
<servicedir>$run/services</servicedir>
|
||||||
|
<policy context="default"><allow send_destination="*" eavesdrop="true"/><allow eavesdrop="true"/><allow own="*"/></policy>
|
||||||
|
</busconfig>
|
||||||
|
CONF
|
||||||
|
printf '[D-BUS Service]\nName=com.mitchellh.ghostty\nExec=%s --gtk-single-instance=true --initial-window=false\n' "$bin" \
|
||||||
|
>"$run/services/com.mitchellh.ghostty.service"
|
||||||
|
LDB_LABEL=$label LDB_TAG=$TAG WAYLAND_DISPLAY=$SOCK DISPLAY=$XDISPLAY LDB_BIN=$bin LDB_RUN=$run LDB_ROOT=$ROOT \
|
||||||
|
timeout 240 dbus-run-session --config-file="$run/session.conf" -- \
|
||||||
|
bash "$LD/scenario.sh" "$s" "$bin" "$d" >"$d/driver.log" 2>&1
|
||||||
|
LDB_RUN=$run LDB_ROOT=$ROOT python3 "$LD/normalize.py" "$d" >"$d/result.txt" 2>"$d/normalize.err"
|
||||||
|
}
|
||||||
|
for s in $SCEN; do
|
||||||
|
golden=$LD/golden/$s.txt
|
||||||
|
mkdir -p "$OUT/$s"
|
||||||
|
have_ref=0
|
||||||
|
case "$RUST_ONLY" in *" $s "*) ;; *) [ -x "$REF_BIN" ] && [ "${LABELS:-ref rust}" != rust ] && have_ref=1 ;; esac
|
||||||
|
if [ $have_ref = 1 ]; then run_label "$s" ref "$REF_BIN"; fi
|
||||||
|
if [ -x "$RUST_BIN" ] && [ "${LABELS:-rust}" != ref ]; then
|
||||||
|
if [ $have_ref = 1 ] || [ -f "$golden" ] || [ -n "${UPDATE_GOLDEN:-}" ]; then
|
||||||
|
run_label "$s" rust "$RUST_BIN"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
[ "${LABELS:-}" = ref ] && continue
|
||||||
|
ok=1
|
||||||
|
if grep -qs '^ERROR' "$OUT"/$s/*/facts.txt; then echo "live-dbus: FAIL $s (scenario error, see $OUT/$s/*/facts.txt)"; rc=1; continue; fi
|
||||||
|
if [ $have_ref = 1 ] && [ -f "$OUT/$s/rust/result.txt" ]; then
|
||||||
|
if diff -u "$OUT/$s/ref/result.txt" "$OUT/$s/rust/result.txt" >"$OUT/$s/diff.txt"; then :; else
|
||||||
|
echo "live-dbus: FAIL $s (original vs Rust)"; cat "$OUT/$s/diff.txt"; ok=0; fi
|
||||||
|
if [ -n "${UPDATE_GOLDEN:-}" ] || [ ! -f "$golden" ]; then cut_bus "$OUT/$s/ref/result.txt" >"$golden"; fi
|
||||||
|
fi
|
||||||
|
if [ -f "$golden" ] && [ -f "$OUT/$s/rust/result.txt" ]; then
|
||||||
|
if [ -n "${UPDATE_GOLDEN:-}" ] && [ $have_ref = 0 ]; then cut_bus "$OUT/$s/rust/result.txt" >"$golden"; fi
|
||||||
|
if ! diff -u "$golden" <(cut_bus "$OUT/$s/rust/result.txt") >"$OUT/$s/diff-golden.txt"; then
|
||||||
|
echo "live-dbus: FAIL $s (Rust vs recorded original, golden/$s.txt)"; cat "$OUT/$s/diff-golden.txt"; ok=0; fi
|
||||||
|
fi
|
||||||
|
if [ $ok = 1 ]; then
|
||||||
|
echo "live-dbus: PASS $s$([ $have_ref = 1 ] && echo ' (original + golden)' || echo ' (golden only)')"
|
||||||
|
else rc=1; fi
|
||||||
|
done
|
||||||
|
exit $rc
|
||||||
Executable
+5
@@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Iteration build of the Rust binary used by live-dbus-test.sh / visual-parity.
|
||||||
|
cd "$(dirname "$0")/../.." || exit 1
|
||||||
|
CARGO_TARGET_DIR=target/vp CARGO_PROFILE_PRODUCTION_LTO=off CARGO_PROFILE_PRODUCTION_CODEGEN_UNITS=16 \
|
||||||
|
CARGO_PROFILE_PRODUCTION_INCREMENTAL=true exec cargo build --profile production "$@"
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Prints links on known rows, then reports the text area size (CSI 18 t).
|
||||||
|
esc=$(printf '\033'); bel=$(printf '\007')
|
||||||
|
stty raw -echo
|
||||||
|
printf '%s[H%s[2J' "$esc" "$esc"
|
||||||
|
printf 'https://example.org/plain\r\n'
|
||||||
|
printf '%s]8;;https://example.com/osc8%s\\osc8-link%s]8;;%s\\\r\n' "$esc" "$esc" "$esc" "$esc"
|
||||||
|
printf 'file:///etc/hostname\r\n'
|
||||||
|
printf '/etc/hostname\r\n'
|
||||||
|
printf '%s[18t' "$esc"
|
||||||
|
exec cat -v >>"$OUTF"
|
||||||
Executable
+20
@@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Terminal child for the notifications scenario.
|
||||||
|
esc=$(printf '\033'); bel=$(printf '\007')
|
||||||
|
sleep 2
|
||||||
|
printf '%s]9;osc9 body%s' "$esc" "$bel"; sleep 1.2
|
||||||
|
printf '%s]777;notify;Title777;Body777%s' "$esc" "$bel"; sleep 1.2
|
||||||
|
printf '%s]777;notify;;OnlyBody%s' "$esc" "$bel"; sleep 1.2
|
||||||
|
printf '%s]777;notify;Only title;%s' "$esc" "$bel"; sleep 1.2
|
||||||
|
printf '%s]9;<b>markup</b> & "quotes" \303\251\342\202\254%s' "$esc" "$bel"; sleep 1.2
|
||||||
|
printf '%s]9;osc9 body%s' "$esc" "$bel"; sleep 1.2
|
||||||
|
printf '%s]777;notify;semi;colon;body%s' "$esc" "$bel"; sleep 1.2
|
||||||
|
# command finished (OSC 133 prompt marks): C then D;3 after >1s
|
||||||
|
printf '%s]133;A%s%s]133;B%s%s]133;C%s' "$esc" "$bel" "$esc" "$bel" "$esc" "$bel"; sleep 2
|
||||||
|
printf '%s]133;D;3%s' "$esc" "$bel"; sleep 1.5
|
||||||
|
printf '%s]133;A%s%s]133;B%s%s]133;C%s' "$esc" "$bel" "$esc" "$bel" "$esc" "$bel"; sleep 2
|
||||||
|
printf '%s]133;D;0%s' "$esc" "$bel"
|
||||||
|
# wait for the harness to ask for one more notification
|
||||||
|
while [ ! -e "$FLAG2" ]; do sleep 0.3; done
|
||||||
|
printf '%s]9;after reload%s' "$esc" "$bel"
|
||||||
|
sleep 60
|
||||||
Executable
+8
@@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Enables color-scheme reporting (mode 2031), asks for the current scheme
|
||||||
|
# (CSI ? 996 n) and keeps asking for the background colour (OSC 11).
|
||||||
|
esc=$(printf '\033'); bel=$(printf '\007')
|
||||||
|
stty raw -echo
|
||||||
|
printf '%s[?2031h%s[?996n' "$esc" "$esc"
|
||||||
|
( while :; do printf '%s]11;?%s' "$esc" "$bel"; sleep 0.7; done ) &
|
||||||
|
exec cat -v >>"$OUTF"
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
== facts ==
|
||||||
|
phase default:
|
||||||
|
phase always:
|
||||||
|
call: {"args": ["app-ghostty-surface-transient-PID.scope", "fail", [["ManagedOOMMemoryPressure", "kill"], ["PIDs", [PID]]], []], "iface": "org.freedesktop.systemd1.Manager", "method": "StartTransientUnit"}
|
||||||
|
phase always-limits:
|
||||||
|
call: {"args": ["app-ghostty-surface-transient-PID.scope", "fail", [["MemoryHigh", 123456789], ["TasksMax", 77], ["ManagedOOMMemoryPressure", "kill"], ["PIDs", [PID]]], []], "iface": "org.freedesktop.systemd1.Manager", "method": "StartTransientUnit"}
|
||||||
|
phase never:
|
||||||
|
phase always-failing:
|
||||||
|
call: {"args": ["app-ghostty-surface-transient-PID.scope", "fail", [["ManagedOOMMemoryPressure", "kill"], ["PIDs", [PID]]], []], "iface": "org.freedesktop.systemd1.Manager", "method": "StartTransientUnit"}
|
||||||
|
cgroup done
|
||||||
|
== mock received ==
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "color-scheme"]
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "contrast"]
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "accent-color"]
|
||||||
|
org.freedesktop.systemd1.Manager.StartTransientUnit ["app-ghostty-surface-transient-PID.scope", "fail", [["ManagedOOMMemoryPressure", "kill"], ["PIDs", [PID]]], []]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
== facts ==
|
||||||
|
notifications so far: 7
|
||||||
|
alive after ActionInvoked: instances=1 windows=1
|
||||||
|
alive after more ActionInvoked: instances=1 windows=1
|
||||||
|
notifications after desktop-notifications=false: 7
|
||||||
|
== mock received ==
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "color-scheme"]
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "contrast"]
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "accent-color"]
|
||||||
|
org.freedesktop.Notifications.Notify ["", 0, "", "Ghostty", "osc9 body", ["default", ""], {"desktop-entry": "com.mitchellh.ghostty", "image-path": "com.mitchellh.ghostty", "urgency": 1}, -1]
|
||||||
|
org.freedesktop.Notifications.Notify ["", 0, "", "Title777", "Body777", ["default", ""], {"desktop-entry": "com.mitchellh.ghostty", "image-path": "com.mitchellh.ghostty", "urgency": 1}, -1]
|
||||||
|
org.freedesktop.Notifications.Notify ["", 0, "", "Ghostty", "OnlyBody", ["default", ""], {"desktop-entry": "com.mitchellh.ghostty", "image-path": "com.mitchellh.ghostty", "urgency": 1}, -1]
|
||||||
|
org.freedesktop.Notifications.Notify ["", 0, "", "Only title", "", ["default", ""], {"desktop-entry": "com.mitchellh.ghostty", "image-path": "com.mitchellh.ghostty", "urgency": 1}, -1]
|
||||||
|
org.freedesktop.Notifications.Notify ["", 0, "", "Ghostty", "<b>markup</b> & \"quotes\" \u00e9\u20ac", ["default", ""], {"desktop-entry": "com.mitchellh.ghostty", "image-path": "com.mitchellh.ghostty", "urgency": 1}, -1]
|
||||||
|
org.freedesktop.Notifications.Notify ["", 1, "", "Ghostty", "osc9 body", ["default", ""], {"desktop-entry": "com.mitchellh.ghostty", "image-path": "com.mitchellh.ghostty", "urgency": 1}, -1]
|
||||||
|
org.freedesktop.Notifications.Notify ["", 0, "", "semi", "colon;body", ["default", ""], {"desktop-entry": "com.mitchellh.ghostty", "image-path": "com.mitchellh.ghostty", "urgency": 1}, -1]
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
== facts ==
|
||||||
|
spawn: xdg-open argc=1 [$RUN/cfg/ghostty/config] cwd=$ROOT
|
||||||
|
portal OpenURI/OpenFile calls: 0
|
||||||
|
== mock received ==
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "color-scheme"]
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "contrast"]
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "accent-color"]
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
== facts ==
|
||||||
|
portal frontend: (<uint32 2>,)
|
||||||
|
tty: startup: 997 reports=1 last bg=rgb:1111/2222/3333
|
||||||
|
tty: after gsettings color-scheme=prefer-light: 997 reports=1,2 last bg=rgb:ffff/eeee/dddd
|
||||||
|
tty: after gsettings color-scheme=prefer-dark: 997 reports=2,1 last bg=rgb:1111/2222/3333
|
||||||
|
tty: after gsettings color-scheme=default: 997 reports=1,2 last bg=rgb:ffff/eeee/dddd
|
||||||
|
tty: after gsettings color-scheme=prefer-dark: 997 reports=2,1 last bg=rgb:1111/2222/3333
|
||||||
|
global-shortcuts request error: GDBus.Error:org.freedesktop.portal.Error.NotAllowed
|
||||||
|
global-shortcuts request error: GDBus.Error:org.freedesktop.portal.Error.NotAllowed
|
||||||
|
global-shortcuts request error: GDBus.Error:org.freedesktop.portal.Error.NotAllowed
|
||||||
|
global-shortcuts request error: GDBus.Error:org.freedesktop.portal.Error.NotAllowed
|
||||||
|
global-shortcuts request error: GDBus.Error:org.freedesktop.portal.Error.NotAllowed
|
||||||
|
alive: instances=1
|
||||||
|
== mock received ==
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
== facts ==
|
||||||
|
tty: startup: 997 reports=1 last bg=rgb:1111/2222/3333
|
||||||
|
tty: after portal color-scheme=2: 997 reports=1,2 last bg=rgb:ffff/eeee/dddd
|
||||||
|
tty: after portal color-scheme=1: 997 reports=2,1 last bg=rgb:1111/2222/3333
|
||||||
|
tty: after portal color-scheme=0: 997 reports=1,2 last bg=rgb:ffff/eeee/dddd
|
||||||
|
tty: after portal color-scheme=2: 997 reports=- last bg=rgb:ffff/eeee/dddd
|
||||||
|
tty: after reload window-theme=dark: 997 reports=2 last bg=rgb:ffff/eeee/dddd
|
||||||
|
tty: after portal color-scheme=2 with window-theme=dark: 997 reports=- last bg=rgb:ffff/eeee/dddd
|
||||||
|
== mock received ==
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "color-scheme"]
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "contrast"]
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "accent-color"]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
== facts ==
|
||||||
|
step start: windows=1
|
||||||
|
new_window shortcut id: CTRL+ALT+x
|
||||||
|
step Activated(new_window): windows=2
|
||||||
|
step Activated(unknown): windows=2
|
||||||
|
step reload without global: windows=2
|
||||||
|
step reload with global again: windows=2
|
||||||
|
== mock received ==
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "color-scheme"]
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "contrast"]
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "accent-color"]
|
||||||
|
org.freedesktop.portal.GlobalShortcuts.CreateSession [{"handle_token": "ghostty_TOKEN", "session_handle_token": "ghostty_TOKEN"}]
|
||||||
|
org.freedesktop.portal.GlobalShortcuts.BindShortcuts ["/org/freedesktop/portal/desktop/session/SENDER/ghostty_TOKEN", [["CTRL+ALT+x", {"description": "new_window", "preferred_trigger": "CTRL+ALT+x"}], ["SHIFT+LOGO+b", {"description": "new_tab", "preferred_trigger": "SHIFT+LOGO+b"}], ["SHIFT+CTRL+F12", {"description": "reload_config", "preferred_trigger": "SHIFT+CTRL+F12"}], ["CTRL+ALT+q", {"description": "text:hi", "preferred_trigger": "CTRL+ALT+q"}]], "", {"handle_token": "ghostty_TOKEN"}]
|
||||||
|
org.freedesktop.portal.Session.Close {"path": "/org/freedesktop/portal/desktop/session/SENDER/ghostty_TOKEN"}
|
||||||
|
org.freedesktop.portal.GlobalShortcuts.CreateSession [{"handle_token": "ghostty_TOKEN", "session_handle_token": "ghostty_TOKEN"}]
|
||||||
|
org.freedesktop.portal.GlobalShortcuts.BindShortcuts ["/org/freedesktop/portal/desktop/session/SENDER/ghostty_TOKEN", [["CTRL+ALT+x", {"description": "new_window", "preferred_trigger": "CTRL+ALT+x"}]], "", {"handle_token": "ghostty_TOKEN"}]
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
== facts ==
|
||||||
|
plain launch owns name: 0
|
||||||
|
step plain: windows=1 instances=1
|
||||||
|
second plain launch rc=0
|
||||||
|
step second plain: windows=2 instances=1
|
||||||
|
step TERM_PROGRAM: windows=2 instances=2
|
||||||
|
step TERM_PROGRAM ended: windows=2 instances=1
|
||||||
|
step CLI arg: windows=2 instances=2
|
||||||
|
step CLI arg ended: windows=2 instances=1
|
||||||
|
ipc newwin rc=0
|
||||||
|
step +new-window: windows=3 instances=1
|
||||||
|
== mock received ==
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "color-scheme"]
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "contrast"]
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "accent-color"]
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "color-scheme"]
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "contrast"]
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "accent-color"]
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "color-scheme"]
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "contrast"]
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "accent-color"]
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "color-scheme"]
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "contrast"]
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "accent-color"]
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
== facts ==
|
||||||
|
first instance owns name: 0
|
||||||
|
step first: windows=1 instances=1
|
||||||
|
ipc newwin rc=0
|
||||||
|
step +new-window: windows=2 instances=1
|
||||||
|
ipc newwin-e rc=0
|
||||||
|
step +new-window -e: windows=3 instances=1
|
||||||
|
ipc newwin-wd rc=0
|
||||||
|
step +new-window --working-directory: windows=4 instances=1
|
||||||
|
ipc newwin-e-wd rc=0
|
||||||
|
step +new-window -e..--working-directory: windows=5 instances=1
|
||||||
|
ipc newwin-class rc=1
|
||||||
|
ipc-err: D-Bus method call returned an error err=GDBus.Error:org.freedesktop.DBus.Error.ServiceUnknown: The name com.example.nobody was not provided by any .service files
|
||||||
|
step +new-window --class=nobody: windows=5 instances=1
|
||||||
|
second launch rc=0
|
||||||
|
step second launch: windows=6 instances=1
|
||||||
|
child: /usr/bin/bash cwd=$HOME
|
||||||
|
child: /usr/bin/bash cwd=$HOME
|
||||||
|
child: /usr/bin/bash cwd=$ROOT
|
||||||
|
child: /usr/bin/bash cwd=/usr
|
||||||
|
child: sleep 601 cwd=$ROOT
|
||||||
|
step -e launch: windows=6 instances=2
|
||||||
|
step -e launch ended: windows=6 instances=1
|
||||||
|
custom class owns name: 0
|
||||||
|
step custom: windows=1 instances=2
|
||||||
|
ipc newwin-custom rc=0
|
||||||
|
step +new-window --class=custom: custom windows=2 default windows=6
|
||||||
|
ipc newwin-custom-e rc=0
|
||||||
|
step +new-window --class=custom -e: custom windows=3 default windows=6
|
||||||
|
ipc newwin-badclass rc=1
|
||||||
|
ipc-err: D-Bus bus name is not valid: 1bad
|
||||||
|
badclass done
|
||||||
|
bus name: com.example.custom-app
|
||||||
|
bus name: com.mitchellh.ghostty
|
||||||
|
actions: ([open-config, present-surface, quit, new-window-command, new-window, reload-config],)
|
||||||
|
custom actions: ([open-config, present-surface, quit, new-window-command, new-window, reload-config],)
|
||||||
|
== mock received ==
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "color-scheme"]
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "contrast"]
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "accent-color"]
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "color-scheme"]
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "contrast"]
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "accent-color"]
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "color-scheme"]
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "contrast"]
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "accent-color"]
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "color-scheme"]
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "contrast"]
|
||||||
|
org.freedesktop.portal.Settings.Read ["org.freedesktop.appearance", "accent-color"]
|
||||||
Executable
+251
@@ -0,0 +1,251 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Mock D-Bus services for the live D-Bus comparison (scripts/live-dbus-test.sh).
|
||||||
|
|
||||||
|
Runs on the private session bus and claims:
|
||||||
|
org.freedesktop.Notifications Notify/CloseNotification/GetCapabilities/GetServerInformation
|
||||||
|
org.freedesktop.systemd1 Manager.StartTransientUnit
|
||||||
|
org.freedesktop.portal.Desktop GlobalShortcuts, Settings, OpenURI (+ Request objects)
|
||||||
|
test.ghostty.Mock control: Activate(s), SetColorScheme(u), Quit()
|
||||||
|
|
||||||
|
Every received call is appended to $MOCK_LOG as one JSON line:
|
||||||
|
{"iface": ..., "method": ..., "args": ...}
|
||||||
|
The portal pieces are mocks (the real xdg-desktop-portal needs a running
|
||||||
|
backend and a desktop session); they implement just what Ghostty calls.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version("Gio", "2.0")
|
||||||
|
from gi.repository import Gio, GLib # noqa: E402
|
||||||
|
|
||||||
|
LOG = os.environ.get("MOCK_LOG", "/dev/stderr")
|
||||||
|
COLOR_SCHEME = int(os.environ.get("MOCK_COLOR_SCHEME", "0")) # 0 none, 1 dark, 2 light
|
||||||
|
BIND_REPLY = os.environ.get("MOCK_BIND", "ok")
|
||||||
|
|
||||||
|
XML = """
|
||||||
|
<node>
|
||||||
|
<interface name='org.freedesktop.Notifications'>
|
||||||
|
<method name='Notify'>
|
||||||
|
<arg type='s' direction='in'/><arg type='u' direction='in'/><arg type='s' direction='in'/>
|
||||||
|
<arg type='s' direction='in'/><arg type='s' direction='in'/><arg type='as' direction='in'/>
|
||||||
|
<arg type='a{sv}' direction='in'/><arg type='i' direction='in'/><arg type='u' direction='out'/>
|
||||||
|
</method>
|
||||||
|
<method name='CloseNotification'><arg type='u' direction='in'/></method>
|
||||||
|
<method name='GetCapabilities'><arg type='as' direction='out'/></method>
|
||||||
|
<method name='GetServerInformation'>
|
||||||
|
<arg type='s' direction='out'/><arg type='s' direction='out'/><arg type='s' direction='out'/><arg type='s' direction='out'/>
|
||||||
|
</method>
|
||||||
|
<signal name='ActionInvoked'><arg type='u'/><arg type='s'/></signal>
|
||||||
|
<signal name='NotificationClosed'><arg type='u'/><arg type='u'/></signal>
|
||||||
|
</interface>
|
||||||
|
<interface name='org.freedesktop.systemd1.Manager'>
|
||||||
|
<method name='StartTransientUnit'>
|
||||||
|
<arg type='s' direction='in'/><arg type='s' direction='in'/><arg type='a(sv)' direction='in'/>
|
||||||
|
<arg type='a(sa(sv))' direction='in'/><arg type='o' direction='out'/>
|
||||||
|
</method>
|
||||||
|
</interface>
|
||||||
|
<interface name='org.freedesktop.portal.GlobalShortcuts'>
|
||||||
|
<method name='CreateSession'><arg type='a{sv}' direction='in'/><arg type='o' direction='out'/></method>
|
||||||
|
<method name='BindShortcuts'>
|
||||||
|
<arg type='o' direction='in'/><arg type='a(sa{sv})' direction='in'/><arg type='s' direction='in'/>
|
||||||
|
<arg type='a{sv}' direction='in'/><arg type='o' direction='out'/>
|
||||||
|
</method>
|
||||||
|
<signal name='Activated'><arg type='o'/><arg type='s'/><arg type='t'/><arg type='a{sv}'/></signal>
|
||||||
|
</interface>
|
||||||
|
<interface name='org.freedesktop.portal.Session'><method name='Close'/></interface>
|
||||||
|
<interface name='org.freedesktop.portal.Settings'>
|
||||||
|
<method name='ReadAll'><arg type='as' direction='in'/><arg type='a{sa{sv}}' direction='out'/></method>
|
||||||
|
<method name='Read'><arg type='s' direction='in'/><arg type='s' direction='in'/><arg type='v' direction='out'/></method>
|
||||||
|
<signal name='SettingChanged'><arg type='s'/><arg type='s'/><arg type='v'/></signal>
|
||||||
|
<property name='version' type='u' access='read'/>
|
||||||
|
</interface>
|
||||||
|
<interface name='org.freedesktop.portal.OpenURI'>
|
||||||
|
<method name='OpenURI'>
|
||||||
|
<arg type='s' direction='in'/><arg type='s' direction='in'/><arg type='a{sv}' direction='in'/><arg type='o' direction='out'/>
|
||||||
|
</method>
|
||||||
|
<method name='OpenFile'>
|
||||||
|
<arg type='s' direction='in'/><arg type='h' direction='in'/><arg type='a{sv}' direction='in'/><arg type='o' direction='out'/>
|
||||||
|
</method>
|
||||||
|
<property name='version' type='u' access='read'/>
|
||||||
|
</interface>
|
||||||
|
<interface name='org.freedesktop.portal.Request'><method name='Close'/>
|
||||||
|
<signal name='Response'><arg type='u'/><arg type='a{sv}'/></signal></interface>
|
||||||
|
</node>
|
||||||
|
"""
|
||||||
|
|
||||||
|
CTRL_XML = """
|
||||||
|
<node><interface name='test.ghostty.Mock'>
|
||||||
|
<method name='Activate'><arg type='s' direction='in'/></method>
|
||||||
|
<method name='SetColorScheme'><arg type='u' direction='in'/></method>
|
||||||
|
<method name='Invoke'><arg type='u' direction='in'/><arg type='s' direction='in'/></method>
|
||||||
|
<method name='SetFail'><arg type='b' direction='in'/></method>
|
||||||
|
<method name='Quit'/>
|
||||||
|
</interface></node>
|
||||||
|
"""
|
||||||
|
|
||||||
|
nid = [0]
|
||||||
|
state = {"session": None, "scheme": COLOR_SCHEME}
|
||||||
|
loop = GLib.MainLoop()
|
||||||
|
conns = {}
|
||||||
|
|
||||||
|
|
||||||
|
def log(iface, method, args):
|
||||||
|
def conv(o):
|
||||||
|
if isinstance(o, (bytes, bytearray)):
|
||||||
|
return list(o)
|
||||||
|
return str(o)
|
||||||
|
|
||||||
|
with open(LOG, "a") as f:
|
||||||
|
f.write(json.dumps({"iface": iface, "method": method, "args": args}, default=conv, sort_keys=True) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def unpack(v):
|
||||||
|
return v.unpack() if v is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
def request_path(sender, options):
|
||||||
|
tok = options.get("handle_token", "tok")
|
||||||
|
return "/org/freedesktop/portal/desktop/request/" + sender[1:].replace(".", "_") + "/" + tok
|
||||||
|
|
||||||
|
|
||||||
|
def reply_request(conn, sender, options, results):
|
||||||
|
"""Emit Request.Response after the method reply was sent."""
|
||||||
|
path = request_path(sender, options)
|
||||||
|
|
||||||
|
def emit():
|
||||||
|
conn.emit_signal(
|
||||||
|
sender, path, "org.freedesktop.portal.Request", "Response",
|
||||||
|
GLib.Variant("(ua{sv})", (0, results)),
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
GLib.timeout_add(100, emit)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def settings_all():
|
||||||
|
return {"org.freedesktop.appearance": {"color-scheme": GLib.Variant("u", state["scheme"])}}
|
||||||
|
|
||||||
|
|
||||||
|
def on_call(conn, sender, path, iface, method, params, inv):
|
||||||
|
args = unpack(params)
|
||||||
|
if iface == "org.freedesktop.Notifications":
|
||||||
|
if method == "Notify":
|
||||||
|
log(iface, method, args)
|
||||||
|
nid[0] += 1
|
||||||
|
inv.return_value(GLib.Variant("(u)", (nid[0],)))
|
||||||
|
elif method == "GetCapabilities":
|
||||||
|
inv.return_value(GLib.Variant("(as)", (["actions", "body", "body-markup", "icon-static"],)))
|
||||||
|
elif method == "GetServerInformation":
|
||||||
|
inv.return_value(GLib.Variant("(ssss)", ("mock", "ghostty-test", "1.0", "1.2")))
|
||||||
|
else:
|
||||||
|
log(iface, method, args)
|
||||||
|
inv.return_value(None)
|
||||||
|
elif iface == "org.freedesktop.systemd1.Manager":
|
||||||
|
log(iface, method, args)
|
||||||
|
if state.get("fail"):
|
||||||
|
inv.return_dbus_error("org.freedesktop.DBus.Error.AccessDenied", "mock failure")
|
||||||
|
return
|
||||||
|
inv.return_value(GLib.Variant("(o)", ("/org/freedesktop/systemd1/job/1",)))
|
||||||
|
elif iface == "org.freedesktop.portal.GlobalShortcuts":
|
||||||
|
log(iface, method, args)
|
||||||
|
if method == "CreateSession":
|
||||||
|
opts = args[0]
|
||||||
|
sess = "/org/freedesktop/portal/desktop/session/" + sender[1:].replace(".", "_") + "/" + opts.get("session_handle_token", "s")
|
||||||
|
state["session"] = sess
|
||||||
|
state["sender"] = sender
|
||||||
|
sess_info = Gio.DBusNodeInfo.new_for_xml(XML).lookup_interface("org.freedesktop.portal.Session")
|
||||||
|
conn.register_object(sess, sess_info, on_call, None, None)
|
||||||
|
rp = reply_request(conn, sender, opts, {"session_handle": GLib.Variant("s", sess)})
|
||||||
|
inv.return_value(GLib.Variant("(o)", (rp,)))
|
||||||
|
else:
|
||||||
|
rp = reply_request(conn, sender, args[3], {"shortcuts": GLib.Variant("a(sa{sv})", [])})
|
||||||
|
inv.return_value(GLib.Variant("(o)", (rp,)))
|
||||||
|
elif iface == "org.freedesktop.portal.Session":
|
||||||
|
log(iface, method, {"path": path})
|
||||||
|
inv.return_value(None)
|
||||||
|
elif iface == "org.freedesktop.portal.Settings":
|
||||||
|
log(iface, method, args)
|
||||||
|
if method == "ReadAll":
|
||||||
|
inv.return_value(GLib.Variant("(a{sa{sv}})", (settings_all(),)))
|
||||||
|
else:
|
||||||
|
inv.return_value(GLib.Variant("(v)", (GLib.Variant("v", GLib.Variant("u", state["scheme"])),)))
|
||||||
|
elif iface == "org.freedesktop.portal.OpenURI":
|
||||||
|
log(iface, method, args)
|
||||||
|
rp = reply_request(conn, sender, args[-1], {})
|
||||||
|
inv.return_value(GLib.Variant("(o)", (rp,)))
|
||||||
|
elif iface == "org.freedesktop.portal.Request":
|
||||||
|
inv.return_value(None)
|
||||||
|
elif iface == "test.ghostty.Mock":
|
||||||
|
if method == "Activate":
|
||||||
|
conn.emit_signal(
|
||||||
|
None, "/org/freedesktop/portal/desktop", "org.freedesktop.portal.GlobalShortcuts", "Activated",
|
||||||
|
GLib.Variant("(osta{sv})", (state["session"], args[0], 0, {})),
|
||||||
|
)
|
||||||
|
elif method == "SetColorScheme":
|
||||||
|
state["scheme"] = args[0]
|
||||||
|
conn.emit_signal(
|
||||||
|
None, "/org/freedesktop/portal/desktop", "org.freedesktop.portal.Settings", "SettingChanged",
|
||||||
|
GLib.Variant("(ssv)", ("org.freedesktop.appearance", "color-scheme", GLib.Variant("u", args[0]))),
|
||||||
|
)
|
||||||
|
elif method == "Invoke":
|
||||||
|
conn.emit_signal(
|
||||||
|
None, "/org/freedesktop/Notifications", "org.freedesktop.Notifications", "ActionInvoked",
|
||||||
|
GLib.Variant("(us)", (args[0], args[1])),
|
||||||
|
)
|
||||||
|
elif method == "SetFail":
|
||||||
|
state["fail"] = args[0]
|
||||||
|
elif method == "Quit":
|
||||||
|
loop.quit()
|
||||||
|
inv.return_value(None)
|
||||||
|
|
||||||
|
|
||||||
|
def on_prop(conn, sender, path, iface, name):
|
||||||
|
return GLib.Variant("u", 1 if iface == "org.freedesktop.portal.Settings" else 3)
|
||||||
|
|
||||||
|
|
||||||
|
def on_bus(conn, name):
|
||||||
|
node = Gio.DBusNodeInfo.new_for_xml(XML)
|
||||||
|
paths = {
|
||||||
|
"org.freedesktop.Notifications": ["/org/freedesktop/Notifications"],
|
||||||
|
"org.freedesktop.systemd1.Manager": ["/org/freedesktop/systemd1"],
|
||||||
|
"org.freedesktop.portal.GlobalShortcuts": ["/org/freedesktop/portal/desktop"],
|
||||||
|
"org.freedesktop.portal.Settings": ["/org/freedesktop/portal/desktop"],
|
||||||
|
"org.freedesktop.portal.OpenURI": ["/org/freedesktop/portal/desktop"],
|
||||||
|
"org.freedesktop.portal.Session": [],
|
||||||
|
}
|
||||||
|
for ifc in node.interfaces:
|
||||||
|
for p in paths.get(ifc.name, []):
|
||||||
|
conn.register_object(p, ifc, on_call, on_prop, None)
|
||||||
|
ctrl = Gio.DBusNodeInfo.new_for_xml(CTRL_XML)
|
||||||
|
conn.register_object("/test/ghostty/Mock", ctrl.interfaces[0], on_call, None, None)
|
||||||
|
conns["c"] = conn
|
||||||
|
|
||||||
|
|
||||||
|
def acquired(conn, name):
|
||||||
|
if name == "test.ghostty.Mock":
|
||||||
|
sys.stdout.write("READY\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
|
||||||
|
# Own all names on one connection; announce READY once the last is acquired.
|
||||||
|
bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
|
||||||
|
on_bus(bus, None)
|
||||||
|
names = os.environ.get("MOCK_NAMES", "notifications,systemd,portal").split(",")
|
||||||
|
want = []
|
||||||
|
if "notifications" in names:
|
||||||
|
want.append("org.freedesktop.Notifications")
|
||||||
|
if "systemd" in names:
|
||||||
|
want.append("org.freedesktop.systemd1")
|
||||||
|
if "portal" in names:
|
||||||
|
want.append("org.freedesktop.portal.Desktop")
|
||||||
|
want.append("test.ghostty.Mock")
|
||||||
|
for n in want:
|
||||||
|
Gio.bus_own_name_on_connection(bus, n, Gio.BusNameOwnerFlags.NONE, acquired, None)
|
||||||
|
signal.signal(signal.SIGTERM, lambda *a: loop.quit())
|
||||||
|
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, lambda *a: loop.quit())
|
||||||
|
loop.run()
|
||||||
Executable
+74
@@ -0,0 +1,74 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Turn a scenario output dir into a comparable text: facts + filtered bus
|
||||||
|
traffic + mock-received calls, with volatile values (unique names, pids,
|
||||||
|
cookies, tokens, paths) normalized."""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import pwd
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
d = sys.argv[1]
|
||||||
|
out = []
|
||||||
|
|
||||||
|
|
||||||
|
def rd(name):
|
||||||
|
p = os.path.join(d, name)
|
||||||
|
return open(p).read() if os.path.exists(p) else ""
|
||||||
|
|
||||||
|
|
||||||
|
def norm(s):
|
||||||
|
s = re.sub(r"app-ghostty-surface-transient-\d+", "app-ghostty-surface-transient-PID", s)
|
||||||
|
s = re.sub(r'"PIDs", (\[\d+\]|\{"data": \[\d+\])', lambda m: re.sub(r"\d+", "PID", m.group(0)), s)
|
||||||
|
s = re.sub(r"ghostty_[0-9a-f]{7}", "ghostty_TOKEN", s)
|
||||||
|
s = re.sub(r"gtk[0-9]{6,}", "gtkTOKEN", s)
|
||||||
|
s = re.sub(r"/request/[0-9_]+/", "/request/SENDER/", s)
|
||||||
|
s = re.sub(r"/session/[0-9_]+/", "/session/SENDER/", s)
|
||||||
|
s = s.replace(os.environ.get("LDB_RUN", "\0"), "$RUN")
|
||||||
|
s = s.replace(os.environ.get("LDB_ROOT", "\0"), "$ROOT")
|
||||||
|
s = s.replace(os.path.expanduser("~" + os.environ.get("USER", "")) if False else pwd.getpwuid(os.getuid()).pw_dir, "$HOME")
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
# Scope creation races with the end of a scenario; only the cgroup scenario checks it.
|
||||||
|
CGROUP = os.path.basename(os.path.dirname(d.rstrip("/"))) == "cgroup"
|
||||||
|
|
||||||
|
out.append("== facts ==")
|
||||||
|
for l in rd("facts.txt").splitlines():
|
||||||
|
out.append(norm(l))
|
||||||
|
|
||||||
|
out.append("== mock received ==")
|
||||||
|
for l in rd("mock.jsonl").splitlines():
|
||||||
|
j = json.loads(l)
|
||||||
|
if j["method"] == "StartTransientUnit" and not CGROUP:
|
||||||
|
continue
|
||||||
|
out.append(norm(f"{j['iface']}.{j['method']} {json.dumps(j['args'], sort_keys=True)}"))
|
||||||
|
|
||||||
|
out.append("== bus traffic (filtered) ==")
|
||||||
|
names = {}
|
||||||
|
|
||||||
|
|
||||||
|
def label(n):
|
||||||
|
if n is None:
|
||||||
|
return "-"
|
||||||
|
if n.startswith(":"):
|
||||||
|
names.setdefault(n, f"conn{len(names)+1}")
|
||||||
|
return names[n]
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
KEEP = re.compile(r"^(org\.gtk\.|org\.freedesktop\.(Notifications|systemd1|portal))")
|
||||||
|
for l in rd("bus.json").splitlines():
|
||||||
|
try:
|
||||||
|
m = json.loads(l)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
iface = m.get("interface") or ""
|
||||||
|
if not KEEP.match(iface):
|
||||||
|
continue
|
||||||
|
if m.get("member") == "StartTransientUnit" and not CGROUP:
|
||||||
|
continue
|
||||||
|
payload = m.get("payload") or {}
|
||||||
|
data = json.dumps(payload.get("data"), sort_keys=True)
|
||||||
|
out.append(norm(f"{m['type']} {iface}.{m.get('member')} path={m.get('path')} -> {data}"))
|
||||||
|
print("\n".join(out))
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
wait 5000
|
||||||
|
windows-dump
|
||||||
|
wait 4000
|
||||||
|
windows-dump
|
||||||
Executable
+89
@@ -0,0 +1,89 @@
|
|||||||
|
#!/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//-/_}"
|
||||||
Executable
+275
@@ -0,0 +1,275 @@
|
|||||||
|
# Scenario bodies, sourced by scenario.sh.
|
||||||
|
|
||||||
|
scn_single_instance() {
|
||||||
|
cfg "gtk-single-instance = true"
|
||||||
|
start_bus_tools
|
||||||
|
# (A launch with -e is never single-instance: see the "-e launch" step.)
|
||||||
|
bg env GHOSTTY_DEBUG_INPUT_SCRIPT="$LD/replay-windows.txt" "$BIN" >"$D/app1.log" 2>&1
|
||||||
|
wait_name com.mitchellh.ghostty; fact "first instance owns name: $?"
|
||||||
|
settle 4; fact "step first: windows=$(windows) instances=$(instances)"
|
||||||
|
run_ipc newwin +new-window; settle
|
||||||
|
fact "step +new-window: windows=$(windows) instances=$(instances)"
|
||||||
|
run_ipc newwin-e +new-window -e sleep 601; settle
|
||||||
|
fact "step +new-window -e: windows=$(windows) instances=$(instances)"
|
||||||
|
run_ipc newwin-wd +new-window --working-directory=/usr; settle
|
||||||
|
fact "step +new-window --working-directory: windows=$(windows) instances=$(instances)"
|
||||||
|
run_ipc newwin-e-wd +new-window -e sleep 602 --working-directory=/usr; settle
|
||||||
|
fact "step +new-window -e..--working-directory: windows=$(windows) instances=$(instances)"
|
||||||
|
run_ipc newwin-class +new-window --class=com.example.nobody; settle 1
|
||||||
|
fact "step +new-window --class=nobody: windows=$(windows) instances=$(instances)"
|
||||||
|
# A second GUI launch is forwarded to the first instance and exits.
|
||||||
|
timeout 20 "$BIN" >"$D/second.log" 2>&1; fact "second launch rc=$?"
|
||||||
|
settle; fact "step second launch: windows=$(windows) instances=$(instances)"
|
||||||
|
children | sed 's/^/child: /' >>"$FACTS"
|
||||||
|
# A launch with -e is not single-instance: it runs on its own.
|
||||||
|
bg timeout 8 "$BIN" -e sleep 603 >"$D/dash-e.log" 2>&1
|
||||||
|
settle 5; fact "step -e launch: windows=$(windows) instances=$(instances)"
|
||||||
|
settle 5; fact "step -e launch ended: windows=$(windows) instances=$(instances)"
|
||||||
|
# Custom class: a separate instance on its own bus name / object path.
|
||||||
|
bg "$BIN" --class=com.example.custom-app >"$D/app2.log" 2>&1
|
||||||
|
wait_name com.example.custom-app; fact "custom class owns name: $?"
|
||||||
|
settle 4; fact "step custom: windows=$(windows com.example.custom-app) instances=$(instances)"
|
||||||
|
run_ipc newwin-custom +new-window --class=com.example.custom-app; settle
|
||||||
|
fact "step +new-window --class=custom: custom windows=$(windows com.example.custom-app) default windows=$(windows)"
|
||||||
|
run_ipc newwin-custom-e +new-window --class=com.example.custom-app -e sleep 605; settle
|
||||||
|
fact "step +new-window --class=custom -e: custom windows=$(windows com.example.custom-app) default windows=$(windows)"
|
||||||
|
run_ipc newwin-badclass +new-window --class=1bad; fact "badclass done"
|
||||||
|
busctl --user list --acquired --no-pager | awk '$1 ~ /^(com\.mitchellh|com\.example)/ {print "bus name: "$1}' >>"$FACTS"
|
||||||
|
gdbus call --session --dest com.mitchellh.ghostty --object-path /com/mitchellh/ghostty \
|
||||||
|
--method org.gtk.Actions.List 2>&1 | tr -d "'" >"$D/actions.txt"; fact "actions: $(cat "$D/actions.txt")"
|
||||||
|
gdbus call --session --dest com.example.custom-app --object-path /com/example/custom_app \
|
||||||
|
--method org.gtk.Actions.List 2>&1 | tr -d "'" >"$D/actions2.txt"; fact "custom actions: $(cat "$D/actions2.txt")"
|
||||||
|
}
|
||||||
|
|
||||||
|
scn_single_instance_detect() {
|
||||||
|
cfg "gtk-single-instance = detect"
|
||||||
|
start_bus_tools
|
||||||
|
bg "$BIN" >"$D/app1.log" 2>&1
|
||||||
|
wait_name com.mitchellh.ghostty; fact "plain launch owns name: $?"
|
||||||
|
settle 4; fact "step plain: windows=$(windows) instances=$(instances)"
|
||||||
|
timeout 20 "$BIN" >"$D/second.log" 2>&1; fact "second plain launch rc=$?"
|
||||||
|
settle; fact "step second plain: windows=$(windows) instances=$(instances)"
|
||||||
|
# TERM_PROGRAM set: a dedicated instance (false) that does not use the bus name.
|
||||||
|
bg env TERM_PROGRAM=x timeout 12 "$BIN" >"$D/termprog.log" 2>&1
|
||||||
|
settle 6; fact "step TERM_PROGRAM: windows=$(windows) instances=$(instances)"
|
||||||
|
settle 7; fact "step TERM_PROGRAM ended: windows=$(windows) instances=$(instances)"
|
||||||
|
# Any CLI argument: dedicated instance.
|
||||||
|
bg timeout 12 "$BIN" --title=cli >"$D/cliarg.log" 2>&1
|
||||||
|
settle 6; fact "step CLI arg: windows=$(windows) instances=$(instances)"
|
||||||
|
settle 7; fact "step CLI arg ended: windows=$(windows) instances=$(instances)"
|
||||||
|
run_ipc newwin +new-window; settle
|
||||||
|
fact "step +new-window: windows=$(windows) instances=$(instances)"
|
||||||
|
}
|
||||||
|
|
||||||
|
scn_notifications() {
|
||||||
|
export FLAG2=$W/go2
|
||||||
|
rm -f "$FLAG2"
|
||||||
|
cfg "desktop-notifications = true" "notify-on-command-finished = always" "notify-on-command-finished-after = 1s" \
|
||||||
|
"command = sh $LD/child-notify.sh"
|
||||||
|
start_bus_tools
|
||||||
|
bg "$BIN" >"$D/app.log" 2>&1
|
||||||
|
settle 16
|
||||||
|
fact "notifications so far: $(grep -c "\"method\": \"Notify\"" "$MOCK_LOG" 2>/dev/null || true)"
|
||||||
|
# Activate the first notifications (default action): must not crash.
|
||||||
|
mockctl Invoke 1 default; settle 2
|
||||||
|
fact "alive after ActionInvoked: instances=$(instances) windows=$(windows)"
|
||||||
|
mockctl Invoke 2 default; mockctl Invoke 999 default; settle 1
|
||||||
|
fact "alive after more ActionInvoked: instances=$(instances) windows=$(windows)"
|
||||||
|
cfg "desktop-notifications = false" "command = sh $LD/child-notify.sh"
|
||||||
|
gdbus call --session --dest com.mitchellh.ghostty --object-path /com/mitchellh/ghostty \
|
||||||
|
--method org.gtk.Actions.Activate reload-config '[]' '{}' >/dev/null; settle 2
|
||||||
|
touch "$FLAG2"; settle 3
|
||||||
|
fact "notifications after desktop-notifications=false: $(grep -c "\"method\": \"Notify\"" "$MOCK_LOG" 2>/dev/null || true)"
|
||||||
|
}
|
||||||
|
|
||||||
|
scn_shortcuts() {
|
||||||
|
cfg "keybind = global:ctrl+alt+x=new_window" "keybind = global:super+shift+b=new_tab" \
|
||||||
|
"keybind = global:ctrl+shift+f12=reload_config" "keybind = global:alt+space>q=quit" \
|
||||||
|
"keybind = global:ctrl+alt+q=text:hi" "keybind = ctrl+alt+z=new_window"
|
||||||
|
start_bus_tools
|
||||||
|
bg "$BIN" >"$D/app.log" 2>&1
|
||||||
|
wait_name com.mitchellh.ghostty; settle 5
|
||||||
|
fact "step start: windows=$(windows)"
|
||||||
|
id=$(python3 - "$MOCK_LOG" <<'PY'
|
||||||
|
import json, sys
|
||||||
|
for l in open(sys.argv[1]):
|
||||||
|
j = json.loads(l)
|
||||||
|
if j["method"] == "BindShortcuts":
|
||||||
|
for sid, opts in j["args"][1]:
|
||||||
|
if opts.get("description", "").startswith("new_window"):
|
||||||
|
print(sid)
|
||||||
|
PY
|
||||||
|
)
|
||||||
|
fact "new_window shortcut id: $id"
|
||||||
|
mockctl Activate "$id"; settle 3
|
||||||
|
fact "step Activated(new_window): windows=$(windows)"
|
||||||
|
mockctl Activate nonexistent; settle 1
|
||||||
|
fact "step Activated(unknown): windows=$(windows)"
|
||||||
|
# Reload with the keybinds removed: the old session is closed.
|
||||||
|
cfg "keybind = ctrl+alt+z=new_window"
|
||||||
|
gdbus call --session --dest com.mitchellh.ghostty --object-path /com/mitchellh/ghostty \
|
||||||
|
--method org.gtk.Actions.Activate reload-config '[]' '{}' >/dev/null; settle 3
|
||||||
|
fact "step reload without global: windows=$(windows)"
|
||||||
|
cfg "keybind = global:ctrl+alt+x=new_window"
|
||||||
|
gdbus call --session --dest com.mitchellh.ghostty --object-path /com/mitchellh/ghostty \
|
||||||
|
--method org.gtk.Actions.Activate reload-config '[]' '{}' >/dev/null; settle 3
|
||||||
|
fact "step reload with global again: windows=$(windows)"
|
||||||
|
}
|
||||||
|
|
||||||
|
scn_settings() {
|
||||||
|
mkdir -p "$W/themes"
|
||||||
|
printf 'background = ffeedd\nforeground = 111111\n' >"$W/themes/light"
|
||||||
|
printf 'background = 112233\nforeground = eeeeee\n' >"$W/themes/dark"
|
||||||
|
cfg "theme = light:$W/themes/light,dark:$W/themes/dark" "window-theme = auto" "command = sh $LD/child-theme.sh"
|
||||||
|
MOCK_COLOR_SCHEME=1 start_bus_tools
|
||||||
|
export OUTF=$D/tty.txt
|
||||||
|
: >"$OUTF"
|
||||||
|
bg "$BIN" >"$D/app.log" 2>&1
|
||||||
|
for _ in $(seq 600); do [ -s "$OUTF" ] && break; sleep 0.1; done
|
||||||
|
[ -s "$OUTF" ] || fact "ERROR: terminal child produced no output"
|
||||||
|
settle 3
|
||||||
|
for scheme in 2 1 0 2; do
|
||||||
|
printf "\n--- after portal color-scheme=$scheme\n" >>"$OUTF"
|
||||||
|
mockctl SetColorScheme $scheme; settle 2
|
||||||
|
done
|
||||||
|
printf "\n--- after reload window-theme=dark\n" >>"$OUTF"
|
||||||
|
cfg "theme = light:$W/themes/light,dark:$W/themes/dark" "window-theme = dark" "command = sh $LD/child-theme.sh"
|
||||||
|
gdbus call --session --dest com.mitchellh.ghostty --object-path /com/mitchellh/ghostty \
|
||||||
|
--method org.gtk.Actions.Activate reload-config '[]' '{}' >/dev/null; settle 2
|
||||||
|
printf "\n--- after portal color-scheme=2 with window-theme=dark\n" >>"$OUTF"
|
||||||
|
mockctl SetColorScheme 2; settle 2
|
||||||
|
python3 "$LD/summarize-tty.py" "$OUTF" | sed 's/^/tty: /' >>"$FACTS"
|
||||||
|
}
|
||||||
|
|
||||||
|
scn_cgroup() {
|
||||||
|
start_bus_tools
|
||||||
|
phase() { # name, config lines...
|
||||||
|
local name=$1; shift
|
||||||
|
: >"$MOCK_LOG"
|
||||||
|
cfg "$@"
|
||||||
|
bg "$BIN" -e sleep 30 >"$D/app-$name.log" 2>&1
|
||||||
|
local pid=$!
|
||||||
|
# wait for the terminal child (the scope is created right before it is exec'd)
|
||||||
|
for _ in $(seq 250); do pgrep -P $pid -x sleep >/dev/null && break; sleep 0.1; done
|
||||||
|
settle 3
|
||||||
|
fact "phase $name:"
|
||||||
|
grep StartTransientUnit "$MOCK_LOG" | sed 's/^/ call: /' >>"$FACTS"
|
||||||
|
kill $pid 2>/dev/null
|
||||||
|
for _ in $(seq 100); do kill -0 $pid 2>/dev/null || break; sleep 0.1; done
|
||||||
|
kill -9 $pid 2>/dev/null; kill $(tagged sleep) 2>/dev/null; settle 1
|
||||||
|
}
|
||||||
|
phase default "gtk-single-instance = false"
|
||||||
|
phase always "gtk-single-instance = false" "linux-cgroup = always"
|
||||||
|
phase always-limits "gtk-single-instance = false" "linux-cgroup = always" "linux-cgroup-memory-limit = 123456789" "linux-cgroup-processes-limit = 77"
|
||||||
|
phase never "gtk-single-instance = false" "linux-cgroup = never"
|
||||||
|
mockctl SetFail true
|
||||||
|
phase always-failing "gtk-single-instance = false" "linux-cgroup = always"
|
||||||
|
fact "cgroup done"
|
||||||
|
}
|
||||||
|
|
||||||
|
scn_openuri() {
|
||||||
|
# Ghostty 1.3.1 does not use the OpenURI portal (upstream TODO): links and
|
||||||
|
# the config file are opened by spawning `xdg-open`. A fake xdg-open on
|
||||||
|
# PATH records the invocations; the mock records any portal OpenURI call.
|
||||||
|
mkdir -p "$W/bin"
|
||||||
|
cat >"$W/bin/xdg-open" <<XDG
|
||||||
|
#!/bin/sh
|
||||||
|
printf 'xdg-open argc=%s' "\$#" >>"$D/xdg-open.log"
|
||||||
|
for a in "\$@"; do printf ' [%s]' "\$a" >>"$D/xdg-open.log"; done
|
||||||
|
printf ' cwd=%s\n' "\$(pwd)" >>"$D/xdg-open.log"
|
||||||
|
XDG
|
||||||
|
chmod +x "$W/bin/xdg-open"
|
||||||
|
export PATH=$W/bin:$PATH GTK_USE_PORTAL=1
|
||||||
|
: >"$D/xdg-open.log"
|
||||||
|
cfg "gtk-single-instance = true"
|
||||||
|
start_bus_tools
|
||||||
|
bg "$BIN" >"$D/app.log" 2>&1
|
||||||
|
wait_name com.mitchellh.ghostty; settle 5
|
||||||
|
gdbus call --session --dest com.mitchellh.ghostty --object-path /com/mitchellh/ghostty \
|
||||||
|
--method org.gtk.Actions.Activate open-config '[]' '{}' >/dev/null; settle 3
|
||||||
|
sed "s|$W|\$RUN|g" "$D/xdg-open.log" | sed 's/^/spawn: /' >>"$FACTS"
|
||||||
|
fact "portal OpenURI/OpenFile calls: $(grep -c 'portal.OpenURI' "$MOCK_LOG")"
|
||||||
|
}
|
||||||
|
|
||||||
|
scn_openurl_click() {
|
||||||
|
# The original has no input replay and weston's headless pointer is not
|
||||||
|
# reachable through Xwayland, so this scenario only runs for the Rust
|
||||||
|
# build; its result is compared against golden/openurl-click.txt, which is
|
||||||
|
# derived from the Zig source (internal_os/open.zig spawns `xdg-open <url>`).
|
||||||
|
[ "${LDB_LABEL:-}" = rust ] || return
|
||||||
|
mkdir -p "$W/bin"
|
||||||
|
cat >"$W/bin/xdg-open" <<XDG
|
||||||
|
#!/bin/sh
|
||||||
|
printf 'xdg-open argc=%s' "\$#" >>"$D/xdg-open.log"
|
||||||
|
for a in "\$@"; do printf ' [%s]' "\$a" >>"$D/xdg-open.log"; done
|
||||||
|
printf '\n' >>"$D/xdg-open.log"
|
||||||
|
XDG
|
||||||
|
chmod +x "$W/bin/xdg-open"
|
||||||
|
export PATH=$W/bin:$PATH GDK_BACKEND=x11 OUTF=$D/tty.txt
|
||||||
|
: >"$D/xdg-open.log"; : >"$OUTF"
|
||||||
|
cfg "gtk-single-instance = false" "gtk-titlebar = false" "window-padding-x = 0" "window-padding-y = 0" \
|
||||||
|
"font-size = 14" "window-decoration = none" "command = sh $LD/child-links.sh" "window-width = 60" "window-height = 12"
|
||||||
|
start_bus_tools
|
||||||
|
# Launch 1 measures the cell size.
|
||||||
|
bg "$BIN" >"$D/app-measure.log" 2>&1
|
||||||
|
local pid=$!
|
||||||
|
settle 7
|
||||||
|
rows=$(grep -o '\^\[\[8;[0-9]*;[0-9]*t' "$OUTF" | head -1 | cut -d';' -f2)
|
||||||
|
cols=$(grep -o '\^\[\[8;[0-9]*;[0-9]*t' "$OUTF" | head -1 | cut -d';' -f3 | tr -d t)
|
||||||
|
wid=$(xdotool search --onlyvisible --class ghostty 2>/dev/null | head -1)
|
||||||
|
eval "$(xdotool getwindowgeometry --shell "$wid" 2>/dev/null)"
|
||||||
|
kill $pid; settle 1
|
||||||
|
[ -n "${WIDTH:-}" ] && [ -n "${rows:-}" ] || { fact "no geometry"; return; }
|
||||||
|
cw=$((WIDTH / cols)); ch=$((HEIGHT / rows))
|
||||||
|
{
|
||||||
|
for row in 0 1 2 3; do
|
||||||
|
echo "mouse-move $((cw * 3)) $((row * ch + ch / 2)) ctrl"
|
||||||
|
echo "wait 600"
|
||||||
|
echo "click 1 ctrl"
|
||||||
|
echo "wait 1500"
|
||||||
|
done
|
||||||
|
echo "wait 500"
|
||||||
|
} >"$W/click.txt"
|
||||||
|
: >"$D/xdg-open.log"; : >"$OUTF"
|
||||||
|
bg env GHOSTTY_DEBUG_INPUT_SCRIPT="$W/click.txt" "$BIN" >"$D/app.log" 2>&1
|
||||||
|
settle 16
|
||||||
|
sed "s|$W|\$RUN|g" "$D/xdg-open.log" | sed 's/^/spawn: /' >>"$FACTS"
|
||||||
|
}
|
||||||
|
|
||||||
|
# The real xdg-desktop-portal frontend + the gtk backend (gsettings keyfile
|
||||||
|
# backend, so we can change color-scheme live) on the private bus.
|
||||||
|
scn_real_portal() {
|
||||||
|
mkdir -p "$W/cfg/xdg-desktop-portal" "$W/cfg/glib-2.0/settings" "$W/themes"
|
||||||
|
printf '[preferred]\ndefault=gtk\n' >"$W/cfg/xdg-desktop-portal/portals.conf"
|
||||||
|
portal_env="/usr/bin/env XDG_CURRENT_DESKTOP=GNOME GSETTINGS_BACKEND=keyfile XDG_CONFIG_HOME=$W/cfg HOME=$W/home XDG_DATA_HOME=$W/data XDG_CACHE_HOME=$W/cache WAYLAND_DISPLAY=$WAYLAND_DISPLAY"
|
||||||
|
svc() { printf '[D-BUS Service]\nName=%s\nExec=%s\n' "$1" "$2" >"$LDB_RUN/services/$1.service"; }
|
||||||
|
svc org.freedesktop.portal.Desktop "$portal_env /usr/lib/xdg-desktop-portal"
|
||||||
|
svc org.freedesktop.impl.portal.desktop.gtk "$portal_env /usr/lib/xdg-desktop-portal-gtk"
|
||||||
|
svc org.freedesktop.impl.portal.PermissionStore "$portal_env /usr/lib/xdg-permission-store"
|
||||||
|
printf 'background = ffeedd\nforeground = 111111\n' >"$W/themes/light"
|
||||||
|
printf 'background = 112233\nforeground = eeeeee\n' >"$W/themes/dark"
|
||||||
|
keyfile=$W/cfg/glib-2.0/settings/keyfile
|
||||||
|
setscheme() { printf '[org/gnome/desktop/interface]\ncolor-scheme='"'"'%s'"'"'\n' "$1" >"$keyfile"; }
|
||||||
|
setscheme prefer-dark
|
||||||
|
cfg "theme = light:$W/themes/light,dark:$W/themes/dark" "window-theme = auto" "command = sh $LD/child-theme.sh" \
|
||||||
|
"keybind = global:ctrl+alt+x=new_window"
|
||||||
|
start_bus_tools notifications,systemd
|
||||||
|
# Start the portal first so that its backends are up.
|
||||||
|
gdbus call --session --dest org.freedesktop.portal.Desktop --object-path /org/freedesktop/portal/desktop \
|
||||||
|
--method org.freedesktop.DBus.Properties.Get org.freedesktop.portal.Settings version >"$D/portal-version.txt" 2>&1
|
||||||
|
fact "portal frontend: $(tr -d '\n' <"$D/portal-version.txt" | cut -c1-80)"
|
||||||
|
settle 2
|
||||||
|
export OUTF=$D/tty.txt; : >"$OUTF"
|
||||||
|
bg env GSETTINGS_BACKEND=memory "$BIN" >"$D/app.log" 2>&1
|
||||||
|
for _ in $(seq 600); do [ -s "$OUTF" ] && break; sleep 0.1; done
|
||||||
|
[ -s "$OUTF" ] || fact "ERROR: terminal child produced no output"
|
||||||
|
settle 3
|
||||||
|
for scheme in prefer-light prefer-dark default prefer-dark; do
|
||||||
|
printf "\n--- after gsettings color-scheme=$scheme\n" >>"$OUTF"
|
||||||
|
setscheme $scheme; settle 3
|
||||||
|
done
|
||||||
|
python3 "$LD/summarize-tty.py" "$OUTF" | sed 's/^/tty: /' >>"$FACTS"
|
||||||
|
# Only the D-Bus error names (the log prefixes differ between the builds).
|
||||||
|
sed -n 's/.*request failed=\(GDBus.Error:[A-Za-z.]*\).*/\1/p' "$D/app.log" | sed 's/^/global-shortcuts request error: /' >>"$FACTS"
|
||||||
|
fact "alive: instances=$(instances)"
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Summarize the raw `cat -v` capture of child-theme.sh: per phase (split by
|
||||||
|
the `--- after` markers) the collapsed sequence of CSI ? 997 reports and the
|
||||||
|
last OSC 11 background answer."""
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
text = open(sys.argv[1]).read()
|
||||||
|
parts = re.split(r"\n--- (after [^\n]*)\n", "\n" + text)
|
||||||
|
# parts: [startup, name1, body1, name2, body2, ...]; markers are written
|
||||||
|
# right before the action, so the body after a marker is its effect.
|
||||||
|
names = ["startup"] + parts[1::2]
|
||||||
|
chunks = [parts[0]] + parts[2::2]
|
||||||
|
for name, chunk in zip(names, chunks):
|
||||||
|
reports = re.findall(r"\^\[\[\?997;(\d)n", chunk)
|
||||||
|
collapsed = [r for i, r in enumerate(reports) if i == 0 or r != reports[i - 1]]
|
||||||
|
bgs = re.findall(r"\^\[\]11;(rgb:[0-9a-f/]+)\^G", chunk)
|
||||||
|
print(f"{name}: 997 reports={','.join(collapsed) or '-'} last bg={bgs[-1] if bgs else '-'}")
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
//! Runs `scripts/live-dbus-test.sh` in golden mode: the Rust build is started
|
||||||
|
//! on a private D-Bus session bus inside a private headless weston, with mock
|
||||||
|
//! Notifications / systemd1 / portal services (and the real
|
||||||
|
//! xdg-desktop-portal + gtk backend), and its observable D-Bus behaviour is
|
||||||
|
//! compared with `scripts/live-dbus/golden/*.txt`, recorded from the original
|
||||||
|
//! Ghostty 1.3.1 (`UPDATE_GOLDEN=1 just live-dbus` re-records them against the
|
||||||
|
//! installed original). See `docs/rewrite/notes/live-dbus.md`.
|
||||||
|
//!
|
||||||
|
//! Skipped (passes) when a needed tool is missing, when there is no
|
||||||
|
//! release-profile Rust build to test (debug builds use a different D-Bus
|
||||||
|
//! application id, `com.mitchellh.ghostty-debug`) or when that build is older
|
||||||
|
//! than the sources (build it with `scripts/live-dbus/build-rust.sh`). Set
|
||||||
|
//! `GHOSTTY_SKIP_LIVE_DBUS` to skip explicitly.
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
process::Command,
|
||||||
|
time::SystemTime,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn newest_source(dir: &Path, newest: &mut SystemTime) {
|
||||||
|
let Ok(entries) = std::fs::read_dir(dir) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
for e in entries.flatten() {
|
||||||
|
let p = e.path();
|
||||||
|
let name = e.file_name();
|
||||||
|
if p.is_dir() {
|
||||||
|
if name != "target" && name != ".git" {
|
||||||
|
newest_source(&p, newest);
|
||||||
|
}
|
||||||
|
} else if matches!(
|
||||||
|
p.extension().and_then(|x| x.to_str()),
|
||||||
|
Some("rs" | "toml" | "blp" | "ui" | "css")
|
||||||
|
) && let Ok(m) = e.metadata().and_then(|m| m.modified())
|
||||||
|
{
|
||||||
|
*newest = (*newest).max(m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn have(tool: &str) -> bool {
|
||||||
|
Command::new("sh")
|
||||||
|
.args(["-c", &format!("command -v {tool}")])
|
||||||
|
.output()
|
||||||
|
.is_ok_and(|o| o.status.success())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rust_build_matches_recorded_original_on_private_bus() {
|
||||||
|
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
if std::env::var_os("GHOSTTY_SKIP_LIVE_DBUS").is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for tool in ["weston", "dbus-run-session", "busctl", "python3", "xdotool"] {
|
||||||
|
if !have(tool) {
|
||||||
|
eprintln!("live-dbus: skipped ({tool} missing)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let bin = std::env::var_os("RUST_BIN")
|
||||||
|
.map_or_else(|| root.join("target/vp/production/ghostty"), PathBuf::from);
|
||||||
|
let Ok(built) = std::fs::metadata(&bin).and_then(|m| m.modified()) else {
|
||||||
|
eprintln!("live-dbus: skipped (no release build at {})", bin.display());
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mut newest = SystemTime::UNIX_EPOCH;
|
||||||
|
for d in ["crates", "src", "resources"] {
|
||||||
|
newest_source(&root.join(d), &mut newest);
|
||||||
|
}
|
||||||
|
if newest > built {
|
||||||
|
eprintln!(
|
||||||
|
"live-dbus: skipped ({} is older than the sources)",
|
||||||
|
bin.display()
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let out = Command::new("bash")
|
||||||
|
.arg(root.join("scripts/live-dbus-test.sh"))
|
||||||
|
.env("LABELS", "rust")
|
||||||
|
.env("RUST_BIN", &bin)
|
||||||
|
.env("OUT", root.join("target/live-dbus"))
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
let text = String::from_utf8_lossy(&out.stdout);
|
||||||
|
eprintln!("{text}");
|
||||||
|
assert!(out.status.success(), "live-dbus comparison failed:\n{text}");
|
||||||
|
}
|
||||||
Reference in new issue
Block a user