Skip to content

Commit 961141a

Browse files
committed
composefs-gc: Add --assert-no-op flag; use it in readonly smoke test
Regression test for #1808 where `bootc internals cfs gc` was deleting objects backing live deployments. Add `--assert-no-op` to `bootc internals composefs-gc` (implies `--dry-run`) which exits non-zero if GC would remove any objects or prune any OCI symlinks. This gives tests and health-checks a clean machine-readable signal without parsing human-readable output. Use it in the composefs readonly smoke test in place of the previous approach of capturing stdout and grepping for count strings. Assisted-by: OpenCode (Claude Sonnet 4.6) Signed-off-by: Colin Walters <walters@verbum.org>
1 parent 5050820 commit 961141a

3 files changed

Lines changed: 41 additions & 12 deletions

File tree

crates/lib/src/cli.rs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,10 @@ pub(crate) enum InternalsOpts {
718718
ComposefsGC {
719719
#[clap(long)]
720720
dry_run: bool,
721+
/// Exit with an error if GC would remove any objects or prune any symlinks.
722+
/// Implies `--dry-run`. Intended for use in tests and health-checks.
723+
#[clap(long)]
724+
assert_no_op: bool,
721725
},
722726
/// Block device inspection tools.
723727
#[clap(subcommand)]
@@ -2201,7 +2205,10 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
22012205
}
22022206
}
22032207
}
2204-
InternalsOpts::ComposefsGC { dry_run } => {
2208+
InternalsOpts::ComposefsGC {
2209+
dry_run,
2210+
assert_no_op,
2211+
} => {
22052212
let storage = &get_storage().await?;
22062213

22072214
match storage.kind()? {
@@ -2210,9 +2217,11 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
22102217
}
22112218

22122219
BootedStorageKind::Composefs(booted_cfs) => {
2213-
let gc_result = composefs_gc(storage, &booted_cfs, dry_run).await?;
2220+
let effective_dry_run = dry_run || assert_no_op;
2221+
let gc_result =
2222+
composefs_gc(storage, &booted_cfs, effective_dry_run).await?;
22142223

2215-
if dry_run {
2224+
if effective_dry_run {
22162225
println!("Dry run (no files deleted)");
22172226
}
22182227

@@ -2228,6 +2237,20 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
22282237
);
22292238
}
22302239

2240+
if assert_no_op {
2241+
let is_noop = gc_result.objects_removed == 0
2242+
&& gc_result.images_pruned == 0
2243+
&& gc_result.streams_pruned == 0;
2244+
if !is_noop {
2245+
anyhow::bail!(
2246+
"--assert-no-op: GC would remove {} object(s), {} image symlink(s), {} stream symlink(s) (issue #1808)",
2247+
gc_result.objects_removed,
2248+
gc_result.images_pruned,
2249+
gc_result.streams_pruned,
2250+
);
2251+
}
2252+
}
2253+
22312254
Ok(())
22322255
}
22332256
}

tmt/tests/booted/readonly/030-test-composefs.nu

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,17 @@ if $is_composefs {
3636
print "# TODO composefs: skipping pull test - cfs oci pull requires write access to sysroot"
3737
bootc internals cfs --help
3838

39-
# Verify that GC on a freshly booted system would not prune any
40-
# images or streams. This validates that our OCI tags and
41-
# manifest→image refs correctly root the entire chain.
42-
# Note: a small number of orphaned objects is expected (e.g. from
43-
# manifest splitstream rewrites) and is harmless.
44-
print "# Verifying composefs GC dry-run does not prune images or streams"
45-
let gc_output = (bootc internals composefs-gc --dry-run)
46-
print $gc_output
47-
assert (not ($gc_output | str contains "Pruned symlinks")) "GC dry-run should not prune any images or streams on a freshly booted system"
39+
# Regression test for https://github.com/bootc-dev/bootc/issues/1808 :
40+
# `bootc internals cfs gc` was deleting live deployment objects.
41+
# Verify GC dry-run does not prune any OCI image or stream symlinks.
42+
# A small number of raw object orphans (~4) is expected: pull rewrites
43+
# the config+manifest splitstreams to add EROFS refs, leaving the
44+
# originals unreferenced until the next GC run. Those are harmless.
45+
# We use --assert-no-op in the dedicated writable GC test plan instead.
46+
print "# Verifying composefs GC dry-run does not prune OCI structure (issue #1808)"
47+
let gc_out = (bootc internals composefs-gc --dry-run)
48+
print $gc_out
49+
assert (not ($gc_out | str contains "Pruned symlinks")) "GC must not prune any OCI image or stream symlinks on a live system"
4850
} else {
4951
# When not on composefs, run the full test including initialization
5052
bootc internals test-composefs

tmt/tests/booted/test-composefs-gc.nu

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,10 @@ def sixth_boot [i: int] {
182182

183183
# Just this being booted counts as success
184184
if $i == 3 {
185+
# After multiple GC cycles, assert the repo is fully clean.
186+
# Regression check for issue #1808: GC must not identify any live
187+
# deployment objects as garbage.
188+
bootc internals composefs-gc --assert-no-op
185189
tap ok
186190
return
187191
}

0 commit comments

Comments
 (0)