ftr 0.10.0

A fast, parallel ICMP traceroute with ASN lookup, reverse DNS, and ISP detection
Documentation
name: CI

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
  workflow_dispatch:
  workflow_call:  # Allow this workflow to be called by other workflows

env:
  CARGO_TERM_COLOR: always

jobs:
  test:
    name: Test
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        rust: [stable]  # Test on stable only
        include:
          # Also test MSRV on Ubuntu only
          - os: ubuntu-latest
            rust: 1.85.0
    steps:
    - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
    - name: Install Rust
      uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master (2026-06-30)
      with:
        toolchain: ${{ matrix.rust }}
    - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
      with:
        # Share cache between different jobs but keep OS-specific
        shared-key: "ci-${{ matrix.os }}"
    - name: Build
      run: cargo build --verbose
    - name: Run tests
      # On Windows, explicitly pass empty filter to avoid potential issues
      # where an unexpected filter argument causes tests to be skipped
      run: |
        if [ "${{ matrix.os }}" = "windows-latest" ]; then
          cargo test --verbose -- ""
        else
          cargo test --verbose
        fi
      shell: bash

  fmt:
    name: Rustfmt
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
    - uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master (2026-06-30)
      with:
        toolchain: stable
        components: rustfmt
    - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
      with:
        shared-key: "ci-ubuntu-latest"
    - name: Check formatting
      run: cargo fmt -- --check

  clippy:
    name: Clippy
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
    - uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master (2026-06-30)
      with:
        toolchain: stable
        components: clippy
    - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
      with:
        shared-key: "ci-ubuntu-latest"
    - name: Run clippy
      # --all-targets also lints tests, benches, and examples so lint debt
      # cannot re-accumulate outside the library/binary targets.
      run: cargo clippy --all-targets -- -D warnings

  coverage:
    name: Code coverage
    runs-on: ubuntu-latest
    # Coverage is informational for now: integration tests exercise live
    # network services (DNS, WHOIS, STUN) and may flake under instrumentation.
    # Consider making this job blocking once it has a track record of stability.
    continue-on-error: true
    steps:
    - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
    - uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master (2026-06-30)
      with:
        toolchain: stable
        components: llvm-tools-preview
    - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
      with:
        shared-key: "ci-ubuntu-latest"
    - uses: taiki-e/install-action@c7eb1735f09259a5035e8e5d44b1406b1cddc0fb # v2.83.0
      with:
        tool: cargo-llvm-cov
    - name: Run coverage
      run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info
    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
      with:
        token: ${{ secrets.CODECOV_TOKEN }}
        files: lcov.info
        flags: unittests
        name: codecov-umbrella
        fail_ci_if_error: false  # informational, consistent with continue-on-error above
        verbose: true

  # cargo-deny subsumes the previous cargo-audit job: its advisories check
  # uses the same RustSec database, and it additionally enforces the license
  # allowlist, duplicate-crate policy, and crates.io-only sources (deny.toml).
  # The pre-push git hook still runs cargo audit locally.
  cargo-deny:
    name: Cargo deny (advisories, licenses, bans, sources)
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
    - uses: EmbarkStudios/cargo-deny-action@bb137d7af7e4fb67e5f82a49c4fce4fad40782fe # v2.0.20
      with:
        command: check
        arguments: --all-features

  # ftr is a published library crate: catch accidental breaking API changes
  # against the latest release on crates.io before they ship.
  semver-checks:
    name: Semver checks
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
    - uses: obi1kenobi/cargo-semver-checks-action@6b69fcf40e9b5fb17adeb57e4b6ecd020649a239 # v2.9

  unused-deps:
    name: Check unused dependencies
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
    - uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master (2026-06-30)
      with:
        toolchain: stable
    - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
      with:
        shared-key: "ci-ubuntu-latest"
    - uses: taiki-e/install-action@c7eb1735f09259a5035e8e5d44b1406b1cddc0fb # v2.83.0
      with:
        tool: cargo-machete
    - name: Check unused dependencies
      run: cargo machete

  outdated:
    name: Check outdated dependencies
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
    - uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master (2026-06-30)
      with:
        toolchain: stable
    - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
      with:
        shared-key: "ci-ubuntu-latest"
    - uses: taiki-e/install-action@c7eb1735f09259a5035e8e5d44b1406b1cddc0fb # v2.83.0
      with:
        tool: cargo-outdated
    - name: Check outdated dependencies
      run: cargo outdated --exit-code 1 || echo "::warning::Outdated dependencies found"

  doc:
    name: Documentation
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
    - uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master (2026-06-30)
      with:
        toolchain: stable
    - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
      with:
        shared-key: "ci-ubuntu-latest"
    - name: Check documentation
      run: cargo doc --no-deps --all-features
      env:
        RUSTDOCFLAGS: -D warnings

  msrv:
    name: Check MSRV
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
    - uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master (2026-06-30)
      with:
        toolchain: 1.85.0
    - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
      with:
        shared-key: "ci-ubuntu-latest"
    - name: Check MSRV
      run: cargo check --all-features

  freebsd:
    name: FreeBSD
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
    - name: Test on FreeBSD 14.0
      uses: vmactions/freebsd-vm@5a72679103d223925653750faa878a143340fbd0 # v1.5.0
      with:
        release: '14.0'
        usesh: true
        # Note: vmactions/freebsd-vm runs as root user by default
        # sudo is not installed, so we conditionally use it only when not root
        prepare: |
          # Set environment variable to ignore OS version mismatch
          export IGNORE_OSVERSION=yes
          # Update package database with IGNORE_OSVERSION
          env IGNORE_OSVERSION=yes pkg update -f || true
          # Install build dependencies
          env IGNORE_OSVERSION=yes pkg install -y rust openssl perl5 pkgconf
          # Install runtime dependencies
          env IGNORE_OSVERSION=yes pkg install -y ca_root_nss
          # Install test utilities
          env IGNORE_OSVERSION=yes pkg install -y jq
        run: |
          # Show environment info
          echo "FreeBSD version:"
          freebsd-version
          echo "Rust version:"
          rustc --version
          echo "Cargo version:"
          cargo --version
          
          # Build the project
          echo "Building ftr..."
          cargo build --release --verbose
          
          # Run unit tests
          echo "Running unit tests..."
          cargo test --lib --verbose
          
          # Run FreeBSD-specific tests
          echo "Running FreeBSD-specific tests..."
          cargo test --lib socket::factory::tests::test_freebsd
          cargo test --lib socket::factory::tests::test_has_non_root_capability

          # Run FreeBSD integration tests (includes the raw-ICMPv6 IPv6
          # tests: the VM runs as root, so the ::1 loopback trace exercises
          # the real v6 probe path; the external v6 trace skips itself when
          # the VM has no IPv6 route, which GitHub-hosted runners don't)
          echo "Running FreeBSD integration tests..."
          cargo test --test freebsd_integration --verbose -- --nocapture
          
          # Test that non-root execution fails appropriately
          echo "Testing non-root error..."
          echo "Current user: $(whoami)"
          echo "User ID: $(id -u)"
          
          if [ "$(id -u)" = "0" ]; then
            echo "⚠ Running as root, skipping non-root error test"
            # When running as root, just verify it works
            ./target/release/ftr --max-hops 1 127.0.0.1 > /dev/null
            echo "✓ Root execution works"
          else
            ERROR_OUTPUT=$(./target/release/ftr 127.0.0.1 2>&1 || true)
            if echo "$ERROR_OUTPUT" | grep -q "requires root privileges"; then
              echo "✓ Non-root error message correct"
            else
              echo "✗ Non-root error message incorrect"
              echo "Error output was: $ERROR_OUTPUT"
              echo "Expected to find 'requires root privileges' in error output"
              exit 1
            fi
          fi
          
          # Test with root (only basic tests due to CI limitations)
          echo "Testing with root privileges..."
          if [ "$(id -u)" = "0" ]; then
            # Already root, no sudo needed
            ./target/release/ftr --max-hops 3 127.0.0.1
          else
            sudo ./target/release/ftr --max-hops 3 127.0.0.1
          fi
          
          # Test JSON output
          echo "Testing JSON output..."
          if [ "$(id -u)" = "0" ]; then
            JSON_OUTPUT=$(./target/release/ftr --json --max-hops 1 127.0.0.1)
          else
            JSON_OUTPUT=$(sudo ./target/release/ftr --json --max-hops 1 127.0.0.1)
          fi
          
          # Validate JSON with jq if available
          if command -v jq >/dev/null 2>&1; then
            echo "$JSON_OUTPUT" | jq .
            echo "✓ JSON output is valid"
          else
            echo "⚠ jq not found, skipping JSON validation"
            echo "$JSON_OUTPUT"
          fi
          
          # Test verbose mode
          echo "Testing verbose mode..."
          if [ "$(id -u)" = "0" ]; then
            ./target/release/ftr -v --max-hops 1 127.0.0.1 2>&1 | grep "Using Raw ICMP"
          else
            sudo ./target/release/ftr -v --max-hops 1 127.0.0.1 2>&1 | grep "Using Raw ICMP"
          fi

          # Test IPv6 loopback trace (raw ICMPv6; needs no external IPv6
          # connectivity, so it must pass even in this v4-only VM)
          echo "Testing IPv6 loopback trace..."
          if [ "$(id -u)" = "0" ]; then
            ./target/release/ftr --max-hops 1 --no-enrich ::1
            ./target/release/ftr -v --max-hops 1 --no-enrich ::1 2>&1 | grep "Using raw ICMPv6"
          else
            sudo ./target/release/ftr --max-hops 1 --no-enrich ::1
            sudo ./target/release/ftr -v --max-hops 1 --no-enrich ::1 2>&1 | grep "Using raw ICMPv6"
          fi
          echo "✓ IPv6 loopback trace works"