use ftr::{Ftr, PreferredFamily, TracerouteConfig, TracerouteError};
use std::net::IpAddr;
const V6_TARGET: &str = "2001:4860:4860::8888";
fn skip_live_ipv6() -> bool {
if std::env::var("FTR_TEST_IPV6").as_deref() != Ok("1") {
eprintln!("Skipping live IPv6 test (set FTR_TEST_IPV6=1 to enable)");
return true;
}
false
}
fn assert_canonical(addr: IpAddr) {
let s = addr.to_string();
assert_eq!(
s.parse::<IpAddr>().expect("address string must re-parse"),
addr,
"address must round-trip through its Display form"
);
assert_eq!(
s,
s.to_lowercase(),
"RFC 5952 requires lowercase hex digits"
);
assert!(!s.contains("%"), "IpAddr display never carries a zone");
}
#[tokio::test]
async fn test_live_ipv6_trace_to_google_dns() {
if skip_live_ipv6() {
return;
}
let config = TracerouteConfig::builder()
.target(V6_TARGET)
.max_hops(30)
.build()
.expect("config builds");
let ftr = Ftr::new();
let result = ftr.trace_with_config(config).await;
if matches!(result, Err(TracerouteError::Ipv6NotSupported)) {
eprintln!("IPv6 probing not supported on this platform — typed error OK");
return;
}
let result = result.expect("live IPv6 trace should succeed");
assert_eq!(
result.target_ip,
V6_TARGET.parse::<IpAddr>().expect("valid literal")
);
assert!(
result.destination_reached,
"destination {V6_TARGET} should be reached"
);
let intermediate_hops: Vec<_> = result
.hops
.iter()
.filter(|h| h.addr.is_some() && h.addr != Some(result.target_ip))
.collect();
assert!(
!intermediate_hops.is_empty(),
"expected at least one intermediate hop, got only: {:?}",
result.hops
);
for hop in result.hops.iter().filter_map(|h| h.addr) {
assert!(hop.is_ipv6(), "IPv6 trace must not report IPv4 hops: {hop}");
assert_canonical(hop);
}
eprintln!(
"live IPv6 trace: {} hops, {} intermediate, destination reached in {:?}",
result.hop_count(),
intermediate_hops.len(),
result.total_duration
);
for hop in &result.hops {
eprintln!(
" {:2} [{}] {:?} {:?}",
hop.ttl, hop.segment, hop.addr, hop.rtt
);
}
}
#[tokio::test]
async fn test_live_ipv6_hostname_resolution_forced_v6() {
if skip_live_ipv6() {
return;
}
let ip = ftr::resolve_target_with_family("google.com", PreferredFamily::V6)
.await
.expect("google.com must resolve under -6");
assert!(ip.is_ipv6(), "forced V6 resolution returned {ip}");
assert_canonical(ip);
let ip = ftr::resolve_target_with_family("google.com", PreferredFamily::Auto)
.await
.expect("google.com must resolve under Auto");
assert!(ip.is_ipv4(), "Auto must prefer IPv4 for dual-stack hosts");
}
#[tokio::test]
async fn test_live_ipv6_trace_via_hostname() {
if skip_live_ipv6() {
return;
}
let config = TracerouteConfig::builder()
.target("google.com")
.preferred_family(PreferredFamily::V6)
.max_hops(30)
.build()
.expect("config builds");
let ftr = Ftr::new();
let result = ftr.trace_with_config(config).await;
if matches!(result, Err(TracerouteError::Ipv6NotSupported)) {
eprintln!("IPv6 probing not supported on this platform — typed error OK");
return;
}
let result = result.expect("live IPv6 hostname trace should succeed");
assert!(result.target_ip.is_ipv6(), "-6 trace must probe IPv6");
assert!(
result.destination_reached,
"google.com over IPv6 should be reachable"
);
}