Compare commits

...

3 Commits

10 changed files with 371 additions and 256 deletions

View File

@ -12,8 +12,8 @@ async fn main() -> Result<(), MpvError> {
let playlist = mpv.get_playlist().await?; let playlist = mpv.get_playlist().await?;
println!("playlist: {:?}", playlist); println!("playlist: {:?}", playlist);
let playback_time: f64 = mpv.get_property("playback-time").await?; let playback_time: Option<f64> = mpv.get_property("playback-time").await?;
println!("playback-time: {}", playback_time); println!("playback-time: {:?}", playback_time);
Ok(()) Ok(())
} }

View File

@ -30,6 +30,7 @@
]) ])
pkgs.mpv pkgs.mpv
pkgs.grcov pkgs.grcov
pkgs.cargo-nextest
]; ];
RUST_SRC_PATH = "${toolchain.rust-src}/lib/rustlib/src/rust/"; RUST_SRC_PATH = "${toolchain.rust-src}/lib/rustlib/src/rust/";
}); });

View File

@ -134,18 +134,22 @@ impl IntoRawCommandPart for SeekOptions {
pub trait GetPropertyTypeHandler: Sized { pub trait GetPropertyTypeHandler: Sized {
// TODO: fix this // TODO: fix this
#[allow(async_fn_in_trait)] #[allow(async_fn_in_trait)]
async fn get_property_generic(instance: &Mpv, property: &str) -> Result<Self, MpvError>; async fn get_property_generic(instance: &Mpv, property: &str)
-> Result<Option<Self>, MpvError>;
} }
impl<T> GetPropertyTypeHandler for T impl<T> GetPropertyTypeHandler for T
where where
T: TypeHandler, T: TypeHandler,
{ {
async fn get_property_generic(instance: &Mpv, property: &str) -> Result<T, MpvError> { async fn get_property_generic(instance: &Mpv, property: &str) -> Result<Option<T>, MpvError> {
instance instance
.get_property_value(property) .get_property_value(property)
.await .await
.and_then(T::get_value) .and_then(|value| match value {
Some(v) => T::get_value(v).map(|v| Some(v)),
None => Ok(None),
})
} }
} }
@ -172,7 +176,7 @@ where
instance instance
.command_sender .command_sender
.send(( .send((
MpvIpcCommand::SetProperty(property.to_owned(), value), MpvIpcCommand::SetProperty(property.to_owned(), value.to_owned()),
res_tx, res_tx,
)) ))
.await .await
@ -277,7 +281,7 @@ impl Mpv {
command: &str, command: &str,
args: &[&str], args: &[&str],
) -> Result<Option<Value>, MpvError> { ) -> Result<Option<Value>, MpvError> {
let command = Vec::from( let command_vec = Vec::from(
[command] [command]
.iter() .iter()
.chain(args.iter()) .chain(args.iter())
@ -287,7 +291,7 @@ impl Mpv {
); );
let (res_tx, res_rx) = oneshot::channel(); let (res_tx, res_rx) = oneshot::channel();
self.command_sender self.command_sender
.send((MpvIpcCommand::Command(command), res_tx)) .send((MpvIpcCommand::Command(command_vec.clone()), res_tx))
.await .await
.map_err(|err| MpvError::InternalConnectionError(err.to_string()))?; .map_err(|err| MpvError::InternalConnectionError(err.to_string()))?;
@ -462,7 +466,10 @@ impl Mpv {
pub async fn get_property<T: GetPropertyTypeHandler>( pub async fn get_property<T: GetPropertyTypeHandler>(
&self, &self,
property: &str, property: &str,
) -> Result<T, MpvError> { ) -> Result<Option<T>, MpvError> {
// NOTE: `get_property_generic` is implemented in terms of `get_property_value`,
// which wraps errors in `MpvError::GetPropertyFailure`. That is why it is
// not needed here.
T::get_property_generic(self, property).await T::get_property_generic(self, property).await
} }
@ -487,7 +494,7 @@ impl Mpv {
/// Ok(()) /// Ok(())
/// } /// }
/// ``` /// ```
pub async fn get_property_value(&self, property: &str) -> Result<Value, MpvError> { pub async fn get_property_value(&self, property: &str) -> Result<Option<Value>, MpvError> {
let (res_tx, res_rx) = oneshot::channel(); let (res_tx, res_rx) = oneshot::channel();
self.command_sender self.command_sender
.send((MpvIpcCommand::GetProperty(property.to_owned()), res_tx)) .send((MpvIpcCommand::GetProperty(property.to_owned()), res_tx))
@ -495,9 +502,7 @@ impl Mpv {
.map_err(|err| MpvError::InternalConnectionError(err.to_string()))?; .map_err(|err| MpvError::InternalConnectionError(err.to_string()))?;
match res_rx.await { match res_rx.await {
Ok(MpvIpcResponse(response)) => { Ok(MpvIpcResponse(response)) => response,
response.and_then(|value| value.ok_or(MpvError::MissingMpvData))
}
Err(err) => Err(MpvError::InternalConnectionError(err.to_string())), Err(err) => Err(MpvError::InternalConnectionError(err.to_string())),
} }
} }
@ -526,11 +531,10 @@ impl Mpv {
/// Ok(()) /// Ok(())
/// } /// }
/// ``` /// ```
pub async fn set_property<T: SetPropertyTypeHandler<T>>( pub async fn set_property<T>(&self, property: &str, value: T) -> Result<(), MpvError>
&self, where
property: &str, T: SetPropertyTypeHandler<T> + Clone + fmt::Debug,
value: T, {
) -> Result<(), MpvError> { T::set_property_generic(self, property, value.clone()).await
T::set_property_generic(self, property, value).await
} }
} }

View File

@ -46,3 +46,48 @@ pub enum MpvError {
#[error("Unknown error: {0}")] #[error("Unknown error: {0}")]
Other(String), Other(String),
} }
impl PartialEq for MpvError {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::MpvError(l0), Self::MpvError(r0)) => l0 == r0,
(Self::MpvSocketConnectionError(l0), Self::MpvSocketConnectionError(r0)) => l0 == r0,
(Self::InternalConnectionError(l0), Self::InternalConnectionError(r0)) => l0 == r0,
(Self::JsonParseError(l0), Self::JsonParseError(r0)) => {
l0.to_string() == r0.to_string()
}
(
Self::ValueContainsUnexpectedType {
expected_type: l_expected_type,
received: l_received,
},
Self::ValueContainsUnexpectedType {
expected_type: r_expected_type,
received: r_received,
},
) => l_expected_type == r_expected_type && l_received == r_received,
(
Self::DataContainsUnexpectedType {
expected_type: l_expected_type,
received: l_received,
},
Self::DataContainsUnexpectedType {
expected_type: r_expected_type,
received: r_received,
},
) => l_expected_type == r_expected_type && l_received == r_received,
(
Self::MissingKeyInObject {
key: l_key,
map: l_map,
},
Self::MissingKeyInObject {
key: r_key,
map: r_map,
},
) => l_key == r_key && l_map == r_map,
_ => core::mem::discriminant(self) == core::mem::discriminant(other),
}
}
}

View File

@ -1,9 +1,13 @@
//! JSON parsing logic for properties returned in [`Event::PropertyChange`] //! JSON parsing logic for properties returned either by
//! [`Mpv::get_property`] or [`Event::PropertyChange`]
//! //!
//! This module is used to parse the json data from the `data` field of the //! This module is used to parse the json data from the `data` field of
//! [`Event::PropertyChange`] variant. Mpv has about 1000 different properties //! known properties. Mpv has about 1000 different properties
//! as of `v0.38.0`, so this module will only implement the most common ones. //! as of `v0.38.0`, so this module will only implement the most common ones.
// TODO: reuse this logic for providing a more typesafe response API to `Mpv::get_property()`
// Although this data is currently of type `Option<`
use std::collections::HashMap; use std::collections::HashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -129,14 +133,19 @@ pub fn parse_event_property(event: Event) -> Result<(usize, Property), MpvError>
}; };
Ok((id, Property::Metadata(metadata))) Ok((id, Property::Metadata(metadata)))
} }
// "playlist" => { "playlist" => {
// let playlist = match data { let playlist = match data {
// MpvDataType::Array(a) => json_array_to_playlist(&a), Some(MpvDataType::Array(a)) => mpv_array_to_playlist(&a)?,
// MpvDataType::Null => Vec::new(), None => Vec::new(),
// _ => return Err(Error(ErrorCode::ValueDoesNotContainPlaylist)), Some(data) => {
// }; return Err(MpvError::DataContainsUnexpectedType {
// Ok((id, Property::Playlist(playlist))) expected_type: "Array".to_owned(),
// } received: data,
})
}
};
Ok((id, Property::Playlist(playlist)))
}
"playlist-pos" => { "playlist-pos" => {
let playlist_pos = match data { let playlist_pos = match data {
Some(MpvDataType::Usize(u)) => Some(u), Some(MpvDataType::Usize(u)) => Some(u),
@ -246,3 +255,59 @@ pub fn parse_event_property(event: Event) -> Result<(usize, Property), MpvError>
_ => Ok((id, Property::Unknown { name, data })), _ => Ok((id, Property::Unknown { name, data })),
} }
} }
fn mpv_data_to_playlist_entry(
map: &HashMap<String, MpvDataType>,
) -> Result<PlaylistEntry, MpvError> {
let filename = match map.get("filename") {
Some(MpvDataType::String(s)) => s.to_string(),
Some(data) => {
return Err(MpvError::DataContainsUnexpectedType {
expected_type: "String".to_owned(),
received: data.clone(),
})
}
None => return Err(MpvError::MissingMpvData),
};
let title = match map.get("title") {
Some(MpvDataType::String(s)) => s.to_string(),
Some(data) => {
return Err(MpvError::DataContainsUnexpectedType {
expected_type: "String".to_owned(),
received: data.clone(),
})
}
None => return Err(MpvError::MissingMpvData),
};
let current = match map.get("current") {
Some(MpvDataType::Bool(b)) => *b,
Some(data) => {
return Err(MpvError::DataContainsUnexpectedType {
expected_type: "bool".to_owned(),
received: data.clone(),
})
}
None => return Err(MpvError::MissingMpvData),
};
Ok(PlaylistEntry {
id: 0,
filename,
title,
current,
})
}
fn mpv_array_to_playlist(array: &[MpvDataType]) -> Result<Vec<PlaylistEntry>, MpvError> {
array
.iter()
.map(|value| match value {
MpvDataType::HashMap(map) => mpv_data_to_playlist_entry(map),
_ => Err(MpvError::DataContainsUnexpectedType {
expected_type: "HashMap".to_owned(),
received: value.clone(),
}),
})
.enumerate()
.map(|(id, entry)| entry.map(|entry| PlaylistEntry { id, ..entry }))
.collect()
}

View File

@ -141,12 +141,15 @@ pub trait MpvExt {
impl MpvExt for Mpv { impl MpvExt for Mpv {
async fn get_metadata(&self) -> Result<HashMap<String, MpvDataType>, MpvError> { async fn get_metadata(&self) -> Result<HashMap<String, MpvDataType>, MpvError> {
self.get_property("metadata").await self.get_property("metadata")
.await?
.ok_or(MpvError::MissingMpvData)
} }
async fn get_playlist(&self) -> Result<Playlist, MpvError> { async fn get_playlist(&self) -> Result<Playlist, MpvError> {
self.get_property::<Vec<PlaylistEntry>>("playlist") self.get_property::<Vec<PlaylistEntry>>("playlist")
.await .await?
.ok_or(MpvError::MissingMpvData)
.map(Playlist) .map(Playlist)
} }
@ -174,15 +177,15 @@ impl MpvExt for Mpv {
let enabled = match option { let enabled = match option {
Switch::On => "yes", Switch::On => "yes",
Switch::Off => "no", Switch::Off => "no",
Switch::Toggle => { Switch::Toggle => self
self.get_property::<String>("pause") .get_property::<String>("pause")
.await .await?
.ok_or(MpvError::MissingMpvData)
.map(|s| match s.as_str() { .map(|s| match s.as_str() {
"yes" => "no", "yes" => "no",
"no" => "yes", "no" => "yes",
_ => "no", _ => "no",
})? })?,
}
}; };
self.set_property("pause", enabled).await self.set_property("pause", enabled).await
} }
@ -238,17 +241,17 @@ impl MpvExt for Mpv {
} }
async fn playlist_play_next(&self, id: usize) -> Result<(), MpvError> { async fn playlist_play_next(&self, id: usize) -> Result<(), MpvError> {
match self.get_property::<usize>("playlist-pos").await { let current_id = self
Ok(current_id) => { .get_property::<usize>("playlist-pos")
.await?
.ok_or(MpvError::MissingMpvData)?;
self.run_command(MpvCommand::PlaylistMove { self.run_command(MpvCommand::PlaylistMove {
from: id, from: id,
to: current_id + 1, to: current_id + 1,
}) })
.await .await
} }
Err(msg) => Err(msg),
}
}
async fn playlist_remove_id(&self, id: usize) -> Result<(), MpvError> { async fn playlist_remove_id(&self, id: usize) -> Result<(), MpvError> {
self.run_command(MpvCommand::PlaylistRemove(id)).await self.run_command(MpvCommand::PlaylistRemove(id)).await
@ -266,15 +269,15 @@ impl MpvExt for Mpv {
let enabled = match option { let enabled = match option {
Switch::On => "inf", Switch::On => "inf",
Switch::Off => "no", Switch::Off => "no",
Switch::Toggle => { Switch::Toggle => self
self.get_property::<String>("loop-file") .get_property::<String>("loop-file")
.await .await?
.ok_or(MpvError::MissingMpvData)
.map(|s| match s.as_str() { .map(|s| match s.as_str() {
"inf" => "no", "inf" => "no",
"no" => "inf", "no" => "inf",
_ => "no", _ => "no",
})? })?,
}
}; };
self.set_property("loop-file", enabled).await self.set_property("loop-file", enabled).await
} }
@ -283,32 +286,32 @@ impl MpvExt for Mpv {
let enabled = match option { let enabled = match option {
Switch::On => "inf", Switch::On => "inf",
Switch::Off => "no", Switch::Off => "no",
Switch::Toggle => { Switch::Toggle => self
self.get_property::<String>("loop-playlist") .get_property::<String>("loop-playlist")
.await .await?
.ok_or(MpvError::MissingMpvData)
.map(|s| match s.as_str() { .map(|s| match s.as_str() {
"inf" => "no", "inf" => "no",
"no" => "inf", "no" => "inf",
_ => "no", _ => "no",
})? })?,
}
}; };
self.set_property("loo-playlist", enabled).await self.set_property("loop-playlist", enabled).await
} }
async fn set_mute(&self, option: Switch) -> Result<(), MpvError> { async fn set_mute(&self, option: Switch) -> Result<(), MpvError> {
let enabled = match option { let enabled = match option {
Switch::On => "yes", Switch::On => "yes",
Switch::Off => "no", Switch::Off => "no",
Switch::Toggle => { Switch::Toggle => self
self.get_property::<String>("mute") .get_property::<String>("mute")
.await .await?
.ok_or(MpvError::MissingMpvData)
.map(|s| match s.as_str() { .map(|s| match s.as_str() {
"yes" => "no", "yes" => "no",
"no" => "yes", "no" => "yes",
_ => "no", _ => "no",
})? })?,
}
}; };
self.set_property("mute", enabled).await self.set_property("mute", enabled).await
} }
@ -318,19 +321,15 @@ impl MpvExt for Mpv {
input_speed: f64, input_speed: f64,
option: NumberChangeOptions, option: NumberChangeOptions,
) -> Result<(), MpvError> { ) -> Result<(), MpvError> {
match self.get_property::<f64>("speed").await { let speed = self
Ok(speed) => match option { .get_property::<f64>("speed")
NumberChangeOptions::Increase => { .await?
self.set_property("speed", speed + input_speed).await .ok_or(MpvError::MissingMpvData)?;
}
NumberChangeOptions::Decrease => {
self.set_property("speed", speed - input_speed).await
}
match option {
NumberChangeOptions::Increase => self.set_property("speed", speed + input_speed).await,
NumberChangeOptions::Decrease => self.set_property("speed", speed - input_speed).await,
NumberChangeOptions::Absolute => self.set_property("speed", input_speed).await, NumberChangeOptions::Absolute => self.set_property("speed", input_speed).await,
},
Err(msg) => Err(msg),
} }
} }
@ -339,19 +338,19 @@ impl MpvExt for Mpv {
input_volume: f64, input_volume: f64,
option: NumberChangeOptions, option: NumberChangeOptions,
) -> Result<(), MpvError> { ) -> Result<(), MpvError> {
match self.get_property::<f64>("volume").await { let volume = self
Ok(volume) => match option { .get_property::<f64>("volume")
.await?
.ok_or(MpvError::MissingMpvData)?;
match option {
NumberChangeOptions::Increase => { NumberChangeOptions::Increase => {
self.set_property("volume", volume + input_volume).await self.set_property("volume", volume + input_volume).await
} }
NumberChangeOptions::Decrease => { NumberChangeOptions::Decrease => {
self.set_property("volume", volume - input_volume).await self.set_property("volume", volume - input_volume).await
} }
NumberChangeOptions::Absolute => self.set_property("volume", input_volume).await, NumberChangeOptions::Absolute => self.set_property("volume", input_volume).await,
},
Err(msg) => Err(msg),
} }
} }

View File

@ -109,7 +109,7 @@ impl TypeHandler for Vec<PlaylistEntry> {
expected_type: "Array<Value>".to_string(), expected_type: "Array<Value>".to_string(),
received: value.clone(), received: value.clone(),
}) })
.map(|array| json_array_to_playlist(array)) .and_then(|array| json_array_to_playlist(array))
} }
fn as_string(&self) -> String { fn as_string(&self) -> String {
@ -155,29 +155,65 @@ pub(crate) fn json_array_to_vec(array: &[Value]) -> Result<Vec<MpvDataType>, Mpv
array.iter().map(json_to_value).collect() array.iter().map(json_to_value).collect()
} }
pub(crate) fn json_array_to_playlist(array: &[Value]) -> Vec<PlaylistEntry> { fn json_map_to_playlist_entry(
let mut output: Vec<PlaylistEntry> = Vec::new(); map: &serde_json::map::Map<String, Value>,
for (id, entry) in array.iter().enumerate() { ) -> Result<PlaylistEntry, MpvError> {
let mut filename: String = String::new(); let filename = match map.get("filename") {
let mut title: String = String::new(); Some(Value::String(s)) => s.to_string(),
let mut current: bool = false; Some(data) => {
if let Value::String(ref f) = entry["filename"] { return Err(MpvError::ValueContainsUnexpectedType {
filename = f.to_string(); expected_type: "String".to_owned(),
received: data.clone(),
})
} }
if let Value::String(ref t) = entry["title"] { None => return Err(MpvError::MissingMpvData),
title = t.to_string(); };
let title = match map.get("title") {
Some(Value::String(s)) => s.to_string(),
Some(data) => {
return Err(MpvError::ValueContainsUnexpectedType {
expected_type: "String".to_owned(),
received: data.clone(),
})
} }
if let Value::Bool(ref b) = entry["current"] { None => return Err(MpvError::MissingMpvData),
current = *b; };
let current = match map.get("current") {
Some(Value::Bool(b)) => *b,
Some(data) => {
return Err(MpvError::ValueContainsUnexpectedType {
expected_type: "bool".to_owned(),
received: data.clone(),
})
} }
output.push(PlaylistEntry { None => return Err(MpvError::MissingMpvData),
id, };
Ok(PlaylistEntry {
id: 0,
filename, filename,
title, title,
current, current,
}); })
} }
output
pub(crate) fn json_array_to_playlist(array: &[Value]) -> Result<Vec<PlaylistEntry>, MpvError> {
array
.iter()
.map(|entry| match entry {
Value::Object(map) => json_map_to_playlist_entry(map),
data => Err(MpvError::ValueContainsUnexpectedType {
expected_type: "Map<String, Value>".to_owned(),
received: data.clone(),
}),
})
.enumerate()
.map(|(id, entry)| {
entry.map(|mut entry| {
entry.id = id;
entry
})
})
.collect()
} }
#[cfg(test)] #[cfg(test)]
@ -277,7 +313,7 @@ mod test {
} }
#[test] #[test]
fn test_json_array_to_playlist() { fn test_json_array_to_playlist() -> Result<(), MpvError> {
let json = json!([ let json = json!([
{ {
"filename": "file1", "filename": "file1",
@ -306,6 +342,8 @@ mod test {
}, },
]; ];
assert_eq!(json_array_to_playlist(json.as_array().unwrap()), expected); assert_eq!(json_array_to_playlist(json.as_array().unwrap())?, expected);
Ok(())
} }
} }

View File

@ -1,26 +1,30 @@
use mpvipc::MpvExt; use mpvipc::{MpvError, MpvExt};
use super::*; use super::*;
#[tokio::test] #[tokio::test]
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
async fn test_get_mpv_version() { async fn test_get_mpv_version() -> Result<(), MpvError> {
let (mut proc, mpv) = spawn_headless_mpv().await.unwrap(); let (mut proc, mpv) = spawn_headless_mpv().await.unwrap();
let version: String = mpv.get_property("mpv-version").await.unwrap(); let version: String = mpv.get_property("mpv-version").await?.unwrap();
assert!(version.starts_with("mpv")); assert!(version.starts_with("mpv"));
mpv.kill().await.unwrap(); mpv.kill().await.unwrap();
proc.kill().await.unwrap(); proc.kill().await.unwrap();
Ok(())
} }
#[tokio::test] #[tokio::test]
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
async fn test_set_property() { async fn test_set_property() -> Result<(), MpvError> {
let (mut proc, mpv) = spawn_headless_mpv().await.unwrap(); let (mut proc, mpv) = spawn_headless_mpv().await.unwrap();
mpv.set_property("pause", true).await.unwrap(); mpv.set_property("pause", true).await.unwrap();
let paused: bool = mpv.get_property("pause").await.unwrap(); let paused: bool = mpv.get_property("pause").await?.unwrap();
assert!(paused); assert!(paused);
mpv.kill().await.unwrap(); mpv.kill().await.unwrap();
proc.kill().await.unwrap(); proc.kill().await.unwrap();
Ok(())
} }

View File

@ -22,73 +22,77 @@ fn test_socket(answers: Vec<String>) -> (UnixStream, JoinHandle<Result<(), Lines
} }
#[test(tokio::test)] #[test(tokio::test)]
async fn test_get_property_successful() { async fn test_get_property_successful() -> Result<(), MpvError> {
let (server, join_handle) = test_socket(vec![ let (server, join_handle) = test_socket(vec![
json!({ "data": 100.0, "request_id": 0, "error": "success" }).to_string(), json!({ "data": 100.0, "request_id": 0, "error": "success" }).to_string(),
]); ]);
let mpv = Mpv::connect_socket(server).await.unwrap(); let mpv = Mpv::connect_socket(server).await?;
let volume: f64 = mpv.get_property("volume").await.unwrap(); let volume: Option<f64> = mpv.get_property("volume").await?;
assert_eq!(volume, 100.0); assert_eq!(volume, Some(100.0));
join_handle.await.unwrap().unwrap(); join_handle.await.unwrap().unwrap();
Ok(())
} }
#[test(tokio::test)] #[test(tokio::test)]
async fn test_get_property_broken_pipe() { async fn test_get_property_broken_pipe() -> Result<(), MpvError> {
let (server, join_handle) = test_socket(vec![]); let (server, join_handle) = test_socket(vec![]);
let mpv = Mpv::connect_socket(server).await.unwrap(); let mpv = Mpv::connect_socket(server).await.unwrap();
let maybe_volume = mpv.get_property::<f64>("volume").await; let maybe_volume = mpv.get_property::<f64>("volume").await;
match maybe_volume { assert_eq!(
Err(MpvError::MpvSocketConnectionError(err)) => { maybe_volume,
assert_eq!(err.to_string(), "Broken pipe (os error 32)"); Err(MpvError::MpvSocketConnectionError(
} "Broken pipe (os error 32)".to_string()
_ => panic!("Unexpected result: {:?}", maybe_volume), ))
} );
join_handle.await.unwrap().unwrap(); join_handle.await.unwrap().unwrap();
Ok(())
} }
#[test(tokio::test)] #[test(tokio::test)]
async fn test_get_property_wrong_type() { async fn test_get_property_wrong_type() -> Result<(), MpvError> {
let (server, join_handle) = test_socket(vec![ let (server, join_handle) = test_socket(vec![
json!({ "data": 100.0, "request_id": 0, "error": "success" }).to_string(), json!({ "data": 100.0, "request_id": 0, "error": "success" }).to_string(),
]); ]);
let mpv = Mpv::connect_socket(server).await.unwrap(); let mpv = Mpv::connect_socket(server).await?;
let maybe_volume = mpv.get_property::<bool>("volume").await; let maybe_volume = mpv.get_property::<bool>("volume").await;
match maybe_volume { assert_eq!(
maybe_volume,
Err(MpvError::ValueContainsUnexpectedType { Err(MpvError::ValueContainsUnexpectedType {
expected_type, expected_type: "bool".to_string(),
received, received: json!(100.0)
}) => { })
assert_eq!(expected_type, "bool"); );
assert_eq!(received, json!(100.0));
}
_ => panic!("Unexpected result: {:?}", maybe_volume),
}
join_handle.await.unwrap().unwrap(); join_handle.await.unwrap().unwrap();
Ok(())
} }
#[test(tokio::test)] #[test(tokio::test)]
async fn test_get_property_error() { async fn test_get_property_error() -> Result<(), MpvError> {
let (server, join_handle) = test_socket(vec![ let (server, join_handle) = test_socket(vec![
json!({ "error": "property unavailable", "request_id": 0 }).to_string(), json!({ "error": "property unavailable", "request_id": 0 }).to_string(),
]); ]);
let mpv = Mpv::connect_socket(server).await.unwrap(); let mpv = Mpv::connect_socket(server).await?;
let maybe_volume = mpv.get_property::<f64>("volume").await; let maybe_volume = mpv.get_property::<f64>("volume").await;
match maybe_volume { assert_eq!(
Err(MpvError::MpvError(err)) => { maybe_volume,
assert_eq!(err, "property unavailable"); Err(MpvError::MpvError("property unavailable".to_string()))
} );
_ => panic!("Unexpected result: {:?}", maybe_volume),
}
join_handle.await.unwrap().unwrap(); join_handle.await.unwrap().unwrap();
Ok(())
} }
#[test(tokio::test)] #[test(tokio::test)]
@ -130,8 +134,8 @@ async fn test_get_property_simultaneous_requests() {
let mpv_clone_1 = mpv.clone(); let mpv_clone_1 = mpv.clone();
let mpv_poller_1 = tokio::spawn(async move { let mpv_poller_1 = tokio::spawn(async move {
loop { loop {
let volume: f64 = mpv_clone_1.get_property("volume").await.unwrap(); let volume: Option<f64> = mpv_clone_1.get_property("volume").await.unwrap();
assert_eq!(volume, 100.0); assert_eq!(volume, Some(100.0));
} }
}); });
@ -139,8 +143,8 @@ async fn test_get_property_simultaneous_requests() {
let mpv_poller_2 = tokio::spawn(async move { let mpv_poller_2 = tokio::spawn(async move {
loop { loop {
tokio::time::sleep(Duration::from_millis(1)).await; tokio::time::sleep(Duration::from_millis(1)).await;
let paused: bool = mpv_clone_2.get_property("pause").await.unwrap(); let paused: Option<bool> = mpv_clone_2.get_property("pause").await.unwrap();
assert!(paused); assert_eq!(paused, Some(true));
} }
}); });
@ -173,7 +177,7 @@ async fn test_get_property_simultaneous_requests() {
} }
#[test(tokio::test)] #[test(tokio::test)]
async fn test_get_playlist() { async fn test_get_playlist() -> Result<(), MpvError> {
let expected = Playlist(vec![ let expected = Playlist(vec![
PlaylistEntry { PlaylistEntry {
id: 0, id: 0,
@ -208,22 +212,26 @@ async fn test_get_playlist() {
}) })
.to_string()]); .to_string()]);
let mpv = Mpv::connect_socket(server).await.unwrap(); let mpv = Mpv::connect_socket(server).await?;
let playlist = mpv.get_playlist().await.unwrap(); let playlist = mpv.get_playlist().await?;
assert_eq!(playlist, expected); assert_eq!(playlist, expected);
join_handle.await.unwrap().unwrap(); join_handle.await.unwrap().unwrap();
Ok(())
} }
#[test(tokio::test)] #[test(tokio::test)]
async fn test_get_playlist_empty() { async fn test_get_playlist_empty() -> Result<(), MpvError> {
let (server, join_handle) = test_socket(vec![ let (server, join_handle) = test_socket(vec![
json!({ "data": [], "request_id": 0, "error": "success" }).to_string(), json!({ "data": [], "request_id": 0, "error": "success" }).to_string(),
]); ]);
let mpv = Mpv::connect_socket(server).await.unwrap(); let mpv = Mpv::connect_socket(server).await?;
let playlist = mpv.get_playlist().await.unwrap(); let playlist = mpv.get_playlist().await?;
assert_eq!(playlist, Playlist(vec![])); assert_eq!(playlist, Playlist(vec![]));
join_handle.await.unwrap().unwrap(); join_handle.await.unwrap().unwrap();
Ok(())
} }

View File

@ -1,7 +1,7 @@
use std::{panic, time::Duration}; use std::{panic, time::Duration};
use futures::{stream::FuturesUnordered, SinkExt, StreamExt}; use futures::{stream::FuturesUnordered, SinkExt, StreamExt};
use mpvipc::{Mpv, MpvError, MpvExt, Playlist, PlaylistEntry}; use mpvipc::{Mpv, MpvError};
use serde_json::{json, Value}; use serde_json::{json, Value};
use test_log::test; use test_log::test;
use tokio::{net::UnixStream, task::JoinHandle}; use tokio::{net::UnixStream, task::JoinHandle};
@ -22,69 +22,76 @@ fn test_socket(answers: Vec<String>) -> (UnixStream, JoinHandle<Result<(), Lines
} }
#[test(tokio::test)] #[test(tokio::test)]
async fn test_set_property_successful() { async fn test_set_property_successful() -> Result<(), MpvError> {
let (server, join_handle) = test_socket(vec![ let (server, join_handle) = test_socket(vec![
json!({ "data": null, "request_id": 0, "error": "success" }).to_string(), json!({ "data": null, "request_id": 0, "error": "success" }).to_string(),
]); ]);
let mpv = Mpv::connect_socket(server).await.unwrap(); let mpv = Mpv::connect_socket(server).await?;
let volume = mpv.set_property("volume", 64.0).await; mpv.set_property("volume", 64.0).await?;
assert!(volume.is_ok());
join_handle.await.unwrap().unwrap(); join_handle.await.unwrap().unwrap();
Ok(())
} }
#[test(tokio::test)] #[test(tokio::test)]
async fn test_set_property_broken_pipe() { async fn test_set_property_broken_pipe() -> Result<(), MpvError> {
let (server, join_handle) = test_socket(vec![]); let (server, join_handle) = test_socket(vec![]);
let mpv = Mpv::connect_socket(server).await.unwrap(); let mpv = Mpv::connect_socket(server).await?;
let maybe_set_volume = mpv.set_property("volume", 64.0).await; let maybe_set_volume = mpv.set_property("volume", 64.0).await;
match maybe_set_volume { assert_eq!(
Err(MpvError::MpvSocketConnectionError(err)) => { maybe_set_volume,
assert_eq!(err.to_string(), "Broken pipe (os error 32)"); Err(MpvError::MpvSocketConnectionError(
} "Broken pipe (os error 32)".to_string()
_ => panic!("Unexpected result: {:?}", maybe_set_volume), ))
} );
join_handle.await.unwrap().unwrap(); join_handle.await.unwrap().unwrap();
Ok(())
} }
#[test(tokio::test)] #[test(tokio::test)]
async fn test_set_property_wrong_type() { async fn test_set_property_wrong_type() -> Result<(), MpvError> {
let (server, join_handle) = test_socket(vec![ let (server, join_handle) = test_socket(vec![
json!({"request_id":0,"error":"unsupported format for accessing property"}).to_string(), json!({"request_id":0,"error":"unsupported format for accessing property"}).to_string(),
]); ]);
let mpv = Mpv::connect_socket(server).await.unwrap(); let mpv = Mpv::connect_socket(server).await?;
let maybe_volume = mpv.set_property::<bool>("volume", true).await; let maybe_volume = mpv.set_property::<bool>("volume", true).await;
match maybe_volume { assert_eq!(
Err(MpvError::MpvError(err)) => { maybe_volume,
assert_eq!(err, "unsupported format for accessing property"); Err(MpvError::MpvError(
} "unsupported format for accessing property".to_string()
_ => panic!("Unexpected result: {:?}", maybe_volume), ))
} );
join_handle.await.unwrap().unwrap(); join_handle.await.unwrap().unwrap();
Ok(())
} }
#[test(tokio::test)] #[test(tokio::test)]
async fn test_get_property_error() { async fn test_get_property_error() -> Result<(), MpvError> {
let (server, join_handle) = test_socket(vec![ let (server, join_handle) = test_socket(vec![
json!({"request_id":0,"error":"property not found"}).to_string(), json!({"request_id":0,"error":"property not found"}).to_string(),
]); ]);
let mpv = Mpv::connect_socket(server).await.unwrap(); let mpv = Mpv::connect_socket(server).await?;
let maybe_volume = mpv.set_property("nonexistent", true).await; let maybe_volume = mpv.set_property("nonexistent", true).await;
match maybe_volume { assert_eq!(
Err(MpvError::MpvError(err)) => { maybe_volume,
assert_eq!(err, "property not found"); Err(MpvError::MpvError("property not found".to_string()))
} );
_ => panic!("Unexpected result: {:?}", maybe_volume),
}
join_handle.await.unwrap().unwrap(); join_handle.await.unwrap().unwrap();
Ok(())
} }
#[test(tokio::test)] #[test(tokio::test)]
@ -175,59 +182,3 @@ async fn test_set_property_simultaneous_requests() {
panic!("One of the pollers quit unexpectedly"); panic!("One of the pollers quit unexpectedly");
}; };
} }
#[test(tokio::test)]
async fn test_get_playlist() {
let expected = Playlist(vec![
PlaylistEntry {
id: 0,
filename: "file1".to_string(),
title: "title1".to_string(),
current: false,
},
PlaylistEntry {
id: 1,
filename: "file2".to_string(),
title: "title2".to_string(),
current: true,
},
PlaylistEntry {
id: 2,
filename: "file3".to_string(),
title: "title3".to_string(),
current: false,
},
]);
let (server, join_handle) = test_socket(vec![json!({
"data": expected.0.iter().map(|entry| {
json!({
"filename": entry.filename,
"title": entry.title,
"current": entry.current
})
}).collect::<Vec<Value>>(),
"request_id": 0,
"error": "success"
})
.to_string()]);
let mpv = Mpv::connect_socket(server).await.unwrap();
let playlist = mpv.get_playlist().await.unwrap();
assert_eq!(playlist, expected);
join_handle.await.unwrap().unwrap();
}
#[test(tokio::test)]
async fn test_get_playlist_empty() {
let (server, join_handle) = test_socket(vec![
json!({ "data": [], "request_id": 0, "error": "success" }).to_string(),
]);
let mpv = Mpv::connect_socket(server).await.unwrap();
let playlist = mpv.get_playlist().await.unwrap();
assert_eq!(playlist, Playlist(vec![]));
join_handle.await.unwrap().unwrap();
}