pipecircuit-visualization 0.0.7

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

use expect_test::expect;

use super::*;

fn diagram(name: &str, qualified_name: &str) -> StateMachineDiagram {
    StateMachineDiagram {
        name: name.to_owned(),
        qualified_name: qualified_name.to_owned(),
        steps: vec![StepDiagram {
            input: "Advance".to_owned(),
            outcome: DiagramNode {
                label: "Advanced".to_owned(),
                children: vec![DiagramNode {
                    label: "Done".to_owned(),
                    children: Vec::new(),
                }],
            },
        }],
    }
}

fn fixture() -> Result<CrateArchitecture, ArchitectureError> {
    CrateArchitecture::read(Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/door"))
}

#[test]
fn derives_every_step_and_outcome_from_the_crate() -> Result<(), Box<dyn Error>> {
    let architecture = fixture()?;
    let diagram = architecture.state_machine("Door")?;
    assert_eq!(architecture.state_machines().len(), 1);

    expect![[r#"
        ● state machine  ▸ step input  ◆ outcome

        ● Door
        ├── ▸ CloseDoor
        │   └── ◆ CloseDoorOutcome
        │       ├── Closed
        │       └── AlreadyClosed
        └── ▸ OpenDoor
            └── ◆ Result
                ├── Ok
                │   └── OpenDoorOutcome
                │       ├── Opened
                │       └── AlreadyOpen
                └── Err
                    └── DoorFaultReason
                        ├── Jammed
                        └── Locked
    "#]]
    .assert_eq(&diagram.render(DiagramTarget::TextTree));
    Ok(())
}

#[test]
fn projects_one_graph_to_mermaid_and_graphviz() -> Result<(), Box<dyn Error>> {
    let architecture = fixture()?;
    let diagram = architecture.state_machine_qualified("crate::door::Door")?;

    expect![[r#"
        flowchart LR
            n0["Door"]
            n1["CloseDoor"]
            n2["CloseDoorOutcome"]
            n3["Closed"]
            n4["AlreadyClosed"]
            n5["OpenDoor"]
            n6["Result"]
            n7["Ok"]
            n8["OpenDoorOutcome"]
            n9["Opened"]
            n10["AlreadyOpen"]
            n11["Err"]
            n12["DoorFaultReason"]
            n13["Jammed"]
            n14["Locked"]
            n0 --> n1
            n1 --> n2
            n2 --> n3
            n2 --> n4
            n0 --> n5
            n5 --> n6
            n6 --> n7
            n7 --> n8
            n8 --> n9
            n8 --> n10
            n6 --> n11
            n11 --> n12
            n12 --> n13
            n12 --> n14
    "#]]
    .assert_eq(&diagram.render(DiagramTarget::MermaidFlowchart));
    expect![[r#"
        digraph "Door" {
            rankdir=LR;
            n0 [label="Door"];
            n1 [label="CloseDoor"];
            n2 [label="CloseDoorOutcome"];
            n3 [label="Closed"];
            n4 [label="AlreadyClosed"];
            n5 [label="OpenDoor"];
            n6 [label="Result"];
            n7 [label="Ok"];
            n8 [label="OpenDoorOutcome"];
            n9 [label="Opened"];
            n10 [label="AlreadyOpen"];
            n11 [label="Err"];
            n12 [label="DoorFaultReason"];
            n13 [label="Jammed"];
            n14 [label="Locked"];
            n0 -> n1;
            n1 -> n2;
            n2 -> n3;
            n2 -> n4;
            n0 -> n5;
            n5 -> n6;
            n6 -> n7;
            n7 -> n8;
            n8 -> n9;
            n8 -> n10;
            n6 -> n11;
            n11 -> n12;
            n12 -> n13;
            n12 -> n14;
        }
    "#]]
    .assert_eq(&diagram.render(DiagramTarget::GraphvizDot));
    Ok(())
}

#[test]
fn ansi_text_colors_only_role_markers() {
    let rendering = diagram("Clock", "crate::clock::Clock").render(DiagramTarget::AnsiTextTree);
    assert!(rendering.contains("\u{1b}[38;5;114m●\u{1b}[0m Clock"));
    assert!(rendering.contains("\u{1b}[38;5;114m▸\u{1b}[0m Advance"));
    assert!(rendering.contains("\u{1b}[38;5;114m◆\u{1b}[0m Advanced"));
    assert!(!rendering.contains("\u{1b}[38;5;114mClock"));
    assert!(!rendering.contains("\u{1b}[38;5;114mAdvance"));
    assert!(!rendering.contains("\u{1b}[38;5;114mAdvanced"));
}

#[test]
fn exposes_diagram_structure() {
    let diagram = diagram("Clock", "crate::clock::Clock");
    assert_eq!(diagram.name(), "Clock");
    assert_eq!(diagram.qualified_name(), "crate::clock::Clock");
    assert_eq!(diagram.steps().len(), 1);
    for step in diagram.steps() {
        assert_eq!(step.input(), "Advance");
        assert_eq!(step.outcome().label(), "Advanced");
        assert_eq!(step.outcome().children().len(), 1);
        for child in step.outcome().children() {
            assert_eq!(child.label(), "Done");
        }
    }
}

#[test]
fn state_machine_lookup_preserves_absence_and_ambiguity() {
    let architecture = CrateArchitecture {
        state_machines: vec![
            diagram("Clock", "crate::east::Clock"),
            diagram("Clock", "crate::west::Clock"),
        ],
    };
    assert_eq!(architecture.state_machines().len(), 2);
    assert_eq!(
        architecture
            .state_machine("Missing")
            .map(|_| ())
            .map_err(|error| error.to_string()),
        Err("state machine `Missing` was not found".to_owned())
    );
    assert_eq!(
        architecture
            .state_machine("Clock")
            .map(|_| ())
            .map_err(|error| error.to_string()),
        Err("state machine name `Clock` was ambiguous; use its qualified name".to_owned())
    );
    assert_eq!(
        architecture
            .state_machine_qualified("crate::east::Clock")
            .map(StateMachineDiagram::qualified_name),
        Ok("crate::east::Clock")
    );
    assert_eq!(
        architecture
            .state_machine_qualified("crate::north::Clock")
            .map(|_| ())
            .map_err(|error| error.to_string()),
        Err("state machine `crate::north::Clock` was not found".to_owned())
    );
}