* test(testkit): add the testkit workspace for end-to-end suites Add tests/testkit as its own Cargo workspace: testkit, testkit-macros, and testkit-spec. testkit reads a target file, connects to the gateway through openshell-sdk, creates and cleans up sandboxes, and runs the installed CLI. #[testkit::test] requires a stable ID and a doc comment and declares capability requirements. testkit-ready waits for a target's gateway and CLI before tests run. Nothing uses testkit yet. The lockfile is seeded from the root lockfile so shared crates resolve to the same versions. Signed-off-by: Matthew Grossman <mgrossman@nvidia.com> * ci(tests): format, lint and test testkit in Branch Checks Nothing else builds testkit, so check it in Branch Checks and the matching mise tasks to catch openshell-sdk changes that break it. Signed-off-by: Matthew Grossman <mgrossman@nvidia.com> * docs(tests): describe how to write and run testkit tests Signed-off-by: Matthew Grossman <mgrossman@nvidia.com> * fix(testkit): give each sandbox its own cleanup timeout Cleanup shared one two-minute budget across all of a test's sandboxes, so a slow delete left the rest with no time and could leak them on a shared gateway. Delete them concurrently, each with the full timeout. Signed-off-by: Matthew Grossman <mgrossman@nvidia.com> * docs(testkit): show how to exclude tests by capability Signed-off-by: Matthew Grossman <mgrossman@nvidia.com> * docs: list tests/testkit in the project structure tables Signed-off-by: Matthew Grossman <mgrossman@nvidia.com> * refactor(testkit): drop test IDs and capabilities for now Nothing reads test IDs or capability requirements until a runner, waivers, or result history exist, so remove the id and requires arguments and the target file's capabilities section. Both can return as optional attribute arguments without changing existing tests. With them gone, testkit-spec only checked the function shape and read the doc comment, so fold it into testkit-macros. Signed-off-by: Matthew Grossman <mgrossman@nvidia.com> * docs(testkit): describe what testkit provides today Signed-off-by: Matthew Grossman <mgrossman@nvidia.com> * refactor(testkit): find the gateway the way the CLI does Replace the target file (OPENSHELL_TEST_TARGET) with the CLI's own gateway selection: OPENSHELL_GATEWAY_ENDPOINT, then OPENSHELL_GATEWAY, then the active gateway. A registered gateway's endpoint, auth mode, and mTLS material come from its CLI config directory. cx.cli runs OPENSHELL_BIN against the same gateway and workspace as the SDK. Suites now run unchanged under e2e/with-docker-gateway.sh and e2e/with-podman-gateway.sh, and environments need no extra file. Signed-off-by: Matthew Grossman <mgrossman@nvidia.com> * fix(testkit): keep labels when creating a sandbox with a policy The policy path creates the sandbox through gRPC and dropped the spec's labels without an error. Pass them in the create request. Signed-off-by: Matthew Grossman <mgrossman@nvidia.com> * fix(testkit): use the registered gateway's credentials for an endpoint When OPENSHELL_GATEWAY_ENDPOINT is set, the CLI takes credentials from the gateway named by OPENSHELL_GATEWAY, or else the registered gateway with that endpoint. testkit connected without a client certificate instead, so an mTLS endpoint such as the one e2e/rust/e2e-vm.sh exports worked for cx.cli but not for the SDK. Resolve it the way the CLI does, and pass both the name and the endpoint to cx.cli. Signed-off-by: Matthew Grossman <mgrossman@nvidia.com> * docs(tests): list testkit's gateway variables in precedence order OPENSHELL_GATEWAY_ENDPOINT takes precedence over OPENSHELL_GATEWAY. Also mention OPENSHELL_BIN. Signed-off-by: Matthew Grossman <mgrossman@nvidia.com> * fix(testkit): apply other attributes to the generated test #[testkit::test] left attributes such as #[ignore] and #[cfg] on the nested body function, where they have no effect on the test. Move them to the generated test, and reject a second test attribute such as Signed-off-by: Matthew Grossman <mgrossman@nvidia.com> #[test] or #[tokio::test]. * fix(testkit): show usage for testkit-ready --help Signed-off-by: Matthew Grossman <mgrossman@nvidia.com> * test(testkit): cover cleanup after pass, failure, and panic Signed-off-by: Matthew Grossman <mgrossman@nvidia.com> * fix(testkit): reject #[should_panic] on testkit tests A testkit test panics on any failure, including an unreachable gateway or a failed cleanup, so #[should_panic] would let such a failure pass. Reject it and point to checking the expected failure in the body. Signed-off-by: Matthew Grossman <mgrossman@nvidia.com> * test(testkit): keep only the macro's misuse checks as unit tests Suites built on testkit exercise gateway resolution, the CLI wrapper, polling, errors, and cleanup against a real gateway. Keep one unit test for the macro's compile errors, which a passing suite never reaches, and drop the test-only seams the other tests needed. Signed-off-by: Matthew Grossman <mgrossman@nvidia.com> * refactor(testkit): connect through the SDK's mTLS support The SDK's ClientConfig now takes a client identity, so testkit no longer builds its own channel. It still resolves the registered gateway and reads its credential files, then hands the CA and client certificate to OpenShellClient::connect. That keeps TLS behavior, connect timeouts and keep-alive settings in the SDK, and drops testkit's direct tonic dependency. Signed-off-by: Matthew Grossman <mgrossman@nvidia.com> * fix(testkit): bound readiness and cleanup with timeouts wait_ready checked elapsed time only after a health call returned, and cleanup started its timeout only after the delete returned, so a gateway that accepts a connection but never answers could hang either one. Wrap each whole operation in tokio::time::timeout: the readiness wait, including every connect and health call, and each sandbox's delete plus wait for deletion. A test covers a peer that accepts connections and never replies. Signed-off-by: Matthew Grossman <mgrossman@nvidia.com> --------- Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
testkit
Shared support for end-to-end test suites. Suites test a running OpenShell
gateway through the Rust SDK and the installed openshell CLI.
This is a separate Cargo workspace with its own lockfile:
| Crate | Purpose |
|---|---|
testkit |
Finds and connects to the gateway, creates and cleans up sandboxes, runs the CLI. Also builds testkit-ready. |
testkit-macros |
The #[testkit::test] attribute. |
Writing a test
A test is an async fn that takes &mut testkit::Context and returns
testkit::Result:
use testkit::Context;
/// A long-running sandbox becomes ready, runs a command with exec, and is gone
/// from get and list after delete.
#[testkit::test]
async fn long_running_exec_delete(cx: &mut Context) -> testkit::Result {
let sandbox = cx.sandbox("lr").command(["sleep", "infinity"]).create().await?;
sandbox.wait_ready().await?;
// ...
Ok(())
}
- The doc comment is required. It is the test's description.
- The function name is the nextest test name.
- Other attributes apply to the test. For example,
#[ignore = "reason"]skips it unless run with--run-ignored.#[should_panic]is rejected, because a testkit test panics on any failure. To check that something is refused, assert the failure in the body, for example withcx.cli([...]).await?.failure()?andtestkit::ensure!.
Cleanup runs after every test, whether it passes, fails, or panics. It only
deletes sandboxes created through cx.sandbox(...). Delete anything else a
test creates, such as providers, in the test itself.
What a test can do
Context currently provides:
cx.sandbox(suffix)to build and create a sandbox with a run-unique name. The returnedSandboxcan wait for ready or finished, stop, start, exec, read its config and draft policy, set a setting, and delete.cx.expect_listedandcx.expect_absentto check a sandbox in list and get.cx.cli([...])to run the installed CLI against the same gateway, withsuccess(),failure(), andjson()on the output.cx.client()for SDK calls testkit does not wrap.poll_untilfor waiting on other state.
Running against a gateway
Tests find the gateway the same way the openshell CLI does:
OPENSHELL_GATEWAY_ENDPOINT: a gateway URL. Its credentials come from the gateway named byOPENSHELL_GATEWAY, or else the registered gateway with that endpoint. A URL no gateway is registered for is used as is: plaintext, or TLS without a client certificate.OPENSHELL_GATEWAY: the name of a gateway registered with the CLI.- The CLI's active gateway, set by
openshell gateway select.
A registered gateway's endpoint and auth mode come from
$XDG_CONFIG_HOME/openshell/gateways/<name>/metadata.json. For mTLS, the CA
and client certificate come from its mtls/ directory. testkit supports
plaintext and mTLS gateways. OPENSHELL_WORKSPACE selects the workspace
(default default), and OPENSHELL_BIN selects the CLI that cx.cli runs
(default openshell on PATH). The CLI always targets the same gateway and
workspace as the SDK. Each test logs the gateway it used.
Wait for the gateway, then run a suite. Run tests that share a gateway one at a time:
export OPENSHELL_GATEWAY=<name>
cargo run --manifest-path tests/testkit/Cargo.toml --bin testkit-ready -- 300
cargo nextest run --manifest-path <suite>/Cargo.toml --test-threads 1
testkit-ready [TIMEOUT_SECS] exits non-zero if the gateway is not healthy in
time or the CLI does not report it connected.
e2e/with-docker-gateway.sh and e2e/with-podman-gateway.sh start an
ephemeral gateway and export OPENSHELL_GATEWAY and OPENSHELL_BIN, so a
suite runs under them unchanged.
Checks
cargo fmt --manifest-path tests/testkit/Cargo.toml --all -- --check
cargo clippy --locked --manifest-path tests/testkit/Cargo.toml --all-targets -- -D warnings
cargo nextest run --locked --config-file .config/nextest.toml --manifest-path tests/testkit/Cargo.toml
mise run rust:lint, mise run rust:format:check, and mise run test include
these, and Branch Checks runs them.