ftr 0.9.0

A fast, parallel ICMP traceroute with ASN lookup, reverse DNS, and ISP detection
Documentation
//! Test that async implementations show multiple hops

#[cfg(feature = "async")]
#[cfg(test)]
mod tests {
    use ftr::Ftr;
    #[cfg(target_os = "macos")]
    use ftr::TracerouteConfig;

    #[tokio::test]
    async fn test_async_shows_multiple_hops() {
        // Test with a well-known destination that should have multiple hops
        let target = "8.8.8.8";

        let ftr_instance = Ftr::new();
        let result = ftr_instance.trace(target).await;
        if let Err(e) = &result {
            // If we get a permission error, skip the test
            if e.to_string().contains("Permission denied")
                || e.to_string().contains("requires root")
            {
                eprintln!("Skipping test due to permission error: {}", e);
                return;
            }
        }
        let result = result.expect("trace to 8.8.8.8 failed with unexpected error");

        eprintln!("Async trace completed:");
        eprintln!("  Protocol: {:?}", result.protocol_used);
        eprintln!("  Socket mode: {:?}", result.socket_mode_used);
        eprintln!("  Total hops: {}", result.hop_count());

        // Count how many hops actually responded
        let hops_with_responses: Vec<_> = result
            .hops
            .iter()
            .filter(|hop| hop.addr.is_some())
            .collect();

        eprintln!("  Hops with responses: {}", hops_with_responses.len());

        // Debug info if we got no responses
        if hops_with_responses.is_empty() {
            eprintln!("\n=== DEBUG: Got 0 responses ===");
            eprintln!("Environment info:");
            eprintln!("  OS: {}", std::env::consts::OS);
            eprintln!("  CI: {:?}", std::env::var("CI"));
            eprintln!("  GITHUB_ACTIONS: {:?}", std::env::var("GITHUB_ACTIONS"));

            // GitHub Actions runners often have restricted ICMP
            // Windows runners are hosted in Azure which blocks inbound ICMP
            // Ubuntu runners also have unreliable ICMP responses
            // See: https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners
            if std::env::var("GITHUB_ACTIONS").is_ok() {
                let os = std::env::consts::OS;
                if os == "windows" || os == "linux" {
                    eprintln!("\nNo ICMP responses on GitHub Actions {} runner", os);
                    eprintln!("This is expected - GitHub Actions runners have restricted ICMP.");
                    eprintln!("Windows: Azure blocks inbound ICMP packets.");
                    eprintln!("Linux: Unreliable ICMP due to virtualization/network restrictions.");
                    eprintln!("Skipping test on GitHub Actions {}.", os);
                    return;
                }
            }

            eprintln!("\nUnexpected: No ICMP responses received");
            eprintln!("This could indicate:");
            eprintln!("  - Firewall blocking ICMP");
            eprintln!("  - Network configuration issues");
            eprintln!("  - Target unreachable");
        }

        // Verify we have responses from different addresses
        let unique_addresses: std::collections::HashSet<_> =
            result.hops.iter().filter_map(|hop| hop.addr).collect();

        // We should see at least 3 hops for internet destinations
        assert!(
            result.hop_count() >= 3,
            "Expected at least 3 hops, got {}",
            result.hop_count()
        );

        // Verify we have responses from different addresses
        // Note: On some systems (like macOS with certain network configs),
        // we might see the same address (e.g., 127.0.0.1) for multiple hops
        assert!(
            !unique_addresses.is_empty(),
            "Expected responses from at least 1 address, got {}",
            unique_addresses.len()
        );
    }

    #[tokio::test]
    #[cfg(target_os = "macos")]
    async fn test_macos_async_icmp_works() {
        // Test specifically that macOS async ICMP implementation works
        let config = TracerouteConfig::builder()
            .target("1.1.1.1")
            .protocol(ftr::ProbeProtocol::Icmp)
            .build()
            .expect("failed to build traceroute config");

        let ftr_instance = Ftr::new();
        let result = ftr_instance.trace_with_config(config).await;
        if let Err(e) = &result {
            // Skip if permission denied
            if e.to_string().contains("Permission denied") {
                eprintln!("Skipping macOS ICMP test due to permissions");
                return;
            }
        }
        let result = result.expect("macOS async ICMP trace failed");

        // Should get multiple hops
        assert!(
            result.hop_count() >= 3,
            "macOS async ICMP should show multiple hops, got {}",
            result.hop_count()
        );

        // Check we got actual responses
        let hops_with_responses = result.hops.iter().filter(|hop| hop.addr.is_some()).count();

        assert!(
            hops_with_responses >= 2,
            "macOS async ICMP should show responses from multiple hops, got {}",
            hops_with_responses
        );
    }

    #[tokio::test]
    async fn test_parallel_probing_to_external_target() {
        // Test that parallel probing works correctly for an external target
        // This exercises the concurrent probe sending and response collection
        let target = "1.1.1.1"; // Cloudflare DNS - reliable and fast

        let ftr_instance = Ftr::new();
        let result = ftr_instance.trace(target).await;
        if let Err(e) = &result {
            // Skip on permission error or network issues
            if e.to_string().contains("Permission denied") {
                eprintln!("Skipping external target test due to permissions");
                return;
            }
            if e.to_string().contains("timed out") || e.to_string().contains("unreachable") {
                eprintln!("Skipping external target test due to network issues");
                return;
            }
        }
        let result = result.expect("external trace failed");

        // Should have multiple hops for an external target
        assert!(
            result.hop_count() >= 2,
            "External target should have multiple hops, got {}",
            result.hop_count()
        );

        // Verify TTLs are in ascending order
        let mut prev_ttl = 0;
        for hop in &result.hops {
            assert!(
                hop.ttl > prev_ttl,
                "TTLs should be in ascending order: {} <= {}",
                hop.ttl,
                prev_ttl
            );
            prev_ttl = hop.ttl;
        }

        // Should not have huge gaps in TTL sequence (no more than 5 missing hops)
        let ttls: Vec<u8> = result.hops.iter().map(|h| h.ttl).collect();
        for window in ttls.windows(2) {
            assert!(
                window[1] - window[0] <= 5,
                "Large gap in TTL sequence: {} to {}",
                window[0],
                window[1]
            );
        }
    }
}