pipecircuit-visualization 0.0.7

Code-derived architectural visualizations for Pipecircuit programs
Documentation
use super::*;

#[test]
fn relocated_modules_preserve_logical_names_and_nested_source_directories()
-> Result<(), Box<dyn Error>> {
    let root = temporary_root("relocated-modules")?;
    fs::create_dir_all(root.join("src/physical"))?;
    fs::write(
        root.join("src/lib.rs"),
        r#"#[allow(dead_code)] #[path = "physical/owner.rs"] mod logical;"#,
    )?;
    fs::write(root.join("src/logical.rs"), "invalid Rust source")?;
    fs::write(
        root.join("src/physical/owner.rs"),
        r#"
        mod inputs;
        #[path = "answer.rs"] mod outputs;
        pub struct Owner;
        trait StateMachine {}
        impl StateMachine for Owner {}
        "#,
    )?;
    fs::write(
        root.join("src/physical/inputs.rs"),
        r#"
        use super::Owner;
        use super::outputs::Outcome;
        trait StepInputOf<T> { type Outcome; }
        pub struct Change;
        impl StepInputOf<Owner> for Change { type Outcome = Outcome; }
        "#,
    )?;
    fs::write(
        root.join("src/physical/answer.rs"),
        "pub enum Outcome { Applied, Refused }",
    )?;
    let architecture = read_crate(&root)?;
    let machine = architecture.state_machine_qualified("crate::logical::Owner")?;
    expect_test::expect![[r#"
        ● state machine  ▸ step input  ◆ outcome

        ● Owner
        └── ▸ Change
            └── ◆ Outcome
                ├── Applied
                └── Refused
    "#]]
    .assert_eq(&machine.render(crate::DiagramTarget::TextTree));
    fs::remove_dir_all(root)?;
    Ok(())
}

#[test]
fn path_attributes_follow_source_files_and_inline_module_directories() -> Result<(), Box<dyn Error>>
{
    let root = temporary_root("inline-module-paths")?;
    for directory in ["outer/nested", "redirected", "inline", "folder"] {
        fs::create_dir_all(root.join("src").join(directory))?;
    }
    for (path, source) in [
        (
            "lib.rs",
            r#"
            mod outer;
            mod inline { #[path = "actual.rs"] mod child; }
            #[path = "folder/mod.rs"] mod grouped;
            "#,
        ),
        (
            "outer.rs",
            r#"
            #[path = "sibling.rs"] mod sibling;
            mod nested { #[path = "actual.rs"] mod child; }
            #[path = "redirected"] mod moved { mod child; }
            "#,
        ),
        ("sibling.rs", "pub struct Sibling;"),
        ("outer/nested/actual.rs", "pub struct Nested;"),
        ("redirected/child.rs", "pub struct Redirected;"),
        ("inline/actual.rs", "pub struct Inline;"),
        ("folder/mod.rs", "mod child;"),
        ("folder/child.rs", "pub struct Grouped;"),
    ] {
        fs::write(root.join("src").join(path), source)?;
    }
    let mut model = SourceModel::new();
    model.read_module_file(&root.join("src/lib.rs"), Vec::new(), &root.join("src"))?;
    assert_eq!(
        model
            .declarations
            .keys()
            .map(|path| qualified(path))
            .collect::<Vec<_>>(),
        [
            "crate::grouped::child::Grouped",
            "crate::inline::child::Inline",
            "crate::outer::moved::child::Redirected",
            "crate::outer::nested::child::Nested",
            "crate::outer::sibling::Sibling",
        ]
    );
    fs::remove_dir_all(root)?;
    Ok(())
}

#[test]
fn invalid_or_missing_explicit_paths_never_fall_back_to_conventional_sources()
-> Result<(), Box<dyn Error>> {
    let root = temporary_root("invalid-module-paths")?;
    fs::write(root.join("src/owner.rs"), "pub struct Owner;")?;
    for attribute in [
        "#[path]",
        "#[path(\"owner.rs\")]",
        "#[path = 3]",
        "#[path = concat!(\"owner\", \".rs\")]",
    ] {
        fs::write(root.join("src/lib.rs"), format!("{attribute} mod owner;"))?;
        assert!(
            error_message(read_crate(&root)).contains("path attribute must be a string literal")
        );
    }
    fs::write(
        root.join("src/lib.rs"),
        r#"#[path = "owner.rs"] #[path = "other.rs"] mod owner;"#,
    )?;
    assert!(error_message(read_crate(&root)).contains("more than one path attribute"));
    fs::write(
        root.join("src/lib.rs"),
        r#"#[path = "missing.rs"] mod owner;"#,
    )?;
    let missing = error_message(read_crate(&root));
    assert!(missing.contains("could not resolve"));
    assert!(missing.contains("missing.rs"));
    fs::remove_dir_all(root)?;
    Ok(())
}

#[test]
fn one_source_can_define_distinct_modules_but_recursive_inclusion_is_refused()
-> Result<(), Box<dyn Error>> {
    let root = temporary_root("shared-module-source")?;
    fs::write(
        root.join("src/lib.rs"),
        r#"#[path = "shared.rs"] mod first; #[path = "shared.rs"] mod second;"#,
    )?;
    fs::write(root.join("src/shared.rs"), "pub struct Value;")?;
    let mut model = SourceModel::new();
    model.read_module_file(&root.join("src/lib.rs"), Vec::new(), &root.join("src"))?;
    assert_eq!(
        model
            .declarations
            .keys()
            .map(|path| qualified(path))
            .collect::<Vec<_>>(),
        ["crate::first::Value", "crate::second::Value"]
    );
    fs::write(
        root.join("src/shared.rs"),
        r#"#[path = "shared.rs"] mod again;"#,
    )?;
    assert!(error_message(read_crate(&root)).contains("recursive module source"));
    fs::remove_dir_all(root)?;
    Ok(())
}