bbtree 0.1.3

Fastest B-tree.
Documentation
# Named for its README badge.
name: fuzzing

# Fuzzing (doc/08-fuzzing-verification.md), for 30 minutes: weekly; for
# every release, before it merges (ci.yml) and on main once it's published
# (publish.yml); and on demand. Every target in turn, each for its share of the
# run in as many processes as the machine has threads, continuing from the
# corpora the last run saved, so that the runs add up. A crash fails the run
# and is saved as an artifact. A release merges, and so publishes, only once
# its run passes (ci.yml's merge gate): every version on crates.io was fuzzed.
#
# It runs on the self-hosted bare-metal runner that perf.yml measures on
# (label `bbtree-perf`, nixhome's hosts/pittsburgh): 12 threads where
# GitHub's runners have 4. The runner takes one job at a time, so a fuzzing
# run and a measurement queue behind each other.
on:
  schedule:
    # Wednesdays at 10:17 UTC: 5:17 in the morning in Madison under daylight
    # time, 4:17 otherwise. Off the hour, when GitHub delays scheduled runs
    # the most.
    - cron: "17 10 * * 3"
  # By hand, and by publish.yml once a release is out: the README badge
  # follows this workflow's runs on main, which the release's own fuzzing,
  # run from ci.yml, isn't.
  workflow_dispatch:
    inputs:
      minutes:
        description: Fuzzing time, in minutes, shared among the targets
        type: number
        default: 30
  workflow_call:
    inputs:
      minutes:
        description: Fuzzing time, in minutes, shared among the targets
        type: number
        required: true

permissions:
  contents: read
  # To fetch the corpora that the last run saved.
  actions: read

# Weekly and dispatched runs that overlap would both start from the same
# corpora. A release's run is in a group of its own, so that it never waits behind
# them; ci.yml cancels it when a newer commit replaces the release's.
concurrency:
  group: fuzz-${{ github.event_name == 'pull_request' && github.ref || 'corpora' }}
  cancel-in-progress: false

env:
  CARGO_TERM_COLOR: always

jobs:
  fuzz:
    name: fuzz
    runs-on: [self-hosted, bbtree-perf]
    # A run of 30 minutes; the rest is building and saving.
    timeout-minutes: 60
    defaults:
      run:
        # The nightly, cargo-fuzz and gh, from the flake (`devShells.fuzz`).
        shell: nix develop .#fuzz --command bash -euo pipefail {0}
    steps:
      - uses: actions/checkout@v4
        with:
          # The code checked out runs next; it gets no token. The corpora
          # fetch below gets its own.
          persist-credentials: false

      - name: Fetch the corpora of the last run
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          mkdir -p fuzz/corpus
          # The newest corpora that a run saved, weekly, dispatched or a
          # release's (those are ci.yml's runs); the first run starts from
          # nothing.
          run_id=$(gh api "repos/$GITHUB_REPOSITORY/actions/artifacts?name=fuzz-corpora&per_page=100" \
            --jq '[.artifacts[] | select(.expired | not)] | max_by(.created_at) | .workflow_run.id // empty')
          if [ -n "$run_id" ]; then
            gh run download "$run_id" --repo "$GITHUB_REPOSITORY" \
              --name fuzz-corpora --dir fuzz/corpus
            echo "Continuing from the corpora of run $run_id"
          fi

      # With AddressSanitizer, cargo-fuzz's default, and the library's debug
      # assertions, its own cross-checks.
      - name: Build the targets
        run: cargo fuzz build --debug-assertions

      - name: Fuzz
        env:
          RUN_MINUTES: ${{ inputs.minutes || 30 }}
        run: |
          # Each target's share of the run, in percent, by how much code a
          # bug there would reach and how much harm it would do
          # (doc/08-fuzzing-verification.md).
          budget=(
            "sync_map 20"
            "map_heap_byte_strings 18"
            "user_code_panics 16"
            "map_integers 11"
            "map_user_inline_keys 11"
            "map_plain_keys 9"
            "set_algebra 9"
            "map_byte_arrays 6"
          )
          failed=()
          echo "| target | seconds | result |" >> "$GITHUB_STEP_SUMMARY"
          echo "|---|---|---|" >> "$GITHUB_STEP_SUMMARY"
          for entry in "${budget[@]}"; do
            read -r target percent <<< "$entry"
            seconds=$(( percent * 60 * RUN_MINUTES / 100 ))
            echo "::group::$target, $seconds s"
            # A crash ends the target's run, not the others'. An input that
            # runs over a minute is reported as a hang.
            if cargo fuzz run --debug-assertions "$target" -- \
                -max_total_time="$seconds" -fork="$(nproc)" -timeout=60; then
              result=clean
            else
              result="**crashed**"
              failed+=("$target")
            fi
            echo "::endgroup::"
            echo "| $target | $seconds | $result |" >> "$GITHUB_STEP_SUMMARY"
          done
          if [ ${#failed[@]} -gt 0 ]; then
            {
              echo
              echo "The crashing inputs are in the \`fuzz-crashes\` artifact. To replay"
              echo "one, minimize it, and keep it once fixed:"
              echo
              echo '```'
              echo "cargo fuzz run --debug-assertions <target> <input>"
              echo "cargo fuzz tmin --debug-assertions <target> <input>"
              echo "cp <minimized input> fuzz/regressions/<target>/<what it found>"
              echo '```'
            } >> "$GITHUB_STEP_SUMMARY"
            echo "::error::crashed: ${failed[*]}"
            exit 1
          fi

      - name: Save the corpora
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: fuzz-corpora
          path: fuzz/corpus
          # Three weeks: the next run, or the one after a skipped week,
          # continues from them.
          retention-days: 21

      - name: Save the crashing inputs
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: fuzz-crashes
          path: fuzz/artifacts
          retention-days: 90