#![allow(clippy::unwrap_used, clippy::expect_used)]
mod common;
use std::{net::Ipv4Addr, time::Duration};
use hick_compio::{
Endpoint, Name, QueryEvent, QuerySpec, ServerOptions, ServiceRecords, ServiceSpec, ServiceUpdate,
wire::ResourceType,
};
fn loopback_index() -> Option<u32> {
let ifs = getifs::interfaces().ok()?;
ifs
.iter()
.find(|i| i.flags().contains(getifs::Flags::LOOPBACK))
.map(|i| i.index())
}
fn http_service(instance: &str) -> ServiceSpec {
let mut recs = ServiceRecords::new(
Name::try_from_str("_http._tcp.local.").unwrap(),
Name::try_from_str(&format!("{instance}._http._tcp.local.")).unwrap(),
Name::try_from_str(&format!("{instance}.local.")).unwrap(),
80,
120,
);
recs.add_a(Ipv4Addr::new(127, 0, 0, 1));
ServiceSpec::new(recs)
}
async fn loopback_v4_endpoint() -> Option<Endpoint> {
let idx = loopback_index()?;
let opts = ServerOptions::default()
.with_interface_index(Some(idx))
.with_ipv6(false);
match Endpoint::server(opts).await {
Ok(ep) => Some(ep),
Err(e) => {
eprintln!("loopback multicast bind unavailable ({e}); skipping");
None
}
}
}
#[compio::test]
async fn registered_service_reaches_advertised_state() {
let Some(ep) = loopback_v4_endpoint().await else {
return;
};
let svc = ep.register_service(http_service("alpha")).await.unwrap();
match compio::time::timeout(Duration::from_secs(10), svc.next()).await {
Ok(Some(ServiceUpdate::Established)) => {}
Ok(Some(ServiceUpdate::Renamed(_))) => {}
Ok(Some(other)) => panic!("service failed to advertise: {other:?}"),
Ok(None) => panic!("service update channel closed before the service advertised"),
Err(_) => panic!("timed out waiting for the service to finish probing"),
}
}
#[compio::test]
async fn browse_drives_both_run_loops() {
let Some(server) = loopback_v4_endpoint().await else {
return;
};
let Some(client) = loopback_v4_endpoint().await else {
return;
};
let _svc = server.register_service(http_service("beta")).await.unwrap();
let query = client
.start_query(QuerySpec::new(
Name::try_from_str("_http._tcp.local.").unwrap(),
ResourceType::Ptr,
))
.await
.unwrap();
match compio::time::timeout(Duration::from_secs(8), query.next()).await {
Ok(Some(QueryEvent::Answer(a))) => {
assert_eq!(a.rtype(), ResourceType::Ptr);
}
Ok(Some(QueryEvent::Terminal(_))) | Ok(None) | Err(_) => {
eprintln!("no cross-socket loopback delivery here; run loops still exercised");
}
}
let _ = query.handle();
}
#[compio::test]
async fn dropping_handles_tears_down_via_driver() {
let Some(ep) = loopback_v4_endpoint().await else {
return;
};
let query = ep
.start_query(QuerySpec::new(
Name::try_from_str("_absent._tcp.local.").unwrap(),
ResourceType::Ptr,
))
.await
.unwrap();
let _ = query.handle();
drop(query);
let svc = ep.register_service(http_service("gamma")).await.unwrap();
let _ = svc.handle();
drop(svc);
compio::time::sleep(Duration::from_millis(50)).await;
}
#[compio::test]
async fn server_rejects_no_family_enabled() {
let opts = ServerOptions::default().with_ipv4(false).with_ipv6(false);
match Endpoint::server(opts).await {
Err(hick_compio::ServerError::NoFamilyEnabled) => {}
Err(e) => panic!("expected NoFamilyEnabled, got {e:?}"),
Ok(_) => panic!("expected NoFamilyEnabled, but server() succeeded"),
}
}
#[compio::test]
async fn dual_stack_loopback_exercises_v6_setup() {
let Some(idx) = loopback_index() else {
return;
};
let opts = ServerOptions::default().with_interface_index(Some(idx));
match Endpoint::server(opts).await {
Ok(_ep) => {}
Err(hick_compio::ServerError::BindV6(_) | hick_compio::ServerError::JoinV6(_)) => {}
Err(e) => panic!("unexpected dual-stack setup error: {e:?}"),
}
}