crabshell-services 0.2.1

Crabshell's services: workspaces, tray, audio, clock, power, Bluetooth, network, media players, notifications, apps, programs, files, PAM, greetd and polkit
Documentation
//! Raw events for a config's handlers, against the fake Sway.

use std::cell::RefCell;

use super::*;
use crate::sway::ipc::GET_OUTPUTS;
use crate::sway::{SwayEvent, SwayEventKind};

const MODE_EVENT: u32 = 0x8000_0002;
const WINDOW_EVENT: u32 = 0x8000_0003;

/// A handler that keeps each event it hears, and what it heard.
fn recorder() -> (impl Fn(&SwayEvent), Rc<RefCell<Vec<SwayEvent>>>) {
  let heard = Rc::new(RefCell::new(Vec::new()));
  let keep = Rc::clone(&heard);
  (
    move |event: &SwayEvent| keep.borrow_mut().push(event.clone()),
    heard,
  )
}

fn event(kind: SwayEventKind, data: &str) -> SwayEvent {
  SwayEvent {
    kind,
    data: data.to_owned(),
  }
}

#[test]
fn each_kind_has_its_own_code_and_name() {
  for kind in SwayEventKind::ALL {
    let (code, name) = kind.wire();
    assert_eq!(SwayEventKind::from_code(code), Some(kind), "{name}");
    let same_name = SwayEventKind::ALL
      .iter()
      .filter(|other| other.wire().1 == name);
    assert_eq!(same_name.count(), 1, "{name}");
  }
  assert_eq!(SwayEventKind::from_code(GET_WORKSPACES), None);
}

#[test]
fn a_listener_subscribes_to_its_kinds_alone_and_hears_their_events() {
  let fake = FakeSway::new();
  let (mut shell, mut peer) = Harness::connect(&fake);
  let sway = shell.cx.sway();
  let (first, heard) = recorder();
  sway.on_event(&[SwayEventKind::Mode, SwayEventKind::Window], first);
  assert_eq!(peer.read(), (SUBSCRIBE, r#"["mode","window"]"#.to_owned()));
  peer.send(SUBSCRIBE, r#"{"success":true}"#);
  // Kinds already asked for aren't asked for again.
  let (second, also_heard) = recorder();
  sway.on_event(&[SwayEventKind::Mode], second);
  let (third, workspace_heard) = recorder();
  sway.on_event(&[SwayEventKind::Workspace], third);
  peer.read_nothing();

  let mode = r#"{"change":"resize","pango_markup":false}"#;
  peer.send(MODE_EVENT, mode);
  peer.send(WINDOW_EVENT, r#"{"change":"focus"}"#);
  shell.turn();
  let mode = event(SwayEventKind::Mode, mode);
  let window = event(SwayEventKind::Window, r#"{"change":"focus"}"#);
  assert_eq!(*heard.borrow(), [mode.clone(), window]);
  assert_eq!(*also_heard.borrow(), [mode]);

  // A workspace listener hears the event, and the list is fetched too.
  peer.send_event(&mut shell);
  assert_eq!(
    *workspace_heard.borrow(),
    [event(SwayEventKind::Workspace, "{}")]
  );
  peer.read_nothing();
}

#[test]
fn a_bad_event_is_logged_and_skipped() {
  let logs = capture_logs(|| {
    let fake = FakeSway::new();
    let (mut shell, mut peer) = Harness::connect(&fake);
    let (handler, heard) = recorder();
    shell.cx.sway().on_event(&[SwayEventKind::Mode], handler);
    peer.read();
    let mut bytes = Vec::new();
    ipc::encode(MODE_EVENT, b"{not json", &mut bytes);
    ipc::encode(MODE_EVENT, b"\"\xff\"", &mut bytes);
    ipc::encode(0x8000_0099, b"{}", &mut bytes);
    ipc::encode(MODE_EVENT, br#"{"change":"default"}"#, &mut bytes);
    peer.0.write_all(&bytes).expect("a write to the service");
    shell.turn();
    let default = event(SwayEventKind::Mode, r#"{"change":"default"}"#);
    assert_eq!(*heard.borrow(), [default]);
  });
  assert_eq!(logs.matches("a bad event from Sway").count(), 2, "{logs}");
}

#[test]
fn a_panicking_listener_is_logged_and_dropped_and_the_rest_go_on() {
  let mut at = String::new();
  let logs = capture_logs(|| {
    let fake = FakeSway::new();
    let (mut shell, mut peer) = Harness::connect(&fake);
    let sway = shell.cx.sway();
    let runs = Rc::new(Cell::new(0));
    let counted = Rc::clone(&runs);
    at = format!("{}:{}", file!(), line!() + 1);
    sway.on_event(&[SwayEventKind::Mode], move |_| {
      counted.set(counted.get() + 1);
      panic!("a bad binding");
    });
    peer.read();
    // A handler may call the service back: nothing is borrowed.
    let calls = sway.clone();
    let (record, heard) = recorder();
    sway.on_event(&[SwayEventKind::Mode], move |event| {
      calls.dispatch("mode default");
      calls.outputs();
      record(event);
    });

    peer.send(MODE_EVENT, r#"{"change":"resize"}"#);
    peer.send(MODE_EVENT, r#"{"change":"default"}"#);
    shell.turn();
    assert_eq!(runs.get(), 1, "not called after its panic");
    assert_eq!(heard.borrow().len(), 2);
    assert_eq!(peer.read(), (RUN_COMMAND, "mode default".to_owned()));
    assert_eq!(peer.read(), (SUBSCRIBE, r#"["output"]"#.to_owned()));
    peer.read();
    assert_eq!(peer.read(), (RUN_COMMAND, "mode default".to_owned()));

    // The workspaces go on too.
    peer.send_event(&mut shell);
    assert_eq!(peer.read(), (GET_OUTPUTS, String::new()));
    peer.send(GET_WORKSPACES, &two().0);
    shell.turn();
    assert_eq!(sway.workspaces().get_untracked(), two().1);
  });
  let logged = format!("Sway on_event (called at {at}");
  assert_eq!(logs.matches(&logged).count(), 1, "{logs}");
  assert!(logs.contains("panicked: a bad binding"), "{logs}");
}

#[test]
fn a_listener_goes_with_its_context_and_its_kind_with_the_next_event() {
  let runtime = Runtime::current();
  let fake = FakeSway::new();
  let (mut shell, mut peer) = Harness::connect(&fake);
  // An event never asked for, as from a compositor that sends them anyway,
  // brings no new connection: there is nothing to drop.
  peer.send(0x8000_0007, r#"{"first":true,"payload":""}"#);
  shell.turn();
  peer.read_nothing();
  assert!(fake.try_accept().is_none());

  let nodes = runtime.node_count();
  let output = shell.cx.child();
  let (handler, heard) = recorder();
  output.sway().on_event(&[SwayEventKind::Window], handler);
  assert_eq!(peer.read(), (SUBSCRIBE, r#"["window"]"#.to_owned()));
  peer.send(SUBSCRIBE, r#"{"success":true}"#);
  output.owner().dispose();
  assert_eq!(runtime.node_count(), nodes);
  peer.read_nothing();

  // Sway can't unsubscribe, so the next window event, which no handler
  // wants, brings a new connection without it at once.
  peer.send(WINDOW_EVENT, r#"{"change":"focus"}"#);
  shell.turn();
  peer.read_end();
  let mut peer = fake.accept();
  peer.read_opening();
  assert!(heard.borrow().is_empty());
  assert_eq!(
    shell.cx.sway().workspaces().get_untracked(),
    one().1,
    "the list stays"
  );
  peer.send(SUBSCRIBE, r#"{"success":true}"#);
  peer.send(GET_WORKSPACES, &one().0);
  shell.turn();
  peer.read_nothing();
}

/// Sway drops a client that hangs up with requests unread, so a
/// reconnect for a kind nobody wants waits until Sway answered all.
#[test]
fn a_reconnect_waits_until_sway_answered_everything_sent() {
  let fake = FakeSway::new();
  let (mut shell, mut peer) = Harness::connect(&fake);
  let child = shell.cx.child();
  let (handler, _heard) = recorder();
  child.sway().on_event(&[SwayEventKind::Mode], handler);
  assert_eq!(peer.read(), (SUBSCRIBE, r#"["mode"]"#.to_owned()));
  peer.send(SUBSCRIBE, r#"{"success":true}"#);
  child.owner().dispose();
  shell.cx.sway().dispatch("workspace 2");
  assert_eq!(peer.read().0, RUN_COMMAND);
  // A mode event nobody wants now, before Sway answered the command.
  peer.send(MODE_EVENT, "{}");
  shell.turn();
  assert!(fake.try_accept().is_none(), "no reconnect before the reply");
  peer.send(RUN_COMMAND, r#"[{"success":true}]"#);
  shell.turn();
  let mut new = fake.accept();
  assert_eq!(new.read(), (SUBSCRIBE, r#"["workspace"]"#.to_owned()));
}

#[test]
fn on_event_on_a_context_that_went_is_logged_and_does_nothing() {
  let fake = FakeSway::new();
  let (shell, mut peer) = Harness::connect(&fake);
  let child = shell.cx.child();
  let stale = child.sway();
  child.owner().dispose();
  let (handler, heard) = recorder();
  let logs = capture_logs(|| stale.on_event(&[SwayEventKind::Mode], handler));
  assert!(logs.contains("context that is gone"), "{logs}");
  peer.read_nothing();
  assert_eq!(Rc::strong_count(&heard), 1, "the handler wasn't kept");
}

#[test]
fn a_handler_that_takes_anothers_context_stops_it() {
  let fake = FakeSway::new();
  let (mut shell, mut peer) = Harness::connect(&fake);
  let other = shell.cx.child();
  let other_owner = other.owner();
  shell
    .cx
    .sway()
    .on_event(&[SwayEventKind::Mode], move |_| other_owner.dispose());
  let (handler, heard) = recorder();
  other.sway().on_event(&[SwayEventKind::Mode], handler);
  peer.read();
  peer.send(MODE_EVENT, "{}");
  shell.turn();
  assert!(heard.borrow().is_empty(), "called after its context went");
}

#[test]
fn a_new_connection_after_a_drop_keeps_the_handlers_kinds() {
  let fake = FakeSway::new();
  let (mut shell, mut peer) = Harness::connect(&fake);
  let (handler, heard) = recorder();
  shell.cx.sway().on_event(&[SwayEventKind::Mode], handler);
  assert_eq!(peer.read(), (SUBSCRIBE, r#"["mode"]"#.to_owned()));
  drop(peer);
  shell.turn();
  shell.run_for(RETRY + RETRY / 2);
  let mut peer = fake.accept();
  assert_eq!(
    peer.read(),
    (SUBSCRIBE, r#"["workspace","mode"]"#.to_owned())
  );
  peer.read();
  peer.send(MODE_EVENT, "{}");
  shell.turn();
  assert_eq!(heard.borrow().len(), 1);
}

/// A new connection for a kind nobody wants, with Sway gone meanwhile, is
/// logged once, as a lost connection.
#[test]
fn a_new_connection_sway_refuses_is_logged_once() {
  let logs = capture_logs(|| {
    let mut fake = FakeSway::new();
    let (mut shell, mut peer) = Harness::connect(&fake);
    let child = shell.cx.child();
    let (handler, _heard) = recorder();
    child.sway().on_event(&[SwayEventKind::Mode], handler);
    peer.read();
    peer.send(SUBSCRIBE, r#"{"success":true}"#);
    child.owner().dispose();
    fake.close();
    peer.send(MODE_EVENT, "{}");
    shell.turn();
  });
  assert!(logs.contains("lost the connection to Sway"), "{logs}");
  assert!(!logs.contains("shows no workspaces"), "{logs}");
}