#![cfg(unix)]
mod common;
use anyhow::Result;
use common::cargo_ktstr_subprocess::{combined_output, run_cargo_ktstr_shell};
use ktstr::assert::AssertResult;
use ktstr::ktstr_test;
use ktstr::prelude::{Scheduler, SchedulerSpec};
use ktstr::scenario::Ctx;
const SHELL_LIFECYCLE_FIXTURE_BOTH: Scheduler = Scheduler::named("shell_lifecycle_fixture_both")
.binary(SchedulerSpec::KernelBuiltin {
enable: &[
"echo SHELL_LIFECYCLE_ENABLE_MARKER > /proc/1/fd/1",
"echo SHELL_LIFECYCLE_PRE_SENTINEL > /tmp/shell_lifecycle_pre_marker",
],
disable: &["echo SHELL_LIFECYCLE_DISABLE_MARKER > /proc/1/fd/1"],
});
const SHELL_LIFECYCLE_FIXTURE_ENABLE_ONLY: Scheduler =
Scheduler::named("shell_lifecycle_fixture_enable_only").binary(SchedulerSpec::KernelBuiltin {
enable: &["echo SHELL_LIFECYCLE_ENABLE_MARKER > /proc/1/fd/1"],
disable: &[],
});
const SHELL_LIFECYCLE_FIXTURE_DISABLE_ONLY: Scheduler =
Scheduler::named("shell_lifecycle_fixture_disable_only").binary(SchedulerSpec::KernelBuiltin {
enable: &[],
disable: &["echo SHELL_LIFECYCLE_DISABLE_MARKER > /proc/1/fd/1"],
});
const SHELL_LIFECYCLE_FIXTURE_PARTIAL_FAIL: Scheduler =
Scheduler::named("shell_lifecycle_fixture_partial_fail").binary(SchedulerSpec::KernelBuiltin {
enable: &[
"echo bogus > /this/path/does/not/exist",
"echo SHELL_LIFECYCLE_ENABLE_MARKER > /proc/1/fd/1",
],
disable: &[],
});
#[ktstr_test(
scheduler = SHELL_LIFECYCLE_FIXTURE_BOTH,
llcs = 1,
cores = 2,
threads = 1,
memory_mib = 128,
duration_s = 1,
watchdog_timeout_s = 10,
auto_repro = false,
ignore,
)]
fn shell_lifecycle_fixture_both(_ctx: &Ctx) -> Result<AssertResult> {
Ok(AssertResult::pass())
}
#[ktstr_test(
scheduler = SHELL_LIFECYCLE_FIXTURE_ENABLE_ONLY,
llcs = 1,
cores = 2,
threads = 1,
memory_mib = 128,
duration_s = 1,
watchdog_timeout_s = 10,
auto_repro = false,
ignore,
)]
fn shell_lifecycle_fixture_enable_only(_ctx: &Ctx) -> Result<AssertResult> {
Ok(AssertResult::pass())
}
#[ktstr_test(
scheduler = SHELL_LIFECYCLE_FIXTURE_DISABLE_ONLY,
llcs = 1,
cores = 2,
threads = 1,
memory_mib = 128,
duration_s = 1,
watchdog_timeout_s = 10,
auto_repro = false,
ignore,
)]
fn shell_lifecycle_fixture_disable_only(_ctx: &Ctx) -> Result<AssertResult> {
Ok(AssertResult::pass())
}
#[ktstr_test(
scheduler = SHELL_LIFECYCLE_FIXTURE_PARTIAL_FAIL,
llcs = 1,
cores = 2,
threads = 1,
memory_mib = 128,
duration_s = 1,
watchdog_timeout_s = 10,
auto_repro = false,
ignore,
)]
fn shell_lifecycle_fixture_partial_fail(_ctx: &Ctx) -> Result<AssertResult> {
Ok(AssertResult::pass())
}
#[test]
fn shell_mode_enable_fires_before_payload() {
let out = run_cargo_ktstr_shell(
"shell_lifecycle_fixture_both",
"echo SHELL_LIFECYCLE_PAYLOAD_MARKER",
);
let combined = combined_output(&out);
assert!(
out.status.success(),
"cargo ktstr shell exited non-zero (exit={:?})\n{combined}",
out.status.code(),
);
let enable_count = combined.matches("SHELL_LIFECYCLE_ENABLE_MARKER").count();
assert_eq!(
enable_count, 1,
"SHELL_LIFECYCLE_ENABLE_MARKER count = {enable_count}; \
expected exactly 1. Zero = /sched_enable never ran \
(initramfs pack broken in `pack_sched_scripts` OR \
shell-mode dispatch skipped `exec_shell_script` in \
`ktstr_guest_init`'s shell branch). >1 = duplicate \
invocation. Combined output:\n{combined}",
);
let payload_count = combined.matches("SHELL_LIFECYCLE_PAYLOAD_MARKER").count();
assert_eq!(
payload_count, 1,
"SHELL_LIFECYCLE_PAYLOAD_MARKER count = {payload_count}; \
expected exactly 1. The --exec payload should have echoed \
it once. Combined:\n{combined}",
);
let enable_pos = combined
.find("SHELL_LIFECYCLE_ENABLE_MARKER")
.expect("enable marker located above");
let payload_pos = combined
.find("SHELL_LIFECYCLE_PAYLOAD_MARKER")
.expect("payload marker located above");
assert!(
enable_pos < payload_pos,
"SHELL_LIFECYCLE_ENABLE_MARKER at byte {enable_pos} must \
precede SHELL_LIFECYCLE_PAYLOAD_MARKER at byte {payload_pos} \
— proves /sched_enable ran BEFORE the busybox sh payload. \
Combined output:\n{combined}",
);
}
#[test]
fn shell_mode_enable_side_effects_visible_to_payload() {
let out = run_cargo_ktstr_shell(
"shell_lifecycle_fixture_both",
"if test -f /tmp/shell_lifecycle_pre_marker; then \
echo SHELL_LIFECYCLE_PAYLOAD_SAW_PRE; \
else echo SHELL_LIFECYCLE_PAYLOAD_SAW_NO_PRE; fi",
);
let combined = combined_output(&out);
assert!(
out.status.success(),
"cargo ktstr shell exited non-zero (exit={:?})\n{combined}",
out.status.code(),
);
assert!(
combined.contains("SHELL_LIFECYCLE_PAYLOAD_SAW_PRE"),
"payload did not observe /tmp/shell_lifecycle_pre_marker — \
/sched_enable's filesystem effects must be visible to the \
payload that runs AFTER. Combined:\n{combined}",
);
assert!(
!combined.contains("SHELL_LIFECYCLE_PAYLOAD_SAW_NO_PRE"),
"payload reported SHELL_LIFECYCLE_PAYLOAD_SAW_NO_PRE — \
/sched_enable's `echo > /tmp/shell_lifecycle_pre_marker` \
did NOT propagate before payload ran. Either the script \
never executed OR ordering inverted. Combined:\n{combined}",
);
}
#[test]
fn shell_mode_only_enable_set_payload_still_runs() {
let out = run_cargo_ktstr_shell(
"shell_lifecycle_fixture_enable_only",
"echo SHELL_LIFECYCLE_PAYLOAD_MARKER",
);
let combined = combined_output(&out);
assert!(
out.status.success(),
"cargo ktstr shell exited non-zero (exit={:?})\n{combined}",
out.status.code(),
);
assert_eq!(
combined.matches("SHELL_LIFECYCLE_ENABLE_MARKER").count(),
1,
"SHELL_LIFECYCLE_ENABLE_MARKER must appear exactly once \
even when disable cmds are empty. Combined:\n{combined}",
);
assert!(
!combined.contains("SHELL_LIFECYCLE_DISABLE_MARKER"),
"SHELL_LIFECYCLE_DISABLE_MARKER appeared despite the \
fixture's empty disable cmds — a regression silently \
injected a default disable script OR cross-test marker \
contamination. Combined:\n{combined}",
);
assert!(
combined.contains("SHELL_LIFECYCLE_PAYLOAD_MARKER"),
"payload must still run when only enable cmds are set. \
Combined:\n{combined}",
);
}
#[test]
fn shell_mode_only_disable_set_no_enable_marker() {
let out = run_cargo_ktstr_shell(
"shell_lifecycle_fixture_disable_only",
"echo SHELL_LIFECYCLE_PAYLOAD_MARKER",
);
let combined = combined_output(&out);
assert!(
out.status.success(),
"cargo ktstr shell exited non-zero (exit={:?})\n{combined}",
out.status.code(),
);
assert!(
!combined.contains("SHELL_LIFECYCLE_ENABLE_MARKER"),
"SHELL_LIFECYCLE_ENABLE_MARKER appeared despite empty enable \
cmds — either the descriptor leaked a default OR a sibling \
fixture's marker bled through. Combined:\n{combined}",
);
assert!(
combined.contains("SHELL_LIFECYCLE_PAYLOAD_MARKER"),
"payload must run independent of enable/disable cmd \
presence. Combined:\n{combined}",
);
assert_eq!(
combined.matches("SHELL_LIFECYCLE_DISABLE_MARKER").count(),
1,
"SHELL_LIFECYCLE_DISABLE_MARKER expected exactly once in \
captured output. Zero matches means the post-`/sched_disable` \
tcdrain in ktstr_guest_init's shell-mode disable path \
regressed; more than one match means a refactor invoked the \
disable script multiple times. Combined:\n{combined}",
);
}
#[test]
fn shell_mode_payload_nonzero_exit_propagates_enable_still_fires() {
let out = run_cargo_ktstr_shell(
"shell_lifecycle_fixture_both",
"echo SHELL_LIFECYCLE_PAYLOAD_MARKER; exit 17",
);
let combined = combined_output(&out);
assert_eq!(
out.status.code(),
Some(17),
"subprocess exit code = {:?}; expected 17 (the payload's \
own `exit 17`). The exit-code-propagation chain is \
payload → send_exec_exit (bulk port) → cargo-ktstr host \
→ subprocess exit. Combined:\n{combined}",
out.status.code(),
);
assert_eq!(
combined.matches("SHELL_LIFECYCLE_ENABLE_MARKER").count(),
1,
"SHELL_LIFECYCLE_ENABLE_MARKER count = {}; expected 1 — \
enable must fire BEFORE the payload regardless of the \
payload's eventual exit code. Combined:\n{combined}",
combined.matches("SHELL_LIFECYCLE_ENABLE_MARKER").count(),
);
assert!(
combined.contains("SHELL_LIFECYCLE_PAYLOAD_MARKER"),
"payload's pre-exit echo must surface in captured output \
even on non-zero exit. Combined:\n{combined}",
);
}
#[test]
fn shell_mode_payload_zero_exit_propagates() {
let out = run_cargo_ktstr_shell(
"shell_lifecycle_fixture_both",
"echo SHELL_LIFECYCLE_PAYLOAD_MARKER; exit 0",
);
let combined = combined_output(&out);
assert_eq!(
out.status.code(),
Some(0),
"subprocess exit code = {:?}; expected 0 (the payload's own \
`exit 0`) — a CRC-valid ExecExit(0) frame must surface as 0, \
not trip the missing-frame bail. Combined:\n{combined}",
out.status.code(),
);
assert!(
combined.contains("SHELL_LIFECYCLE_PAYLOAD_MARKER"),
"payload's echo must surface in captured output. Combined:\n{combined}",
);
}
#[test]
fn shell_mode_enable_partial_apply_failure_continues_to_payload() {
let out = run_cargo_ktstr_shell(
"shell_lifecycle_fixture_partial_fail",
"echo SHELL_LIFECYCLE_PAYLOAD_MARKER",
);
let combined = combined_output(&out);
assert!(
out.status.success(),
"cargo ktstr shell exited non-zero (exit={:?}); the \
partial-apply contract is that per-line failures are \
counted but the script + payload continue. A non-zero \
exit here means abort-on-first-failure regressed. \
Combined:\n{combined}",
out.status.code(),
);
assert_eq!(
combined.matches("SHELL_LIFECYCLE_ENABLE_MARKER").count(),
1,
"SHELL_LIFECYCLE_ENABLE_MARKER count = {}; expected 1 — \
the second enable cmd MUST execute despite the first \
failing. A zero count means the script aborted on the \
first failure, violating the partial-apply contract in \
`exec_shell_script`. Combined:\n{combined}",
combined.matches("SHELL_LIFECYCLE_ENABLE_MARKER").count(),
);
assert!(
combined.contains("SHELL_LIFECYCLE_PAYLOAD_MARKER"),
"payload must run even when /sched_enable had per-line \
failures (the entire script returns silently per \
`exec_shell_script`; the partial-apply summary is \
informational). Combined:\n{combined}",
);
assert!(
combined.contains("partial-apply"),
"expected `partial-apply` summary in captured output \
(tracing::error in `exec_shell_script`) — a silent \
partial-apply violates the no-silent-drops rule. \
Combined:\n{combined}",
);
assert!(
combined.contains("/this/path/does/not/exist"),
"expected `/this/path/does/not/exist` in captured output \
alongside the `partial-apply` summary — the failing path \
must surface so the operator can diagnose which line(s) \
dropped. Combined:\n{combined}",
);
}
#[test]
fn shell_mode_enable_and_disable_both_fire_with_payload() {
let out = run_cargo_ktstr_shell(
"shell_lifecycle_fixture_both",
"echo SHELL_LIFECYCLE_PAYLOAD_MARKER",
);
let combined = combined_output(&out);
assert!(
out.status.success(),
"cargo ktstr shell exited non-zero (exit={:?})\n{combined}",
out.status.code(),
);
for marker in &[
"SHELL_LIFECYCLE_ENABLE_MARKER",
"SHELL_LIFECYCLE_PAYLOAD_MARKER",
"SHELL_LIFECYCLE_DISABLE_MARKER",
] {
assert_eq!(
combined.matches(marker).count(),
1,
"{marker} expected exactly once in captured output. \
Zero matches means the corresponding stage failed to \
fire or its bytes were truncated (drain regression for \
disable); more than one match means a refactor invoked \
the stage multiple times. Combined:\n{combined}",
);
}
let enable_off = combined
.find("SHELL_LIFECYCLE_ENABLE_MARKER")
.expect("enable marker presence asserted above");
let payload_off = combined
.find("SHELL_LIFECYCLE_PAYLOAD_MARKER")
.expect("payload marker presence asserted above");
let disable_off = combined
.find("SHELL_LIFECYCLE_DISABLE_MARKER")
.expect("disable marker presence asserted above");
assert!(
enable_off < payload_off,
"enable marker at byte offset {enable_off} must precede \
payload marker at byte offset {payload_off} — /sched_enable \
runs BEFORE the --exec payload per ktstr_guest_init's \
shell-mode bracket. A refactor that moved the enable script \
after the payload would invert the bracket semantic without \
tripping the per-marker count assertions. Combined:\n{combined}",
);
assert!(
payload_off < disable_off,
"payload marker at byte offset {payload_off} must precede \
disable marker at byte offset {disable_off} — /sched_disable \
runs AFTER the --exec payload returns per ktstr_guest_init's \
shell-mode bracket. A refactor that moved the disable script \
before the payload would invert the bracket semantic without \
tripping the per-marker count assertions. Combined:\n{combined}",
);
}