execsurface 1.0.0

Runtime execution-surface drift detection for Linux CI and developer workflows
use std::collections::BTreeSet;
use std::fs;
use std::path::Path;

fn workflow_text() -> String {
    let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
    fs::read_to_string(root.join(".github/workflows/adversarial-regression.yml"))
        .expect("read adversarial regression workflow")
}

fn paths_for_section(document: &str, section: &str) -> BTreeSet<String> {
    let header = format!("  {section}:");
    let mut in_section = false;
    let mut in_paths = false;
    let mut paths = BTreeSet::new();

    for line in document.lines() {
        if line == header {
            in_section = true;
            continue;
        }

        if in_section && line.starts_with("  ") && !line.starts_with("    ") {
            break;
        }

        if !in_section {
            continue;
        }

        if line == "    paths:" {
            in_paths = true;
            continue;
        }

        if in_paths {
            if let Some(value) = line.strip_prefix("      - ") {
                paths.insert(value.trim_matches(['\'', '"']).to_owned());
                continue;
            }

            if !line.trim().is_empty() && !line.starts_with("      ") {
                break;
            }
        }
    }

    assert!(
        in_section && in_paths,
        "workflow must declare {section}.paths explicitly"
    );
    paths
}

#[test]
fn r6_push_and_pull_request_adversarial_paths_are_symmetric() {
    let document = workflow_text();
    let push = paths_for_section(&document, "push");
    let pull_request = paths_for_section(&document, "pull_request");

    assert_eq!(
        push, pull_request,
        "adversarial regression push and pull_request path gates must be identical"
    );
}

#[test]
fn r6_semantic_workspace_changes_trigger_adversarial_regression() {
    let document = workflow_text();
    let required = BTreeSet::from([
        ".github/m12-fixtures/**".to_owned(),
        ".github/workflows/adversarial-regression.yml".to_owned(),
        "crates/**".to_owned(),
        "Cargo.toml".to_owned(),
        "Cargo.lock".to_owned(),
        "README.md".to_owned(),
    ]);

    for section in ["push", "pull_request"] {
        let actual = paths_for_section(&document, section);
        for path in &required {
            assert!(
                actual.contains(path),
                "{section}.paths is missing required adversarial trigger {path:?}; actual={actual:?}"
            );
        }
    }
}