pipecircuit-cli 0.0.13

Command-line architectural tools for Pipecircuit programs
use std::{error::Error, fs, process::Command};

#[test]
fn json_analysis_distinguishes_violations_from_incomplete_work() -> Result<(), Box<dyn Error>> {
    let scratch = tempfile::tempdir()?;
    fs::create_dir(scratch.path().join("src"))?;
    fs::write(
        scratch.path().join("src/lib.rs"),
        "pub struct Unclassified;\n",
    )?;
    let manifest = scratch.path().join("Cargo.toml");
    fs::write(
        &manifest,
        "[workspace]\n[package]\nname = \"unclassified\"\nversion = \"0.1.0\"\nedition = \"2024\"\n",
    )?;
    let output = Command::new(env!("CARGO_BIN_EXE_pipecircuit"))
        .args(["arch", "check", "--format", "json", "--manifest-path"])
        .arg(&manifest)
        .output()?;
    assert_eq!(output.status.code(), Some(1));
    let report: serde_json::Value = serde_json::from_slice(&output.stdout)?;
    assert!(
        report
            .get("findings")
            .and_then(serde_json::Value::as_array)
            .is_some_and(|findings| findings.iter().any(|finding| finding
                .get("code")
                .and_then(serde_json::Value::as_str)
                == Some("arch.role.missing")))
    );
    fs::write(&manifest, "malformed manifest")?;
    let output = Command::new(env!("CARGO_BIN_EXE_pipecircuit"))
        .args(["arch", "check", "--format", "json", "--manifest-path"])
        .arg(&manifest)
        .output()?;
    assert_eq!(output.status.code(), Some(2));
    let report: serde_json::Value = serde_json::from_slice(&output.stdout)?;
    assert_eq!(
        report.get("status").and_then(serde_json::Value::as_str),
        Some("incomplete")
    );
    assert!(!output.stderr.is_empty());
    Ok(())
}

#[test]
fn installed_version_is_explicit_and_rejects_trailing_arguments() -> Result<(), Box<dyn Error>> {
    for flag in ["--version", "-V"] {
        let output = Command::new(env!("CARGO_BIN_EXE_pipecircuit"))
            .arg(flag)
            .output()?;
        assert!(output.status.success());
        assert_eq!(
            String::from_utf8(output.stdout)?,
            format!("pipecircuit {}\n", env!("CARGO_PKG_VERSION"))
        );
        assert!(output.stderr.is_empty());
    }
    let output = Command::new(env!("CARGO_BIN_EXE_pipecircuit"))
        .args(["--version", "unexpected"])
        .output()?;
    assert_eq!(output.status.code(), Some(2));
    assert!(output.stdout.is_empty());
    assert!(String::from_utf8(output.stderr)?.contains("does not accept arguments"));
    Ok(())
}

#[cfg(unix)]
#[test]
fn visualization_opens_a_saved_snapshot_and_preserves_violation_status()
-> Result<(), Box<dyn Error>> {
    use std::os::unix::fs::PermissionsExt as _;
    let scratch = tempfile::Builder::new()
        .prefix("pipecircuit browser path ")
        .tempdir()?;
    fs::create_dir(scratch.path().join("src"))?;
    fs::write(
        scratch.path().join("src/lib.rs"),
        "pub struct Unclassified;\n",
    )?;
    fs::write(
        scratch.path().join("Cargo.toml"),
        "[workspace]\n[package]\nname = \"unclassified\"\nversion = \"0.1.0\"\nedition = \"2024\"\n",
    )?;
    let launcher = scratch.path().join(if cfg!(target_os = "macos") {
        "open"
    } else {
        "xdg-open"
    });
    fs::write(
        &launcher,
        "#!/bin/sh\nprintf '%s' \"$1\" > \"$PIPEC_TEST_OPENED\"\n",
    )?;
    fs::set_permissions(&launcher, fs::Permissions::from_mode(0o700))?;
    let mut paths = vec![scratch.path().to_owned()];
    paths.extend(std::env::split_paths(
        &std::env::var_os("PATH").ok_or("missing PATH")?,
    ));
    let marker = scratch.path().join("opened");
    let output = Command::new(env!("CARGO_BIN_EXE_pipecircuit"))
        .current_dir(scratch.path())
        .args(["arch", "visualize"])
        .env("PATH", std::env::join_paths(paths)?)
        .env("TMPDIR", scratch.path())
        .env("PIPEC_TEST_OPENED", &marker)
        .output()?;
    assert_eq!(output.status.code(), Some(1));
    let stdout = String::from_utf8(output.stdout)?;
    let saved = stdout
        .lines()
        .find_map(|line| line.strip_prefix("Saved view: "))
        .ok_or("saved path missing")?;
    assert!(fs::read_to_string(saved)?.contains("arch.role.missing"));
    let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
    loop {
        if fs::read_to_string(&marker).is_ok_and(|opened| opened == saved) {
            break;
        }
        if std::time::Instant::now() >= deadline {
            return Err("browser did not receive the saved file path".into());
        }
        std::thread::sleep(std::time::Duration::from_millis(10));
    }
    assert!(output.stderr.is_empty());
    Ok(())
}