Skip to content
GitHub
documentation

bud-runner

Register Test Stations, run Python test suites and upload results and artifacts to Bud with the bud_runner CLI.

bud_runner is the command-line runner for Bud TMP. It runs Python test case lists from CI or local automation, generates JUnit XML for CI systems, creates and updates Bud test runs, uploads results, and can operate a persistent runner daemon. It’s built to run on machines you manage: self-hosted CI runners and dedicated lab hosts.

bud-runner on PyPI · bud-runner on GitHub

Install both execution packages on a trusted lab host or self-hosted CI worker:

python -m pip install bud-runner budtestlibrary

Every command and option is listed in the Command reference.

A split-configuration model keeps secret tokens out of your repositories. Secret tokens and daemon settings live in a global machine vault at ~/.bud/config.json. Non-sensitive project metadata (account name, backend URL) lives in your repository’s app.properties. Link a project to a registered runner with:

budRunnerAccount=station-01
budBackend=https://bud.example.com

Never commit budRunnerToken, runnerApiKey, budToken, or passwords: app.properties is for project metadata only.

The runner stores its machine identity and tokens under ~/.bud/. Keep that directory private and never commit it.

By design, bud_runner imports and executes Python test code from your local workspace via importlib. Only run trusted test code: a malicious test class can execute arbitrary code on the host.

Each test class runs in its own OS process (spawned with multiprocessing.get_context("spawn")), so a crash or hang in one test can’t corrupt another. A per-test timeout (default 5 min) and a global suite timeout (default 30 min) bound execution. Change them with --test-timeout and --suite-timeout.

A Test Station enrols with its own key, minted by an administrator. There is no shared secret: a key belongs to one station, pins to the first station that registers with it, and from then on only that station can use it.

In Bud, first. Sign in as an administrator, open Test Stations, and create a key under Enrolment keys, naming the station it is for. That name is the one the station takes, whatever the station passes as --username, so stations are named by whoever runs Bud. Copy the key when it is shown: it is displayed only once and can never be retrieved afterwards.

On the station. Send that station’s key as RUNNER_API_KEY. Identity and tokens are saved to the machine vault. If --password is omitted, the runner generates one and prints it once: save it.

export RUNNER_API_KEY="<your-station-enrolment-key>"
export BUD_BACKEND_URL="https://bud.example.com"
python -m bud_runner register \
--username "station-01" \
--socket-port 53035

Bud assigns the station name from the enrolment key, and the runner adopts the returned name. A later rename in Bud is picked up on the next heartbeat. Existing stations upgrading from the shared-key flow need their own enrolment key.

Use --re-register with the existing password to refresh the token or update socket settings.

Repeat for each station, with a fresh key each time. One machine may host several stations: each is a separate account with its own named enrolment key and socket port. Logs and PIDs are isolated under ~/.bud/daemons/, prefixed bud_<username>.

An administrator manages stations from the same Test Stations screen:

  • Rename a station at any time. It keeps its credentials and its runs, and picks the new name up on its next heartbeat.
  • Revoke a key.
  • Remove a station. This revokes its credentials and keeps every run it produced.

Runner tokens are issued at registration and are long-lived: 90 days by default, set with RUNNER_TOKEN_EXPIRE_HOURS. A change of Bud’s SECRET_KEY invalidates runner tokens, so runners must re-register. See the Configuration reference.

Uploads are enabled by default. If an upload returns 401 Unauthorized and you supplied --username and --password, the runner re-authenticates, refreshes the cached token, and retries once. If upload still fails, it exits non-zero and spools the payload under ~/.bud/spool/results/ for replay on the next successful run.

python -m bud_runner run-tests \
--test-case-list tests.smoke.SMOKE_TEST_LIST \
--backend-url "https://bud.example.com/" \
--username "station-01" \
--password "<runner-password>" \
--upload

Each suite execution becomes a test run in Bud. To attach results to an existing run, pass --test-run-id.

python -m bud_runner daemon runs the long-lived runner (heartbeat plus a local control socket). The socket binds to 127.0.0.1 by default. Non-loopback binding is not a LAN-safe deployment mode: the socket protocol has no authentication. Run the daemon under a service manager (systemd, launchd, or a Windows service wrapper like NSSM) with the Bud config directory owned by that service identity.

To register and start the runner as two steps:

python -m bud_runner register \
--username "station-01" \
--socket-port 53035 \
--no-start
python -m bud_runner daemon \
--username "station-01" \
--location "Hardware Lab"

A station can pull work instead of being driven locally: schedule the run in Bud, then have the station claim it. Completed claims are acknowledged idempotently, so a retry after a dropped connection will not run the work twice.

# poll continuously from the directory holding this station's tests
python -m bud_runner claim-run --workspace /opt/station/tests --interval 30
# claim once and exit: the form to use from cron or a CI job
python -m bud_runner claim-run --interval 0

--workspace is where the station’s test modules live and defaults to the current directory, so run it from the right place or set it explicitly. claim-run takes its own --artifact patterns and forwards them to each claimed run.

In Bud, users schedule this work through Custom Run:

  • A custom-run request contains the selected test classes and the assigned Test Station. A selection spanning two stations becomes one queued run per station.
  • The request appears in the station’s queue, and links to an execution record once that station claims it. Queued requests are left out of run reports and dashboard execution totals.
  • The station executes the selection in its local test workspace, uploads results and artifacts, and acknowledges completion with its process exit code.
  • Dashboard totals and completion notifications wait for that acknowledgement. An unavailable selected class or another execution error counts as a failed run, even when no test results were produced.
  • Completion retries keep the original answer and do not record it twice.

Attach files to a run with --artifact (or -A). It takes a path or a glob and is repeatable, so several patterns can be passed in one invocation. Anything inside --artifact-dir is uploaded too when that directory exists: bud-artifacts by default, which is the simpler route when a test writes its own output. The report and whatever files the run leaves behind are sent with the results.

python -m bud_runner run-tests \
--test-case-list tests.smoke.SMOKE_TEST_LIST \
--artifact "logs/*.log" \
--artifact screenshots/failure.png \
--upload

Bud limits what one run can upload:

  • Each file is capped at 25 MiB by default.
  • All artifacts of one run together are capped at 250 MiB by default. The budget belongs to the run, not the suite name, so separate executions of the same suite get separate budgets.
  • The upload rate and the one-active-upload guard apply per runner, so different runners have independent allowances.

The operator sets these limits. See Artifact storage.