use std::path::{Path, PathBuf};
fn main_rs() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("src/main.rs")
}
fn read_main() -> String {
let p = main_rs();
std::fs::read_to_string(&p).unwrap_or_else(|e| panic!("read {}: {e}", p.display()))
}
fn code_only(src: &str) -> String {
src.lines()
.map(|line| match line.find("//") {
Some(i) => &line[..i],
None => line,
})
.collect::<Vec<_>>()
.join("\n")
}
fn fn_body(src: &str, name: &str) -> String {
let sig = format!("fn {name}");
let start = src
.find(&sig)
.unwrap_or_else(|| panic!("`{sig}` must exist in main.rs"));
let open = src[start..]
.find('{')
.unwrap_or_else(|| panic!("`{sig}` must have a body"))
+ start;
let bytes = src.as_bytes();
let mut depth = 0usize;
for (i, b) in bytes.iter().enumerate().skip(open) {
match b {
b'{' => depth += 1,
b'}' => {
depth -= 1;
if depth == 0 {
return src[open..=i].to_string();
}
}
_ => {}
}
}
panic!("`{sig}` body is not brace-balanced");
}
#[test]
fn the_clean_verb_runs_the_state_file_diagnostic() {
let src = code_only(&read_main());
let body = fn_body(&src, "clean_iceoryx2_state");
assert!(
body.contains("report_shm_state_population("),
"`cerulion clean` must call the shm-state population diagnostic; body was:\n{body}"
);
}
#[test]
fn the_dead_node_sweep_runs_before_the_state_file_diagnostic() {
let src = code_only(&read_main());
let body = fn_body(&src, "clean_iceoryx2_state");
let sweep = body
.find("clean_dead_nodes(")
.expect("the sweep must still be called");
let diagnostic = body
.find("report_shm_state_population(")
.expect("the diagnostic must still be called");
assert!(
sweep < diagnostic,
"the dead-node sweep must precede the state-file diagnostic; body was:\n{body}"
);
}
#[test]
fn the_reclamation_stands_down_when_dead_nodes_are_still_registered() {
let src = code_only(&read_main());
let body = fn_body(&src, "clean_iceoryx2_state");
let normalized = body.split_whitespace().collect::<Vec<_>>().join(" ");
assert!(
normalized.contains("let reclaim = mode.removes() && converged;"),
"`cerulion clean` must reclaim only when the operator asked AND the sweep left no \
dead nodes registered — an `||` would reclaim on a wedged desk; body was:\n{body}"
);
let call = body
.find("report_shm_state_population(")
.expect("the diagnostic must still be called");
let arg_end = body[call..]
.find(')')
.expect("the diagnostic call must be closed");
let arg = &body[call..call + arg_end];
assert!(
arg.contains("reclaim") || arg.contains("converged"),
"the diagnostic's report-only argument must carry the convergence gate, not just \
the flag; argument was:\n{arg}"
);
}
fn if_else_arms(body: &str, condition: &str) -> (String, String) {
let needle = format!("if {condition}");
let start = body
.find(&needle)
.unwrap_or_else(|| panic!("`{needle}` must exist in the body:\n{body}"));
let then = block_after(body, start);
let after_then = start + body[start..].find(&then).expect("then-block offset") + then.len();
let else_at = body[after_then..]
.find("else")
.unwrap_or_else(|| panic!("`{needle}` must have an else arm:\n{body}"))
+ after_then;
let otherwise = block_after(body, else_at);
(then, otherwise)
}
fn block_after(src: &str, from: usize) -> String {
let open = src[from..]
.find('{')
.unwrap_or_else(|| panic!("expected a block after byte {from}"))
+ from;
let bytes = src.as_bytes();
let mut depth = 0usize;
for (i, b) in bytes.iter().enumerate().skip(open) {
match b {
b'{' => depth += 1,
b'}' => {
depth -= 1;
if depth == 0 {
return src[open..=i].to_string();
}
}
_ => {}
}
}
panic!("unbalanced block after byte {from}");
}
#[test]
fn the_diagnostic_reclaims_only_when_the_operator_did_not_ask_for_a_report() {
let src = code_only(&read_main());
let body = fn_body(&src, "report_shm_state_population");
let (reclaim_arm, report_arm) = if_else_arms(&body, "mode.removes()");
assert!(
report_arm.contains("ScanMode::ReportOnly") && !report_arm.contains("ScanMode::Reclaim"),
"the `--report-only` arm must choose ReportOnly and nothing else; arm was:\n{report_arm}"
);
assert!(
reclaim_arm.contains("ScanMode::Reclaim") && !reclaim_arm.contains("ScanMode::ReportOnly"),
"the default arm must choose Reclaim and nothing else; arm was:\n{reclaim_arm}"
);
assert!(
report_arm.contains("SHM_STATE_REPORT_BUDGET")
&& !report_arm.contains("SHM_STATE_RECLAIM_BUDGET"),
"the report arm must take the report budget; arm was:\n{report_arm}"
);
assert!(
reclaim_arm.contains("SHM_STATE_RECLAIM_BUDGET")
&& !reclaim_arm.contains("SHM_STATE_REPORT_BUDGET"),
"the reclaim arm must take the reclaim budget; arm was:\n{reclaim_arm}"
);
}
#[test]
fn the_walk_reads_the_code_it_claims_to_read() {
let raw = read_main();
let src = code_only(&raw);
assert!(
src.contains("fn clean_iceoryx2_state"),
"the stripped view must still hold the function under test"
);
assert!(
src.contains("fn report_shm_state_population"),
"and the diagnostic it calls"
);
assert!(
src.len() < raw.len(),
"the stripped view must be shorter than the source it came from"
);
}
use cerulion_cli_engine::ipc_cleanup::{
classify_cleanup_failures, sweep_one_node, DeadNodeIdentity, FailedNodeCleanup,
NodeSweepOutcome, SurveyedNode, SweepMode,
};
use cerulion_core::iceoryx_logger::CapturedLog;
use iceoryx2::node::NodeCleanupFailure;
use iceoryx2::prelude::LogLevel;
const NODE_A: &str =
"UniqueNodeId(UniqueSystemId { value: 1, pid: 4242, creation_time: Time { seconds: 7, nanoseconds: 0 } })";
fn refusal(node: &str, variant: &str, causes: &[&str]) -> FailedNodeCleanup {
FailedNodeCleanup {
node: node.to_string(),
variant: variant.to_string(),
causes: causes.iter().map(|c| c.to_string()).collect(),
}
}
#[test]
fn a_refused_node_renders_its_variant_and_every_sub_cause_indented_beneath_it() {
let failures = [refusal(
NODE_A,
"InternalError",
&[
"Unable to remove stale resources since the service tags could not be read due to an internal error.",
"Unable to remove stale resources since the stale resources of the port 9 could not be removed due to an internal failure.",
],
)];
let expected = vec![
"Refused nodes:".to_string(),
format!(" node {NODE_A}: InternalError"),
" Unable to remove stale resources since the service tags could not be read due to an internal error.".to_string(),
" Unable to remove stale resources since the stale resources of the port 9 could not be removed due to an internal failure.".to_string(),
];
assert_eq!(super::render_refused_nodes(&failures), expected);
}
#[test]
fn a_converged_sweep_renders_no_listing_at_all() {
assert_eq!(super::render_refused_nodes(&[]), Vec::<String>::new());
}
#[test]
fn absences_are_stated_never_fabricated() {
let failures = [refusal(NODE_A, "", &[])];
let lines = super::render_refused_nodes(&failures);
assert_eq!(lines[1], format!(" node {NODE_A}: (variant not reported)"));
assert_eq!(
lines.len(),
3,
"header + node + the absence line:\n{lines:?}"
);
assert!(
lines[2].starts_with(" (no sub-cause captured"),
"the absence must be stated:\n{lines:?}"
);
assert!(
lines[2].contains("RUST_LOG=iceoryx2=trace"),
"the hint must name the env var that actually gates the raw lines for this verb \
(the sweep already pins iceoryx2 at Trace, so `IOX2_LOG_LEVEL` is inert):\n{lines:?}"
);
let explained = [refusal(NODE_A, "InternalError", &["x since the y."])];
assert!(
!super::render_refused_nodes(&explained)
.iter()
.any(|l| l.contains("RUST_LOG")),
"an explained node must not carry the trace hint"
);
}
#[test]
fn the_listing_is_capped_and_says_how_many_it_folded() {
let cap = super::NODES_SHOWN;
let node = |i: usize| {
format!("UniqueNodeId(UniqueSystemId {{ value: {i}, pid: 1, creation_time: 0 }})")
};
let make = |n: usize| -> Vec<FailedNodeCleanup> {
(0..n)
.map(|i| refusal(&node(i), "InternalError", &["c since the d."]))
.collect()
};
let lines = super::render_refused_nodes(&make(cap + 2));
let node_lines: Vec<&String> = lines.iter().filter(|l| l.starts_with(" node ")).collect();
assert_eq!(node_lines.len(), cap, "{lines:?}");
for (i, l) in node_lines.iter().enumerate() {
assert_eq!(**l, format!(" node {}: InternalError", node(i)));
}
assert_eq!(lines.last().unwrap(), " … and 2 more", "{lines:?}");
assert!(
!lines.iter().any(|l| l.contains(&node(cap))),
"a folded node must not leak past the cap:\n{lines:?}"
);
let lines = super::render_refused_nodes(&make(cap));
assert_eq!(
lines.iter().filter(|l| l.starts_with(" node ")).count(),
cap
);
assert!(!lines.iter().any(|l| l.contains("more")), "{lines:?}");
let lines = super::render_refused_nodes(&make(cap + 1));
assert_eq!(lines.last().unwrap(), " … and 1 more", "{lines:?}");
}
#[test]
fn the_cap_is_ten_nodes() {
assert_eq!(super::NODES_SHOWN, 10);
}
#[test]
fn the_clean_verb_prints_the_listing_between_the_breakdown_and_the_unclassified_arm() {
let src = code_only(&read_main());
let body = fn_body(&src, "report_sweep");
let breakdown = body
.find("Failure breakdown:")
.expect("the per-cause breakdown must still be printed");
let listing = body
.find("render_refused_nodes(")
.expect("`cerulion clean` must render the refused-nodes per-node listing");
let unclassified = body
.find("Unclassified failures")
.expect("the unclassified arm must still be printed");
assert!(
breakdown < listing && listing < unclassified,
"breakdown, then listing, then unclassified; body was:\n{body}"
);
let internal = body
.find("\"iceoryx2 internal error\" =>")
.expect("the internal-error arm must still exist");
let arm_end = body[internal..].find("}").expect("arm closes") + internal;
let arm = &body[internal..arm_end];
assert!(
!arm.contains("IOX2_LOG_LEVEL") && !arm.contains("RUST_LOG"),
"the internal-error remediation must point at the listing, not a trace flag; arm was:\n{arm}"
);
assert!(
arm.contains("refused nodes"),
"the internal-error remediation must point the reader at the listing; arm was:\n{arm}"
);
for promise in ["reclaim", "right after this listing", "sweeps once more"] {
assert!(
!arm.contains(promise),
"the internal-error remediation says `{promise}`, promising work this verb no \
longer does; it makes one sweep. arm was:\n{arm}"
);
}
let tail = &body[unclassified..];
assert!(
tail.contains("RAW_IOX2_LINES_HINT"),
"the unclassified arm must keep the raw-lines hint; tail was:\n{tail}"
);
}
fn call_args(body: &str, callee: &str) -> String {
let needle = format!("{callee}(");
let at = body
.find(&needle)
.unwrap_or_else(|| panic!("`{needle}` must be called; body was:\n{body}"));
let open = at + callee.len();
let bytes = body.as_bytes();
let mut depth = 0usize;
for (i, b) in bytes.iter().enumerate().skip(open) {
match b {
b'(' => depth += 1,
b')' => {
depth -= 1;
if depth == 0 {
let args = body[open + 1..i]
.split_whitespace()
.collect::<Vec<_>>()
.join(" ");
return args.strip_suffix(',').unwrap_or(&args).to_string();
}
}
_ => {}
}
}
panic!("`{needle}` is not paren-balanced; body was:\n{body}");
}
#[test]
fn each_renderer_is_handed_the_reports_own_field_never_an_empty_slice() {
let src = code_only(&read_main());
let printing = fn_body(&src, "report_sweep");
assert_eq!(
call_args(&printing, "render_refused_nodes"),
"&report.failures",
"the per-node listing must render the sweep's refusals; body was:\n{printing}"
);
assert_eq!(
call_args(&printing, "render_registry_errors"),
"&report.registry_errors",
"the registry block must render the sweep's registry errors; body was:\n{printing}"
);
assert!(
!printing.contains("let report"),
"`report` must be the parameter, never re-bound; body was:\n{printing}"
);
let sig_start = src
.find("fn report_sweep")
.expect("the reporter must exist");
let sig = &src[sig_start..sig_start + src[sig_start..].find('{').expect("body")];
let sig = sig.split_whitespace().collect::<Vec<_>>().join(" ");
assert!(
sig.contains("(report: &cerulion_cli_engine::ipc_cleanup::CleanupReport, mode: SweepMode)"),
"the reporter takes the sweep report as `report` and the verb's mode; signature \
was:\n{sig}"
);
}
fn convergence_positions(body: &str) -> Vec<String> {
body.match_indices("Ok(()),")
.map(|(at, _)| {
let open = body[..at]
.rfind(|c: char| !c.is_whitespace())
.expect("a tuple opens before `Ok(()),`");
assert_eq!(
&body[open..=open],
"(",
"every `Ok(()),` sits inside a tuple; body was:\n{body}"
);
let bytes = body.as_bytes();
let mut depth = 0usize;
for (i, b) in bytes.iter().enumerate().skip(open) {
match b {
b'(' => depth += 1,
b')' => {
depth -= 1;
if depth == 0 {
let expr = body[open + 1..i]
.trim_start()
.strip_prefix("Ok(()),")
.expect("the tuple opens with `Ok(()),`")
.trim_end();
let expr = expr.strip_suffix(',').unwrap_or(expr);
return expr.split_whitespace().collect::<Vec<_>>().join(" ");
}
}
_ => {}
}
}
panic!("a `(Ok(()),` tuple is not paren-balanced; body was:\n{body}");
})
.collect()
}
#[test]
fn every_convergence_the_verb_hands_back_is_computed_never_a_literal_true() {
let src = code_only(&read_main());
let body = fn_body(&src, "clean_dead_nodes");
assert_eq!(
convergence_positions(&body),
vec!["converged".to_string()],
"the verb hands back exactly one convergence, the computed conjunction; body was:\n{body}"
);
let normalized = body.split_whitespace().collect::<Vec<_>>().join(" ");
assert!(
normalized.contains(
"let converged = report.failed_cleanups == 0 && report.registry_errors.is_empty();"
),
"convergence is the failure count AND an empty registry-error list; body was:\n{body}"
);
}
#[test]
fn a_cleaner_lock_refusal_renders_its_captured_explanation_not_the_trace_hint() {
let rows: [(&str, &str); 4] = [
(
"Unable to remove stale resources since another instance is already cleaning up the dead nodes resources.",
"AnotherInstanceIsCleaningUpTheNode",
),
(
"Unable to acquire monitor cleaner since another instance is already cleaning up all resources.",
"AnotherInstanceIsCleaningUpTheNode",
),
(
"Unable to acquire monitor cleaner since another instance has already cleaned up all resources.",
"ResourcesAlreadyCleanedUp",
),
(
"Unable to acquire monitor cleaner since an interrupt signal was received.",
"Interrupt",
),
];
let sweep_origin = "Node::<iceoryx2::service::ipc_threadsafe::Service>::cleanup_dead_nodes()";
for (message, variant) in rows {
let captured = vec![
CapturedLog {
level: LogLevel::Debug,
origin: sweep_origin.to_string(),
message: format!("Dead node ({NODE_A}) detected"),
},
CapturedLog {
level: LogLevel::Debug,
origin: format!("DeadNodeView(AliveNodeView {{ id: {NODE_A}, details: None, _service: PhantomData<iceoryx2::service::ipc_threadsafe::Service> }})"),
message: message.to_string(),
},
CapturedLog {
level: LogLevel::Trace,
origin: sweep_origin.to_string(),
message: format!("Unable to remove dead node {NODE_A} ({variant})."),
},
];
let lines = super::render_refused_nodes(&classify_cleanup_failures(&captured).failures);
assert_eq!(
lines,
vec![
"Refused nodes:".to_string(),
format!(" node {NODE_A}: {variant}"),
format!(" {message}"),
],
"{variant}: the captured explanation must be the listing, with no trace hint"
);
assert!(
!lines.iter().any(|l| l.contains("RUST_LOG")),
"{variant}: the hint must not be printed for a line the tool already holds"
);
}
}
const FULL_SCAN_LINE: &str =
"Unable to perform a full scan for dead nodes since the all existing nodes could not be listed (InsufficientPermissions).";
const LIST_LINE: &str =
"Unable to iterate over Node list since the node list could not be acquired (InsufficientPermissions).";
const LIST_ALL_NODES_LINE: &str =
"Unable to list all nodes due to insufficient permissions while listing all nodes.";
#[test]
fn registry_wide_errors_render_under_their_own_heading_verbatim_and_in_order() {
let errors = [
LIST_ALL_NODES_LINE.to_string(),
LIST_LINE.to_string(),
FULL_SCAN_LINE.to_string(),
];
let expected = vec![
"registry-wide sweep errors:".to_string(),
format!(" {LIST_ALL_NODES_LINE}"),
format!(" {LIST_LINE}"),
format!(" {FULL_SCAN_LINE}"),
" (the registry itself could not be fully scanned — not any node's fault; a dead node \
the sweep never reached may still be registered and is counted nowhere above)"
.to_string(),
];
assert_eq!(super::render_registry_errors(&errors), expected);
}
#[test]
fn a_fully_scanned_registry_renders_no_registry_block_at_all() {
assert_eq!(super::render_registry_errors(&[]), Vec::<String>::new());
}
#[test]
fn a_registry_wide_line_before_a_refusal_renders_in_the_registry_block_not_under_the_node() {
let sweep_origin = "Node::<iceoryx2::service::ipc_threadsafe::Service>::cleanup_dead_nodes()";
let own = "Unable to remove stale resources since the port tags could not be read due to an internal error.";
let captured = vec![
CapturedLog {
level: LogLevel::Debug,
origin: sweep_origin.to_string(),
message: FULL_SCAN_LINE.to_string(),
},
CapturedLog {
level: LogLevel::Debug,
origin: sweep_origin.to_string(),
message: format!("Dead node ({NODE_A}) detected"),
},
CapturedLog {
level: LogLevel::Debug,
origin: format!("DeadNodeView(AliveNodeView {{ id: {NODE_A}, details: None, _service: PhantomData<iceoryx2::service::ipc_threadsafe::Service> }})"),
message: own.to_string(),
},
CapturedLog {
level: LogLevel::Trace,
origin: sweep_origin.to_string(),
message: format!("Unable to remove dead node {NODE_A} (InternalError)."),
},
];
let parts = classify_cleanup_failures(&captured);
assert_eq!(
super::render_refused_nodes(&parts.failures),
vec![
"Refused nodes:".to_string(),
format!(" node {NODE_A}: InternalError"),
format!(" {own}"),
],
"the node's listing carries exactly its own explanation — never the scan failure"
);
assert_eq!(
super::render_registry_errors(&parts.registry_errors),
vec![
"registry-wide sweep errors:".to_string(),
format!(" {FULL_SCAN_LINE}"),
" (the registry itself could not be fully scanned — not any node's fault; a dead node \
the sweep never reached may still be registered and is counted nowhere above)"
.to_string(),
],
"the scan failure renders as global"
);
}
const NODE_B: &str =
"UniqueNodeId(UniqueSystemId { value: 2, pid: 2020, creation_time: Time { seconds: 8, nanoseconds: 0 } })";
const NODE_C: &str =
"UniqueNodeId(UniqueSystemId { value: 3, pid: 3030, creation_time: Time { seconds: 9, nanoseconds: 0 } })";
const NODE_D: &str =
"UniqueNodeId(UniqueSystemId { value: 4, pid: 4040, creation_time: Time { seconds: 10, nanoseconds: 0 } })";
const OWNERLESS_LINE: &str =
"Unable to unlink the port resources since the process does not have sufficient permissions.";
const A_OWN_LINE: &str =
"Unable to remove stale resources since the service tags could not be read due to an internal error.";
const B_LOCK_LINE: &str =
"Unable to acquire monitor cleaner since another instance is already cleaning up all resources.";
fn every_arm_capture() -> Vec<CapturedLog> {
let sweep_origin = "Node::<iceoryx2::service::ipc_threadsafe::Service>::cleanup_dead_nodes()";
let (a, b, c, d) = (NODE_A, NODE_B, NODE_C, NODE_D);
let view = |node: &str| {
format!("DeadNodeView(AliveNodeView {{ id: {node}, details: None, _service: PhantomData<iceoryx2::service::ipc_threadsafe::Service> }})")
};
let l = |origin: String, message: String| CapturedLog {
level: LogLevel::Debug,
origin,
message,
};
vec![
l(sweep_origin.to_string(), FULL_SCAN_LINE.to_string()),
l(
sweep_origin.to_string(),
format!("Dead node ({a}) detected"),
),
l(
"Service::remove_node".to_string(),
OWNERLESS_LINE.to_string(),
),
l(
sweep_origin.to_string(),
format!("Dead node ({b}) detected"),
),
l(view(a), A_OWN_LINE.to_string()),
l(
format!("Node::get_node_state(Config {{ .. }}, {d})"),
"Unable to acquire node monitor due to insufficient permissions.".to_string(),
),
l(view(b), B_LOCK_LINE.to_string()),
l(
sweep_origin.to_string(),
format!("Unable to remove dead node {a} (InternalError)."),
),
l(
sweep_origin.to_string(),
format!("Unable to remove dead node {b} (AnotherInstanceIsCleaningUpTheNode)."),
),
l(
sweep_origin.to_string(),
format!("Unable to remove dead node {c} (Interrupt)."),
),
]
}
#[test]
fn classification_is_bit_identical_across_runs_and_matches_its_oracle() {
let captured = every_arm_capture();
let run1 = classify_cleanup_failures(&captured);
assert_eq!(
run1.failures
.iter()
.map(|f| (f.variant.as_str(), f.causes.len()))
.collect::<Vec<_>>(),
vec![
("InternalError", 2),
("AnotherInstanceIsCleaningUpTheNode", 1),
("Interrupt", 0),
],
"the oracle: A takes the ownerless line and its own, B its cleaner-lock explanation, \
C nothing (the only line near it names the visited node D): {:?}",
run1.failures
);
assert_eq!(run1.registry_errors, vec![FULL_SCAN_LINE.to_string()]);
assert_eq!(run1.unclassified.len(), 1, "{:?}", run1.unclassified);
assert_eq!(
run1.failures_by_cause,
std::collections::BTreeMap::from([
("iceoryx2 internal error".to_string(), 1),
("lock contention".to_string(), 1),
])
);
let run2 = classify_cleanup_failures(&captured);
assert_eq!(
run1, run2,
"two runs over one capture must be bit-identical"
);
}
#[test]
fn the_renderers_are_bit_identical_across_runs_and_match_their_oracles() {
let parts = classify_cleanup_failures(&every_arm_capture());
let refused1 = super::render_refused_nodes(&parts.failures);
let refused2 = super::render_refused_nodes(&parts.failures);
assert_eq!(
refused1,
vec![
"Refused nodes:".to_string(),
format!(" node {NODE_A}: InternalError"),
format!(" {OWNERLESS_LINE}"),
format!(" {A_OWN_LINE}"),
format!(" node {NODE_B}: AnotherInstanceIsCleaningUpTheNode"),
format!(" {B_LOCK_LINE}"),
format!(" node {NODE_C}: Interrupt"),
format!(
" (no sub-cause captured — {})",
super::RAW_IOX2_LINES_HINT
),
],
"the listing, line for line"
);
assert_eq!(refused1, refused2);
let registry1 = super::render_registry_errors(&parts.registry_errors);
let registry2 = super::render_registry_errors(&parts.registry_errors);
assert_eq!(registry1[1], format!(" {FULL_SCAN_LINE}"));
assert_eq!(registry1, registry2);
}
#[test]
fn the_clean_verb_prints_the_registry_block_first_and_gates_convergence_on_it() {
let src = code_only(&read_main());
let printing = fn_body(&src, "report_sweep");
let body = fn_body(&src, "clean_dead_nodes");
let registry = printing
.find("render_registry_errors(")
.expect("`cerulion clean` must render the registry-wide block");
let nothing_to_clean = printing
.find("No dead iceoryx2 nodes found")
.expect("the converged early return must still exist");
let listing = printing
.find("render_refused_nodes(")
.expect("the per-node listing must still be rendered");
assert!(
registry < nothing_to_clean && registry < listing,
"the registry block must precede both the early return and the listing; body was:\n{printing}"
);
assert_eq!(
body.matches("report_sweep(").count(),
1,
"the sweep must print through `report_sweep`; body was:\n{body}"
);
let normalized = body.split_whitespace().collect::<Vec<_>>().join(" ");
assert!(
normalized.contains(
"let converged = report.failed_cleanups == 0 && report.registry_errors.is_empty();"
),
"convergence must require an empty registry-error set; body was:\n{body}"
);
let (scanned, unscanned) = if_else_arms(&printing, "report.registry_errors.is_empty()");
assert!(
scanned.contains("No dead iceoryx2 nodes found") && !scanned.contains("not evidence"),
"the fully-scanned arm alone may claim nothing to clean; arm was:\n{scanned}"
);
assert!(
unscanned.contains("not evidence") && !unscanned.contains("No dead iceoryx2 nodes found"),
"the unscanned arm must refuse the absence claim; arm was:\n{unscanned}"
);
}
const FIXTURE_NODE_DIR: &str = "/fixture/iox2-root/nodes";
fn planted(value: u128, pid: u32) -> DeadNodeIdentity {
DeadNodeIdentity {
name: value.to_string(),
token: format!(
"UniqueNodeId(UniqueSystemId {{ value: {value}, pid: {pid}, creation_time: Time {{ \
seconds: 7, nanoseconds: 0 }} }})"
),
}
}
fn would_remove(dead: &[DeadNodeIdentity]) -> Vec<String> {
super::render_would_remove(dead, std::path::Path::new(FIXTURE_NODE_DIR))
}
#[test]
fn the_report_only_listing_names_every_planted_node_and_claims_no_removal() {
let dead = [planted(1, 4242), planted(2, 4243)];
assert_eq!(
would_remove(&dead),
vec![
format!(
"Dead iceoryx2 node(s) `cerulion clean` would sweep: 2 under \
{FIXTURE_NODE_DIR} (report only; none was removed)"
),
" node 1".to_string(),
" node 2".to_string(),
super::REMOVAL_CAN_STILL_BE_REFUSED.to_string(),
],
"the whole listing, line for line"
);
assert!(
!would_remove(&dead)[0].contains("would remove"),
"the heading must not promise a removal: {:?}",
would_remove(&dead)[0]
);
assert!(
super::REMOVAL_CAN_STILL_BE_REFUSED.contains("not a promise each one comes off"),
"the caveat must say the sweep can still refuse: {}",
super::REMOVAL_CAN_STILL_BE_REFUSED
);
assert!(
!would_remove(&dead)
.iter()
.any(|line| line.contains("port tag")),
"the listing must not mention orphan port tags; the reclaim is gone: {:?}",
would_remove(&dead)
);
assert!(
!would_remove(&dead)
.iter()
.any(|line| line.contains("Cleaned") || line.contains("removed 1")),
"a report must never render a removal"
);
}
#[test]
fn a_registry_with_no_dead_node_renders_no_would_remove_listing_at_all() {
assert_eq!(would_remove(&[]), Vec::<String>::new());
}
#[test]
fn the_would_remove_listing_folds_at_the_cap_and_at_the_cap_plus_one() {
let cap = super::NODES_SHOWN;
let plant =
|n: usize| -> Vec<DeadNodeIdentity> { (0..n).map(|i| planted(i as u128, 1)).collect() };
let at = would_remove(&plant(cap));
assert_eq!(
at.iter().filter(|l| l.starts_with(" node ")).count(),
cap,
"{at:?}"
);
assert!(
!at.iter().any(|l| l.contains("more")),
"at the cap there is no remainder to fold: {at:?}"
);
let over = would_remove(&plant(cap + 1));
assert_eq!(
over.iter().filter(|l| l.starts_with(" node ")).count(),
cap,
"{over:?}"
);
assert_eq!(
over[over.len() - 2],
" … and 1 more",
"one past the cap folds exactly one, above the closing caveat: {over:?}"
);
assert!(
!over.iter().any(|l| **l == format!(" node {cap}")),
"a folded node must not leak past the cap: {over:?}"
);
assert!(
over[0].contains(&format!(": {} under ", cap + 1)),
"the heading counts every dead node, not the shown ones: {over:?}"
);
}
fn drive(
nodes: Vec<SurveyedNode<Result<(), NodeCleanupFailure>>>,
mode: SweepMode,
) -> (Vec<NodeSweepOutcome>, usize, Vec<(LogLevel, String)>) {
let mut calls = 0usize;
let mut traced = Vec::new();
let outcomes = nodes
.into_iter()
.map(|node| {
sweep_one_node(
node,
mode,
|result: Result<(), NodeCleanupFailure>| {
calls += 1;
result
},
|level, message| traced.push((level, message)),
)
})
.collect();
(outcomes, calls, traced)
}
#[test]
fn a_report_only_sweep_classifies_every_dead_node_and_never_calls_the_removal() {
let (a, b) = (planted(1, 4242), planted(2, 4243));
let survey = vec![
SurveyedNode::Dead {
identity: a.clone(),
view: Ok(()),
},
SurveyedNode::NotDead,
SurveyedNode::Dead {
identity: b.clone(),
view: Ok(()),
},
];
let (outcomes, calls, traced) = drive(survey, SweepMode::ReportOnly);
assert_eq!(
outcomes,
vec![
NodeSweepOutcome::WouldRemove(a.clone()),
NodeSweepOutcome::NotDead,
NodeSweepOutcome::WouldRemove(b.clone()),
],
"every dead node is classified, and no live one is"
);
assert_eq!(
calls, 0,
"a report must not call the removal, not once, for any node"
);
assert_eq!(
traced,
vec![
(LogLevel::Debug, format!("Dead node ({}) detected", a.token)),
(LogLevel::Debug, format!("Dead node ({}) detected", b.token)),
]
);
}
#[test]
fn a_removing_sweep_is_the_same_classification_followed_by_the_removal() {
let (a, b) = (planted(1, 4242), planted(2, 4243));
let survey = vec![
SurveyedNode::Dead {
identity: a.clone(),
view: Ok(()),
},
SurveyedNode::NotDead,
SurveyedNode::Dead {
identity: b.clone(),
view: Err(NodeCleanupFailure::InsufficientPermissions),
},
];
let (outcomes, calls, traced) = drive(survey, SweepMode::Remove);
assert_eq!(
outcomes,
vec![
NodeSweepOutcome::Removed(a.clone()),
NodeSweepOutcome::NotDead,
NodeSweepOutcome::Refused {
identity: b.clone(),
failure: "InsufficientPermissions".to_string(),
},
]
);
assert_eq!(calls, 2, "one call per DEAD node, and none for a live one");
assert_eq!(
traced,
vec![
(LogLevel::Debug, format!("Dead node ({}) detected", a.token)),
(
LogLevel::Trace,
format!("The dead node ({}) was successfully removed.", a.token)
),
(LogLevel::Debug, format!("Dead node ({}) detected", b.token)),
(
LogLevel::Trace,
format!(
"Unable to remove dead node {} (InsufficientPermissions).",
b.token
)
),
],
"the refusal line is the marker `classify_cleanup_failures` parses"
);
}
#[test]
fn the_clean_verb_renders_the_would_remove_listing_from_the_sweeps_own_field() {
let src = code_only(&read_main());
let body = fn_body(&src, "report_sweep");
assert_eq!(
call_args(&body, "render_would_remove"),
"&report.dead_nodes, &cerulion_cli_engine::shm_state::iceoryx2_node_dir()",
"the listing must render the sweep's own dead-node set, under the registry the sweep \
read; body was:\n{body}"
);
let registry = body
.find("render_registry_errors(")
.expect("the registry block must still be rendered");
let fork = body
.find("if mode.reports_only() && !report.dead_nodes.is_empty() {")
.expect("the report-only fork must guard the listing");
let summary = body
.find("Cleaned {} dead iceoryx2 node(s)")
.expect("the summary line must still exist");
assert!(
registry < fork && fork < summary,
"registry block, then the report-only fork, then the summary; body was:\n{body}"
);
let block = block_after(&body, fork);
assert!(
block.contains("return;"),
"the report-only fork must return rather than fall through to the summary; block \
was:\n{block}"
);
}
#[test]
fn the_verb_maps_the_flag_to_the_mode_in_both_directions() {
let src = code_only(&read_main());
let at = src
.find("Commands::Clean { report_only } =>")
.expect("the clean verb must still be dispatched on its own flag");
assert_eq!(
call_args(&src[at..], "clean_iceoryx2_state"),
"if report_only { SweepMode::ReportOnly } else { SweepMode::Remove }",
"the flag maps to the mode in exactly this direction; a swap would make \
`--report-only` destructive or `clean` inert, and only the first of those \
is caught anywhere else; dispatch was:\n{}",
&src[at..at + 200.min(src.len() - at)]
);
}
#[test]
fn the_verb_closes_a_report_only_run_by_saying_nothing_was_removed() {
let src = code_only(&read_main());
let body = fn_body(&src, "clean_iceoryx2_state");
let diagnostic = body
.find("report_shm_state_population(")
.expect("the diagnostic must still be called");
let closing = body
.find("NOTHING_WAS_REMOVED")
.expect("a report-only run must close by saying nothing was removed");
assert!(
diagnostic < closing,
"the closing claim comes after both halves have reported; body was:\n{body}"
);
let guard = body
.find("if mode.reports_only() {")
.expect("the closing line is printed only under the flag");
assert!(
guard < closing,
"the closing line is guarded by the mode; body was:\n{body}"
);
assert!(
super::NOTHING_WAS_REMOVED.contains("nothing was removed"),
"the sentence must say it: {}",
super::NOTHING_WAS_REMOVED
);
}