liblevenshtein 0.9.1

Levenshtein/Universal Automata for approximate string matching using various dictionary backends
Documentation
name: CI - Tests and Linting

on:
  push:
    branches: [ master, develop, dylon/* ]
  pull_request:
    branches: [ master, develop ]
  workflow_dispatch:

env:
  CARGO_TERM_COLOR: always
  RUST_BACKTRACE: 1
  # Note: RUSTFLAGS is configured per-platform in job env sections
  # to provide platform-specific CPU features (aes+sse2 for x86, aes+neon for ARM)

jobs:
  test:
    name: Test Suite
    runs-on: ${{ matrix.platform.os }}
    strategy:
      fail-fast: false
      matrix:
        rust: [stable, nightly]
        platform:
          - os: ubuntu-latest
            target: x86_64-unknown-linux-gnu
            rustflags: "-C target-feature=+aes,+sse2"
          - os: macos-latest
            target: aarch64-apple-darwin
            rustflags: "-C target-feature=+aes,+neon"

    steps:
    - name: Checkout repository
      uses: actions/checkout@v4
      with:
        fetch-depth: 1

    - name: Checkout dev sibling crates
      uses: ./.github/actions/checkout-dev-siblings

    - name: Register Tesseract Repository (Linux)
      if: runner.os == 'Linux'
      run: sudo add-apt-repository ppa:alex-p/tesseract-ocr5

    - name: Update package index (Linux)
      if: runner.os == 'Linux'
      run: sudo apt-get update

    - name: Install protobuf compiler (Linux)
      if: runner.os == 'Linux'
      run: sudo apt-get install -y protobuf-compiler

    - name: Install protobuf compiler (macOS)
      if: runner.os == 'macOS'
      run: brew install protobuf

    - name: Install Leptonica (Linux)
      if: runner.os == 'Linux'
      run: sudo apt-get install -y libleptonica-dev

    - name: Install Leptonica (macOS)
      if: runner.os == 'macOS'
      run: brew install leptonica

    - name: Install Tesseract (Linux)
      if: runner.os == 'Linux'
      run: sudo apt-get install -y tesseract-ocr libtesseract-dev

    - name: Install Tesseract (macOS)
      if: runner.os == 'macOS'
      run: brew install tesseract

    - name: Install Rust toolchain
      uses: actions-rust-lang/setup-rust-toolchain@v1
      with:
        toolchain: ${{ matrix.rust }}
        target: ${{ matrix.platform.target }}
        components: rustfmt, clippy

    - name: Cache cargo registry
      uses: actions/cache@v4
      with:
        path: ~/.cargo/registry
        key: ${{ runner.os }}-${{ matrix.rust }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
        restore-keys: |
          ${{ runner.os }}-${{ matrix.rust }}-cargo-registry-

    - name: Cache cargo index
      uses: actions/cache@v4
      with:
        path: ~/.cargo/git
        key: ${{ runner.os }}-${{ matrix.rust }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
        restore-keys: |
          ${{ runner.os }}-${{ matrix.rust }}-cargo-index-

    - name: Cache cargo build
      uses: actions/cache@v4
      with:
        path: target
        key: ${{ runner.os }}-${{ matrix.rust }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
        restore-keys: |
          ${{ runner.os }}-${{ matrix.rust }}-cargo-build-target-

    - name: Build library
      env:
        RUSTFLAGS: ${{ matrix.platform.rustflags }}
      run: |
        echo "::group::Building liblevenshtein"
        cargo build --all-features --target ${{ matrix.platform.target }} --verbose
        echo "::endgroup::"

    - name: Build CLI
      env:
        RUSTFLAGS: ${{ matrix.platform.rustflags }}
      run: |
        echo "::group::Building CLI"
        cargo build --bin liblevenshtein --features cli,compression,protobuf --target ${{ matrix.platform.target }} --verbose
        echo "::endgroup::"

    - name: Run unit tests
      env:
        RUSTFLAGS: ${{ matrix.platform.rustflags }}
      run: |
        echo "::group::Running unit tests"
        cargo test --lib --all-features --target ${{ matrix.platform.target }} --verbose
        echo "::endgroup::"

    - name: Run integration tests
      env:
        RUSTFLAGS: ${{ matrix.platform.rustflags }}
      run: |
        echo "::group::Running integration tests"
        cargo test --test '*' --all-features --target ${{ matrix.platform.target }} --verbose -- --nocapture
        echo "::endgroup::"

    - name: Run doc tests
      env:
        RUSTFLAGS: ${{ matrix.platform.rustflags }}
      run: |
        echo "::group::Running documentation tests"
        cargo test --doc --all-features --target ${{ matrix.platform.target }} --verbose
        echo "::endgroup::"

    - name: Build examples
      env:
        RUSTFLAGS: ${{ matrix.platform.rustflags }}
      run: |
        echo "::group::Building examples"
        cargo build --examples --all-features --target ${{ matrix.platform.target }} --verbose
        echo "::endgroup::"

    - name: Upload test artifacts on failure
      if: failure()
      uses: actions/upload-artifact@v4
      with:
        name: test-results-${{ matrix.platform.os }}-${{ matrix.rust }}
        path: |
          target/${{ matrix.platform.target }}/debug/
          tests/*.rs
          examples/*.rs
        retention-days: 7

  # lint:
  #   name: Lint and Format Check
  #   runs-on: ubuntu-latest
  #   env:
  #     # Use x86_64 Linux flags for linting
  #     RUSTFLAGS: "-C target-feature=+aes,+sse2"

  #   steps:
  #   - name: Checkout repository
  #     uses: actions/checkout@v4
  #     with:
  #       fetch-depth: 1

  #   - name: Install protobuf compiler
  #     run: sudo apt-get update && sudo apt-get install -y protobuf-compiler

  #   - name: Install Rust toolchain
  #     uses: actions-rust-lang/setup-rust-toolchain@v1
  #     with:
  #       toolchain: stable
  #       target: x86_64-unknown-linux-gnu
  #       components: rustfmt, clippy

  #   - name: Check formatting
  #     run: cargo fmt -- --check

  #   - name: Run clippy
  #     run: |
  #       # Lint workspace code
  #       # --no-deps: Don't lint dependencies (avoids failing on upstream warnings)
  #       # cargo clippy --no-deps --all-features -- -D warnings
  #       cargo clippy --no-deps -- -D warnings

  benchmarks:
    name: Benchmarks
    runs-on: ubuntu-latest
    if: github.event_name == 'push' && github.ref == 'refs/heads/master'
    env:
      RUSTFLAGS: "-C target-feature=+aes,+sse2"

    steps:
    - name: Checkout repository
      uses: actions/checkout@v4
      with:
        fetch-depth: 1

    - name: Checkout dev sibling crates
      uses: ./.github/actions/checkout-dev-siblings

    - name: Install protobuf compiler
      run: sudo apt-get update && sudo apt-get install -y protobuf-compiler

    - name: Install Rust toolchain
      uses: actions-rust-lang/setup-rust-toolchain@v1
      with:
        toolchain: nightly
        target: x86_64-unknown-linux-gnu
        components: rustfmt, clippy

    - name: Run benchmarks
      run: |
        echo "::group::Running benchmarks"
        cargo bench --all-features --target x86_64-unknown-linux-gnu --no-fail-fast -- --output-format bencher | tee output.txt
        echo "::endgroup::"

    - name: Parse and format benchmark results
      run: |
        echo "## Benchmark Results" > benchmark-summary.md
        echo "" >> benchmark-summary.md
        echo "| Benchmark | Time | Throughput |" >> benchmark-summary.md
        echo "|-----------|------|------------|" >> benchmark-summary.md

        # Parse bencher output format: test name ... bench: <time> ns/iter (+/- <variance>)
        grep "bench:" output.txt | while read -r line; do
          # Extract benchmark name (everything before '...') and trim whitespace
          name=$(echo "$line" | sed 's/test \(.*\) \.\.\..*/\1/' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')

          # Extract time and unit
          time=$(echo "$line" | grep -oP '[\d,]+ ns/iter' | head -1)

          # Calculate throughput if available
          if [[ "$time" =~ ([0-9,]+) ]]; then
            ns="${BASH_REMATCH[1]//,/}"
            if [ "$ns" -gt 0 ]; then
              ops_per_sec=$(( 1000000000 / ns ))
              # Format with thousand separators
              throughput=$(printf "%'d ops/s" $ops_per_sec)
            else
              throughput="-"
            fi
          else
            throughput="-"
          fi

          echo "| \`$name\` | $time | $throughput |" >> benchmark-summary.md
        done

        echo "" >> benchmark-summary.md
        echo "**Note:** Benchmarks are run on GitHub Actions runners and may vary." >> benchmark-summary.md
        echo "" >> benchmark-summary.md
        echo "Full results available in artifacts." >> benchmark-summary.md

    - name: Upload benchmark results
      uses: actions/upload-artifact@v4
      with:
        name: benchmark-results
        path: |
          output.txt
          benchmark-summary.md
        retention-days: 30

  test-report:
    name: Test Report Summary
    runs-on: ubuntu-latest
    # needs: [test, lint, benchmarks]
    needs: [test, benchmarks]
    if: always()

    steps:
    - name: Download benchmark results
      if: needs.benchmarks.result == 'success'
      uses: actions/download-artifact@v4
      with:
        name: benchmark-results
        path: ./benchmark-results
      continue-on-error: true

    - name: Generate summary
      run: |
        echo "## Test Results Summary" >> $GITHUB_STEP_SUMMARY
        echo "" >> $GITHUB_STEP_SUMMARY
        echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY
        echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY
        echo "| Tests | ${{ needs.test.result }} |" >> $GITHUB_STEP_SUMMARY
        # echo "| Lint | ${{ needs.lint.result }} |" >> $GITHUB_STEP_SUMMARY
        echo "| Benchmarks | ${{ needs.benchmarks.result }} |" >> $GITHUB_STEP_SUMMARY
        echo "" >> $GITHUB_STEP_SUMMARY

        # if [ "${{ needs.test.result }}" == "success" ] && [ "${{ needs.lint.result }}" == "success" ]; then
        #   echo "✅ All checks passed!" >> $GITHUB_STEP_SUMMARY
        # else
        #   echo "❌ Some checks failed. Please review the logs." >> $GITHUB_STEP_SUMMARY
        # fi

        echo "" >> $GITHUB_STEP_SUMMARY

        # Include benchmark summary if available
        if [ -f "./benchmark-results/benchmark-summary.md" ]; then
          echo "---" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          cat ./benchmark-results/benchmark-summary.md >> $GITHUB_STEP_SUMMARY
        elif [ "${{ needs.benchmarks.result }}" == "skipped" ]; then
          echo "---" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "## Benchmark Results" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "⏭️ Benchmarks skipped (only run on master branch)" >> $GITHUB_STEP_SUMMARY
        fi