Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

tests/support/ci_commands.rs

//! **What `.woodpecker.yml` actually RUNS**, as opposed to what it mentions.
//!
//! Six guards in this suite exist to keep an off-by-default feature from going
//! dark: `inotify`, `atom-cli`, `solo,robot-remote`, `testmatrix`, the
//! feature-combo gate and the linkage audit are each compiled by exactly ONE
//! line of CI, and each has a companion `#[test]` that is supposed to go red
//! when that line disappears. Every one of them was written as a substring
//! search over the WHOLE file — and a CI file is mostly prose.
//!
//! MEASURED 2026-08-23, the red probe that produced this module: putting a `#`
//! in front of all four `- cargo test -p nornir --features …` lines — the shape
//! a human reaches for when a step is slow or flaky — left
//! `ci_compiles_the_testmatrix_arm`, `ci_compiles_the_atom_marker_feature`,
//! `something_in_ci_compiles_the_feature` and `ci_compiles_the_solo_proof`
//! **all four GREEN**. The needle they search for is still in the file; it is
//! just commented out. A guard a `#` defeats guards nothing. Worse for
//! `testmatrix`: its first assertion (`ci.contains("testmatrix")`) could not go
//! red even on outright DELETION of the command, because the word `testmatrix`
//! appears five times in the comment block above it.
//!
//! So the needle has to be looked for in the COMMANDS, which is what this
//! module extracts. One parser, six callers (LAW 5): a commented-out step is not
//! a command by construction, and no guard has to remember to say so.

use std::path::PathBuf;

/// Repo root (this file lives at `<root>/tests/support/`).
fn repo_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}

/// The shell commands a Woodpecker YAML will actually execute, in order, one
/// `String` per list item under a `commands:` key.
///
/// * A `#`-commented line is NOT a command — that is the whole point.
/// * A continuation line (deeper indent, no `- `) is folded onto the command it
///   continues, so the multi-line `apt-get install` step reads as one string and
///   a flag wrapped onto the next line is still findable.
/// * Lists that are not `commands:` (`when:`, `variables:`) are skipped, so a
///   needle appearing in a branch filter cannot satisfy a build guard.
///
/// Split from [`ci_commands`] so the fixture test at the bottom exercises the
/// same code path the guards use, instead of a second copy of it.
pub fn parse_commands(text: &str) -> Vec<String> {
    let mut out: Vec<String> = Vec::new();
    let mut list_indent: Option<usize> = None;
    for raw in text.lines() {
        let trimmed = raw.trim_start();
        if trimmed.is_empty() || trimmed.starts_with('#') {
            // A comment or a blank line neither opens nor closes a block: a
            // comment BETWEEN two commands must not end the list, and a
            // commented-out command must not join it.
            continue;
        }
        let indent = raw.len() - trimmed.len();
        if let Some(key_indent) = list_indent {
            if indent > key_indent {
                if let Some(item) = trimmed.strip_prefix("- ") {
                    out.push(item.trim().to_string());
                } else if let Some(last) = out.last_mut() {
                    // Folded continuation of the previous command.
                    last.push(' ');
                    last.push_str(trimmed.trim_end());
                }
                continue;
            }
            list_indent = None;
        }
        if trimmed.starts_with("commands:") {
            list_indent = Some(indent);
        }
    }
    out
}

/// [`parse_commands`] over this repo's `.woodpecker.yml`.
pub fn ci_commands() -> Vec<String> {
    let path = repo_root().join(".woodpecker.yml");
    let text =
        std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
    let cmds = parse_commands(&text);
    assert!(
        !cmds.is_empty(),
        "parsed ZERO commands out of {} — every CI guard in this suite would then \
         be asserting against an empty list, which is the vacuous pass they exist \
         to prevent. Either the file lost its `commands:` blocks or this parser \
         stopped understanding them.",
        path.display()
    );
    cmds
}

/// True when some CI command contains `needle`.
pub fn ci_runs(needle: &str) -> bool {
    ci_commands().iter().any(|c| c.contains(needle))
}

/// The commands, one per line, so an assertion message can show what WAS found
/// and not only what was missing.
pub fn ci_commands_listing() -> String {
    ci_commands()
        .iter()
        .map(|c| format!("    {c}"))
        .collect::<Vec<_>>()
        .join("\n")
}

/// ★ THE PARSER'S OWN RED. Everything above is only worth having if a `#`
/// actually removes a command, so that is asserted on a fixture — the live file
/// is what the guards assert, and a parser tested only against the live file
/// would pass while ignoring comments in neither.
///
/// Not `#[cfg(test)]`: this file is `#[path]`-included into integration test
/// targets, where `cfg(test)` is false and the module would be dead code.
#[test]
fn a_commented_command_is_not_a_command() {
    let yml = concat!(
        "steps:\n",
        "  build:\n",
        "    commands:\n",
        "      - cargo test --features live\n",
        "      # - cargo test --features dead\n",
        "      - apt-get install\n",
        "          libfoo-dev\n",
        "when:\n",
        "  - event: [push]\n",
    );
    let cmds = parse_commands(yml);
    assert_eq!(
        cmds,
        vec![
            "cargo test --features live".to_string(),
            "apt-get install libfoo-dev".to_string()
        ],
        "the commented command must be absent, the folded line must be joined onto \
         its command, and `when:`'s list must not be read as commands"
    );
    assert!(
        !cmds.iter().any(|c| c.contains("dead")),
        "a `#`-commented step counted as a command — the exact blindness this module \
         exists to remove"
    );
}