# Run the project's test suite on every PR push and on push to main. # # The suite uses stdlib `unittest` discovery — no external Python # dependencies are required to execute it. Tests are split by directory: # # tests/unit/ — pure unit tests; always run # tests/integration/ — need a reachable backend; skip cleanly when # the backend isn't available on the runner # tests/canaries/ — upstream regression canaries; run on a separate # schedule (see canaries.yml), not here # # Integration tests run once per backend in separate jobs. Each job sets # BOT_BOTTLE_BACKEND explicitly so the test suite uses the right backend. # Backends that aren't available on the runner fail the preflight step # rather than silently skipping inside the test output. name: test on: push: branches: - main paths: - '**.py' - '.gitea/workflows/**.yml' - 'scripts/**' - 'README.md' pull_request: paths: - '**.py' - '.gitea/workflows/**.yml' - 'scripts/**' - 'README.md' workflow_dispatch: jobs: unit: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 # No actions/setup-python: the runner image already ships Python 3.12, # and older act_runner engines mishandle setup-python's PATH (coverage # lands in one interpreter, `python3` resolves to another). Install # straight into the ephemeral job container's system Python — # --break-system-packages is safe because the container is disposable. - name: Install dev requirements run: python3 -m pip install --break-system-packages -r requirements-dev.txt - name: Run unit tests run: python3 -m coverage run -m unittest discover -t . -s tests/unit -v - name: Report unit coverage run: python3 -m coverage report -m integration-docker: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 # No actions/setup-python (see the note in the `unit` job); the # container's system Python 3.12 runs the stdlib test suite directly. - name: Show environment run: | python3 --version if command -v docker >/dev/null 2>&1; then docker version || true else echo "docker not on PATH — integration tests will skip" fi - name: Run integration tests (docker) env: BOT_BOTTLE_BACKEND: docker run: python3 -m unittest discover -t . -s tests/integration -v # Integration tests against the Firecracker backend. Runs on a self-hosted # KVM runner (label `kvm`) where /dev/kvm and the TAP/nft pool are available. # # Restricted to same-repo PRs, push to main, and workflow_dispatch — fork # PRs don't execute untrusted code on the privileged runner. # # Runner prerequisites (provision once; see README "Firecracker on Linux"): # `firecracker` on PATH, `/dev/kvm` accessible, Docker, cached kernel + # static dropbear, and the pool as a persistent systemd unit. integration-firecracker: runs-on: [self-hosted, kvm] if: >- github.event_name == 'push' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) steps: - name: Checkout uses: actions/checkout@v4 - name: Preflight — Firecracker host is ready run: | command -v firecracker >/dev/null || { echo "firecracker not on PATH — provision the runner (README: Firecracker on Linux)"; exit 1; } test -e /dev/kvm || { echo "/dev/kvm missing — KVM not available on this runner"; exit 1; } # `backend status` exits non-zero unless the TAP pool is up + no # range overlap; it prints the exact `backend setup` fix. python3 cli.py backend status --backend=firecracker # No dev-requirements install: the integration suite runs on stdlib # `unittest` (pylint/pyright are lint.yml's concern, not this job's), # and the self-hosted runner's Nix python env has no `pip` module # (`python3 -m pip` → "No module named pip"). Nothing to install. - name: Run integration tests (firecracker) env: BOT_BOTTLE_BACKEND: firecracker run: python3 -m unittest discover -t . -s tests/integration -v # Combined unit+integration coverage + the diff-coverage gate (the hard # gate: new/changed lines >= 90%). See docs/decisions/0004-coverage-policy.md. # # This runs on a self-hosted KVM runner (label `kvm`), NOT ubuntu-latest, # because the Firecracker backend's subprocess/VM orchestration # (launch/boot/SSH/isolation-probe) is covered by the integration suite, # and that suite needs `/dev/kvm` + the provisioned TAP/nft pool — which a # container-based runner doesn't have. On such a runner the firecracker # integration test skips and its ~230 orchestration lines read as # uncovered, so the gate can't pass there. # # Restricted to the same events as integration-firecracker (same-repo PRs, # push, workflow_dispatch) for the same security reason. # # See #414 for the planned follow-up: artifact-based coverage combination # (run tests once in their respective jobs, combine .coverage files here). coverage: timeout-minutes: 15 runs-on: [self-hosted, kvm] if: >- github.event_name == 'push' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Preflight — Firecracker host is ready run: | command -v firecracker >/dev/null || { echo "firecracker not on PATH — provision the runner (README: Firecracker on Linux)"; exit 1; } test -e /dev/kvm || { echo "/dev/kvm missing — KVM not available on this runner"; exit 1; } # `backend status` exits non-zero unless the TAP pool is up + no # range overlap; it prints the exact `backend setup` fix. python3 cli.py backend status --backend=firecracker # No dev-requirements install: `coverage` is already provided by the # self-hosted runner's Nix python env, and that env has no `pip` # module to install into anyway. `scripts/coverage.sh` + # `diff_coverage.py` need only `coverage` (not pylint/pyright). - name: Combined coverage (unit + integration, incl. firecracker) run: PYTHON=python3 bash scripts/coverage.sh critical - name: Diff-coverage gate (changed lines >= 90%) run: | git fetch --no-tags origin main:refs/remotes/origin/main python3 scripts/diff_coverage.py --base origin/main --min 90