Skip to content

Commit b8bacec

Browse files
committed
test(e2e): close Podman driver test coverage gaps
Add podman_gateway_resume test following the VM pattern (no container-state assertions since Podman keeps containers running across gateway restarts). Widen websocket_conformance feature gate from e2e-docker to e2e-host-gateway so it runs on both Docker and Podman. Fix e2e-podman.sh to default to the e2e-podman feature so Podman- specific and host-gateway tests are actually included in Podman CI runs.
1 parent b4c7bc4 commit b8bacec

4 files changed

Lines changed: 188 additions & 5 deletions

File tree

e2e/rust/Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ name = "gateway_resume"
4646
path = "tests/gateway_resume.rs"
4747
required-features = ["e2e-docker"]
4848

49+
[[test]]
50+
name = "podman_gateway_resume"
51+
path = "tests/podman_gateway_resume.rs"
52+
required-features = ["e2e-podman"]
53+
4954
[[test]]
5055
name = "vm_gateway_resume"
5156
path = "tests/vm_gateway_resume.rs"
@@ -54,7 +59,7 @@ required-features = ["e2e-vm"]
5459
[[test]]
5560
name = "websocket_conformance"
5661
path = "tests/websocket_conformance.rs"
57-
required-features = ["e2e-docker"]
62+
required-features = ["e2e-host-gateway"]
5863

5964
[[test]]
6065
name = "user_namespaces"

e2e/rust/e2e-podman.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ set -euo pipefail
1010

1111
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
1212
E2E_TEST="${OPENSHELL_E2E_PODMAN_TEST:-}"
13-
E2E_FEATURES="${OPENSHELL_E2E_PODMAN_FEATURES:-e2e}"
13+
E2E_FEATURES="${OPENSHELL_E2E_PODMAN_FEATURES:-e2e-podman}"
1414

1515
cargo build -p openshell-cli --features openshell-core/dev-settings
1616

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
#![cfg(feature = "e2e-podman")]
5+
6+
//! Podman-specific E2E coverage for resuming sandboxes after a standalone
7+
//! gateway restart.
8+
//!
9+
//! Unlike the Docker driver, Podman does not stop sandbox containers when the
10+
//! gateway process exits — the containers keep running and the restarted
11+
//! gateway re-adopts them. This test follows the `vm_gateway_resume.rs`
12+
//! pattern: verify sandbox survival at the application level without asserting
13+
//! intermediate container-state transitions.
14+
15+
use std::process::Stdio;
16+
use std::time::{Duration, Instant};
17+
18+
use openshell_e2e::harness::binary::openshell_cmd;
19+
use openshell_e2e::harness::gateway::ManagedGateway;
20+
use openshell_e2e::harness::output::strip_ansi;
21+
use openshell_e2e::harness::sandbox::SandboxGuard;
22+
use tokio::time::sleep;
23+
24+
const READY_MARKER: &str = "podman-gateway-resume-ready";
25+
const RESUME_FILE: &str = "/sandbox/podman-gateway-resume-state";
26+
27+
async fn run_cli(args: &[&str]) -> (String, i32) {
28+
let mut cmd = openshell_cmd();
29+
cmd.args(args).stdout(Stdio::piped()).stderr(Stdio::piped());
30+
31+
let output = cmd.output().await.expect("spawn openshell");
32+
let stdout = String::from_utf8_lossy(&output.stdout);
33+
let stderr = String::from_utf8_lossy(&output.stderr);
34+
let combined = format!("{stdout}{stderr}");
35+
let code = output.status.code().unwrap_or(-1);
36+
(combined, code)
37+
}
38+
39+
async fn wait_for_healthy(timeout: Duration) -> Result<(), String> {
40+
let start = Instant::now();
41+
let mut last_output: String;
42+
43+
loop {
44+
let (output, code) = run_cli(&["status"]).await;
45+
let clean = strip_ansi(&output);
46+
let lower = clean.to_lowercase();
47+
if code == 0
48+
&& (lower.contains("healthy")
49+
|| lower.contains("running")
50+
|| lower.contains("connected"))
51+
{
52+
return Ok(());
53+
}
54+
last_output = clean;
55+
56+
if start.elapsed() > timeout {
57+
return Err(format!(
58+
"gateway did not become healthy within {}s. Last output:\n{last_output}",
59+
timeout.as_secs()
60+
));
61+
}
62+
sleep(Duration::from_secs(2)).await;
63+
}
64+
}
65+
66+
async fn sandbox_names() -> Result<Vec<String>, String> {
67+
let (output, code) = run_cli(&["sandbox", "list", "--names"]).await;
68+
let clean = strip_ansi(&output);
69+
if code != 0 {
70+
return Err(format!("sandbox list failed (exit {code}):\n{clean}"));
71+
}
72+
73+
Ok(clean
74+
.lines()
75+
.map(str::trim)
76+
.filter(|line| !line.is_empty())
77+
.map(ToOwned::to_owned)
78+
.collect())
79+
}
80+
81+
async fn wait_for_sandbox_exec_contains(
82+
sandbox_name: &str,
83+
command: &[&str],
84+
expected: &str,
85+
timeout: Duration,
86+
) -> Result<(), String> {
87+
let start = Instant::now();
88+
let mut last_output: String;
89+
90+
loop {
91+
let mut cmd = openshell_cmd();
92+
cmd.args(["sandbox", "exec", "--name", sandbox_name, "--no-tty", "--"])
93+
.args(command)
94+
.stdout(Stdio::piped())
95+
.stderr(Stdio::piped());
96+
97+
match cmd.output().await {
98+
Ok(output) => {
99+
let stdout = String::from_utf8_lossy(&output.stdout);
100+
let stderr = String::from_utf8_lossy(&output.stderr);
101+
last_output = strip_ansi(&format!("{stdout}{stderr}"));
102+
if output.status.success() && last_output.contains(expected) {
103+
return Ok(());
104+
}
105+
}
106+
Err(err) => {
107+
last_output = format!("failed to spawn openshell sandbox exec: {err}");
108+
}
109+
}
110+
111+
if start.elapsed() > timeout {
112+
return Err(format!(
113+
"sandbox '{sandbox_name}' exec did not produce '{expected}' within {}s. Last output:\n{last_output}",
114+
timeout.as_secs()
115+
));
116+
}
117+
sleep(Duration::from_secs(2)).await;
118+
}
119+
}
120+
121+
#[tokio::test]
122+
async fn podman_gateway_restart_resumes_running_sandbox() {
123+
if std::env::var("OPENSHELL_E2E_DRIVER").as_deref() != Ok("podman") {
124+
eprintln!("Skipping Podman gateway resume test: e2e driver is not podman");
125+
return;
126+
}
127+
let Some(gateway) = ManagedGateway::from_env().expect("load managed e2e gateway metadata")
128+
else {
129+
eprintln!(
130+
"Skipping Podman gateway resume test: e2e gateway is not managed by this test run"
131+
);
132+
return;
133+
};
134+
135+
wait_for_healthy(Duration::from_secs(30))
136+
.await
137+
.expect("gateway should start healthy");
138+
139+
let script = format!(
140+
"echo before-restart > {RESUME_FILE}; echo {READY_MARKER}; while true; do sleep 1; done"
141+
);
142+
let mut sandbox = SandboxGuard::create_keep(&["sh", "-lc", &script], READY_MARKER)
143+
.await
144+
.expect("create long-running Podman sandbox");
145+
146+
let before_restart = sandbox
147+
.exec(&["cat", RESUME_FILE])
148+
.await
149+
.expect("read Podman sandbox state before restart");
150+
assert!(
151+
before_restart.contains("before-restart"),
152+
"sandbox state was not written before restart:\n{before_restart}"
153+
);
154+
155+
gateway.stop().expect("stop e2e gateway");
156+
gateway.start().expect("restart e2e gateway");
157+
wait_for_healthy(Duration::from_secs(120))
158+
.await
159+
.expect("gateway should become healthy after restart");
160+
161+
let names = sandbox_names().await.expect("list sandboxes after restart");
162+
assert!(
163+
names.contains(&sandbox.name),
164+
"sandbox '{}' should still be listed after gateway restart. Names: {names:?}",
165+
sandbox.name
166+
);
167+
168+
wait_for_sandbox_exec_contains(
169+
&sandbox.name,
170+
&["cat", RESUME_FILE],
171+
"before-restart",
172+
Duration::from_secs(240),
173+
)
174+
.await
175+
.expect("Podman sandbox should become ready again with its state preserved");
176+
177+
sandbox.cleanup().await;
178+
}

e2e/rust/tests/websocket_conformance.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33

44
#![cfg(feature = "e2e")]
55

6-
//! E2E regression: WebSocket credential placeholders are resolved on the real
7-
//! Docker-backed sandbox path after an RFC 6455 upgrade.
6+
//! E2E regression: WebSocket credential placeholders are resolved on the
7+
//! sandbox path after an RFC 6455 upgrade.
88
//!
99
//! The sandbox process sends its provider-managed placeholder in a masked text
1010
//! frame. The local upstream only reports whether it saw the real secret and
@@ -425,7 +425,7 @@ with connect_with_retry(HOST, PORT) as sock:
425425
}
426426

427427
#[tokio::test]
428-
async fn websocket_text_placeholder_is_rewritten_in_docker_sandbox() {
428+
async fn websocket_text_placeholder_is_rewritten_in_sandbox() {
429429
let _provider_lock = PROVIDER_LOCK
430430
.lock()
431431
.unwrap_or_else(std::sync::PoisonError::into_inner);

0 commit comments

Comments
 (0)