testing: Refactor interface comparison code out for future use

This commit is contained in:
Vicki Pfau 2024-08-01 21:55:00 -07:00
parent 428350b4c7
commit 5ddb709f0b
2 changed files with 188 additions and 93 deletions

View file

@ -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<String, &'a Method<'a>> {
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<String, &'a Property<'a>> {
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(
"<node name=\"/\" xmlns:doc=\"http://www.freedesktop.org/dbus/1.0/doc.dtd\">",
);
manager.introspect_to_writer(&mut remote_interface_string, 0);
remote_interface_string.push_str("</node>");
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::<SteamOSManager, _>(
&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));
}
}

View file

@ -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<Option<Rc<Test>>> = 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<Self>
where
I: Interface,
P: TryInto<ObjectPath<'p>>,
P::Error: Into<zbus::Error>,
{
let iface_ref = connection.object_server().interface::<_, I>(path).await?;
let iface = iface_ref.get().await;
let mut remote_interface_string = String::from(
"<node name=\"/\" xmlns:doc=\"http://www.freedesktop.org/dbus/1.0/doc.dtd\">",
);
iface.introspect_to_writer(&mut remote_interface_string, 0);
remote_interface_string.push_str("</node>");
Self::from_xml(remote_interface_string.as_bytes(), I::name().to_string())
}
pub async fn from_local<'p, P: AsRef<Path>, S: AsRef<str>>(
path: P,
interface: S,
) -> Result<Self> {
let local_interface_string = read(path.as_ref()).await?;
Self::from_xml(local_interface_string.as_ref(), interface)
}
fn from_xml<S: AsRef<str>>(xml: &[u8], iface_name: S) -> Result<Self> {
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<String, &Method<'_>> {
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<String, &Property<'_>> {
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
}
}