ftr 0.7.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@v4
    - name: Install Rust
      uses: dtolnay/rust-toolchain@master
      with:
        toolchain: ${{ matrix.rust }}
    - uses: Swatinem/rust-cache@v2
      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@v4
    - uses: dtolnay/rust-toolchain@stable
      with:
        components: rustfmt
    - uses: Swatinem/rust-cache@v2
      with:
        shared-key: "ci-ubuntu-latest"
    - name: Check formatting
      run: cargo fmt -- --check

  clippy:
    name: Clippy
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: dtolnay/rust-toolchain@stable
      with:
        components: clippy
    - uses: Swatinem/rust-cache@v2
      with:
        shared-key: "ci-ubuntu-latest"
    - name: Run clippy
      run: cargo clippy -- -D warnings

  coverage:
    name: Code coverage
    runs-on: ubuntu-latest
    continue-on-error: true  # Coverage is informational; network-dependent tests may flake under instrumentation
    steps:
    - uses: actions/checkout@v4
    - uses: dtolnay/rust-toolchain@stable
    - uses: Swatinem/rust-cache@v2
      with:
        shared-key: "ci-ubuntu-latest"
    - uses: taiki-e/install-action@v2
      with:
        tool: cargo-tarpaulin
    - name: Run coverage
      run: cargo tarpaulin --engine llvm --verbose --all-features --workspace --timeout 120 --out xml --exclude-files "*/tests/*" --exclude-files "*/examples/*"
    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v4
      with:
        token: ${{ secrets.CODECOV_TOKEN }}
        files: ./cobertura.xml
        flags: unittests
        name: codecov-umbrella
        fail_ci_if_error: true
        verbose: true

  security-audit:
    name: Security audit
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: actions-rust-lang/audit@v1
      with:
        token: ${{ secrets.GITHUB_TOKEN }}

  unused-deps:
    name: Check unused dependencies
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: dtolnay/rust-toolchain@stable
    - uses: Swatinem/rust-cache@v2
      with:
        shared-key: "ci-ubuntu-latest"
    - uses: taiki-e/install-action@v2
      with:
        tool: cargo-machete
    - name: Check unused dependencies
      run: cargo machete

  outdated:
    name: Check outdated dependencies
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: dtolnay/rust-toolchain@stable
    - uses: Swatinem/rust-cache@v2
      with:
        shared-key: "ci-ubuntu-latest"
    - uses: taiki-e/install-action@v2
      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@v4
    - uses: dtolnay/rust-toolchain@stable
    - uses: Swatinem/rust-cache@v2
      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@v4
    - uses: dtolnay/rust-toolchain@master
      with:
        toolchain: 1.85.0
    - uses: Swatinem/rust-cache@v2
      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@v4
    - name: Test on FreeBSD 14.0
      uses: vmactions/freebsd-vm@v1
      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
          
          # 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