Test with coverage workflow (sefcom/oxidizer)
The Test with coverage workflow from sefcom/oxidizer, explained and optimized by Latchkey.
CI health: C - fair
Point runs-on at Latchkey and get caching, job timeouts, self-healing for flaky steps, and up to 58% lower cost, applied automatically.
What it does
This is the Test with coverage workflow from the sefcom/oxidizer repository, a real project running GitHub Actions. It is shown here with attribution under its BSD-2-Clause license.
Below, Latchkey shows a faster, safer version produced by its optimization engine.
The workflow
name: Test with coverage
on:
push:
branches:
- master
pull_request:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
SKIP_SLOW_TESTS: 1
RUSTFLAGS: "-C instrument-coverage -C codegen-units=1 -C link-dead-code"
LLVM_PROFILE_FILE: "%p-%m.profraw"
CFLAGS: "--coverage -O0 -g"
LDFLAGS: "--coverage -g"
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
- uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v6
with:
python-version: "3.10"
- name: Restore test durations cache
uses: actions/cache/restore@v5
with:
path: test_durations.json
key: cov-test-durations-${{ github.sha }}
restore-keys: cov-test-durations-
- name: Build
run: uv sync -v -p 3.10 --all-groups
- name: Archive environment
run: tar -cpf /tmp/env.tzst --absolute-names --zstd $PWD $UV_CACHE_DIR $UV_PYTHON_INSTALL_DIR
- name: Upload environment artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
with:
name: env
path: /tmp/env.tzst
if-no-files-found: error
compression-level: 0
test:
needs: [build]
name: Test
runs-on: ubuntu-latest
strategy:
matrix:
runner_id: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
steps:
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: env
path: .
- name: Extract environment
run: |
tar -xpf $PWD/env.tzst -C /
rm env.tzst
- name: Download test binaries
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4
with:
repository: angr/binaries
path: binaries
- name: Relocate binaries directory
run: mv binaries ..
- name: Run tests
run: |
source .venv/bin/activate
pytest -vv \
-n auto --splits 10 --group ${{ matrix.runner_id }} --splitting-algorithm=least_duration \
--cov=angr --cov=tests --cov-report=xml \
--junitxml=junit.xml -o junit_family=legacy \
--store-durations --clean-durations --durations-path=test_durations.json \
|| [[ $? -lt 2 ]] # Accept success and test failures, fail on infrastructure problems (exit codes >1)
gcovr -r native --print-summary --xml-pretty -o coverage-native.xml
- name: Check for results
run: |
[[ -e junit.xml && -e coverage.xml && -e coverage-native.xml && -e test_durations.json ]]
- name: Upload results artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: results-${{ matrix.runner_id }}
if-no-files-found: error
path: |
./junit.xml
./coverage.xml
./coverage-native.xml
./test_durations.json
./*.profraw
test_rust:
name: Test Rust packages
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
- uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1
- uses: taiki-e/install-action@710817a1645ef40daad5bcde7431ceccf6cc3528 # v2
with:
tool: cargo-llvm-cov
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v5
with:
python-version: "3.10"
- name: Run tests
run: |
cargo test --release
cargo llvm-cov --lcov --output-path lcov.info
- name: Upload results artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: results-rust
if-no-files-found: error
path: lcov.info
report:
name: Report
runs-on: ubuntu-latest
permissions:
id-token: write
checks: write
needs: [test, test_rust]
steps:
- name: Download environment
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: env
path: .
- name: Extract environment
run: |
tar -xpf $PWD/env.tzst -C /
rm env.tzst
- name: Download results
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
pattern: "results-*"
path: results
- name: Combine Rust coverage
run: |
find results
# XXX: Workaround slow `Processing triggers for man-db...`
echo 'set man-db/auto-update false' | sudo debconf-communicate
sudo dpkg-reconfigure man-db
sudo apt-get install -qq llvm-20 llvm-20-tools
echo "Merging coverage..."
llvm-profdata-20 merge -sparse $(find . -name '*.profraw') -o default.profdata
echo "Generating report..."
llvm-cov-20 report ./angr/rustylib*.so -instr-profile=default.profdata --sources $(find native -name "*.rs")
llvm-cov-20 export ./angr/rustylib*.so -instr-profile=default.profdata --sources $(find native -name "*.rs") --format=lcov > lcov-py.info
- name: Collect results
id: files
run: |
coverage=$(find . -type f \( -name 'coverage*.xml' -o -name 'lcov*.info' \) | paste -sd, -)
echo "coverage=$coverage" >> "$GITHUB_OUTPUT"
junit=$(find . -type f -name 'junit.xml' | paste -sd, -)
echo "junit=$junit" >> "$GITHUB_OUTPUT"
- name: Upload test coverage to Codecov
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
with:
use_oidc: true
verbose: true
files: ${{ steps.files.outputs.coverage }}
- name: Upload test results to Codecov
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
with:
use_oidc: true
fail_ci_if_error: true
verbose: true
files: ${{ steps.files.outputs.junit }}
report_type: test_results
- name: Combine test durations
run: jq -Ss 'add' results/**/test_durations.json > test_durations.json
- name: Upload combined test durations artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: test_durations
path: ./test_durations.json
if-no-files-found: error
- name: Update test durations cache
uses: actions/cache/save@v5
with:
path: test_durations.json
key: cov-test-durations-${{ github.sha }}
- name: Publish test results
uses: EnricoMi/publish-unit-test-result-action@27d65e188ec43221b20d26de30f4892fad91df2f # v2
with:
files: "results/**/junit.xml"
action_fail_on_inconclusive: true
comment_mode: off # codecov already leaves a comment, and more details
# from this action can be found in the check it creates.The same workflow, on Latchkey
Estimated ~20% faster on cache hits, plus fewer wasted runs and a safer supply chain. Added and changed lines are highlighted.
name: Test with coverage on: push: branches: - master pull_request: workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: SKIP_SLOW_TESTS: 1 RUSTFLAGS: "-C instrument-coverage -C codegen-units=1 -C link-dead-code" LLVM_PROFILE_FILE: "%p-%m.profraw" CFLAGS: "--coverage -O0 -g" LDFLAGS: "--coverage -g" jobs: build: timeout-minutes: 30 name: Build runs-on: latchkey-small steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4 - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 - uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v6 with: python-version: "3.10" - name: Restore test durations cache uses: actions/cache/restore@v5 with: path: test_durations.json key: cov-test-durations-${{ github.sha }} restore-keys: cov-test-durations- - name: Build run: uv sync -v -p 3.10 --all-groups - name: Archive environment run: tar -cpf /tmp/env.tzst --absolute-names --zstd $PWD $UV_CACHE_DIR $UV_PYTHON_INSTALL_DIR - name: Upload environment artifact uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4 with: name: env path: /tmp/env.tzst if-no-files-found: error compression-level: 0 test: timeout-minutes: 30 needs: [build] name: Test runs-on: latchkey-small strategy: matrix: runner_id: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] steps: - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: env path: . - name: Extract environment run: | tar -xpf $PWD/env.tzst -C / rm env.tzst - name: Download test binaries uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4 with: repository: angr/binaries path: binaries - name: Relocate binaries directory run: mv binaries .. - name: Run tests run: | source .venv/bin/activate pytest -vv \ -n auto --splits 10 --group ${{ matrix.runner_id }} --splitting-algorithm=least_duration \ --cov=angr --cov=tests --cov-report=xml \ --junitxml=junit.xml -o junit_family=legacy \ --store-durations --clean-durations --durations-path=test_durations.json \ || [[ $? -lt 2 ]] # Accept success and test failures, fail on infrastructure problems (exit codes >1) gcovr -r native --print-summary --xml-pretty -o coverage-native.xml - name: Check for results run: | [[ -e junit.xml && -e coverage.xml && -e coverage-native.xml && -e test_durations.json ]] - name: Upload results artifact uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: results-${{ matrix.runner_id }} if-no-files-found: error path: | ./junit.xml ./coverage.xml ./coverage-native.xml ./test_durations.json ./*.profraw test_rust: timeout-minutes: 30 name: Test Rust packages runs-on: latchkey-small steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4 - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 - uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1 - uses: taiki-e/install-action@710817a1645ef40daad5bcde7431ceccf6cc3528 # v2 with: tool: cargo-llvm-cov - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v5 with: cache: 'pip' python-version: "3.10" - name: Run tests run: | cargo test --release cargo llvm-cov --lcov --output-path lcov.info - name: Upload results artifact uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: results-rust if-no-files-found: error path: lcov.info report: timeout-minutes: 30 name: Report runs-on: latchkey-small permissions: id-token: write checks: write needs: [test, test_rust] steps: - name: Download environment uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: env path: . - name: Extract environment run: | tar -xpf $PWD/env.tzst -C / rm env.tzst - name: Download results uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: pattern: "results-*" path: results - name: Combine Rust coverage run: | find results # XXX: Workaround slow `Processing triggers for man-db...` echo 'set man-db/auto-update false' | sudo debconf-communicate sudo dpkg-reconfigure man-db sudo apt-get install -qq llvm-20 llvm-20-tools echo "Merging coverage..." llvm-profdata-20 merge -sparse $(find . -name '*.profraw') -o default.profdata echo "Generating report..." llvm-cov-20 report ./angr/rustylib*.so -instr-profile=default.profdata --sources $(find native -name "*.rs") llvm-cov-20 export ./angr/rustylib*.so -instr-profile=default.profdata --sources $(find native -name "*.rs") --format=lcov > lcov-py.info - name: Collect results id: files run: | coverage=$(find . -type f \( -name 'coverage*.xml' -o -name 'lcov*.info' \) | paste -sd, -) echo "coverage=$coverage" >> "$GITHUB_OUTPUT" junit=$(find . -type f -name 'junit.xml' | paste -sd, -) echo "junit=$junit" >> "$GITHUB_OUTPUT" - name: Upload test coverage to Codecov uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5 with: use_oidc: true verbose: true files: ${{ steps.files.outputs.coverage }} - name: Upload test results to Codecov uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5 with: use_oidc: true fail_ci_if_error: true verbose: true files: ${{ steps.files.outputs.junit }} report_type: test_results - name: Combine test durations run: jq -Ss 'add' results/**/test_durations.json > test_durations.json - name: Upload combined test durations artifact uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: test_durations path: ./test_durations.json if-no-files-found: error - name: Update test durations cache uses: actions/cache/save@v5 with: path: test_durations.json key: cov-test-durations-${{ github.sha }} - name: Publish test results uses: EnricoMi/publish-unit-test-result-action@27d65e188ec43221b20d26de30f4892fad91df2f # v2 with: files: "results/**/junit.xml" action_fail_on_inconclusive: true comment_mode: off # codecov already leaves a comment, and more details # from this action can be found in the check it creates.
What changed
- Run on Latchkey managed runners with one line (
runs-on), which apply the fixes below automatically and self-heal transient failures. This example useslatchkey-small; pick the runner size that fits the job. - Cache dependency installs on the setup step so they are served from cache.
- Add a job timeout so a hung step cannot burn hours of runner time.
What Latchkey heals here
This workflow has steps that commonly fail on transient issues (network, registries, flaky browsers). On Latchkey managed runners they are detected, retried, and self-healed instead of failing your build:
- Dependency installs
This workflow runs 4 jobs (13 with the matrix expanded) per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.