Skip to content

feat(tests): add the tmachine Session API and run a conformance smoke test on tmachine - #4773

Open
matthewgrossman wants to merge 9 commits into
mainfrom
test/3954-first-testkit-lane/matthewgrossman
Open

matthewgrossman wants to merge 9 commits into
mainfrom
test/3954-first-testkit-lane/matthewgrossman

Conversation

@matthewgrossman

@matthewgrossman matthewgrossman commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

Summary

This PR makes it possible to run a testkit test on top of tmachine. A small runner builds a test family's tests for the guest, boots one tmachine environment with OpenShell installed, runs the tests inside it with nextest, and reports the result. On CI, an advisory lane runs one smoke test on Ubuntu with rootful Docker.

nix run .#openshell-test-runner -- \
  --environment ubuntu-docker-rootful --installer deb \
  --family conformance --package sandbox

Most of the runner and the tmachine Session API come from @elezar's draft #4629.

Stack

The stack is linear; each PR targets the one before it.

  1. feat(tests): add the tmachine Session API and run a conformance smoke test on tmachine #4773 (this PR): the tmachine Session API, a runner for one family in one tmachine environment, a smoke suite, and an advisory CI lane with an explicit matrix.
  2. test(features): run feature tests against environment services, starting with Keycloak #4891: the features pattern. A tmachine environment runs Keycloak and publishes how to reach it as OPENSHELL_TEST_* variables, and a suite fixture reads them. The Keycloak provider-refresh test is the example.
  3. test(suites): port the conformance suites onto testkit and run conformance-integration on the runner #4774: moves the existing conformance suites to testkit, runs them on Docker, Podman and k3s from explicit matrix rows, and deletes the old tmachine conformance testsuites. Being restacked onto test(features): run feature tests against environment services, starting with Keycloak #4891 without plans.
  4. Later: ship openshell-conformance, move the remaining features and drivers suites, then bring back plans (which suites run where) once features show what they need.

Deferred from this PR

  • Plans, the environment registry, the source catalog, matrix generation and strict mode, plus optional test IDs and testkit-spec. They are kept on test/3954-deferred-test-plans/matthewgrossman (be103d6, the previous head of this PR) and come back after features. Until then, CI lists what runs where as an explicit matrix in e2e-suites.yml.
  • More environments and suites. One environment and one smoke test run here.
  • Gating. The e2e-suites lane is advisory and not part of the Core E2E result.
  • arm64. CI has no arm64 runner with KVM yet.
  • Runner robustness: a hung SSH or QMP connection.

Dropped from #4629, not planned:

  • requires, capabilities and waivers. A test that fails in an environment fails the lane. #[ignore = "<issue link>"] takes a broken test out everywhere.
  • The target file. Tests find the gateway the way the CLI does, through OPENSHELL_GATEWAY and OPENSHELL_BIN.

Out of scope for the whole stack: version compatibility guarantees between the conformance suite and gateways, and running tests from other repositories.

Related Issue

Closes #4934

Part of #3954

Changes

  • tmachine: the Session API; SSH, scp, and exec timeouts; down() never panics; until_signal drops a binary's work on SIGTERM, SIGINT or SIGHUP so QEMU stops with it; the SSH port is TMACHINE_SSH_PORT and is checked before QEMU starts. An openshell-test-runner flake package that builds the runner with Nix. Environment setup is unchanged.
  • tests/runner (openshell-test-runner): picks the tmachine environment and installer by name, builds the family's packages (a workspace under tests/suites/) as a nextest archive for the guest's musl target, waits for the gateway with testkit-ready, runs nextest in the guest with --no-tests fail, and writes junit.xml, nextest.log and, after a failure, the gateway's journal to --results. The result comes from nextest's exit code: a run passes if nextest succeeds, fails if a test failed (the end of the nextest log, which names the failed tests, is printed), and is an error otherwise, including when no test ran. --filter narrows with a nextest filterset, and --keep keeps the VM for debugging until Enter or Ctrl-C.
  • tests/suites/conformance/sandbox: the smoke suite (create a sandbox, exec echo, delete it). mise run e2e:testkit runs it against a local Docker gateway.
  • .github/workflows/e2e-suites.yml (reusable), with an explicit include matrix of one row, and an e2e-suites job in Branch E2E (label test:e2e). JUnit and logs are uploaded as e2e-results-<family>-<package>-<environment>-<installer>. It is advisory: not part of the Core E2E result.
  • The lockfile alignment check, enrolling the testkit and conformance workspaces. fmt, clippy, and nextest for the runner, tmachine, and suite in Branch Checks.
  • TESTING.md and the testkit and suite READMEs describe the runner; AGENTS.md and CONTRIBUTING.md list tests/suites/.

Testing

  • Checks appropriate to the affected code and behavior pass
    • Locally: fmt and clippy -D warnings for the runner, tmachine and the conformance suite; the runner and tmachine unit tests; the lockfile alignment check and its tests. nix build .#openshell-test-runner succeeds.
    • Local tmachine run on macOS arm64 (Ubuntu, rootful Docker, .deb): the smoke test passed with the Nix-built runner, and Ctrl-C at the --keep prompt exited at once with 130 and stopped QEMU. With port 2222 already taken, the run stopped before booting with a clear port error.
    • Branch E2E (amd64) passed the smoke test on d6f93fa (run), and every other lane in that run passed too.
  • Unit tests added/updated
  • E2E tests added/updated

Checklist

  • Follows Conventional Commits
  • Commits are signed off (DCO)
  • Architecture docs updated (TESTING.md, AGENTS.md, CONTRIBUTING.md, tests/testkit/README.md, tests/suites/conformance/sandbox/README.md)

@copy-pr-bot

copy-pr-bot Bot commented Oct 20, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@github-actions

Copy link
Copy Markdown

@matthewgrossman
matthewgrossman force-pushed the test/3954-first-testkit-lane/matthewgrossman branch 3 times, most recently from e06a0eb to a23e140 Compare October 21, 2026 20:41
@matthewgrossman
matthewgrossman force-pushed the test/3954-first-testkit-lane/matthewgrossman branch 2 times, most recently from be4af48 to 63f4141 Compare October 22, 2026 04:51
@matthewgrossman
matthewgrossman removed this pull request from stack #4776 October 22, 2026 04:51
@matthewgrossman
matthewgrossman changed the base branch from test/3954-land-testkit/matthewgrossman to test/3954-testkit-ci-lane/matthewgrossman October 22, 2026 04:51
@matthewgrossman
matthewgrossman force-pushed the test/3954-first-testkit-lane/matthewgrossman branch from 63f4141 to df44e02 Compare October 22, 2026 05:03
@matthewgrossman matthewgrossman changed the title test(sandbox): add the first testkit suite and run it in Branch E2E feat(runner): add the test runner, plans and tmachine Session API, and run a plan on CI Oct 22, 2026
@matthewgrossman
matthewgrossman removed this pull request from stack #4824 October 22, 2026 05:29
@matthewgrossman
matthewgrossman removed this pull request from stack #4825 October 22, 2026 16:01
@matthewgrossman
matthewgrossman force-pushed the test/3954-first-testkit-lane/matthewgrossman branch from df44e02 to 4d19336 Compare October 22, 2026 16:01
@matthewgrossman
matthewgrossman changed the base branch from test/3954-testkit-ci-lane/matthewgrossman to test/3954-land-testkit/matthewgrossman October 22, 2026 16:01
@matthewgrossman
matthewgrossman force-pushed the test/3954-first-testkit-lane/matthewgrossman branch from 4d19336 to 6c9a275 Compare October 22, 2026 16:37
@matthewgrossman
matthewgrossman force-pushed the test/3954-first-testkit-lane/matthewgrossman branch from 6c9a275 to 1da9c3b Compare October 22, 2026 16:56
@matthewgrossman
matthewgrossman marked this pull request as ready for review October 22, 2026 17:10
Base automatically changed from test/3954-land-testkit/matthewgrossman to main October 22, 2026 17:24
@matthewgrossman
matthewgrossman force-pushed the test/3954-first-testkit-lane/matthewgrossman branch from 1da9c3b to 4781a50 Compare October 22, 2026 17:24
Comment thread tests/runner/src/catalog.rs Outdated
@matthewgrossman
matthewgrossman force-pushed the test/3954-first-testkit-lane/matthewgrossman branch 3 times, most recently from 8e7e5d4 to 2b879b0 Compare October 23, 2026 04:00
@matthewgrossman
matthewgrossman requested a review from drew October 23, 2026 04:01
@matthewgrossman
matthewgrossman force-pushed the test/3954-first-testkit-lane/matthewgrossman branch 2 times, most recently from c4f944f to be103d6 Compare October 23, 2026 05:29
@matthewgrossman
matthewgrossman force-pushed the test/3954-first-testkit-lane/matthewgrossman branch from be103d6 to f244394 Compare October 23, 2026 22:44
@matthewgrossman matthewgrossman added the test:e2e Requires end-to-end coverage label Oct 23, 2026
@github-actions

Copy link
Copy Markdown

Label test:e2e applied for f244394. Open the existing run and click Re-run all jobs to execute with the label set. The run will execute the standard E2E suite after building the required gateway, sandbox, and supervisor images once. The matching required CI gate status on this PR will flip green automatically once the run finishes.

@matthewgrossman matthewgrossman changed the title feat(runner): add the test runner, plans and tmachine Session API, and run a plan on CI feat(tests): add the tmachine Session API and run a conformance smoke test on tmachine Oct 26, 2026
@matthewgrossman
matthewgrossman force-pushed the test/3954-first-testkit-lane/matthewgrossman branch from f244394 to 6106cd0 Compare October 26, 2026 20:12
@matthewgrossman
matthewgrossman removed this pull request from stack #4836 October 26, 2026 21:09
matthewgrossman and others added 9 commits October 26, 2026 14:21
Add a library API that boots an environment, runs commands in it with timeouts,
copies files in and out, collects diagnostics and tears it down, so a caller can
drive a machine without writing an Ansible playbook per suite. SSH and exec
timeouts and QEMU cleanup on SIGTERM keep a hung guest or a cancelled CI job from
leaving QEMU running.

Add the `openshell-test-runner` flake package, which runs tests/runner with the
toolchains and VM tools it needs.

The Session API and test-runner package are adapted from #4629.

Co-authored-by: Evan Lezar <elezar@nvidia.com>
Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
…like the root

`cargo --locked` only checks a lockfile against its own manifest. A workspace
outside the root that builds root crates through path dependencies, such as the
testkit suites building `openshell-sdk`, has its own Cargo.lock, so the two
lockfiles can resolve a shared crate to different versions without any check
failing. That is how aws-lc-sys 0.46 was picked up in #4629, which does not link
against the AWS-LC that Nix provides.

check_cargo_lockfile_alignment.py fails when an aligned workspace locks a
registry crate that the root lockfile also contains at a version the root
lockfile does not have. With --fix it re-locks the workspace starting from the
root lockfile. tests/testkit is the first aligned workspace.

The script and its tests come from #4629 (a37ebe9).

Co-authored-by: Evan Lezar <elezar@nvidia.com>
Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
Add the `sandbox` crate to the conformance workspace with one testkit test that
creates a sandbox, runs `echo` in it with exec, deletes it and checks it is gone.
It is deliberately small: it exists so the runner can run a real test in a
tmachine environment on CI before the existing suites move to testkit.

The workspace's other crate, the CLI conformance suite, keeps running as it does
today. The conformance workspace joins the lockfile alignment check, so its
lockfile is realigned with the root lockfile.

The test is adapted from the `sandbox-lifecycle` suite in #4629.

Co-authored-by: Evan Lezar <elezar@nvidia.com>
Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
`openshell-test-runner` builds a family's packages (a workspace under
tests/suites/) on the host as a nextest archive for the guest's musl target,
boots one tmachine environment by name with the named installer through the
Session API, runs nextest in it and brings back the JUnit report and log. No
per-suite Ansible playbook is involved:

  nix run .#openshell-test-runner -- --environment ubuntu-docker-rootful \
    --installer deb --family conformance --package sandbox

The runner points the tests at the system under test with environment
variables: OPENSHELL_GATEWAY (the gateway the installer registered, `--gateway`,
default `openshell`) and OPENSHELL_BIN (the installed CLI). It waits for the
gateway with `testkit-ready` before the tests start. Nextest runs with
`--test-threads 1` and a slow-timeout that terminates a hung test.

A run passes if nextest succeeds and its JUnit report shows at least one test
and no failures. `--filter` narrows the tests with a nextest filterset, and
`--keep` leaves the VM up for debugging. Results from an earlier run are
removed first, and the guest's diagnostics are collected after a failure.

Which tests run in which environments is left to the caller for now; there are
no plans, environment registry or per-test requirements, and a broken test is
`#[ignore]`d.

Most of this comes from #4629, adapted to testkit finding the gateway the way the
CLI does instead of reading a target file.

Co-authored-by: Evan Lezar <elezar@nvidia.com>
Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
Run fmt, clippy and nextest for tests/tmachine and tests/runner, and fmt and
clippy for the new `sandbox` suite crate, wherever tests/testkit already runs
them: the mise tasks and Branch Checks. The suite's tests need a gateway, so they
run through `mise run e2e:testkit` against a local Docker gateway and, on CI,
through the test runner rather than the unit-test job.

The lockfile task and the Cargo lockfiles job also run the alignment check, and
its own tests run with `mise run test`.

Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
…workflow

Add a reusable workflow with an explicit matrix: each row names a tmachine
environment and installer, a family and package, and the runner label. Each
row runs `nix run .#openshell-test-runner` once. The jobs use the same
integration inputs as the existing tmachine lanes, cache the tmachine disks
keyed on the Ansible tree, upload the JUnit report and logs whether or not the
run passes, and an aggregator job reports the overall result.

The only row runs the conformance sandbox suite on amd64 Ubuntu with rootful
Docker and the Debian package, which CI can provision today. Branch E2E calls
the workflow when the test:e2e label is set. The job is not in the Core E2E
result's needs yet, so it starts out advisory. The existing conformance,
feature and driver lanes are unchanged.

Modeled on the E2E Suites workflow in #4629.

Co-authored-by: Evan Lezar <elezar@nvidia.com>
Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
Describe how the runner runs a family in a tmachine environment, locally and
on CI, in TESTING.md, add a README for the sandbox suite, point the testkit
README at the runner, and add rows for tests/suites, tests/tmachine and
tests/runner to the project-structure tables in AGENTS.md and CONTRIBUTING.md.

Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
- Replace tmachine's global QEMU PID registry with until_signal, which
  drops the work on SIGTERM/SIGINT/SIGHUP so kill_on_drop stops QEMU.
  This also lets Ctrl-C end a --keep run.
- Decide a run's status from nextest's exit code (with --no-tests fail)
  instead of parsing JUnit; print the end of the nextest log on failure.
- Move gateway diagnostics from tmachine's Session into the runner, drop
  Session::run_playbooks, and make Session own its VM directly.
- Read TMACHINE_SSH_PORT once and reject invalid values; drop the racy
  port pre-check.
- Build the runner with Nix so tmachine's firmware paths are compiled in,
  and set ANSIBLE_CONFIG only in the wrapper.
- Copy the bundle directory in directly, fold lib.rs into main.rs, and
  make the gateway name required.
- Drop the semver hint from the lockfile alignment check and trim
  duplicated docs.

Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
Restore the check that the SSH forward port is free before QEMU starts. QEMU
reports a taken port only by exiting, so a layer build would otherwise run its
playbooks against the VM that already holds the port and cache the result.

Report nextest's no-tests exit code as "no test ran" instead of a bare exit
status.

Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
@matthewgrossman
matthewgrossman force-pushed the test/3954-first-testkit-lane/matthewgrossman branch from 6106cd0 to d6f93fa Compare October 26, 2026 23:42

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test:e2e Requires end-to-end coverage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

test: run a conformance smoke test on tmachine through a test runner

2 participants