crabidy/cbd-tui/tests/mpris_bus.rs

146 lines
5.1 KiB
Rust

//! The MPRIS player as the desktop sees it: over a real session bus, through
//! a real D-Bus client (architecture/mpris.md).
//!
//! The unit tests in `src/mpris.rs` cover the mapping decisions; this one
//! covers the parts only a bus can answer — that the name is claimed, that
//! the interfaces are served where the spec says, and that a method call
//! becomes a command for the server.
//!
//! Skipped when there is no session bus, which is the normal state of a CI
//! runner. To run it:
//!
//! ```sh
//! devenv shell -- dbus-run-session -- cargo test -p cbd-tui --test mpris_bus
//! ```
#![cfg(feature = "mpris")]
use std::time::Duration;
use cbd_tui::{app::MessageFromUi, mpris};
use crabidy_core::proto::crabidy::{
Album, InitResponse, PlayState, Queue, QueueModifiers, QueueTrack, Track, TrackPosition,
};
use mpris_server::zbus::{zvariant::OwnedValue, Connection, Proxy};
const OBJECT_PATH: &str = "/org/mpris/MediaPlayer2";
const PLAYER_INTERFACE: &str = "org.mpris.MediaPlayer2.Player";
const ROOT_INTERFACE: &str = "org.mpris.MediaPlayer2";
fn playing_state() -> InitResponse {
InitResponse {
queue: Some(Queue {
timestamp: 0,
current_position: 1,
tracks: vec![],
resolving: false,
}),
mods: Some(QueueModifiers {
shuffle: false,
repeat: false,
}),
queue_track: Some(QueueTrack {
queue_position: 1,
track: Some(Track {
path: "/fs/music/song.flac".to_string(),
artist: "the artist".to_string(),
title: "the song".to_string(),
duration: Some(240),
album: Some(Album {
title: "the album".to_string(),
release_date: None,
}),
is_skipped: false,
provider_item_id: String::new(),
is_captured: false,
}),
}),
play_state: PlayState::Playing.into(),
volume: 0.5,
mute: false,
position: Some(TrackPosition {
position: 12_000,
duration: 240_000,
}),
auth_enabled: false,
}
}
/// Waits for the published mirror to catch up with the feed: the state
/// travels a channel and a task, so a property read straight after a publish
/// may still see the previous value.
async fn await_property(proxy: &Proxy<'_>, name: &str, expected: &str) -> OwnedValue {
for _ in 0..100 {
let value: OwnedValue = proxy.get_property(name).await.expect("read property");
if format!("{value:?}").contains(expected) {
return value;
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
panic!("{name} never became {expected}");
}
#[tokio::test]
async fn the_desktop_sees_the_track_and_its_keys_reach_the_server() {
if std::env::var_os("DBUS_SESSION_BUS_ADDRESS").is_none() {
eprintln!("no session bus; skipping (see this file's docs)");
return;
}
let (commands_tx, commands) = flume::unbounded();
let feed = mpris::start(commands_tx)
.await
.expect("a session bus is available, so the player must register");
feed.publish_init(&playing_state());
let connection = Connection::session().await.expect("connect to the bus");
let bus_name = format!(
"org.mpris.MediaPlayer2.crabidy.instance{}",
std::process::id()
);
let player = Proxy::new(&connection, bus_name.clone(), OBJECT_PATH, PLAYER_INTERFACE)
.await
.expect("the player interface is served where the spec says");
let root = Proxy::new(&connection, bus_name, OBJECT_PATH, ROOT_INTERFACE)
.await
.expect("the root interface too");
// What a status bar reads.
let identity: String = root.get_property("Identity").await.expect("Identity");
assert_eq!(identity, "crabidy");
await_property(&player, "PlaybackStatus", "Playing").await;
let metadata = await_property(&player, "Metadata", "the song").await;
let metadata = format!("{metadata:?}");
assert!(metadata.contains("the artist"), "{metadata}");
assert!(metadata.contains("the album"), "{metadata}");
assert!(
metadata.contains("/org/crabidy/queue/1"),
"the trackid is the queue position: {metadata}"
);
assert!(
!metadata.contains("/fs/music/song.flac"),
"no library path and no URL may reach the bus: {metadata}"
);
// What a media key does. Pause, because it is the mapping with a
// condition on it: the server only has a toggle.
player
.call::<_, _, ()>("Pause", &())
.await
.expect("Pause is callable");
assert!(
matches!(
commands.recv_timeout(Duration::from_secs(1)),
Ok(MessageFromUi::TogglePlay)
),
"the pause key must reach the server as a playback command"
);
// And a control the desktop is told it does not have.
let can_quit: bool = root.get_property("CanQuit").await.expect("CanQuit");
assert!(!can_quit);
assert!(
root.call::<_, _, ()>("Quit", &()).await.is_err(),
"Quit must be refused, not close the user's terminal"
);
}