use nlink::netlink::diagnostics::{
BottleneckType, Diagnostics, DiagnosticsConfig, IssueCategory, LinkRates, Severity,
};
use crate::common::TestNamespace;
#[test]
fn test_severity_ordering() {
assert!(Severity::Info < Severity::Warning);
assert!(Severity::Warning < Severity::Error);
assert!(Severity::Error < Severity::Critical);
}
#[test]
fn test_severity_display() {
assert_eq!(format!("{}", Severity::Info), "INFO");
assert_eq!(format!("{}", Severity::Warning), "WARN");
assert_eq!(format!("{}", Severity::Error), "ERROR");
assert_eq!(format!("{}", Severity::Critical), "CRITICAL");
}
#[test]
fn test_issue_category_display() {
assert_eq!(format!("{}", IssueCategory::LinkDown), "LinkDown");
assert_eq!(format!("{}", IssueCategory::NoCarrier), "NoCarrier");
assert_eq!(
format!("{}", IssueCategory::HighPacketLoss),
"HighPacketLoss"
);
assert_eq!(format!("{}", IssueCategory::QdiscDrops), "QdiscDrops");
assert_eq!(format!("{}", IssueCategory::NoRoute), "NoRoute");
}
#[test]
fn test_link_rates() {
let rates = LinkRates {
rx_bps: 1000,
tx_bps: 2000,
rx_pps: 10,
tx_pps: 20,
sample_duration_ms: 1000,
};
assert_eq!(rates.total_bps(), 3000);
assert_eq!(rates.total_pps(), 30);
}
#[test]
fn test_link_rates_default() {
let rates = LinkRates::default();
assert_eq!(rates.rx_bps, 0);
assert_eq!(rates.tx_bps, 0);
assert_eq!(rates.total_bps(), 0);
}
#[test]
fn test_config_defaults() {
let config = DiagnosticsConfig::default();
assert_eq!(config.packet_loss_threshold, 0.01);
assert_eq!(config.error_rate_threshold, 0.001);
assert_eq!(config.qdisc_drop_threshold, 0.01);
assert_eq!(config.backlog_threshold, 100_000);
assert_eq!(config.qlen_threshold, 1000);
assert!(config.skip_loopback);
assert!(!config.skip_down);
assert_eq!(config.min_bytes_for_rate, 1000);
}
#[test]
fn test_bottleneck_type_display() {
assert_eq!(format!("{}", BottleneckType::QdiscDrops), "Qdisc Drops");
assert_eq!(
format!("{}", BottleneckType::InterfaceDrops),
"Interface Drops"
);
assert_eq!(format!("{}", BottleneckType::BufferFull), "Buffer Full");
assert_eq!(format!("{}", BottleneckType::RateLimited), "Rate Limited");
assert_eq!(
format!("{}", BottleneckType::HardwareErrors),
"Hardware Errors"
);
}
#[tokio::test]
async fn test_diagnostics_scan() -> nlink::Result<()> {
nlink::require_root!();
let ns = TestNamespace::new("diag_scan")?;
ns.add_dummy("dummy0")?;
ns.link_up("dummy0")?;
ns.add_addr("dummy0", "10.0.0.1/24")?;
let conn = ns.connection()?;
let diag = Diagnostics::new(conn);
let report = diag.scan().await?;
assert!(!report.interfaces.is_empty());
let dummy = report
.interfaces
.iter()
.find(|i| i.name == "dummy0")
.expect("dummy0 not found in report");
assert_eq!(dummy.name, "dummy0");
assert!(dummy.mtu.is_some());
Ok(())
}
#[tokio::test]
async fn test_diagnostics_scan_interface() -> nlink::Result<()> {
nlink::require_root!();
let ns = TestNamespace::new("diag_scan_if")?;
ns.add_dummy("eth0")?;
ns.link_up("eth0")?;
ns.add_addr("eth0", "192.168.1.1/24")?;
let conn = ns.connection()?;
let diag = Diagnostics::new(conn);
let iface = diag.scan_interface("eth0").await?;
assert_eq!(iface.name, "eth0");
assert!(iface.mtu.is_some());
Ok(())
}
#[tokio::test]
async fn test_diagnostics_scan_interface_not_found() -> nlink::Result<()> {
nlink::require_root!();
let ns = TestNamespace::new("diag_notfound")?;
let conn = ns.connection()?;
let diag = Diagnostics::new(conn);
let result = diag.scan_interface("nonexistent0").await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.is_not_found());
Ok(())
}
#[tokio::test]
async fn test_diagnostics_check_connectivity_no_route() -> nlink::Result<()> {
nlink::require_root!();
let ns = TestNamespace::new("diag_conn")?;
ns.add_dummy("eth0")?;
ns.link_up("eth0")?;
let conn = ns.connection()?;
let diag = Diagnostics::new(conn);
let report = diag
.check_connectivity("8.8.8.8".parse().unwrap())
.await?;
assert!(!report.issues.is_empty());
assert!(
report
.issues
.iter()
.any(|i| i.category == IssueCategory::NoRoute)
);
Ok(())
}
#[tokio::test]
async fn test_diagnostics_check_connectivity_with_route() -> nlink::Result<()> {
nlink::require_root!();
let ns = TestNamespace::new("diag_route")?;
ns.add_dummy("eth0")?;
ns.link_up("eth0")?;
ns.add_addr("eth0", "192.168.1.1/24")?;
let _ = ns.exec("ip", &["route", "add", "default", "via", "192.168.1.254"]);
let conn = ns.connection()?;
let diag = Diagnostics::new(conn);
let report = diag
.check_connectivity("192.168.1.100".parse().unwrap())
.await?;
assert!(
report.route.is_some() || report.issues.is_empty(),
"Expected route or no issues"
);
Ok(())
}
#[tokio::test]
async fn test_diagnostics_find_bottleneck() -> nlink::Result<()> {
nlink::require_root!();
let ns = TestNamespace::new("diag_bottle")?;
ns.add_dummy("eth0")?;
ns.link_up("eth0")?;
let conn = ns.connection()?;
let diag = Diagnostics::new(conn);
let bottleneck = diag.find_bottleneck().await?;
if let Some(b) = bottleneck {
println!("Found bottleneck: {} ({:?})", b.location, b.bottleneck_type);
}
Ok(())
}
#[tokio::test]
async fn test_diagnostics_with_tc() -> nlink::Result<()> {
nlink::require_root!();
nlink::require_module!("sch_htb");
let ns = TestNamespace::new("diag_tc")?;
ns.add_dummy("eth0")?;
ns.link_up("eth0")?;
ns.exec_ignore(
"tc",
&[
"qdisc", "add", "dev", "eth0", "root", "handle", "1:", "htb", "default", "10",
],
);
let conn = ns.connection()?;
let diag = Diagnostics::new(conn);
let report = diag.scan().await?;
let eth0 = report
.interfaces
.iter()
.find(|i| i.name == "eth0")
.expect("eth0 not found in report");
if let Some(tc) = ð0.tc {
assert_eq!(tc.qdisc, "htb");
}
Ok(())
}
#[tokio::test]
async fn test_diagnostics_link_down_detection() -> nlink::Result<()> {
nlink::require_root!();
let ns = TestNamespace::new("diag_down")?;
ns.add_dummy("eth0")?;
let conn = ns.connection()?;
let config = DiagnosticsConfig {
skip_down: false,
..Default::default()
};
let diag = Diagnostics::with_config(conn, config);
let report = diag.scan().await?;
let eth0 = report
.interfaces
.iter()
.find(|i| i.name == "eth0")
.expect("eth0 not found in report");
assert!(
eth0.issues
.iter()
.any(|i| i.category == IssueCategory::LinkDown)
);
Ok(())
}
#[tokio::test]
async fn test_diagnostics_no_address_detection() -> nlink::Result<()> {
nlink::require_root!();
let ns = TestNamespace::new("diag_noaddr")?;
ns.add_dummy("eth0")?;
ns.link_up("eth0")?;
let _ = ns.exec("ip", &["-6", "addr", "flush", "dev", "eth0"]);
let conn = ns.connection()?;
let diag = Diagnostics::new(conn);
let report = diag.scan().await?;
let eth0 = report
.interfaces
.iter()
.find(|i| i.name == "eth0")
.expect("eth0 not found in report");
assert!(
eth0.issues
.iter()
.any(|i| i.category == IssueCategory::NoAddress),
"expected NoAddress issue; got issues={:?}",
eth0.issues.iter().map(|i| &i.category).collect::<Vec<_>>(),
);
Ok(())
}
#[tokio::test]
async fn test_diagnostics_route_summary() -> nlink::Result<()> {
nlink::require_root!();
let ns = TestNamespace::new("diag_routes")?;
ns.add_dummy("eth0")?;
ns.link_up("eth0")?;
ns.add_addr("eth0", "10.0.0.1/24")?;
ns.exec("ip", &["route", "add", "192.168.0.0/16", "dev", "eth0"])?;
let conn = ns.connection()?;
let diag = Diagnostics::new(conn);
let report = diag.scan().await?;
assert!(report.routes.ipv4_route_count >= 1);
Ok(())
}
#[tokio::test]
async fn test_diagnostics_custom_config() -> nlink::Result<()> {
nlink::require_root!();
let ns = TestNamespace::new("diag_config")?;
ns.add_dummy("eth0")?;
ns.link_up("eth0")?;
let conn = ns.connection()?;
let config = DiagnosticsConfig {
packet_loss_threshold: 0.001, error_rate_threshold: 0.0001, skip_loopback: true,
skip_down: true,
..Default::default()
};
let diag = Diagnostics::with_config(conn, config);
assert_eq!(diag.config().packet_loss_threshold, 0.001);
assert_eq!(diag.config().error_rate_threshold, 0.0001);
let report = diag.scan().await?;
assert!(!report.interfaces.is_empty());
Ok(())
}
#[tokio::test]
async fn test_diagnostics_skip_loopback() -> nlink::Result<()> {
nlink::require_root!();
let ns = TestNamespace::new("diag_lo")?;
let conn = ns.connection()?;
let diag = Diagnostics::new(conn);
let report = diag.scan().await?;
assert!(!report.interfaces.iter().any(|i| i.name == "lo"));
Ok(())
}