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
5cb5bbada2
commit
39865d0ae1
25 files changed
+1278
-31
No files matched your search
@@ -293,6 +293,12 @@ mod imp {
|
||||
/// The global shortcut logic.
|
||||
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
|
||||
/// prevents quitting the app before we've shown one window.
|
||||
pub requested_window: Cell<bool>,
|
||||
@@ -827,7 +833,9 @@ impl Application {
|
||||
}
|
||||
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
@@ -1075,6 +1083,7 @@ impl Application {
|
||||
gs.set_connection(self.dbus_connection().as_ref());
|
||||
|
||||
// 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());
|
||||
|
||||
// Setup the signal handler for global shortcut triggers
|
||||
|
||||
@@ -157,6 +157,42 @@ fn vardict(entries: Vec<(&str, glib::Variant)>) -> glib::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 {
|
||||
pub fn new() -> Self {
|
||||
glib::Object::new()
|
||||
@@ -279,39 +315,18 @@ impl GlobalShortcuts {
|
||||
fn make_payload(&self, method: Method, request_token: &str) -> Option<glib::Variant> {
|
||||
match method {
|
||||
// https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html#org-freedesktop-portal-globalshortcuts-createsession
|
||||
Method::CreateSession => {
|
||||
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())
|
||||
}
|
||||
Method::CreateSession => Some(create_session_payload(request_token, &generate_token())),
|
||||
// https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html#org-freedesktop-portal-globalshortcuts-bindshortcuts
|
||||
Method::BindShortcuts => {
|
||||
let imp = self.imp();
|
||||
let handle = imp.handle.borrow().clone()?;
|
||||
let handle = ObjectPath::try_from(handle).ok()?;
|
||||
|
||||
let binds: Vec<(String, glib::Variant)> = imp
|
||||
.map
|
||||
.borrow()
|
||||
.iter()
|
||||
.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())
|
||||
let map = imp.map.borrow();
|
||||
bind_shortcuts_payload(
|
||||
&handle,
|
||||
map.iter()
|
||||
.map(|(trigger, action)| (trigger.as_str(), action.to_string())),
|
||||
request_token,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -459,3 +474,40 @@ impl GlobalShortcuts {
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user