Skip to content

Commit 6625e51

Browse files
authored
test(engine): fix flaky shared alpine:latest image race (#495)
Closes #485 ## Problem `cli_down_rmi_all_succeeds_and_removes_containers` (tests/engine_integration/cli3.rs) ran `down --rmi all` against a compose file referencing the **shared** `alpine:latest` image. Cargo runs integration tests in parallel, so when this test deleted `alpine:latest`, concurrent tests doing `up --pull never` (`cli_up_pull_never_starts_present_image`) or `up --no-start` (`cli_up_no_start_creates_without_starting`) intermittently failed with `no such image: alpine:latest` — a different pair failing each run. ## Changes - **Isolate the rmi-all image.** The test now tags a throwaway, project-unique local image (`localhost/podup-rmitest-<proj>:latest`) from `alpine:latest` and references *that* in its compose file, so `--rmi all` only removes the throwaway tag. Added assertions that the throwaway image is gone after `down` and (implicitly) that the shared base image is left intact, plus best-effort cleanup. - **Remove timing-fragile sleep.** Replaced the fixed `sleep(800ms)` in `cli_logs_tail_limits_output` with a poll-until-condition loop (matching the poll-helper pattern in `tests/engine_integration/watch.rs`) that waits until `logs --tail 2` shows exactly two lines, up to a 30s deadline. ## Verification (local, Podman 5.4.2) - Reproduced the setup and confirmed the fix: ran the rmi test together with `cli_up_pull_never_starts_present_image`, `cli_up_no_start_creates_without_starting` and `cli_logs_tail_limits_output` in parallel — all pass, and `alpine:latest` survives. - Ran the full `cli3` module twice with `--test-threads=8`: 15/15 pass both times, no `no such image` flake, `alpine:latest` still present, no leftover throwaway tags. - `cargo fmt --all`, `cargo clippy --all-targets --all-features -- -D warnings`, `cargo build --all-features` all clean. `cli3.rs` is now 486 lines (under the 500-line limit). Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
1 parent d25d2bf commit 6625e51

1 file changed

Lines changed: 54 additions & 7 deletions

File tree

tests/engine_integration/cli3.rs

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,28 @@ async fn cli_logs_tail_limits_output() {
2525
.args(["-f", c, "-p", &proj, "up", "-d"])
2626
.output()
2727
.unwrap();
28-
// Give the container a moment to emit its lines.
29-
tokio::time::sleep(std::time::Duration::from_millis(800)).await;
3028

31-
let logs = Command::new(bin())
32-
.args(["-f", c, "-p", &proj, "logs", "--tail", "2"])
33-
.output()
34-
.unwrap();
29+
// Poll until the container has emitted its lines instead of sleeping a fixed
30+
// duration: `logs --tail 2` must eventually show exactly 2 `line-` rows.
31+
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(30);
32+
let mut logs;
33+
loop {
34+
logs = Command::new(bin())
35+
.args(["-f", c, "-p", &proj, "logs", "--tail", "2"])
36+
.output()
37+
.unwrap();
38+
let lines = String::from_utf8_lossy(&logs.stdout)
39+
.lines()
40+
.filter(|l| l.contains("line-"))
41+
.count();
42+
if logs.status.success() && lines == 2 {
43+
break;
44+
}
45+
if tokio::time::Instant::now() >= deadline {
46+
break;
47+
}
48+
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
49+
}
3550
assert!(logs.status.success(), "logs failed: {:?}", logs.stderr);
3651
let lines = String::from_utf8_lossy(&logs.stdout)
3752
.lines()
@@ -173,10 +188,28 @@ async fn cli_down_rmi_all_succeeds_and_removes_containers() {
173188
}
174189
let dir = tempdir().unwrap();
175190
let proj = format!("t{}-rmi", std::process::id());
191+
192+
// `--rmi all` deletes the image referenced by the compose file. Tests run in
193+
// parallel and several rely on the shared `alpine:latest` image, so we tag a
194+
// throwaway, project-unique local image and reference THAT here. Removing it
195+
// leaves `alpine:latest` intact for the concurrent tests.
196+
let throwaway = format!("localhost/podup-rmitest-{proj}:latest");
197+
let tag = Command::new("podman")
198+
.args(["tag", "alpine:latest", &throwaway])
199+
.output()
200+
.unwrap();
201+
assert!(
202+
tag.status.success(),
203+
"tagging throwaway image failed: {:?}",
204+
tag.stderr
205+
);
206+
176207
let compose = dir.path().join("docker-compose.yml");
177208
fs::write(
178209
&compose,
179-
"services:\n web:\n image: alpine:latest\n command: [\"sleep\", \"infinity\"]\n",
210+
format!(
211+
"services:\n web:\n image: {throwaway}\n command: [\"sleep\", \"infinity\"]\n"
212+
),
180213
)
181214
.unwrap();
182215
let c = compose.to_str().unwrap();
@@ -189,6 +222,20 @@ async fn cli_down_rmi_all_succeeds_and_removes_containers() {
189222
down.stderr
190223
);
191224
assert_eq!(ps_all_count(c, &proj), 0, "down must remove the containers");
225+
226+
// The shared base image must survive; only the throwaway tag was removed.
227+
let present = Command::new("podman")
228+
.args(["image", "exists", &throwaway])
229+
.status()
230+
.unwrap();
231+
assert!(
232+
!present.success(),
233+
"down --rmi all must remove the throwaway image"
234+
);
235+
// Best-effort cleanup in case the assertion above ever changes.
236+
let _ = Command::new("podman")
237+
.args(["rmi", "-f", &throwaway])
238+
.output();
192239
}
193240

194241
#[tokio::test]

0 commit comments

Comments
 (0)