pipecircuit-visualization 0.0.3

Code-derived architectural visualizations for Pipecircuit programs
Documentation
use std::{error::Error, process::Command};

use expect_test::expect;
use proptest::prelude::*;

use super::*;

#[test]
fn rendering_is_deterministic_and_keeps_ignored_checks_visible() -> Result<(), BehaviorError> {
    let inventory = BehaviorInventory::from_listings(
        "tests::z_last_check: test\ntests::équality_is_preserved: test\ntests::a_first_check: test\n",
        "tests::z_last_check: test\n",
    )?;
    expect![[r#"
        - A first check
        - Z last check [ignored]
        - Équality is preserved
    "#]]
    .assert_eq(&inventory.render());
    assert_eq!(inventory.checks().len(), 3);
    assert_eq!(
        inventory.checks().first().map(BehaviorCheck::disposition),
        Some(CheckDisposition::Runnable)
    );
    Ok(())
}

#[test]
fn equal_sentences_retain_their_distinct_code_locations() -> Result<(), BehaviorError> {
    let inventory = BehaviorInventory::from_listings(
        "east::ownership_is_preserved: test\nwest::ownership_is_preserved: test\n",
        "",
    )?;
    expect![[r#"
        - Ownership is preserved (east::ownership_is_preserved)
        - Ownership is preserved (west::ownership_is_preserved)
    "#]]
    .assert_eq(&inventory.render());
    Ok(())
}

#[test]
fn snapshot_scoping_respects_module_boundaries_and_excludes_its_own_module()
-> Result<(), BehaviorError> {
    let listing = "machine::tests::preserves_state: test\nmachine::behavior::inventory: test\nmachine::behavior::nested::helper: test\nmachine_extra::unrelated: test\nroot_check: test\n";
    let inventory = BehaviorInventory::from_listings(listing, "")?
        .beside_snapshot("consumer::machine::behavior")?;
    assert_eq!(
        inventory
            .checks()
            .iter()
            .map(BehaviorCheck::qualified_name)
            .collect::<Vec<_>>(),
        ["machine::tests::preserves_state"]
    );
    let root =
        BehaviorInventory::from_listings(listing, "")?.beside_snapshot("consumer::behavior")?;
    assert_eq!(root.checks().len(), 5);
    Ok(())
}

#[test]
fn malformed_duplicate_or_inconsistent_listings_are_refused() {
    for line in ["unexpected output", ": test", "a::::check: test"] {
        assert!(matches!(BehaviorInventory::from_listings(line, ""),
            Err(BehaviorError::InvalidListing { line: rejected }) if rejected == line));
    }
    assert!(
        matches!(BehaviorInventory::from_listings("check: test\ncheck: test", ""),
        Err(BehaviorError::DuplicateCheck { name }) if name == "check")
    );
    assert!(
        matches!(BehaviorInventory::from_listings("check: test", "other: test"),
        Err(BehaviorError::UnknownIgnoredCheck { name }) if name == "other")
    );
}

#[test]
fn absent_or_malformed_snapshot_scopes_are_refused() -> Result<(), BehaviorError> {
    for module in [
        "consumer",
        "consumer::",
        "::behavior",
        "consumer::machine::::behavior",
    ] {
        let inventory = BehaviorInventory::from_listings("check: test", "")?;
        assert!(matches!(inventory.beside_snapshot(module),
            Err(BehaviorError::InvalidSnapshotModule { module: rejected }) if rejected == module));
    }
    assert!(matches!(
        BehaviorInventory::from_listings("other::check: test", "")?
            .beside_snapshot("consumer::machine::behavior"),
        Err(BehaviorError::EmptyScope { .. })
    ));
    Ok(())
}

#[test]
fn benchmark_functions_are_not_behavior_checks() -> Result<(), BehaviorError> {
    let inventory = BehaviorInventory::from_listings("\nspeed: benchmark\ncheck: test\n", "")?;
    assert_eq!(inventory.render(), "- Check\n");
    assert_eq!(BehaviorInventory::from_listings("", "")?.render(), "");
    Ok(())
}

macro_rules! generated_check {
    ($name:ident) => {
        #[test]
        fn $name() -> Result<(), BehaviorError> {
            let inventory = BehaviorInventory::from_listings("generated: test", "")?;
            assert_eq!(inventory.render(), "- Generated\n");
            Ok(())
        }
    };
}

generated_check!(macro_generated_checks_remain_discoverable);

#[test]
#[ignore = "Fixture for the compiler's ignored-check listing; not a verified claim"]
fn an_ignored_inventory_fixture() {}

#[cfg(any())]
#[test]
fn an_uncompiled_inventory_fixture() {}

#[test]
fn compiled_discovery_includes_generated_checks_and_preserves_ignored_status()
-> Result<(), Box<dyn Error>> {
    let inventory = BehaviorInventory::from_test_binary(env::current_exe()?)?;
    let check = |name| {
        inventory
            .checks()
            .iter()
            .find(|check| check.qualified_name() == name)
    };
    assert_eq!(
        check("behavior::tests::macro_generated_checks_remain_discoverable")
            .map(BehaviorCheck::disposition),
        Some(CheckDisposition::Runnable)
    );
    assert_eq!(
        check("behavior::tests::an_ignored_inventory_fixture").map(BehaviorCheck::disposition),
        Some(CheckDisposition::Ignored)
    );
    assert_eq!(
        check("behavior::tests::an_uncompiled_inventory_fixture"),
        None
    );
    assert_eq!(
        check("behavior::tests::properties::listing_order_does_not_change_the_inventory")
            .map(BehaviorCheck::disposition),
        Some(CheckDisposition::Runnable)
    );
    Ok(())
}

#[test]
fn configured_runners_preserve_scope_filters_and_ignored_checks() -> Result<(), Box<dyn Error>> {
    let mut runner = Command::new(env::current_exe()?);
    runner.arg("behavior::tests::");
    let inventory = BehaviorInventory::for_snapshot_with_runner(
        "pipecircuit_visualization::behavior::tests::snapshot",
        runner,
    )?;
    let ordinary =
        BehaviorInventory::for_snapshot("pipecircuit_visualization::behavior::tests::snapshot")?;
    assert_eq!(inventory, ordinary);
    assert!(
        inventory
            .checks()
            .iter()
            .all(|check| { check.qualified_name().starts_with("behavior::tests::") })
    );
    assert!(inventory.checks().iter().any(|check| {
        check.qualified_name() == "behavior::tests::an_ignored_inventory_fixture"
            && check.disposition() == CheckDisposition::Ignored
    }));
    assert!(inventory.checks().iter().any(|check| {
        check.qualified_name() == "behavior::tests::macro_generated_checks_remain_discoverable"
            && check.disposition() == CheckDisposition::Runnable
    }));
    Ok(())
}

#[test]
fn configured_runner_failures_preserve_the_executable_and_output() -> Result<(), Box<dyn Error>> {
    let absent = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("absent-test-runner");
    assert!(matches!(
        BehaviorInventory::for_snapshot_with_runner("consumer::behavior", Command::new(&absent)),
        Err(BehaviorError::DiscoveryIo { executable, source })
            if executable == absent && source.kind() == io::ErrorKind::NotFound
    ));
    let executable = env::current_exe()?;
    let mut runner = Command::new(&executable);
    runner.arg("--unknown-inventory-option");
    let expected = runner.args(["--list", "--format", "terse"]).output()?;
    let mut runner = Command::new(&executable);
    runner.arg("--unknown-inventory-option");
    let Err(BehaviorError::DiscoveryFailed {
        executable: rejected,
        output,
    }) = BehaviorInventory::for_snapshot_with_runner("consumer::behavior", runner)
    else {
        return Err("failed runner discovery was accepted".into());
    };
    assert_eq!(rejected, executable);
    assert_eq!(output.status, expected.status);
    assert_eq!(output.stdout, expected.stdout);
    assert_eq!(output.stderr, expected.stderr);
    Ok(())
}

#[test]
fn configured_runners_refuse_missing_or_malformed_snapshot_scopes() -> Result<(), Box<dyn Error>> {
    let executable = env::current_exe()?;
    let runner = || {
        let mut command = Command::new(&executable);
        command.arg("behavior::tests::");
        command
    };
    assert!(matches!(
        BehaviorInventory::for_snapshot_with_runner("consumer", runner()),
        Err(BehaviorError::InvalidSnapshotModule { module }) if module == "consumer"
    ));
    assert!(matches!(
        BehaviorInventory::for_snapshot_with_runner("consumer::absent::behavior", runner()),
        Err(BehaviorError::EmptyScope { module }) if module == "consumer::absent::behavior"
    ));
    Ok(())
}

#[test]
fn process_discovery_failures_retain_their_original_details() -> Result<(), Box<dyn Error>> {
    let absent = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("absent-test-executable");
    assert!(matches!(BehaviorInventory::from_test_binary(&absent),
        Err(BehaviorError::DiscoveryIo { executable, source })
            if executable == absent && source.kind() == io::ErrorKind::NotFound));
    let failed = Command::new(env::current_exe()?)
        .arg("--unknown-inventory-option")
        .output()?;
    assert!(!failed.status.success());
    let executable = env::current_exe()?;
    let Err(failure @ BehaviorError::DiscoveryFailed { .. }) =
        test_listing(&executable, failed.clone())
    else {
        return Err("failed discovery was accepted".into());
    };
    assert!(failure.to_string().contains("failed with"));
    let BehaviorError::DiscoveryFailed {
        executable: retained,
        output,
    } = failure
    else {
        return Err("discovery failure changed kind".into());
    };
    assert_eq!(retained, executable);
    assert_eq!(output.status, failed.status);
    assert_eq!(output.stdout, failed.stdout);
    assert_eq!(output.stderr, failed.stderr);
    let mut invalid_listing = Command::new(&executable)
        .args(["--list", "--format", "terse"])
        .output()?;
    assert!(invalid_listing.status.success());
    invalid_listing.stdout = vec![255];
    let Err(BehaviorError::ListingUtf8(rejected)) = test_listing(&executable, invalid_listing)
    else {
        return Err("invalid listing bytes were accepted".into());
    };
    assert_eq!(rejected.into_bytes(), [255]);
    let Err(invalid_utf8) = String::from_utf8(vec![255]) else {
        return Err("invalid UTF-8 was accepted".into());
    };
    let errors = [
        BehaviorError::CurrentExecutable(io::Error::from(io::ErrorKind::NotFound)),
        BehaviorError::DiscoveryIo {
            executable: absent,
            source: io::Error::from(io::ErrorKind::NotFound),
        },
        BehaviorError::ListingUtf8(invalid_utf8),
        BehaviorError::InvalidListing {
            line: "unknown".to_owned(),
        },
        BehaviorError::DuplicateCheck {
            name: "check".to_owned(),
        },
        BehaviorError::UnknownIgnoredCheck {
            name: "check".to_owned(),
        },
        BehaviorError::InvalidSnapshotModule {
            module: "invalid".to_owned(),
        },
        BehaviorError::EmptyScope {
            module: "empty".to_owned(),
        },
        BehaviorError::RecursiveDiscovery,
    ];
    for error in errors {
        assert!(!error.to_string().is_empty());
    }
    Ok(())
}

mod properties {
    use super::*;

    proptest! {
        #[test]
        fn listing_order_does_not_change_the_inventory(
            names in prop::collection::btree_set("[a-z][a-z_]{0,24}", 0..30),
        ) {
            let forward = names.iter().map(|name| format!("tests::{name}: test\n")).collect::<String>();
            let reverse = names.iter().rev().map(|name| format!("tests::{name}: test\n")).collect::<String>();
            let forward = BehaviorInventory::from_listings(&forward, "")
                .map_err(|error| TestCaseError::fail(error.to_string()))?;
            let reverse = BehaviorInventory::from_listings(&reverse, "")
                .map_err(|error| TestCaseError::fail(error.to_string()))?;
            prop_assert_eq!(&forward, &reverse);
            prop_assert_eq!(forward.render(), reverse.render());
        }
    }
}