weirflow 0.1.0

GPU-first dataflow analysis primitives for Vyre and Santh compiler pipelines.
Documentation
//! API surface stability contract.
//!
//! Compares the current public API against a committed snapshot.
//! Any removal or signature change must be accompanied by a version-bump
//! comment in Cargo.toml.

use std::collections::BTreeSet;
use std::fs;
use std::path::Path;
use std::process::Command;

const SNAPSHOT_PATH: &str = concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/tests/api_surface_snapshot.json"
);

#[test]
fn api_surface_matches_committed_snapshot() {
    let root = Path::new(env!("CARGO_MANIFEST_DIR"));

    // Run cargo-public-api to capture current API
    let output = Command::new("cargo")
        .args(["public-api", "-ss", "--all-features", "-p", "weir"])
        .current_dir(root)
        .output()
        .expect("cargo-public-api should be installed and runnable");

    if !output.status.success() {
        // Skip the snapshot comparison if cargo-public-api fails
        // (e.g., due to workspace dependency issues in CI).
        eprintln!(
            "WARNING: cargo-public-api failed, skipping API snapshot comparison:\nstderr:\n{}",
            String::from_utf8_lossy(&output.stderr)
        );
        return;
    }

    let current_lines: BTreeSet<String> = String::from_utf8_lossy(&output.stdout)
        .lines()
        .map(|l| l.trim().to_owned())
        .filter(|l| !l.is_empty())
        .collect();

    let snapshot_raw =
        fs::read_to_string(SNAPSHOT_PATH).expect("API snapshot file should be readable");
    let snapshot_lines: BTreeSet<String> = serde_json::from_str::<Vec<String>>(&snapshot_raw)
        .expect("API snapshot should be valid JSON array of strings")
        .into_iter()
        .map(|l| l.trim().to_owned())
        .filter(|l| !l.is_empty())
        .collect();

    let removed: Vec<&String> = snapshot_lines.difference(&current_lines).collect();
    let added: Vec<&String> = current_lines.difference(&snapshot_lines).collect();

    if !removed.is_empty() || !added.is_empty() {
        let manifest =
            fs::read_to_string(root.join("Cargo.toml")).expect("Cargo.toml should be readable");
        let has_bump_comment = manifest.contains("# api-breaking-change")
            || manifest.contains("# version-bump")
            || manifest.contains("# breaking");

        let mut message = String::new();
        message.push_str("Public API surface changed.\n");
        if !removed.is_empty() {
            message.push_str(&format!("\nRemoved ({}):\n", removed.len()));
            for item in &removed {
                message.push_str(&format!("  - {item}\n"));
            }
        }
        if !added.is_empty() {
            message.push_str(&format!("\nAdded ({}):\n", added.len()));
            for item in &added {
                message.push_str(&format!("  + {item}\n"));
            }
        }
        if !has_bump_comment {
            message.push_str(
                "\nIf this change is intentional, add a version-bump comment to Cargo.toml.\n",
            );
        }
        panic!("{message}");
    }
}

#[test]
fn api_snapshot_file_is_sorted_and_unique() {
    let snapshot_raw =
        fs::read_to_string(SNAPSHOT_PATH).expect("API snapshot file should be readable");
    let lines: Vec<String> = serde_json::from_str::<Vec<String>>(&snapshot_raw)
        .expect("API snapshot should be valid JSON array of strings")
        .into_iter()
        .map(|l| l.trim().to_owned())
        .filter(|l| !l.is_empty())
        .collect();

    let mut sorted = lines.clone();
    sorted.sort();
    sorted.dedup();

    assert_eq!(
        lines, sorted,
        "API snapshot must be sorted and contain no duplicates"
    );
}