cerulion_cli 0.1.0

Cerulion command-line interface: create workspaces, build nodes, run graphs, and inspect topics
// SPDX-License-Identifier: AGPL-3.0-only
//! CER-1411: pins that `cerulion clean` actually RUNS the state-file
//! diagnostic, and runs it in the right ORDER.
//!
//! # Why a source walk
//!
//! `report_shm_state_population` reads the one directory
//! `iceoryx2-pal-posix` hardcodes — literally `/tmp/` — and, without
//! `--report-only`, DELETES from it. A behavioural test of that function
//! would therefore have to operate on the developer's (or the runner's) real
//! `/tmp`, which the house rule forbids and which would race every other lane
//! on this desk. The engine half is fully oracle-tested over a tempdir with an
//! injected system probe (`cerulion_cli_engine::shm_state`); what CANNOT be
//! reached that way is the one thing the CLI owns:
//!
//! * that the diagnostic is CALLED at all — the classic inert-shipping shape,
//!   where every engine arm stays green while the verb does nothing, and
//! * that the dead-node SWEEP runs FIRST. Ordering is a correctness property
//!   here, not tidiness: the sweep removes state files of its own through
//!   iceoryx2's `shm_unlink`, so a population reported before it over-counts,
//!   and a reclamation running first would unlink objects the sweep was about
//!   to inspect.
//!
//! Reading source is a weaker oracle than running code, so the walk is
//! literal about what it recognises and carries an anti-tautology arm proving
//! the stripped view still contains the code it claims to read.

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()))
}

/// Strip `//`-to-end-of-line comments, so the names this module's own docs and
/// the function's doc comment mention are never read as calls.
fn code_only(src: &str) -> String {
    src.lines()
        .map(|line| match line.find("//") {
            Some(i) => &line[..i],
            None => line,
        })
        .collect::<Vec<_>>()
        .join("\n")
}

/// The brace-matched body of `fn <name>`, so an assertion about ONE function
/// cannot be satisfied by a call somewhere else in the file.
///
/// HONEST SCOPE: the count includes braces inside string literals, so a body
/// holding an UNBALANCED one would end the scope in the wrong place. Both
/// bodies this file reads hold only balanced ones (`println!("{line}")`), and
/// an unbalanced literal would show up as a panic here rather than as a
/// silently weaker assertion — the fail-loud direction.
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() {
    // Without this the whole feature ships INERT: every engine arm stays
    // green while `cerulion clean` prints exactly what it printed before.
    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 CER-1411 diagnostic; body was:\n{body}"
    );
}

#[test]
fn the_dead_node_sweep_runs_before_the_state_file_diagnostic() {
    // The sweep removes state files of its own (iceoryx2's `shm_unlink`
    // removes the file its object was named by), so a population reported
    // before it over-counts, and a reclamation running first would unlink
    // objects the sweep was about to inspect.
    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() {
    // CER-1418: a `.shm_state` file is the ONLY mapping from an iceoryx2
    // resource name to the object behind it, so removing one a still-registered
    // dead node needs makes that node permanently unreclaimable — no later
    // sweep can read its details again. Measured, against a control, in
    // `cerulion_cli_engine/tests/cer1418_reclaim_ordering_test.rs`.
    //
    // Structural rather than behavioural for the same reason the arms above
    // are: the reclamation reads and deletes from the developer's real `/tmp`.
    let src = code_only(&read_main());
    let body = fn_body(&src, "clean_iceoryx2_state");
    // The CONJUNCTION, not merely a mention of `converged`. Reclaiming requires
    // BOTH "the operator asked for it" and "the registry converged", so an
    // `||` here would reclaim on a wedged desk whenever `--report-only` was
    // absent — which is the whole defect. A bare `contains("converged")` is
    // satisfied by that mutant, so it is pinned as a whole expression.
    let normalized = body.split_whitespace().collect::<Vec<_>>().join(" ");
    assert!(
        normalized.contains("let reclaim = !report_only && 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}"
    );
    // The DESTRUCTIVE half is what the gate must govern: `report_only` alone
    // reaching the diagnostic would leave the reclamation ungated.
    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}"
    );
}

/// The two brace-matched arms of `if <cond> { .. } else { .. }`, located by
/// the CONDITION text.
///
/// Both arms are needed because an assertion over the whole body cannot see a
/// SWAP: `ScanMode::ReportOnly` and `ScanMode::Reclaim` both appear either
/// way, and so does `report_only`.
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)
}

/// The first brace-matched `{ .. }` at or after `from`.
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() {
    // `--report-only` is the look-first escape hatch, and the load-bearing
    // property is the MAPPING, not the presence of both names: a diagnostic
    // with its arms SWAPPED still mentions both modes and the flag, so it
    // would delete on the one run whose whole point was not to.
    let src = code_only(&read_main());
    let body = fn_body(&src, "report_shm_state_population");
    let (report_arm, reclaim_arm) = if_else_arms(&body, "report_only");

    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}"
    );
    // The budgets ride the same fork, so a swap there is the same class of
    // defect: a report that spends the remedy's budget, or a remedy bounded
    // like a report.
    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() {
    // ANTI-TAUTOLOGY: every assertion above is a `contains` over a stripped
    // view, so a stripper that emptied the file would make all of them
    // vacuous — except this one.
    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"
    );
    // And the stripper must really strip: `main.rs` is full of `//` comments,
    // so a no-op stripper leaves the view identical to the source.
    assert!(
        src.len() < raw.len(),
        "the stripped view must be shorter than the source it came from"
    );
}