Skip to content

Commit caff38c

Browse files
refactor(testkit): make the sandbox and output helpers uniform
Move expect_listed, expect_absent, wait_absent and the CLI's sandbox get JSON (now get_json) from Context onto Sandbox, and drop delete_and_expect_absent. Give ExecOutput failure() and CliOutput expect_stdout(), so both outputs offer the same checks. Put with_deadline's description first, like poll_until. Make CliSandboxBuilder::run consume the builder like create, and rename its policy setter to policy_yaml. Update the testkit README's list of what a test can do. Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
1 parent 73e5bff commit caff38c

12 files changed

Lines changed: 152 additions & 114 deletions

File tree

‎tests/suites/conformance/src/cli.rs‎

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ async fn cli_create_exec_list_delete(cx: &mut Context) -> testkit::Result {
2525

2626
let sandbox = cx.cli_sandbox("cs").create().await?;
2727
let name = sandbox.name().to_string();
28-
let got = cx.sandbox_details(&name).await?;
28+
let got = sandbox.get_json().await?;
2929
testkit::ensure!(
3030
got["name"] == name.as_str(),
3131
"sandbox get returned {:?}; expected {name:?}",
@@ -41,22 +41,15 @@ async fn cli_create_exec_list_delete(cx: &mut Context) -> testkit::Result {
4141
testkit::ensure!(listed.is_some(), "sandbox {name} is not in sandbox list");
4242

4343
let marker = format!("openshell-e2e-{}", cx.run_id());
44-
let exec = cx
45-
.cli([
46-
"sandbox", "exec", "--name", &name, "--no-tty", "--", "echo", &marker,
47-
])
48-
.await?
49-
.success()?;
50-
testkit::ensure!(
51-
exec.stdout == format!("{marker}\n"),
52-
"sandbox exec stdout was {:?}; expected {:?}",
53-
exec.stdout,
54-
format!("{marker}\n")
55-
);
44+
cx.cli([
45+
"sandbox", "exec", "--name", &name, "--no-tty", "--", "echo", &marker,
46+
])
47+
.await?
48+
.expect_stdout(&format!("{marker}\n"))?;
5649

5750
cx.cli(["sandbox", "delete", &name]).await?.success()?;
5851
wait_unlisted(cx, &name).await?;
59-
cx.expect_absent(&sandbox).await
52+
sandbox.expect_absent().await
6053
}
6154

6255
/// A sandbox created with the CLI can be stopped, rejects `sandbox exec` while
@@ -88,7 +81,7 @@ async fn cli_stop_start_delete(cx: &mut Context) -> testkit::Result {
8881

8982
cx.cli(["sandbox", "delete", &name]).await?.success()?;
9083
wait_unlisted(cx, &name).await?;
91-
cx.expect_absent(&sandbox).await
84+
sandbox.expect_absent().await
9285
}
9386

9487
/// Wait until `sandbox list` no longer shows the sandbox.

‎tests/suites/conformance/src/filesystem_policy.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,8 @@ async fn replaced_policy_path_rejects_next_launch(cx: &mut Context) -> testkit::
133133
// The exec's own error is the rejection report, so keep it apart from
134134
// the deadline's.
135135
let next = with_deadline(
136-
REJECTED_LAUNCH_TIMEOUT,
137136
&format!("({case}) the rejected launch"),
137+
REJECTED_LAUNCH_TIMEOUT,
138138
async { Ok(sandbox.exec(["true"]).await) },
139139
)
140140
.await?;

‎tests/suites/conformance/src/lifecycle.rs‎

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ async fn long_running_exec_delete(cx: &mut Context) -> testkit::Result {
2828
.await?
2929
.expect_stdout(&format!("{marker}\n"))?;
3030

31-
cx.delete_and_expect_absent(&sandbox).await
31+
sandbox.delete().await?;
32+
sandbox.expect_absent().await
3233
}
3334

3435
/// Longer than the supervisor's 30s startup recovery deadline.
@@ -52,8 +53,8 @@ async fn exec_outlives_startup_recovery_deadline(cx: &mut Context) -> testkit::R
5253
PAST_RECOVERY_DEADLINE.as_secs()
5354
);
5455
with_deadline(
55-
PAST_RECOVERY_EXEC_DEADLINE,
5656
"the exec past the recovery deadline",
57+
PAST_RECOVERY_EXEC_DEADLINE,
5758
sandbox.exec(["sh", "-c", script.as_str()]),
5859
)
5960
.await?
@@ -114,9 +115,12 @@ async fn delete_is_prompt_when_workload_is_slow_to_handle_term(
114115
// Boundary containment may kill the workload before its handler finishes;
115116
// this asserts bounded deletion, not a graceful workload exit.
116117
with_deadline(
117-
DELETE_DEADLINE,
118118
&format!("deleting sandbox {}", sandbox.name()),
119-
cx.delete_and_expect_absent(&sandbox),
119+
DELETE_DEADLINE,
120+
async {
121+
sandbox.delete().await?;
122+
sandbox.expect_absent().await
123+
},
120124
)
121125
.await
122126
}
@@ -160,7 +164,8 @@ async fn run_to_completion_exit_delete(cx: &mut Context) -> testkit::Result {
160164
again.restart_count
161165
);
162166

163-
cx.delete_and_expect_absent(&sandbox).await
167+
sandbox.delete().await?;
168+
sandbox.expect_absent().await
164169
}
165170

166171
/// A main command that exits during provisioning is reported as the
@@ -236,7 +241,7 @@ async fn default_command_sandbox_is_listed_ready(cx: &mut Context) -> testkit::R
236241
ready.name,
237242
sandbox.name()
238243
);
239-
cx.expect_listed(&sandbox, SandboxPhase::Ready).await?;
244+
sandbox.expect_listed(SandboxPhase::Ready).await?;
240245

241246
let marker = format!("openshell-e2e-{}", cx.run_id());
242247
sandbox
@@ -246,7 +251,8 @@ async fn default_command_sandbox_is_listed_ready(cx: &mut Context) -> testkit::R
246251
// The gateway's default image is OpenShell's Ubuntu base image.
247252
crate::image::expect_ubuntu(&sandbox).await?;
248253

249-
cx.delete_and_expect_absent(&sandbox).await
254+
sandbox.delete().await?;
255+
sandbox.expect_absent().await
250256
}
251257

252258
/// A stopped sandbox rejects exec, accepts a second stop, and returns to
@@ -291,7 +297,8 @@ async fn stopped_sandbox_can_be_deleted(cx: &mut Context) -> testkit::Result {
291297
.create_ready()
292298
.await?;
293299
sandbox.stop().await?;
294-
cx.delete_and_expect_absent(&sandbox).await
300+
sandbox.delete().await?;
301+
sandbox.expect_absent().await
295302
}
296303

297304
/// Where the main command of [`canonical_main_environment`] records what it
@@ -422,7 +429,7 @@ fi
422429
)
423430
.await?;
424431

425-
let initial = cx.sandbox_details(sandbox.name()).await?;
432+
let initial = sandbox.get_json().await?;
426433
let initial_instance = main_instance(&initial).to_string();
427434
testkit::ensure!(
428435
!initial_instance.is_empty(),
@@ -438,7 +445,7 @@ fi
438445
RESTART_TIMEOUT,
439446
Duration::from_millis(250),
440447
async || {
441-
let details = cx.sandbox_details(sandbox.name()).await?;
448+
let details = sandbox.get_json().await?;
442449
let instance = main_instance(&details);
443450
let replaced = !instance.is_empty() && instance != initial_instance;
444451
Ok(
@@ -471,7 +478,7 @@ fi
471478
}
472479

473480
/// The ID of the sandbox's current main process instance, from
474-
/// [`Context::sandbox_details`]. Empty if none is reported.
481+
/// [`Sandbox::get_json`]. Empty if none is reported.
475482
fn main_instance(details: &serde_json::Value) -> &str {
476483
details["main_process_instance_id"]
477484
.as_str()
@@ -509,7 +516,8 @@ async fn detached_no_keep_sandbox_is_removed_after_main_exits(cx: &mut Context)
509516
.success()?;
510517
}
511518
for (sandbox, _, code) in &sandboxes {
512-
cx.wait_absent(sandbox, EPHEMERAL_CLEANUP_TIMEOUT)
519+
sandbox
520+
.wait_absent(EPHEMERAL_CLEANUP_TIMEOUT)
513521
.await
514522
.context(format!("ephemeral sandbox exited {code}"))?;
515523
}

‎tests/suites/conformance/src/smoke.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,6 @@ async fn create_exec_delete(cx: &mut Context) -> testkit::Result {
2121
.await?
2222
.expect_stdout(&format!("{marker}\n"))?;
2323

24-
cx.delete_and_expect_absent(&sandbox).await
24+
sandbox.delete().await?;
25+
sandbox.expect_absent().await
2526
}

‎tests/suites/drivers/podman/tests/oci_identity.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ USER {OCI_IDENTITY}
6767
let sandbox = cx
6868
.cli_sandbox("oi")
6969
.image(&image.tag)
70-
.policy(IMAGE_IDENTITY_POLICY)
70+
.policy_yaml(IMAGE_IDENTITY_POLICY)
7171
.command(["sleep", "infinity"])
7272
.create()
7373
.await

‎tests/suites/features/host-gateway/tests/host_gateway.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ async fn python_sandbox_reaches_host_server(cx: &mut Context) -> testkit::Result
6363
let sandbox = cx
6464
.cli_sandbox("hg")
6565
.image(&image)
66-
.policy(&policy(server.port))
66+
.policy_yaml(&policy(server.port))
6767
.command(["sh", "-c", "exec sleep infinity"])
6868
.create()
6969
.await

‎tests/suites/features/oci-image/tests/oci_image.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ USER app
225225
);
226226
// Require the condition reason, so another provisioning failure
227227
// cannot pass as a workspace rejection.
228-
let sandbox = oci.cx.sandbox_details(&name).await?;
228+
let sandbox = oci.cx.adopt(&name).await?.get_json().await?;
229229
let reasons: Vec<&str> = sandbox["conditions"]
230230
.as_array()
231231
.into_iter()
@@ -284,7 +284,7 @@ impl Oci<'_> {
284284
);
285285
let mut builder = self.cx.cli_sandbox(short).image(&image.tag);
286286
if let Some(policy) = policy {
287-
builder = builder.policy(policy);
287+
builder = builder.policy_yaml(policy);
288288
}
289289
let name = builder
290290
.command(["sh", "-c", &main])

‎tests/testkit/README.md‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,21 @@ fails. A killed test process skips cleanup, so names are unique to the run.
6363

6464
`Context` currently provides:
6565

66-
- `cx.sandbox(suffix)` to build and create a sandbox with a run-unique name.
67-
The returned `Sandbox` can wait for ready or finished, stop, start, exec,
68-
read its config and draft policy, set a setting, and delete.
66+
- `cx.sandbox(suffix)` to build a sandbox with a run-unique name, then
67+
`create()` it or `create_ready()` to also wait for Ready. The returned
68+
`Sandbox` can wait for ready or finished, stop, start, exec or
69+
`poll_exec` until the output matches, read its config and draft policy, set
70+
a setting, read `sandbox get` JSON with `get_json()`, and delete. Its
71+
`expect_listed`, `expect_absent` and `wait_absent` check list and get.
72+
Exec output has `success()`, `failure()` and `expect_stdout()`.
73+
- `cx.cli_sandbox(suffix)` for a sandbox only `openshell sandbox create`
74+
can make, such as one with `policy_yaml(...)` or `--no-keep`. `create()`
75+
detaches and returns the `Sandbox`; `run()` returns the attached CLI
76+
output.
6977
- `cx.track(name)` to register a sandbox created some other way, such as
7078
through the CLI, for cleanup. Call it before the
7179
create, so a create that fails after the gateway accepted it is still cleaned
7280
up. `cx.adopt(name)` returns a `Sandbox` handle for one the CLI created.
73-
- `cx.expect_listed` and `cx.expect_absent` to check a sandbox in list and get.
7481
- `cx.expect_compute_driver(name)` to fail unless the gateway runs only that
7582
compute driver. Driver suites call it first.
7683
- `testkit::base_image()` for the image to create sandboxes from or build on:
@@ -80,11 +87,12 @@ fails. A killed test process skips cleanup, so names are unique to the run.
8087
(`e2e/python/Dockerfile.workload`). The tmachine installers load it into
8188
every environment; `mise run e2e:workload:build` builds it locally.
8289
- `cx.cli([...])` to run the installed CLI against the same gateway, with
83-
`success()`, `failure()`, and `json()` on the output. `cx.cli_handle()`
90+
`success()`, `failure()`, `expect_stdout()`, and `json()` on the output. `cx.cli_handle()`
8491
returns that CLI for code that can't borrow `cx`, such as a cleanup hook.
8592
- `cx.on_cleanup(name, hook)` to remove something when the test ends.
8693
- `cx.client()` for SDK calls testkit does not wrap.
87-
- `poll_until` for waiting on other state.
94+
- `poll_until` for waiting on other state, and `with_deadline(what, timeout,
95+
future)` for a step that must finish sooner than its own timeout.
8896

8997
## Running against a gateway
9098

‎tests/testkit/testkit/src/cli.rs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,18 @@ impl CliOutput {
176176
Ok(self)
177177
}
178178

179+
/// Fail unless the command exited 0 and printed exactly `expected`.
180+
pub fn expect_stdout(self, expected: &str) -> Result<Self> {
181+
let output = self.success()?;
182+
crate::ensure!(
183+
output.stdout == expected,
184+
"`{}` printed {:?}; expected {expected:?}",
185+
output.command,
186+
output.stdout
187+
);
188+
Ok(output)
189+
}
190+
179191
/// Stdout and stderr as one line of words, for matching an error report.
180192
/// The CLI wraps reports to the terminal width and draws a gutter, so
181193
/// match against this rather than the raw output.

‎tests/testkit/testkit/src/cli_sandbox.rs‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ impl<'a> CliSandboxBuilder<'a> {
4848
/// Create the sandbox with this policy YAML (`--policy`). It is written
4949
/// to a temporary file that lives until the CLI exits.
5050
#[must_use]
51-
pub fn policy(mut self, yaml: &str) -> Self {
51+
pub fn policy_yaml(mut self, yaml: &str) -> Self {
5252
self.policy = Some(yaml.to_string());
5353
self
5454
}
@@ -79,7 +79,7 @@ impl<'a> CliSandboxBuilder<'a> {
7979
/// return the sandbox. It need not be Ready yet.
8080
pub async fn create(mut self) -> Result<Sandbox> {
8181
self.args.push("--detach".to_string());
82-
self.run()
82+
self.invoke()
8383
.await?
8484
.success()
8585
.context(format!("create sandbox {}", self.name))?;
@@ -89,7 +89,11 @@ impl<'a> CliSandboxBuilder<'a> {
8989
/// Create the sandbox attached to its main command and return the CLI's
9090
/// output, whatever its exit code. For a test of the main command's
9191
/// result, or of a create that must fail.
92-
pub async fn run(&self) -> Result<CliOutput> {
92+
pub async fn run(self) -> Result<CliOutput> {
93+
self.invoke().await
94+
}
95+
96+
async fn invoke(&self) -> Result<CliOutput> {
9397
self.cx.track(&self.name);
9498
let policy = self.policy.as_deref().map(PolicyFile::new).transpose()?;
9599
let mut args = vec!["sandbox", "create", "--name", &self.name, "--no-tty"];

0 commit comments

Comments
 (0)