diff --git a/src/manager/user.rs b/src/manager/user.rs index 65b8ca4..fe5c667 100644 --- a/src/manager/user.rs +++ b/src/manager/user.rs @@ -395,14 +395,10 @@ mod test { use crate::daemon::user::UserContext; use crate::testing; - use std::collections::{HashMap, HashSet}; - use std::iter::zip; use std::time::Duration; - use tokio::fs::read; use tokio::sync::mpsc::unbounded_channel; use tokio::time::sleep; - use zbus::{Connection, Interface}; - use zbus_xml::{Method, Node, Property}; + use zbus::Connection; struct TestHandle { _handle: testing::TestHandle, @@ -429,96 +425,22 @@ mod test { }) } - fn collect_methods<'a>(methods: &'a [Method<'a>]) -> HashMap> { - let mut map = HashMap::new(); - for method in methods.iter() { - map.insert(method.name().to_string(), method); - } - map - } - - fn collect_properties<'a>(props: &'a [Property<'a>]) -> HashMap> { - let mut map = HashMap::new(); - for prop in props.iter() { - map.insert(prop.name().to_string(), prop); - } - map - } - #[tokio::test] async fn interface_matches() { let test = start().await.expect("start"); - let manager_ref = test - .connection - .object_server() - .interface::<_, SteamOSManager>("/com/steampowered/SteamOSManager1") - .await - .expect("interface"); - let manager = manager_ref.get().await; - let mut remote_interface_string = String::from( - "", - ); - manager.introspect_to_writer(&mut remote_interface_string, 0); - remote_interface_string.push_str(""); - let remote_interfaces = - Node::from_reader::<&[u8]>(remote_interface_string.as_bytes()).expect("from_reader"); - let remote_interface: Vec<_> = remote_interfaces - .interfaces() - .iter() - .filter(|iface| iface.name() == "com.steampowered.SteamOSManager1.Manager") - .collect(); - assert_eq!(remote_interface.len(), 1); - let remote_interface = remote_interface[0]; - let remote_methods = collect_methods(remote_interface.methods()); - let remote_method_names: HashSet<&String> = remote_methods.keys().collect(); - let remote_properties = collect_properties(remote_interface.properties()); - let remote_property_names: HashSet<&String> = remote_properties.keys().collect(); - - let local_interface_string = read("com.steampowered.SteamOSManager1.Manager.xml") - .await - .expect("read"); - let local_interfaces = - Node::from_reader::<&[u8]>(local_interface_string.as_ref()).expect("from_reader"); - let local_interface: Vec<_> = local_interfaces - .interfaces() - .iter() - .filter(|iface| iface.name() == "com.steampowered.SteamOSManager1.Manager") - .collect(); - assert_eq!(local_interface.len(), 1); - let local_interface = local_interface[0]; - let local_methods = collect_methods(local_interface.methods()); - let local_method_names: HashSet<&String> = local_methods.keys().collect(); - let local_properties = collect_properties(local_interface.properties()); - let local_property_names: HashSet<&String> = local_properties.keys().collect(); - - for key in local_method_names.union(&remote_method_names) { - let local_method = local_methods.get(*key).expect(key); - let remote_method = remote_methods.get(*key).expect(key); - - assert_eq!(local_method.name(), remote_method.name()); - assert_eq!( - local_method.args().len(), - remote_method.args().len(), - "Testing {:?} against {:?}", - local_method, - remote_method - ); - for (local_arg, remote_arg) in - zip(local_method.args().iter(), remote_method.args().iter()) - { - assert_eq!(local_arg.direction(), remote_arg.direction()); - assert_eq!(local_arg.ty(), remote_arg.ty()); - } - } - - for key in local_property_names.union(&remote_property_names) { - let local_property = local_properties.get(*key).expect(key); - let remote_property = remote_properties.get(*key).expect(key); - - assert_eq!(local_property.name(), remote_property.name()); - assert_eq!(local_property.ty(), remote_property.ty()); - assert_eq!(local_property.access(), remote_property.access()); - } + let remote = testing::InterfaceIntrospection::from_remote::( + &test.connection, + "/com/steampowered/SteamOSManager1", + ) + .await + .expect("remote"); + let local = testing::InterfaceIntrospection::from_local( + "com.steampowered.SteamOSManager1.Manager.xml", + "com.steampowered.SteamOSManager1.Manager", + ) + .await + .expect("local"); + assert!(remote.compare(&local)); } } diff --git a/src/testing.rs b/src/testing.rs index cc1fe8e..cc5399c 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -4,17 +4,23 @@ use nix::sys::signal; use nix::sys::signal::Signal; use nix::unistd::Pid; use std::cell::{Cell, RefCell}; +use std::collections::{HashMap, HashSet}; use std::ffi::OsStr; +use std::iter::zip; use std::path::Path; use std::process::Stdio; use std::rc::Rc; use std::str::FromStr; use std::time::Duration; use tempfile::{tempdir, TempDir}; +use tokio::fs::read; use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::process::{Child, Command}; use tokio::sync::Mutex; -use zbus::{Address, Connection, ConnectionBuilder}; +use tracing::error; +use zbus::zvariant::ObjectPath; +use zbus::{Address, Connection, ConnectionBuilder, Interface}; +use zbus_xml::{Method, Node, Property}; thread_local! { static TEST: RefCell>> = RefCell::new(None); @@ -174,3 +180,170 @@ impl Drop for TestHandle { stop(); } } + +pub struct InterfaceIntrospection<'a> { + interface: zbus_xml::Interface<'a>, +} + +impl<'a> InterfaceIntrospection<'a> { + pub async fn from_remote<'p, I, P>(connection: &Connection, path: P) -> Result + where + I: Interface, + P: TryInto>, + P::Error: Into, + { + let iface_ref = connection.object_server().interface::<_, I>(path).await?; + let iface = iface_ref.get().await; + let mut remote_interface_string = String::from( + "", + ); + iface.introspect_to_writer(&mut remote_interface_string, 0); + remote_interface_string.push_str(""); + Self::from_xml(remote_interface_string.as_bytes(), I::name().to_string()) + } + + pub async fn from_local<'p, P: AsRef, S: AsRef>( + path: P, + interface: S, + ) -> Result { + let local_interface_string = read(path.as_ref()).await?; + Self::from_xml(local_interface_string.as_ref(), interface) + } + + fn from_xml>(xml: &[u8], iface_name: S) -> Result { + let node = Node::from_reader(xml)?; + let interfaces = node.interfaces(); + let mut interface = None; + for iface in interfaces { + if iface.name() == iface_name.as_ref() { + interface = Some(iface.clone()); + break; + } + } + Ok(if let Some(interface) = interface { + InterfaceIntrospection { interface } + } else { + bail!("No interface found"); + }) + } + + fn collect_methods(&self) -> HashMap> { + let mut map = HashMap::new(); + for method in self.interface.methods().iter() { + map.insert(method.name().to_string(), method); + } + map + } + + fn collect_properties(&self) -> HashMap> { + let mut map = HashMap::new(); + for prop in self.interface.properties().iter() { + map.insert(prop.name().to_string(), prop); + } + map + } + + fn compare_methods(&self, other: &InterfaceIntrospection<'_>) -> u32 { + let local_methods = self.collect_methods(); + let local_method_names: HashSet<&String> = local_methods.keys().collect(); + let other_methods = other.collect_methods(); + let other_method_names: HashSet<&String> = other_methods.keys().collect(); + + let mut issues = 0; + + for key in local_method_names.union(&other_method_names) { + let local_method = match local_methods.get(*key) { + None => { + error!("Method {key} missing on self"); + issues += 1; + continue; + } + Some(method) => method, + }; + + let other_method = match other_methods.get(*key) { + None => { + error!("Method {key} missing on other"); + issues += 1; + continue; + } + Some(method) => method, + }; + + if local_method.args().len() != other_method.args().len() { + error!("Different arguments between {local_method:?} and {other_method:?}"); + issues += 1; + continue; + } + + for (local_arg, other_arg) in + zip(local_method.args().iter(), other_method.args().iter()) + { + if local_arg.direction() != other_arg.direction() { + error!("Arguments {local_arg:?} and {other_arg:?} differ in direction"); + issues += 1; + continue; + } + if local_arg.ty() != other_arg.ty() { + error!("Arguments {local_arg:?} and {other_arg:?} differ in type"); + issues += 1; + continue; + } + } + } + + issues + } + + fn compare_properties(&self, other: &InterfaceIntrospection<'_>) -> u32 { + let local_properties = self.collect_properties(); + let local_property_names: HashSet<&String> = local_properties.keys().collect(); + + let other_properties = other.collect_properties(); + let other_property_names: HashSet<&String> = other_properties.keys().collect(); + + let mut issues = 0; + + for key in local_property_names.union(&other_property_names) { + let local_property = match local_properties.get(*key) { + None => { + error!("Property {key} missing on self"); + issues += 1; + continue; + } + Some(prop) => prop, + }; + + let other_property = match other_properties.get(*key) { + None => { + error!("Property {key} missing on other"); + issues += 1; + continue; + } + Some(prop) => prop, + }; + + if local_property.ty() != other_property.ty() { + error!("Properties {local_property:?} and {other_property:?} differ in type"); + issues += 1; + continue; + } + + if local_property.access() != other_property.access() { + error!("Properties {local_property:?} and {other_property:?} differ in access"); + issues += 1; + continue; + } + } + + issues + } + + pub fn compare(&self, other: &InterfaceIntrospection<'_>) -> bool { + let mut issues = 0; + issues += self.compare_methods(other); + issues += self.compare_properties(other); + + issues == 0 + } +}