name: Test
on:
push:
branches:
- '**'
pull_request:
types: [opened, synchronize, reopened]
env:
CARGO_TERM_COLOR: always
jobs:
version-check:
name: Version Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
- name: Check version synchronization
run: bash .github/scripts/check_version_sync.sh
format:
name: Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
- name: Install Rust
run: |
rustup toolchain install stable --profile minimal
rustup component add rustfmt
- name: Check formatting
run: cargo fmt -- --check
rust-and-c-test:
name: Rust & C Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 with:
fetch-depth: 0 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 with:
tool: cargo-llvm-cov
- name: Install cargo-nextest
uses: taiki-e/install-action@0bc4cd8a3e21db4cb9519857377b0c8c5e150de5 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 with:
name: rust-coverage
path: lcov.info
retention-days: 7
- name: Upload Clippy report artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f with:
name: clippy-report
path: clippy-report.json
retention-days: 7
- name: Test Summary
uses: EnricoMi/publish-unit-test-result-action@27d65e188ec43221b20d26de30f4892fad91df2f with:
files: |
target/nextest/ci/junit.xml
build/test-results/*.xml
check_name: "Test Results (Rust + C API)"
comment_mode: "off"
if: always()
python-test:
name: Python Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 with:
lfs: true
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 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 with:
name: python-coverage
path: coverage-python.xml
retention-days: 7
- name: Upload test results
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f 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 with:
files: pytest-results.xml
check_name: "Test Results (Python)"
comment_mode: "off"
if: always()
sonarcloud:
name: SonarCloud
runs-on: ubuntu-latest
needs: [rust-and-c-test, python-test]
if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push'
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 with:
fetch-depth: 0
- name: Download combined Rust/C coverage
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 with:
name: rust-coverage
path: .
- name: Download Python coverage
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 with:
name: python-coverage
path: .
- name: Download Clippy report
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 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 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