edgefirst-schemas 2.2.1

Message schemas for EdgeFirst Perception - ROS2 Common Interfaces, Foxglove, and custom types
Documentation
name: Test

on:
  push:
    branches:
      - '**'
  pull_request:
    types: [opened, synchronize, reopened]

env:
  CARGO_TERM_COLOR: always

jobs:
  # ============================================================================
  # Version Sync Check (fast, runs first)
  # ============================================================================
  version-check:
    name: Version Check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1

      - name: Check version synchronization
        run: bash .github/scripts/check_version_sync.sh

  # ============================================================================
  # Rust Format Check (fast, runs in parallel)
  # ============================================================================
  format:
    name: Format
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1

      - name: Install Rust
        run: |
          rustup toolchain install stable --profile minimal
          rustup component add rustfmt

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

  # ============================================================================
  # Rust and C Tests with Combined Coverage
  # ============================================================================
  # Both Rust unit tests and C API tests run in the same job to properly merge
  # coverage data. When tests run in separate jobs, profraw files are generated
  # separately and cannot be merged by cargo-llvm-cov.
  rust-and-c-test:
    name: Rust & C Tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
        with:
          fetch-depth: 0  # Full history for SonarCloud blame
          lfs: true

      - name: Install Rust toolchain
        run: |
          rustup toolchain install stable --profile minimal
          rustup component add llvm-tools-preview clippy

      - name: Install cargo-llvm-cov
        uses: taiki-e/install-action@0bc4cd8a3e21db4cb9519857377b0c8c5e150de5 # v2.67.2
        with:
          tool: cargo-llvm-cov

      - name: Install cargo-nextest
        uses: taiki-e/install-action@0bc4cd8a3e21db4cb9519857377b0c8c5e150de5 # v2.67.2
        with:
          tool: nextest

      - name: Install Criterion test framework
        run: |
          sudo apt-get update
          sudo apt-get install -y libcriterion-dev

      - name: Run Clippy
        run: cargo clippy --all-targets --all-features -- -D warnings

      - name: Generate Clippy report for SonarCloud
        run: |
          # Generate JSON report for SonarCloud Rust analysis
          cargo clippy --all-targets --all-features --message-format=json > clippy-report.json 2>&1 || true
          echo "Clippy report generated: $(wc -l < clippy-report.json) lines"

      - name: Run Rust unit tests with coverage
        run: |
          # Run Rust tests with coverage instrumentation using cargo-llvm-cov
          # The --no-report flag delays report generation so we can add C test coverage
          cargo llvm-cov nextest --all-features --workspace --profile ci --no-report
          echo "=== Rust unit tests completed ==="

      - name: Run Rust doc tests
        run: |
          # Run doc tests separately (nextest doesn't support doc tests)
          # Note: doc tests don't contribute to llvm-cov coverage
          cargo test --doc

      - name: Set up C test coverage environment
        run: |
          # Get the coverage environment from llvm-cov to run C tests with same profraw location
          # This ensures C test coverage is accumulated with Rust test coverage
          source <(cargo llvm-cov show-env --export-prefix)
          # Update profile path to write to llvm-cov-target directory
          export LLVM_PROFILE_FILE="${PWD}/target/llvm-cov-target/schemas-c-%p-%m.profraw"
          echo "LLVM_PROFILE_FILE=$LLVM_PROFILE_FILE" >> $GITHUB_ENV
          echo "C tests will write profraw to: $LLVM_PROFILE_FILE"

      - name: Run C API tests
        run: |
          # Copy instrumented library to where Makefile/gcc expect it
          # This overwrites any non-instrumented library that might exist
          mkdir -p target/debug/deps
          cp -f target/llvm-cov-target/debug/deps/libedgefirst_schemas.* target/debug/deps/ 2>/dev/null || true
          cp -f target/llvm-cov-target/debug/deps/libedgefirst_schemas.* target/debug/ 2>/dev/null || true

          # Create SONAME symlink so the runtime linker can find the library.
          # build.rs sets SONAME to libedgefirst_schemas.so.<major>, but cargo
          # produces the file as libedgefirst_schemas.so — the symlink bridges this.
          MAJOR=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\([0-9]*\).*/\1/')
          ln -sf libedgefirst_schemas.so target/debug/deps/libedgefirst_schemas.so.${MAJOR}

          # Verify we have the instrumented library (check for profile symbols)
          echo "Verifying instrumented library:"
          nm target/debug/deps/libedgefirst_schemas.so 2>/dev/null | grep -c "__llvm_profile" || \
          nm target/debug/deps/libedgefirst_schemas.dylib 2>/dev/null | grep -c "__llvm_profile" || \
          echo "Warning: Library may not be instrumented"
          
          # Build C tests without rebuilding the library (lib target would overwrite instrumented library)
          # Build test binaries directly
          mkdir -p build build/test-results
          for src in tests/c/test_*.c; do
            name=$(basename $src .c)
            echo "Compiling $name..."
            gcc -Wall -Wextra -Werror -std=c11 -I./include -I/usr/include/criterion \
                -o build/$name $src \
                -L./target/debug/deps -ledgefirst_schemas -lcriterion -lm \
                -Wl,-rpath,./target/debug/deps
          done
          
          # Run C tests
          echo "Running C test suite..."
          for test in build/test_*; do
            name=$(basename $test)
            echo "Running $name..."
            ./$test --output=xml:build/test-results/$name.xml || exit 1
          done
          
          echo "=== C API tests completed ==="
          echo "Profraw files in coverage directory:"
          find target/llvm-cov-target -name "*.profraw" | wc -l

      - name: Generate combined coverage report
        run: |
          # Generate single LCOV report from all accumulated profraw files
          # This includes coverage from both Rust unit tests and C API tests
          cargo llvm-cov report --lcov --output-path lcov.info
          
          # Convert absolute paths to relative for SonarCloud compatibility
          sed -i "s|SF:$PWD/|SF:|g" lcov.info
          
          echo "=== Combined Coverage Summary ==="
          if [ -f lcov.info ]; then
            RUST_FILES=$(grep -c "^SF:" lcov.info || echo "0")
            echo "Source files with coverage data: $RUST_FILES"
          fi

      - name: Generate coverage summary
        id: coverage
        run: |
          TOTAL_LINES=$(grep -E "^LF:" lcov.info 2>/dev/null | cut -d: -f2 | paste -sd+ | bc 2>/dev/null || echo "0")
          COVERED_LINES=$(grep -E "^LH:" lcov.info 2>/dev/null | cut -d: -f2 | paste -sd+ | bc 2>/dev/null || echo "0")
          if [ -z "$TOTAL_LINES" ]; then TOTAL_LINES=0; fi
          if [ -z "$COVERED_LINES" ]; then COVERED_LINES=0; fi
          if [ "$TOTAL_LINES" -gt 0 ]; then
            COVERAGE=$(echo "scale=2; $COVERED_LINES * 100 / $TOTAL_LINES" | bc)
          else
            COVERAGE="0"
          fi
          echo "coverage=$COVERAGE" >> $GITHUB_OUTPUT
          echo "Combined coverage: $COVERAGE% ($COVERED_LINES/$TOTAL_LINES lines)"

      - name: Upload combined coverage artifact
        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
        with:
          name: rust-coverage
          path: lcov.info
          retention-days: 7

      - name: Upload Clippy report artifact
        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
        with:
          name: clippy-report
          path: clippy-report.json
          retention-days: 7

      - name: Test Summary
        uses: EnricoMi/publish-unit-test-result-action@27d65e188ec43221b20d26de30f4892fad91df2f # v2.22.0
        with:
          files: |
            target/nextest/ci/junit.xml
            build/test-results/*.xml
          check_name: "Test Results (Rust + C API)"
          comment_mode: "off"
        if: always()

  # ============================================================================
  # Python Tests with Coverage
  # ============================================================================
  python-test:
    name: Python Tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
        with:
          lfs: true

      - name: Set up Python
        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -e ".[test]"

      - name: Run tests with coverage
        run: |
          pytest tests/python/ \
            --cov=edgefirst \
            --cov-report=xml:coverage-python.xml \
            --cov-report=term-missing \
            --junitxml=pytest-results.xml \
            -v

      - name: Verify package import
        run: |
          # Verify the package can be imported successfully
          python -c "import edgefirst.schemas; print('Package import OK')"

      - name: Upload Python coverage artifact
        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
        with:
          name: python-coverage
          path: coverage-python.xml
          retention-days: 7

      - name: Upload test results
        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
        with:
          name: python-test-results
          path: pytest-results.xml
          retention-days: 7
        if: always()

      - name: Test Summary
        uses: EnricoMi/publish-unit-test-result-action@27d65e188ec43221b20d26de30f4892fad91df2f # v2.22.0
        with:
          files: pytest-results.xml
          check_name: "Test Results (Python)"
          comment_mode: "off"
        if: always()

  # ============================================================================
  # SonarCloud Analysis (runs after all tests complete)
  # ============================================================================
  sonarcloud:
    name: SonarCloud
    runs-on: ubuntu-latest
    needs: [rust-and-c-test, python-test]
    # Only run on main repo (not forks) due to secrets
    if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push'
    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
        with:
          fetch-depth: 0  # Full history for accurate blame

      - name: Download combined Rust/C coverage
        uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
        with:
          name: rust-coverage
          path: .

      - name: Download Python coverage
        uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
        with:
          name: python-coverage
          path: .

      - name: Download Clippy report
        uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
        with:
          name: clippy-report
          path: .

      - name: Verify coverage files
        run: |
          echo "=== Coverage Files ==="
          ls -la lcov.info coverage-python.xml clippy-report.json 2>/dev/null || echo "Some files missing"
          echo ""
          if [ -f lcov.info ]; then
            echo "=== Combined LCOV Summary (Rust + C API) ==="
            RUST_FILES=$(grep -c "^SF:" lcov.info || echo "0")
            echo "Source files with coverage: $RUST_FILES"
          fi
          if [ -f coverage-python.xml ]; then
            echo ""
            echo "=== Python Coverage Summary ==="
            # Extract line rate from XML
            LINE_RATE=$(grep -oP 'line-rate="\K[^"]+' coverage-python.xml | head -1 || echo "0")
            COVERAGE_PCT=$(echo "$LINE_RATE * 100" | bc 2>/dev/null || echo "N/A")
            echo "Python line coverage: ${COVERAGE_PCT}%"
          fi

      - name: SonarCloud Scan
        uses: SonarSource/sonarqube-scan-action@fd88b7d7ccbaefd23d8f36f73b59db7a3d246602 # v6.0.0
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
        with:
          args: >
            -Dsonar.rust.lcov.reportPaths=lcov.info
            -Dsonar.rust.clippy.reportPaths=clippy-report.json
            -Dsonar.python.coverage.reportPaths=coverage-python.xml