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]
#[ignore] async fn test_diagnostics_scan() {
let ns = TestNamespace::new("diag_scan").unwrap();
ns.add_dummy("dummy0").unwrap();
ns.link_up("dummy0").unwrap();
ns.add_addr("dummy0", "10.0.0.1/24").unwrap();
let conn = ns.connection().unwrap();
let diag = Diagnostics::new(conn);
let report = diag.scan().await.unwrap();
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());
}
#[tokio::test]
#[ignore] async fn test_diagnostics_scan_interface() {
let ns = TestNamespace::new("diag_scan_if").unwrap();
ns.add_dummy("eth0").unwrap();
ns.link_up("eth0").unwrap();
ns.add_addr("eth0", "192.168.1.1/24").unwrap();
let conn = ns.connection().unwrap();
let diag = Diagnostics::new(conn);
let iface = diag.scan_interface("eth0").await.unwrap();
assert_eq!(iface.name, "eth0");
assert!(iface.mtu.is_some());
}
#[tokio::test]
#[ignore] async fn test_diagnostics_scan_interface_not_found() {
let ns = TestNamespace::new("diag_notfound").unwrap();
let conn = ns.connection().unwrap();
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());
}
#[tokio::test]
#[ignore] async fn test_diagnostics_check_connectivity_no_route() {
let ns = TestNamespace::new("diag_conn").unwrap();
ns.add_dummy("eth0").unwrap();
ns.link_up("eth0").unwrap();
let conn = ns.connection().unwrap();
let diag = Diagnostics::new(conn);
let report = diag
.check_connectivity("8.8.8.8".parse().unwrap())
.await
.unwrap();
assert!(!report.issues.is_empty());
assert!(
report
.issues
.iter()
.any(|i| i.category == IssueCategory::NoRoute)
);
}
#[tokio::test]
#[ignore] async fn test_diagnostics_check_connectivity_with_route() {
let ns = TestNamespace::new("diag_route").unwrap();
ns.add_dummy("eth0").unwrap();
ns.link_up("eth0").unwrap();
ns.add_addr("eth0", "192.168.1.1/24").unwrap();
let _ = ns.exec("ip", &["route", "add", "default", "via", "192.168.1.254"]);
let conn = ns.connection().unwrap();
let diag = Diagnostics::new(conn);
let report = diag
.check_connectivity("192.168.1.100".parse().unwrap())
.await
.unwrap();
assert!(
report.route.is_some() || report.issues.is_empty(),
"Expected route or no issues"
);
}
#[tokio::test]
#[ignore] async fn test_diagnostics_find_bottleneck() {
let ns = TestNamespace::new("diag_bottle").unwrap();
ns.add_dummy("eth0").unwrap();
ns.link_up("eth0").unwrap();
let conn = ns.connection().unwrap();
let diag = Diagnostics::new(conn);
let bottleneck = diag.find_bottleneck().await.unwrap();
if let Some(b) = bottleneck {
println!("Found bottleneck: {} ({:?})", b.location, b.bottleneck_type);
}
}
#[tokio::test]
#[ignore] async fn test_diagnostics_with_tc() {
let ns = TestNamespace::new("diag_tc").unwrap();
ns.add_dummy("eth0").unwrap();
ns.link_up("eth0").unwrap();
ns.exec_ignore(
"tc",
&[
"qdisc", "add", "dev", "eth0", "root", "handle", "1:", "htb", "default", "10",
],
);
let conn = ns.connection().unwrap();
let diag = Diagnostics::new(conn);
let report = diag.scan().await.unwrap();
let eth0 = report.interfaces.iter().find(|i| i.name == "eth0");
assert!(eth0.is_some());
let eth0 = eth0.unwrap();
if let Some(tc) = ð0.tc {
assert_eq!(tc.qdisc, "htb");
}
}
#[tokio::test]
#[ignore] async fn test_diagnostics_link_down_detection() {
let ns = TestNamespace::new("diag_down").unwrap();
ns.add_dummy("eth0").unwrap();
let conn = ns.connection().unwrap();
let config = DiagnosticsConfig {
skip_down: false,
..Default::default()
};
let diag = Diagnostics::with_config(conn, config);
let report = diag.scan().await.unwrap();
let eth0 = report.interfaces.iter().find(|i| i.name == "eth0");
assert!(eth0.is_some());
let eth0 = eth0.unwrap();
assert!(
eth0.issues
.iter()
.any(|i| i.category == IssueCategory::LinkDown)
);
}
#[tokio::test]
#[ignore] async fn test_diagnostics_no_address_detection() {
let ns = TestNamespace::new("diag_noaddr").unwrap();
ns.add_dummy("eth0").unwrap();
ns.link_up("eth0").unwrap();
let conn = ns.connection().unwrap();
let diag = Diagnostics::new(conn);
let report = diag.scan().await.unwrap();
let eth0 = report.interfaces.iter().find(|i| i.name == "eth0");
assert!(eth0.is_some());
let eth0 = eth0.unwrap();
assert!(
eth0.issues
.iter()
.any(|i| i.category == IssueCategory::NoAddress)
);
}
#[tokio::test]
#[ignore] async fn test_diagnostics_route_summary() {
let ns = TestNamespace::new("diag_routes").unwrap();
ns.add_dummy("eth0").unwrap();
ns.link_up("eth0").unwrap();
ns.add_addr("eth0", "10.0.0.1/24").unwrap();
ns.exec("ip", &["route", "add", "192.168.0.0/16", "dev", "eth0"])
.unwrap();
let conn = ns.connection().unwrap();
let diag = Diagnostics::new(conn);
let report = diag.scan().await.unwrap();
assert!(report.routes.ipv4_route_count >= 1);
}
#[tokio::test]
#[ignore] async fn test_diagnostics_custom_config() {
let ns = TestNamespace::new("diag_config").unwrap();
ns.add_dummy("eth0").unwrap();
ns.link_up("eth0").unwrap();
let conn = ns.connection().unwrap();
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.unwrap();
assert!(!report.interfaces.is_empty());
}
#[tokio::test]
#[ignore] async fn test_diagnostics_skip_loopback() {
let ns = TestNamespace::new("diag_lo").unwrap();
let conn = ns.connection().unwrap();
let diag = Diagnostics::new(conn);
let report = diag.scan().await.unwrap();
assert!(!report.interfaces.iter().any(|i| i.name == "lo"));
}