Skip to content

feat(runner): add the test runner, plans and tmachine Session API, and run a plan on CI - #4773

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

matthewgrossman wants to merge 7 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, and checks the JUnit results. 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

  1. feat(runner): add the test runner, plans and tmachine Session API, and run a plan on CI #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(suites): port the universal conformance suites onto testkit and run them from the plan #4774: ports the conformance suites that every driver must pass to testkit and runs them in more environments. It is being reshaped to build on this PR without plans.
  3. Next: the features and drivers suites move to testkit.
  4. Later: 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 a branch at 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, and a signal that arrives while the runner is busy with blocking work.

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

Part of #3954

Changes

  • tmachine: the Session API; SSH, scp, and exec timeouts; down() never panics; QEMU is cleaned up on SIGTERM; the SSH port is TMACHINE_SSH_PORT. An openshell-test-runner flake package. 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, and writes junit.xml, nextest.log and, after a failure, the guest's diagnostics to --results. A run passes if nextest succeeds and its report shows at least one test and no failures. --filter narrows with a nextest filterset, and --keep keeps the VM for debugging.
  • 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 not part of the Core E2E result yet.
  • 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, AGENTS.md, CONTRIBUTING.md and the testkit and suite READMEs describe the runner.

Testing

  • Checks appropriate to the affected code and behavior pass
    • Locally: fmt and clippy -D warnings for testkit, the runner, tmachine and the conformance suite; the runner (10), tmachine (8) and testkit unit tests; the lockfile alignment check and its tests; the SPDX header check. Every commit builds with --locked.
    • nix run .#openshell-test-runner -- --help builds and runs.
    • The runner checks JUnit parsing against a real report from the nextest version the guest installs.
    • Not yet run on this head: a VM run (the amd64 guest can't boot on an arm64 Mac) and Branch E2E. The plans-based version of this lane passed Branch E2E on be103d6 (run).
  • 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.

@matthewgrossman
matthewgrossman added this pull request to stack #4776 October 20, 2026 23:38
@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 added this pull request to stack #4824 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 added this pull request to stack #4825 October 22, 2026 05:30
@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 added this pull request to stack #4836 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
@matthewgrossman
matthewgrossman requested a review from a team as a code owner 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
let specs: Vec<_> = testkit_spec::scan(&source)
.map_err(|error| anyhow::anyhow!("{}: {error}", file.display()))?
.into_iter()
.filter(|spec| !spec.ignored)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The source catalog does not account for conditional attributes, so a healthy lane can fail JUnit verification. For example:

/// Disabled on every target.
#[cfg(any())]
#[testkit::test]
async fn disabled(cx: &mut Context) -> testkit::Result { /* ... */ }

I reproduced the catalog including disabled and result verification reporting it missing, although Cargo correctly excludes it. Platform-specific #[cfg(...)] tests have the same problem, and #[cfg_attr(..., ignore)] is not recognized by the literal #[ignore] check either.

Could we resolve these attributes for the guest build, or explicitly reject unsupported conditional declarations with a clear error, rather than adding them to the expected test set? Reproduced on 1da9c3b832bdae01be7f4022bd9e030a84b2dbe4; the relevant catalog and parser behavior remains present at 4781a500c1f8e4064c1c64e8b37c963fa4a46902.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, good catch. Rather than evaluate cfg for the guest target, testkit-spec now refuses it: a #[cfg] or #[cfg_attr] on a #[testkit::test] function, or a file-level #![cfg] in a file that has tests, is an error from both the macro and the catalog scan, so the catalog and the build can't disagree. A test that only applies to some environments belongs in a features or drivers suite, and #[ignore = "<issue>"] remains the way to skip one everywhere. Tests are in testkit-spec (rejects_conditional_tests, scan_rejects_conditional_files_with_tests), and tests/testkit/README.md documents the rule. Fixed in 1b80fad.

@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 and others added 7 commits October 23, 2026 15:40
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>
@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.

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.

2 participants