Krolyxon
Commit 1dcac38

rmptuitestsmpd_client.rs

164 lines · 5.9 KB · Rust

1//! Talks to a scripted fake MPD server over real TCP sockets.
2
3use std::time::Duration;
4
5use rmptui::mpd::{
6 self, Address, Command, Connection, MpdError, MpdEvent, Query, Reply, Request, Subsystem,
7};
8use tokio::{
9 io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
10 net::TcpListener,
11 sync::mpsc,
12 time::timeout,
13};
14
15/// Serves one connection: greets, then answers each request line with the
16/// response produced by `respond`. Received lines are forwarded to `seen`.
17async fn fake_server<F>(respond: F) -> (Address, mpsc::UnboundedReceiver<String>)
18where
19 F: Fn(&str) -> String + Send + Sync + Clone + 'static,
20{
21 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
22 let address = Address::Tcp(listener.local_addr().unwrap().to_string());
23 let (seen_tx, seen_rx) = mpsc::unbounded_channel();
24 tokio::spawn(async move {
25 loop {
26 let Ok((socket, _)) = listener.accept().await else {
27 return;
28 };
29 let respond = respond.clone();
30 let seen = seen_tx.clone();
31 tokio::spawn(async move {
32 let (read, mut write) = socket.into_split();
33 write.write_all(b"OK MPD 0.24.0\n").await.unwrap();
34 let mut lines = BufReader::new(read).lines();
35 let mut list: Option<Vec<String>> = None;
36 while let Ok(Some(line)) = lines.next_line().await {
37 let _ = seen.send(line.clone());
38 let response = match (line.as_str(), list.as_mut()) {
39 ("command_list_ok_begin", _) => {
40 list = Some(Vec::new());
41 continue;
42 }
43 ("command_list_end", Some(commands)) => {
44 let mut out = String::new();
45 for command in commands.iter() {
46 let body = respond(command);
47 out.push_str(body.strip_suffix("OK\n").unwrap_or(&body));
48 out.push_str("list_OK\n");
49 }
50 list = None;
51 out + "OK\n"
52 }
53 (_, Some(commands)) => {
54 commands.push(line);
55 continue;
56 }
57 // Block forever on idle, like a quiet server.
58 ("idle", None) => std::future::pending().await,
59 (_, None) => respond(&line),
60 };
61 if write.write_all(response.as_bytes()).await.is_err() {
62 return;
63 }
64 }
65 });
66 }
67 });
68 (address, seen_rx)
69}
70
71fn responder(line: &str) -> String {
72 match line {
73 "status" => {
74 "volume: 50\nstate: play\nplaylist: 3\nplaylistlength: 2\nsongid: 1\nOK\n".into()
75 }
76 "currentsong" => "file: a.flac\nTitle: A\nId: 1\nPos: 0\nOK\n".into(),
77 r#"password "secret""# => "OK\n".into(),
78 r#"addid "x.mp3""# => "Id: 42\nOK\n".into(),
79 r#"play "nope""# => "ACK [2@0] {play} Bad song index\n".into(),
80 _ => "OK\n".into(),
81 }
82}
83
84#[tokio::test]
85async fn executes_commands_and_lists() {
86 let (address, mut seen) = fake_server(responder).await;
87 let mut conn = Connection::connect(&address, Some("secret")).await.unwrap();
88 assert_eq!(conn.version(), "0.24.0");
89 assert_eq!(seen.recv().await.unwrap(), r#"password "secret""#);
90
91 let pairs = conn.execute(&Command::new("status")).await.unwrap();
92 assert!(pairs.contains(&("state".into(), "play".into())));
93
94 let responses = conn
95 .execute_list(&[Command::new("status"), Command::new("currentsong")])
96 .await
97 .unwrap();
98 assert_eq!(responses.len(), 2);
99 assert_eq!(responses[1][0], ("file".into(), "a.flac".into()));
100
101 let error = conn
102 .execute(&Command::new("play").arg("nope"))
103 .await
104 .unwrap_err();
105 assert!(matches!(error, MpdError::Ack(ack) if ack.code == 2 && ack.command == "play"));
106 // The connection stays usable after an ACK.
107 conn.execute(&Command::new("ping")).await.unwrap();
108}
109
110#[tokio::test]
111async fn worker_answers_queries_and_runs_compound_requests() {
112 let (address, mut seen) = fake_server(responder).await;
113 let (tx, mut events) = mpsc::unbounded_channel::<MpdEvent>();
114 let handle = mpd::spawn(address, None, tx);
115
116 assert!(matches!(
117 next(&mut events).await,
118 MpdEvent::Connected { .. }
119 ));
120
121 handle.query(Query::Status);
122 match next(&mut events).await {
123 MpdEvent::Reply(Reply::Status { status, current }) => {
124 assert_eq!(status.volume, Some(50));
125 assert_eq!(current.unwrap().title(), Some("A"));
126 }
127 other => panic!("unexpected event {other:?}"),
128 }
129
130 handle.request(Request::AddAndPlay(vec!["x.mp3".into(), "y.mp3".into()]));
131 // Drain the requests seen by the server until the playid arrives.
132 let mut lines = Vec::new();
133 while !lines.iter().any(|l: &String| l.starts_with("playid")) {
134 lines.push(
135 timeout(Duration::from_secs(5), seen.recv())
136 .await
137 .unwrap()
138 .unwrap(),
139 );
140 }
141 assert!(lines.contains(&r#"addid "x.mp3""#.to_owned()));
142 assert!(lines.contains(&r#"add "y.mp3""#.to_owned()));
143 assert!(lines.contains(&r#"playid "42""#.to_owned()));
144
145 handle.exec(Command::new("play").arg("nope"));
146 assert!(
147 matches!(next(&mut events).await, MpdEvent::Error(msg) if msg.contains("Bad song index"))
148 );
149}
150
151async fn next(events: &mut mpsc::UnboundedReceiver<MpdEvent>) -> MpdEvent {
152 timeout(Duration::from_secs(5), events.recv())
153 .await
154 .unwrap()
155 .unwrap()
156}
157
158#[test]
159fn subsystem_names() {
160 assert_eq!(
161 "stored_playlist".parse::<Subsystem>().unwrap(),
162 Subsystem::StoredPlaylist
163 );
164}