diff --git a/crates/lib/src/utils.rs b/crates/lib/src/utils.rs index aa7bee25..391c578d 100644 --- a/crates/lib/src/utils.rs +++ b/crates/lib/src/utils.rs @@ -158,10 +158,10 @@ pub(crate) fn medium_visibility_warning(s: &str) { std::thread::sleep(std::time::Duration::from_secs(1)); } -/// Call an async task function, and write a message to stdout +/// Call an async task function, and write a message to stderr /// with an automatic spinner to show that we're not blocked. /// Note that generally the called function should not output -/// anything to stdout as this will interfere with the spinner. +/// anything to stderr as this will interfere with the spinner. pub(crate) async fn async_task_with_spinner(msg: &str, f: F) -> T where F: Future, @@ -175,8 +175,8 @@ where // We need to handle the case where we aren't connected to // a tty, so indicatif would show nothing by default. if pb.is_hidden() { - print!("{msg}..."); - std::io::stdout().flush().unwrap(); + eprint!("{msg}..."); + std::io::stderr().flush().unwrap(); } let r = f.await; let elapsed = HumanDuration(start_time.elapsed()); @@ -185,7 +185,7 @@ where &format!("completed task in {elapsed}: {msg}"), ); if pb.is_hidden() { - println!("done ({elapsed})"); + eprintln!("done ({elapsed})"); } else { pb.finish_with_message(format!("{msg}: done ({elapsed})")); } diff --git a/tmt/tests/booted/readonly/030-test-locking-read.nu b/tmt/tests/booted/readonly/030-test-locking-read.nu index cb8be9fa..012ff0c1 100644 --- a/tmt/tests/booted/readonly/030-test-locking-read.nu +++ b/tmt/tests/booted/readonly/030-test-locking-read.nu @@ -1,32 +1,51 @@ -# Verify we can spawn multiple bootc status at the same time +# Verify we can spawn multiple bootc status at the same time and get valid JSON use std assert use tap.nu tap begin "concurrent bootc status" -# Fork via systemd-run +# Create a temporary directory for output files +let tmpdir = mktemp -d +print $"Using temporary directory: ($tmpdir)" + +# Number of concurrent invocations let n = 10 -0..$n | each { |v| - # Clean up prior runs - systemctl stop $"bootc-status-($v)" | complete -} -# Fork off a concurrent bootc status -0..$n | each { |v| - systemd-run --no-block -qr -u $"bootc-status-($v)" bootc status + +# Create systemd unit files for concurrent bootc status commands. +# Writing actual unit files allows proper dependency tracking. +let units = 0..<$n | each { |v| + let unit_name = $"bootc-status-test-($v).service" + let outpath = $"($tmpdir)/($v).json" + let unit_content = $"[Unit] +Description=Test bootc status ($v) + +[Service] +Type=oneshot +ExecStart=/bin/sh -c 'bootc status --format=json > ($outpath)' +" + $unit_content | save -f $"/run/systemd/system/($unit_name)" + $unit_name } -# Await completion -0..$n | each { |v| - loop { - let r = systemctl is-active $"bootc-status-($v)" | complete - if $r.exit_code == 0 { - break - } - # check status - systemctl status $"bootc-status-($v)" out> /dev/null - # Clean it up - systemctl reset-failed $"bootc-status-($v)" - } +# Reload systemd to pick up the new units. +systemctl daemon-reload + +# Use systemd-run to create a transient sync unit with After= and Requires= +# dependencies on all worker units. --wait blocks until completion. +let dep_args = $units | each { |u| [$"--property=After=($u)" $"--property=Requires=($u)"] } | flatten +systemd-run --wait ...$dep_args -- true + +# Verify each output file contains valid JSON with the expected structure. +# This is a regression test for spinner output polluting stdout. +for v in 0..<$n { + let path = $"($tmpdir)/($v).json" + # open automatically parses JSON files, so we get a record directly + # If the file had spinner output mixed in, this would fail to parse + let st = open $path + assert equal $st.apiVersion org.containers.bootc/v1 $"($path) should contain valid bootc status JSON" } +# Clean up +rm -rf $tmpdir + tap ok