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 Rust toolchain (with llvm-tools)
uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 with:
toolchain: stable
components: llvm-tools-preview
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@0bc4cd8a3e21db4cb9519857377b0c8c5e150de5 with:
tool: cargo-llvm-cov
- name: Install Python build/test dependencies
run: |
python -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
pip install maturin pytest junitparser numpy
- name: Run pytest under cargo-llvm-cov
run: |
set -euo pipefail
source .venv/bin/activate
source <(cargo llvm-cov show-env --export-prefix)
maturin develop --manifest-path crates/python/Cargo.toml
pytest tests/python/ --junitxml=pytest-results.xml -v
cargo llvm-cov report --lcov --output-path coverage-python.lcov
echo "=== Python-driven Rust source coverage written to coverage-python.lcov ==="
- name: Verify package import
run: |
source .venv/bin/activate
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.lcov
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()
cpp-test:
name: C++ Tests (${{ matrix.compiler }}, ${{ matrix.cxxstd }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
compiler: [gcc, clang]
cxxstd: [c++17, c++20]
include:
- compiler: gcc
cc: gcc
cxx: g++
packages: ""
- compiler: clang
cc: clang
cxx: clang++
packages: "clang"
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 with:
lfs: true
- name: Install Rust toolchain
run: rustup toolchain install stable --profile minimal
- name: Install compiler
if: matrix.packages != ''
run: |
sudo apt-get update
sudo apt-get install -y ${{ matrix.packages }}
- name: Build Rust library
run: cargo build --release
- name: Run C++ tests with XML output
env:
CXX: ${{ matrix.cxx }}
run: make test-cpp-xml CXXSTD=${{ matrix.cxxstd }}
- name: Upload C++ test results
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f with:
name: cpp-test-results-${{ matrix.compiler }}-${{ matrix.cxxstd }}
path: build/test-results/*.xml
retention-days: 7
if: always()
- name: Test Summary
uses: EnricoMi/publish-unit-test-result-action@27d65e188ec43221b20d26de30f4892fad91df2f with:
files: build/test-results/*.xml
check_name: "Test Results (C++ ${{ matrix.compiler }}, ${{ matrix.cxxstd }})"
comment_mode: "off"
if: always()
cpp-test-asan:
name: C++ Tests (ASan/UBSan)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 with:
lfs: true
- name: Install Rust toolchain
run: rustup toolchain install stable --profile minimal
- name: Build Rust library
run: cargo build --release
- name: Run C++ tests under ASan/UBSan
run: make test-cpp-asan
docs-build:
name: Docs Build (Doxygen)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
- name: Install Rust toolchain
run: rustup toolchain install stable --profile minimal
- name: Install Doxygen
run: |
sudo apt-get update
sudo apt-get install -y doxygen graphviz
- name: Build Rust library
run: make lib
- name: Generate documentation
run: make docs
- name: Upload documentation artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f with:
name: doxygen-html
path: build/docs/html
retention-days: 7
cpp-test-cxx23:
name: C++ Tests (C++23 smoke)
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 with:
lfs: true
- name: Install Rust toolchain
run: rustup toolchain install stable --profile minimal
- name: Install GCC 13 (for C++23 std::expected support)
run: |
sudo apt-get update
sudo apt-get install -y g++-13
echo "CXX=g++-13" >> $GITHUB_ENV
- name: Build Rust library
run: make lib
- name: Build and run C++ tests on C++23
run: make test-cpp CXX=$CXX CXXSTD=c++23
sonarcloud:
name: SonarCloud
runs-on: ubuntu-latest
needs: [rust-and-c-test, cpp-test, cpp-test-asan, 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.lcov clippy-report.json 2>/dev/null || echo "Some files missing"
echo ""
if [ -f lcov.info ]; then
echo "=== Combined LCOV Summary (Rust unit + C API) ==="
RUST_FILES=$(grep -c "^SF:" lcov.info || echo "0")
echo "Source files with coverage: $RUST_FILES"
fi
if [ -f coverage-python.lcov ]; then
echo "=== Python-driven Rust LCOV Summary ==="
PY_FILES=$(grep -c "^SF:" coverage-python.lcov || echo "0")
echo "Source files with Python-driven coverage: $PY_FILES"
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,coverage-python.lcov
-Dsonar.rust.clippy.reportPaths=clippy-report.json