name: CI
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: Test (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 10
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - run: rustup toolchain install 1.95.0 --profile minimal --component rustfmt --component clippy
- uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c - run: cargo fmt --check
- run: cargo clippy --locked --all-features --all-targets -- -D warnings
- run: cargo clippy --locked --no-default-features --all-targets -- -D warnings
- run: cargo clippy --locked --no-default-features --features wire,local,launch --all-targets -- -D warnings
- run: cargo doc --locked --all-features --no-deps
env:
RUSTDOCFLAGS: -D warnings
- run: cargo doc --locked --no-default-features --no-deps
env:
RUSTDOCFLAGS: -D warnings
- run: cargo nextest run --locked --all-features
- run: cargo nextest run --locked --no-default-features --features wire,local
- run: cargo nextest run --locked --no-default-features --features wire,local,launch
- run: cargo nextest run --locked --no-default-features --features wire
- run: cargo nextest run --locked --no-default-features
- run: cargo test --locked --all-features --doc
- run: cargo run --locked --no-default-features --features wire --example application_messages
- run: cargo package --locked
cross-version:
name: Cross-version health and drain
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 with:
path: current
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 with:
ref: v0.8.0
path: previous
- run: rustup toolchain install 1.95.0 --profile minimal
- run: sudo apt-get update && sudo apt-get install -y protobuf-compiler python3-protobuf
- run: python3 current/scripts/check-cross-version.py previous current
env:
RUSTUP_TOOLCHAIN: 1.95.0
protobuf:
name: Protobuf schema
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - run: rustup toolchain install 1.95.0 --profile minimal --component rustfmt --component clippy
- run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
- run: cargo fmt --manifest-path tools/proto-gen/Cargo.toml --check
- run: cargo clippy --locked --manifest-path tools/proto-gen/Cargo.toml -- -D warnings
- run: cargo run --locked --manifest-path tools/proto-gen/Cargo.toml -- --check
- run: python3 scripts/check-protobuf.py
msrv:
name: Rust 1.89
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - run: rustup toolchain install 1.89.0 --profile minimal
- run: cargo +1.89.0 test --locked --all-features
- run: cargo +1.89.0 test --locked --no-default-features --features wire
- run: cargo +1.89.0 test --locked --no-default-features
xwin:
name: Windows cross-link (xwin)
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - run: rustup toolchain install 1.95.0 --profile minimal
- run: rustup target add x86_64-pc-windows-msvc
- run: sudo apt-get update && sudo apt-get install -y clang lld
- run: cargo install cargo-xwin --locked --version 0.23.1
- run: cargo xwin build --locked --all-features --all-targets --target x86_64-pc-windows-msvc
kani:
name: Kani proofs
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - run: |
cargo install --locked kani-verifier --version 0.67.0
cargo kani setup
- name: Require at least one harness
run: |
cargo kani --all-features list --format json
count="$(python3 -c 'import json; print(json.load(open("kani-list.json"))["totals"]["standard-harnesses"])')"
echo "harnesses: $count"
test "$count" -ge 1
- run: cargo kani --all-features --output-format terse
loom:
name: Loom (WriterSlot, Outstanding)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - run: rustup toolchain install 1.95.0 --profile minimal
- run: cargo test --release --locked --no-default-features --test loom_writer_slot --test loom_outstanding
env:
RUSTFLAGS: --cfg loom
deny:
name: cargo-deny
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25 with:
command: check
mutation-plan:
name: Mutation plan
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
count: ${{ steps.plan.outputs.count }}
matrix: ${{ steps.plan.outputs.matrix }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 with:
fetch-depth: 0
- run: rustup toolchain install 1.95.0 --profile minimal
- uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c - id: plan
env:
BASE_REF: ${{ github.base_ref }}
PLAN_DIR: ${{ runner.temp }}/mutants
run: |
set -euo pipefail
mkdir -p "$PLAN_DIR"
# Nothing to test must never be the result of a diff that could not
# be produced or read, so every step below fails loudly on its own.
git fetch --no-tags origin "+refs/heads/$BASE_REF:refs/remotes/origin/$BASE_REF"
base="$(git merge-base "origin/$BASE_REF" HEAD)"
# Added or modified library source. A deleted file or a pure rename
# (R100) has no new lines to mutate.
changed="$(
git diff --name-status "$base" HEAD -- 'src/*.rs' |
awk -F '\t' '$1 ~ /^[AMT]/ { print $2 } $1 ~ /^R/ && $1 != "R100" { print $3 }'
)"
if [ -z "$changed" ]; then
echo "::notice title=Mutation testing skipped::This PR adds or changes no Rust source under src/."
echo 'count=0' >> "$GITHUB_OUTPUT"
echo 'matrix={"include":[{"index":0,"display":1,"total":1}]}' >> "$GITHUB_OUTPUT"
exit 0
fi
printf 'Changed Rust source:\n%s\n' "$changed"
# cargo-mutants matches the new side of the diff against this tree
# by its "+++ b/<path>" header. Pin git's default format whatever the
# runner's config says, and check every changed file is there: one
# it cannot match would silently contribute no mutants.
git -c diff.noprefix=false -c diff.mnemonicPrefix=false \
diff --no-color --no-ext-diff "$base" HEAD > "$PLAN_DIR/pr.diff"
while IFS= read -r file; do
if ! grep -qxF "+++ b/$file" "$PLAN_DIR/pr.diff"; then
echo "::error title=Mutation plan::$file changed but the diff has no '+++ b/$file' header"
exit 1
fi
done <<< "$changed"
# --level error keeps log lines off stdout, which then holds only
# the JSON list. cargo-mutants prints nothing when the changed lines
# contain no mutants (comments, tests, signatures, excluded code).
status=0
cargo mutants --in-diff "$PLAN_DIR/pr.diff" --list --json --level error \
> "$PLAN_DIR/mutants.json" || status=$?
if [ "$status" -ne 0 ]; then
echo "::error title=Mutation plan::cargo mutants could not use the PR diff (exit $status; 5: it does not match the tree, 6: it could not be parsed)"
exit 1
fi
count="$(python3 -c '
import json, sys
text = open(sys.argv[1]).read()
mutants = json.loads(text) if text.strip() else []
assert isinstance(mutants, list), "expected a JSON list of mutants"
print(len(mutants))
' "$PLAN_DIR/mutants.json")"
echo "changed-line mutants: $count"
if [ "$count" -eq 0 ]; then
echo "::notice title=Mutation testing skipped::The changed lines in src/ produce no mutants."
fi
# About 20 mutants per shard, at most 12 shards. Round-robin in the
# shards spreads slow mutants evenly.
shards=$(( count == 0 ? 1 : (count + 19) / 20 ))
shards=$(( shards > 12 ? 12 : shards ))
matrix="$(python3 -c '
import json, sys
total = int(sys.argv[1])
print(json.dumps({"include": [
{"index": i, "display": i + 1, "total": total} for i in range(total)
]}, separators=(",", ":")))
' "$shards")"
echo "count=$count" >> "$GITHUB_OUTPUT"
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
- if: steps.plan.outputs.count != '0'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with:
name: mutation-diff
path: ${{ runner.temp }}/mutants/pr.diff
if-no-files-found: error
retention-days: 14
mutation-diff:
name: Mutation testing (${{ matrix.display }}/${{ matrix.total }})
needs: mutation-plan
if: github.event_name == 'pull_request' && needs.mutation-plan.outputs.count != '0'
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.mutation-plan.outputs.matrix) }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - run: rustup toolchain install 1.95.0 --profile minimal
- uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with:
name: mutation-diff
path: ${{ runner.temp }}/mutants
- name: Test mutants of the changed lines
env:
OUT: ${{ runner.temp }}/mutants
SHARD: ${{ matrix.index }}
SHARDS: ${{ matrix.total }}
PLANNED: ${{ needs.mutation-plan.outputs.count }}
run: |
set -euo pipefail
test -s "$OUT/pr.diff"
status=0
cargo mutants --in-diff "$OUT/pr.diff" \
--shard "$SHARD/$SHARDS" --sharding round-robin \
--output "$OUT" || status=$?
# Round-robin gives shard k every mutant i with i % n == k.
expected=$(( PLANNED / SHARDS + (SHARD < PLANNED % SHARDS ? 1 : 0) ))
python3 scripts/mutation-verdict.py "$OUT" "$status" "$expected"
- if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with:
name: mutants-out-${{ matrix.display }}
path: ${{ runner.temp }}/mutants/mutants.out
if-no-files-found: warn
retention-days: 14
mutation:
name: Mutation testing
needs: [mutation-plan, mutation-diff]
if: always() && github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- env:
PLAN: ${{ needs.mutation-plan.result }}
COUNT: ${{ needs.mutation-plan.outputs.count }}
SHARDS: ${{ needs.mutation-diff.result }}
run: |
echo "plan: $PLAN; mutants: ${COUNT:-none}; shards: $SHARDS"
test "$PLAN" = success
if [ "$COUNT" = 0 ]; then exit 0; fi
test "$SHARDS" = success
bench:
name: Benchmarks
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
shell: bash
env:
CARGO_TARGET_DIR: ${{ github.workspace }}/benches/target
GUNGRAUN_HOME: ${{ github.workspace }}/benches/target/gungraun
BENCH_LIMIT: "5"
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 with:
fetch-depth: 2
- run: rustup toolchain install 1.95.0 --profile minimal --component rustfmt --component clippy
- run: sudo apt-get update && sudo apt-get install -y valgrind
- run: cargo fmt --manifest-path benches/Cargo.toml --check
- run: cargo clippy --locked --manifest-path benches/Cargo.toml --all-targets -- -D warnings
- name: Install gungraun-runner
id: runner
run: |
version="$(cargo metadata --locked --format-version 1 --manifest-path benches/Cargo.toml |
jq -r '.packages[] | select(.name == "gungraun") | .version')"
echo "version=$version" >> "$GITHUB_OUTPUT"
cargo install --locked gungraun-runner --version "$version"
- name: Measure the parent commit
id: base
env:
RUNNER_VERSION: ${{ steps.runner.outputs.version }}
run: |
base="$(git rev-parse HEAD^1)"
echo "sha=$base" >> "$GITHUB_OUTPUT"
git worktree add --detach "$RUNNER_TEMP/base" "$base"
cd "$RUNNER_TEMP/base"
if [ ! -f benches/Cargo.toml ]; then
echo "The parent commit has no benchmarks; every benchmark is new."
exit 0
fi
version="$(cargo metadata --locked --format-version 1 --manifest-path benches/Cargo.toml |
jq -r '.packages[] | select(.name == "gungraun") | .version')"
if [ "$version" != "$RUNNER_VERSION" ]; then
echo "::notice::The parent commit uses gungraun $version; no comparison this time."
exit 0
fi
cargo bench --locked --manifest-path benches/Cargo.toml -- --save-baseline=base
- name: Measure this commit
run: |
limits=()
if [ "$GITHUB_EVENT_NAME" = pull_request ]; then
limits=("--callgrind-limits=ir=${BENCH_LIMIT}%")
fi
cargo bench --locked --manifest-path benches/Cargo.toml -- \
--baseline=base --save-summary=json "${limits[@]}"
- name: Report
if: success() || failure()
env:
BASE_SHA: ${{ steps.base.outputs.sha }}
run: |
limit=()
if [ "$GITHUB_EVENT_NAME" = pull_request ]; then
limit=(--limit "$BENCH_LIMIT")
fi
python3 scripts/bench-report.py "$GUNGRAUN_HOME" "${limit[@]}" \
--baseline "the parent commit, $BASE_SHA" >> "$GITHUB_STEP_SUMMARY"