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
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
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, 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