name: CI

on:
  workflow_dispatch:
  pull_request:
    branches:
      - main
    types:
      - opened
      - reopened
      - synchronize
      - labeled
  push:
    branches:
      - main

jobs:
  tests:
    name: Unit tests

    if: |
      github.event_name != 'pull_request' ||
      github.event.pull_request.author_association == 'OWNER' ||
      github.event.pull_request.author_association == 'COLLABORATOR' ||
      github.event.pull_request.author_association == 'MEMBER' ||
      github.event.pull_request.user.login == 'dependabot[bot]' ||
      contains(github.event.pull_request.labels.*.name, 'safe to test')

    runs-on: ${{ matrix.os }}

    strategy:
      fail-fast: false
      matrix:
        os: [windows-latest, macos-latest, ubuntu-latest]
        rust_version: [stable, 1.88.0]

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@master
        with:
          toolchain: ${{ matrix.rust_version }}
          components: llvm-tools-preview

      - name: Cache Rust dependencies
        uses: Swatinem/rust-cache@v2

      - name: Install cargo-llvm-cov
        uses: taiki-e/install-action@cargo-llvm-cov

      - name: Generate code coverage
        run: cargo llvm-cov --lib --lcov --output-path lcov.info

      - name: Filter out tests
        uses: scouten/uncover-tests@v1
        with:
          input-file: lcov.info
          output-file: lcov.filtered.info

      # Tokens aren't available for PRs originating from forks,
      # so we don't attempt to upload code coverage in that case.
      - name: Upload code coverage results
        if: |
          github.event_name != 'pull_request' ||
          github.event.pull_request.author_association == 'OWNER' ||
          github.event.pull_request.author_association == 'COLLABORATOR' ||
          github.event.pull_request.author_association == 'MEMBER' ||
          github.event.pull_request.user.login == 'dependabot[bot]'
        uses: codecov/codecov-action@v5
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          fail_ci_if_error: true
          verbose: true
          files: ./lcov.filtered.info

  doc-tests:
    name: Doc tests (requires nightly Rust)
    # TODO: Remove this once cargo-llvm-cov can run doc tests and generate
    # coverage. (This requires a bug fix that is only available in nightly Rust.)
    # Watch https://github.com/taiki-e/cargo-llvm-cov/issues/2
    # for progress.

    if: |
      github.event_name != 'pull_request' ||
      github.event.pull_request.author_association == 'OWNER' ||
      github.event.pull_request.author_association == 'COLLABORATOR' ||
      github.event.pull_request.author_association == 'MEMBER' ||
      github.event.pull_request.user.login == 'dependabot[bot]' ||
      contains(github.event.pull_request.labels.*.name, 'safe to test')

    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable

      - name: Cache Rust dependencies
        uses: Swatinem/rust-cache@v2

      - name: Install cargo-llvm-cov
        uses: taiki-e/install-action@cargo-llvm-cov

      # Disabling code coverage for doc tests due to a new bug in Rust nightly
      # as of 2025-01-08. Will investigate later to see if there's a repro case.
      # Meanwhile, simply run the tests so we know if there are any failing
      # doc tests.

      - name: Run doc tests (COVERAGE DISABLED)
        run: |
          cargo test --workspace --doc

      # - name: Generate code coverage
      #   env:
      #     RUST_BACKTRACE: "1"
      #   run: |
      #   cargo llvm-cov --workspace --all-features --lcov --doctests --output-path lcov.info

      # Tokens aren't available for PRs originating from forks,
      # so we don't attempt to upload code coverage in that case.
      # - name: Upload code coverage results
      #   if: |
      #     github.event_name != 'pull_request' ||
      #     github.event.pull_request.author_association == 'OWNER' ||
      #     github.event.pull_request.author_association == 'COLLABORATOR' ||
      #     github.event.pull_request.author_association == 'MEMBER' ||
      #     github.event.pull_request.user.login == 'dependabot[bot]'
      #   uses: codecov/codecov-action@v5
      #   with:
      #     token: ${{ secrets.CODECOV_TOKEN }}
      #     fail_ci_if_error: true
      #     verbose: true

  tests-cross:
    name: Unit tests (cross)

    if: |
      github.event_name != 'pull_request' ||
      github.event.pull_request.author_association == 'OWNER' ||
      github.event.pull_request.author_association == 'COLLABORATOR' ||
      github.event.pull_request.author_association == 'MEMBER' ||
      github.event.pull_request.user.login == 'dependabot[bot]' ||
      contains(github.event.pull_request.labels.*.name, 'safe to test')

    runs-on: ubuntu-latest

    strategy:
      fail-fast: false
      matrix:
        target: [aarch64-unknown-linux-gnu]
        rust_version: [stable, 1.88.0]

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@master
        with:
          toolchain: ${{ matrix.rust_version }}
          targets: ${{ matrix.target }}

      - name: Install cross-compilation toolset
        run: cargo install cross

      - name: Cache Rust dependencies
        uses: Swatinem/rust-cache@v2

      # Note that we do not run code coverage because
      # it isn't readily accessible from cross-compilation
      # environment. (A PR to fix this would be welcomed!)

      - name: Run unit tests (cross build)
        run: cross test --lib --target ${{ matrix.target }}

  tests-ios:
    name: Unit tests (iOS simulator)

    if: |
      github.event_name != 'pull_request' ||
      github.event.pull_request.author_association == 'OWNER' ||
      github.event.pull_request.author_association == 'COLLABORATOR' ||
      github.event.pull_request.author_association == 'MEMBER' ||
      github.event.pull_request.user.login == 'dependabot[bot]' ||
      contains(github.event.pull_request.labels.*.name, 'safe to test')

    runs-on: macos-14

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@master
        with:
          toolchain: 1.88.0
          targets: aarch64-apple-ios-sim

      - name: Install cargo-dinghy
        run: |
          VERSION=0.8.1
          URL="https://github.com/sonos/dinghy/releases/download/${VERSION}/cargo-dinghy-macos-${VERSION}.tgz"
          wget -O - $URL | tar -xz --strip-components=1 -C ~/.cargo/bin

      - name: Check cargo-dinghy version.
        run: cargo dinghy --version

      - name: Setup iOS Simulator
        # Use the first installed iOS runtime and the first (i.e. oldest) supported iPhone device.
        run: |
          RUNTIME=$(xcrun simctl list runtimes --json | jq '.runtimes | map(select(.name | contains("iOS"))) | .[0]')
          RUNTIME_ID=$(echo $RUNTIME | jq -r '.identifier')
          echo "Using runtime:" $RUNTIME_ID
          DEVICE_ID=$(echo $RUNTIME | jq -r '.supportedDeviceTypes | map(select(.productFamily == "iPhone")) | .[0].identifier')
          echo "Using device:" $DEVICE_ID
          SIM_ID=$(xcrun simctl create Test-iPhone $DEVICE_ID $RUNTIME_ID)
          echo "Created simulator:" $SIM_ID
          xcrun simctl boot $SIM_ID
          echo "device=$SIM_ID" >> $GITHUB_ENV

      - name: Cache Rust dependencies
        uses: Swatinem/rust-cache@v2

      - name: Run unit tests (cross build)
        run: cargo dinghy -p auto-ios-aarch64-sim -d ${{ env.device }} test --all-targets

  tests-wasm:
    name: Unit tests (Wasm)

    if: |
      github.event_name != 'pull_request' ||
      github.event.pull_request.author_association == 'OWNER' ||
      github.event.pull_request.author_association == 'COLLABORATOR' ||
      github.event.pull_request.author_association == 'MEMBER' ||
      github.event.pull_request.user.login == 'dependabot[bot]' ||
      contains(github.event.pull_request.labels.*.name, 'safe to test') ||
      contains(github.event.pull_request.labels.*.name, 'check-release')

    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable

      - name: Set up Chrome
        uses: browser-actions/setup-chrome@v2
        with:
          chrome-version: 142
          install-chromedriver: true

      - name: Add Wasm web target
        run: rustup target add wasm32-unknown-unknown

      - name: Install cargo-binstall
        uses: cargo-bins/cargo-binstall@main

      - name: Install wasm-bindgen-cli
        run: cargo binstall -y wasm-bindgen-cli --version 0.2.106

      - name: Run Wasm tests
        run: cargo test -p jumbf --target wasm32-unknown-unknown
        env:
          CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER: wasm-bindgen-test-runner
          WASM_BINDGEN_TEST_TIMEOUT: 60

  tests-wasi:
    name: Unit tests (WASI)

    if: |
      github.event_name != 'pull_request' ||
      github.event.pull_request.author_association == 'OWNER' ||
      github.event.pull_request.author_association == 'COLLABORATOR' ||
      github.event.pull_request.author_association == 'MEMBER' ||
      github.event.pull_request.user.login == 'dependabot[bot]' ||
      contains(github.event.pull_request.labels.*.name, 'safe to test') ||
      contains(github.event.pull_request.labels.*.name, 'check-release')

    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

        # Nightly required for testing until this issue is resolved:
        # wasip2 target should not conditionally feature gate stdlib APIs rust-lang/rust#130323 https://github.com/rust-lang/rust/issues/130323
      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@master
        with:
          toolchain: nightly-2026-01-16

      - name: Install wasmtime
        run: |
          curl https://wasmtime.dev/install.sh -sSf | bash
          echo "$HOME/.wasmtime/bin" >> $GITHUB_PATH

      - name: Install clang
        run: |
          sudo apt-get update
          sudo apt-get install -y clang

      - name: Add wasm32-wasip2 target
        run: rustup target add --toolchain nightly-2026-01-16 wasm32-wasip2

      - name: Cache Rust dependencies
        uses: Swatinem/rust-cache@v2

      - name: Run WASI tests
        env:
          CARGO_TARGET_WASM32_WASIP2_RUNNER: "wasmtime -S cli -S http --dir . --env GITHUB_ACTIONS=${GITHUB_ACTIONS}"
          CC: clang
          RUST_MIN_STACK: 16777216
        run: |
          cargo +nightly-2026-01-16 test --target wasm32-wasip2 -p jumbf -- --no-capture

  test-direct-minimal-versions:
    name: Unit tests with minimum versions of direct dependencies

    if: |
      github.event_name != 'pull_request' ||
      github.event.pull_request.author_association == 'OWNER' ||
      github.event.pull_request.author_association == 'COLLABORATOR' ||
      github.event.pull_request.author_association == 'MEMBER' ||
      github.event.pull_request.user.login == 'dependabot[bot]' ||
      contains(github.event.pull_request.labels.*.name, 'safe to test')

    runs-on: ${{ matrix.os }}

    strategy:
      fail-fast: false
      matrix:
        os: [windows-latest, macos-latest, ubuntu-latest]

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@master
        with:
          toolchain: nightly-2025-12-06

      - name: Cache Rust dependencies
        uses: Swatinem/rust-cache@v2

      - name: Run tests
        run: |
          cargo +nightly-2025-12-06 test -Z direct-minimal-versions --all-targets

  benchmarks:
    name: Test performance on benchmarks

    if: |
      github.event_name != 'pull_request' ||
      github.event.pull_request.author_association == 'OWNER' ||
      github.event.pull_request.author_association == 'COLLABORATOR' ||
      github.event.pull_request.author_association == 'MEMBER' ||
      github.event.pull_request.user.login == 'dependabot[bot]' ||
      contains(github.event.pull_request.labels.*.name, 'safe to test') ||
      contains(github.event.pull_request.labels.*.name, 'check-release')

    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable

      - name: Install cargo-binstall
        uses: cargo-bins/cargo-binstall@main

      - name: Install cargo-codspeed
        run: cargo binstall --no-confirm cargo-codspeed

      - name: Cache Rust dependencies
        uses: Swatinem/rust-cache@v2

      - name: Build the benchmark target(s)
        run: cargo codspeed build -p jumbf

      - name: Run the benchmarks
        uses: CodSpeedHQ/action@v4
        with:
          mode: instrumentation
          run: cargo codspeed run
          token: ${{ secrets.CODSPEED_TOKEN }}

  publish-preflight:
    name: Preflight crate publish

    if: |
      github.event_name != 'pull_request' ||
      github.event.pull_request.author_association == 'OWNER' ||
      github.event.pull_request.author_association == 'COLLABORATOR' ||
      github.event.pull_request.author_association == 'MEMBER' ||
      github.event.pull_request.user.login == 'dependabot[bot]' ||
      contains(github.event.pull_request.labels.*.name, 'safe to test')

    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable

      - name: Cache Rust dependencies
        uses: Swatinem/rust-cache@v2

      - name: Dry-run of crate publish
        run: cargo publish -p jumbf --dry-run

  clippy_check:
    name: Clippy

    if: |
      github.event_name != 'pull_request' ||
      github.event.pull_request.author_association == 'OWNER' ||
      github.event.pull_request.author_association == 'COLLABORATOR' ||
      github.event.pull_request.author_association == 'MEMBER' ||
      github.event.pull_request.user.login == 'dependabot[bot]' ||
      contains(github.event.pull_request.labels.*.name, 'safe to test')

    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable
        with:
          components: clippy

      - name: Cache Rust dependencies
        uses: Swatinem/rust-cache@v2

      - name: Run Clippy
        run: |
          cargo clippy --all-targets -- -Dwarnings

  cargo_fmt:
    name: Enforce Rust code format

    if: |
      github.event_name != 'pull_request' ||
      github.event.pull_request.author_association == 'OWNER' ||
      github.event.pull_request.author_association == 'COLLABORATOR' ||
      github.event.pull_request.author_association == 'MEMBER' ||
      github.event.pull_request.user.login == 'dependabot[bot]' ||
      contains(github.event.pull_request.labels.*.name, 'safe to test')

    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Install nightly toolchain
        uses: dtolnay/rust-toolchain@master
        with:
          toolchain: nightly-2026-01-16
          components: rustfmt

      - name: Check format
        run: cargo +nightly-2026-01-16 fmt --all -- --check

  taplo_fmt:
    name: Enforce TOML format
    if: |
      github.event_name != 'pull_request' ||
      github.event.pull_request.author_association == 'OWNER' ||
      github.event.pull_request.author_association == 'COLLABORATOR' ||
      github.event.pull_request.author_association == 'MEMBER' ||
      github.event.pull_request.user.login == 'dependabot[bot]' ||
      contains(github.event.pull_request.labels.*.name, 'safe to test')

    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Install Taplo CLI
        run: cargo install taplo-cli --locked

      - name: Check TOML format
        run: taplo fmt --check

  docs_rs:
    name: Preflight docs.rs build

    if: |
      github.event_name != 'pull_request' ||
      github.event.pull_request.author_association == 'OWNER' ||
      github.event.pull_request.author_association == 'COLLABORATOR' ||
      github.event.pull_request.author_association == 'MEMBER' ||
      github.event.pull_request.user.login == 'dependabot[bot]' ||
      contains(github.event.pull_request.labels.*.name, 'safe to test')

    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Install Rust toolchain
        # Nightly is used here because the docs.rs build
        # uses nightly and we use doc_cfg features that are
        # not in stable Rust as of this writing (Rust 1.88).
        uses: dtolnay/rust-toolchain@nightly

      - name: Cache Rust dependencies
        uses: Swatinem/rust-cache@v2

      - name: Install cargo-docs-rs
        uses: dtolnay/install@cargo-docs-rs

      - name: Preflight docs.rs
        env:
          RUSTDOCFLAGS: -Dwarnings
        run: cargo docs-rs -p jumbf

  internal_docs:
    name: Verify internal crate documentation

    if: |
      github.event_name != 'pull_request' ||
      github.event.pull_request.author_association == 'OWNER' ||
      github.event.pull_request.author_association == 'COLLABORATOR' ||
      github.event.pull_request.author_association == 'MEMBER' ||
      github.event.pull_request.user.login == 'dependabot[bot]' ||
      contains(github.event.pull_request.labels.*.name, 'safe to test')

    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Install nightly Rust toolchain
        # Nightly is used here because we use doc_cfg features
        # that are not in stable Rust as of this writing (Rust 1.88).
        uses: dtolnay/rust-toolchain@nightly

      - name: Preflight cargo doc
        run: cargo +nightly doc --no-deps --document-private-items

  cargo-deny:
    name: License / vulnerability audit

    if: |
      github.event_name != 'pull_request' ||
      github.event.pull_request.author_association == 'OWNER' ||
      github.event.pull_request.author_association == 'COLLABORATOR' ||
      github.event.pull_request.author_association == 'MEMBER' ||
      github.event.pull_request.user.login == 'dependabot[bot]' ||
      contains(github.event.pull_request.labels.*.name, 'safe to test')

    runs-on: ubuntu-latest

    strategy:
      fail-fast: false
      matrix:
        checks:
          - advisories
          - bans licenses sources

    # Prevent sudden announcement of a new advisory from failing CI:
    continue-on-error: ${{ matrix.checks == 'advisories' }}

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Audit crate dependencies
        uses: EmbarkStudios/cargo-deny-action@v2
        with:
          command: check ${{ matrix.checks }}

  unused_deps:
    name: Check for unused dependencies

    if: |
      github.event_name != 'pull_request' ||
      github.event.pull_request.author_association == 'OWNER' ||
      github.event.pull_request.author_association == 'COLLABORATOR' ||
      github.event.pull_request.author_association == 'MEMBER' ||
      github.event.pull_request.user.login == 'dependabot[bot]' ||
      contains(github.event.pull_request.labels.*.name, 'safe to test')

    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@master
        with:
          toolchain: nightly-2025-12-06
          # Pinning to specific nightly build for now. More recent versions
          # introduce a lifetime check that creates a whole slew of build
          # errors.

      - name: Run cargo-udeps
        run: |
          mv ./.github/temp-bin/cargo-udeps /home/runner/.cargo/bin/cargo-udeps
          cargo udeps --all-targets
          # NOTE: Using pre-built binary as a workaround for
          # https://github.com/aig787/cargo-udeps-action/issues/6.
