You've already forked RecordFlux
mirror of
https://github.com/AdaCore/RecordFlux.git
synced 2026-02-12 13:01:56 -08:00
293 lines
12 KiB
Python
293 lines
12 KiB
Python
from pathlib import Path
|
||
|
||
import pytest
|
||
from pydotplus import Dot, InvocationException # type: ignore[attr-defined]
|
||
|
||
from rflx.expr import FALSE, TRUE, Equal, Greater, Less, Number, Variable
|
||
from rflx.graph import create_message_graph, create_state_machine_graph, write_graph
|
||
from rflx.identifier import ID
|
||
from rflx.model import (
|
||
BOOLEAN,
|
||
FINAL,
|
||
INITIAL,
|
||
Field,
|
||
Link,
|
||
Message,
|
||
State,
|
||
StateMachine,
|
||
Transition,
|
||
UnsignedInteger,
|
||
declaration as decl,
|
||
statement as stmt,
|
||
)
|
||
from rflx.rapidflux import Location, RecordFluxError
|
||
from tests.data import models
|
||
|
||
|
||
def assert_graph(graph: Dot, expected: str, tmp_path: Path) -> None:
|
||
path = tmp_path / Path("test.dot")
|
||
write_graph(graph, path, fmt="raw")
|
||
assert path.read_text().split() == expected.split()
|
||
|
||
|
||
def test_graph_object() -> None:
|
||
f_type = UnsignedInteger("P::T", Number(32))
|
||
m = Message(
|
||
ID("P::M", Location((1, 1))),
|
||
structure=[
|
||
Link(INITIAL, Field("X"), location=Location((2, 2))),
|
||
Link(Field("X"), FINAL, location=Location((3, 3))),
|
||
],
|
||
types={Field(ID("X", location=Location((4, 4)))): f_type},
|
||
location=Location((1, 1), end=(1, 2)),
|
||
)
|
||
g = create_message_graph(m)
|
||
assert [(e.get_source(), e.get_destination()) for e in g.get_edges()] == [
|
||
("Initial", "intermediate_0"),
|
||
("intermediate_0", "X"),
|
||
("X", "intermediate_1"),
|
||
("intermediate_1", "Final"),
|
||
]
|
||
assert [n.get_name() for n in g.get_nodes()] == [
|
||
"graph",
|
||
"edge",
|
||
"node",
|
||
"Initial",
|
||
"X",
|
||
"intermediate_0",
|
||
"intermediate_1",
|
||
"Final",
|
||
]
|
||
|
||
|
||
def test_empty_message_graph(tmp_path: Path) -> None:
|
||
m = Message(ID("P::M", Location((1, 1))), [], {}, location=Location((1, 1), end=(1, 2)))
|
||
expected = """
|
||
digraph "P::M" {
|
||
graph [bgcolor="#00000000", pad="0.1", ranksep="0.1 equally", splines=true,
|
||
truecolor=true];
|
||
edge [color="#6f6f6f", fontcolor="#6f6f6f", fontname="Fira Code", penwidth="2.5"];
|
||
node [color="#6f6f6f", fillcolor="#009641", fontcolor="#ffffff", fontname=Arimo,
|
||
shape=box, style="rounded,filled", width="1.5"];
|
||
Initial [fillcolor="#ffffff", label="", shape=circle, width="0.5"];
|
||
intermediate_0 [color="#6f6f6f", fontcolor="#6f6f6f", fontname="Fira Code", height=0,
|
||
label="(⊤, 0, ⋆)", penwidth=0, style="", width=0];
|
||
Initial -> intermediate_0 [arrowhead=none];
|
||
intermediate_0 -> Final [minlen=1];
|
||
Final [fillcolor="#6f6f6f", label="", shape=circle, width="0.5"];
|
||
}
|
||
"""
|
||
|
||
assert_graph(create_message_graph(m), expected, tmp_path)
|
||
|
||
|
||
def test_dot_graph(tmp_path: Path) -> None:
|
||
f_type = UnsignedInteger("P::T", Number(32))
|
||
m = Message(
|
||
ID("P::M", Location((1, 1))),
|
||
structure=[
|
||
Link(INITIAL, Field("X"), location=Location((1, 1))),
|
||
Link(Field("X"), FINAL, location=Location((2, 2))),
|
||
],
|
||
types={Field(ID("X", location=Location((1, 1)))): f_type},
|
||
location=Location((1, 1), end=(1, 2)),
|
||
)
|
||
expected = """
|
||
digraph "P::M" {
|
||
graph [bgcolor="#00000000", pad="0.1", ranksep="0.1 equally", splines=true,
|
||
truecolor=true];
|
||
edge [color="#6f6f6f", fontcolor="#6f6f6f", fontname="Fira Code", penwidth="2.5"];
|
||
node [color="#6f6f6f", fillcolor="#009641", fontcolor="#ffffff", fontname=Arimo,
|
||
shape=box, style="rounded,filled", width="1.5"];
|
||
Initial [fillcolor="#ffffff", label="", shape=circle, width="0.5"];
|
||
X;
|
||
intermediate_0 [color="#6f6f6f", fontcolor="#6f6f6f", fontname="Fira Code", height=0,
|
||
label="(⊤, 32, ⋆)", penwidth=0, style="", width=0];
|
||
Initial -> intermediate_0 [arrowhead=none];
|
||
intermediate_0 -> X [minlen=1];
|
||
intermediate_1 [color="#6f6f6f", fontcolor="#6f6f6f", fontname="Fira Code", height=0,
|
||
label="(⊤, 0, ⋆)", penwidth=0, style="", width=0];
|
||
X -> intermediate_1 [arrowhead=none];
|
||
intermediate_1 -> Final [minlen=1];
|
||
Final [fillcolor="#6f6f6f", label="", shape=circle, width="0.5"];
|
||
}
|
||
"""
|
||
|
||
assert_graph(create_message_graph(m), expected, tmp_path)
|
||
|
||
|
||
def test_dot_graph_with_condition(tmp_path: Path) -> None:
|
||
f_type = UnsignedInteger("P::T", Number(32))
|
||
m = Message(
|
||
ID("P::M", Location((1, 1))),
|
||
structure=[
|
||
Link(INITIAL, Field("X"), location=Location((1, 1))),
|
||
Link(
|
||
Field("X"),
|
||
FINAL,
|
||
Greater(Variable("X"), Number(100), location=Location((2, 1))),
|
||
location=Location((2, 2)),
|
||
),
|
||
],
|
||
types={Field(ID("X", location=Location((1, 1)))): f_type},
|
||
location=Location((1, 1), end=(1, 2)),
|
||
)
|
||
expected = """
|
||
digraph "P::M" {
|
||
graph [bgcolor="#00000000", pad="0.1", ranksep="0.1 equally", splines=true,
|
||
truecolor=true];
|
||
edge [color="#6f6f6f", fontcolor="#6f6f6f", fontname="Fira Code", penwidth="2.5"];
|
||
node [color="#6f6f6f", fillcolor="#009641", fontcolor="#ffffff", fontname=Arimo,
|
||
shape=box, style="rounded,filled", width="1.5"];
|
||
Initial [fillcolor="#ffffff", label="", shape=circle, width="0.5"];
|
||
X;
|
||
intermediate_0 [color="#6f6f6f", fontcolor="#6f6f6f", fontname="Fira Code", height=0,
|
||
label="(⊤, 32, ⋆)", penwidth=0, style="", width=0];
|
||
Initial -> intermediate_0 [arrowhead=none];
|
||
intermediate_0 -> X [minlen=1];
|
||
intermediate_1 [color="#6f6f6f", fontcolor="#6f6f6f", fontname="Fira Code", height=0,
|
||
label="(X > 100, 0, ⋆)", penwidth=0, style="", width=0];
|
||
X -> intermediate_1 [arrowhead=none];
|
||
intermediate_1 -> Final [minlen=1];
|
||
Final [fillcolor="#6f6f6f", label="", shape=circle, width="0.5"];
|
||
}
|
||
"""
|
||
|
||
assert_graph(create_message_graph(m), expected, tmp_path)
|
||
|
||
|
||
def test_dot_graph_with_double_edge(tmp_path: Path) -> None:
|
||
f_type = UnsignedInteger("P::T", Number(32))
|
||
m = Message(
|
||
ID("P::M", Location((1, 1))),
|
||
structure=[
|
||
Link(INITIAL, Field("X"), location=Location((1, 1))),
|
||
Link(
|
||
Field("X"),
|
||
FINAL,
|
||
Greater(Variable("X"), Number(100), location=Location((3, 3))),
|
||
location=Location((2, 2)),
|
||
),
|
||
Link(
|
||
Field("X"),
|
||
FINAL,
|
||
Less(Variable("X"), Number(50), location=Location((4, 4))),
|
||
location=Location((3, 3)),
|
||
),
|
||
],
|
||
types={Field(ID("X", location=Location((1, 1)))): f_type},
|
||
location=Location((1, 1), end=(1, 2)),
|
||
)
|
||
expected = """
|
||
digraph "P::M" {
|
||
graph [bgcolor="#00000000", pad="0.1", ranksep="0.1 equally", splines=true,
|
||
truecolor=true];
|
||
edge [color="#6f6f6f", fontcolor="#6f6f6f", fontname="Fira Code", penwidth="2.5"];
|
||
node [color="#6f6f6f", fillcolor="#009641", fontcolor="#ffffff", fontname=Arimo,
|
||
shape=box, style="rounded,filled", width="1.5"];
|
||
Initial [fillcolor="#ffffff", label="", shape=circle, width="0.5"];
|
||
X;
|
||
intermediate_0 [color="#6f6f6f", fontcolor="#6f6f6f", fontname="Fira Code", height=0,
|
||
label="(⊤, 32, ⋆)", penwidth=0, style="", width=0];
|
||
Initial -> intermediate_0 [arrowhead=none];
|
||
intermediate_0 -> X [minlen=1];
|
||
intermediate_1 [color="#6f6f6f", fontcolor="#6f6f6f", fontname="Fira Code", height=0,
|
||
label="(X < 50, 0, ⋆)", penwidth=0, style="", width=0];
|
||
X -> intermediate_1 [arrowhead=none];
|
||
intermediate_1 -> Final [minlen=1];
|
||
intermediate_2 [color="#6f6f6f", fontcolor="#6f6f6f", fontname="Fira Code", height=0,
|
||
label="(X > 100, 0, ⋆)", penwidth=0, style="", width=0];
|
||
X -> intermediate_2 [arrowhead=none];
|
||
intermediate_2 -> Final [minlen=1];
|
||
Final [fillcolor="#6f6f6f", label="", shape=circle, width="0.5"];
|
||
}
|
||
"""
|
||
|
||
assert_graph(create_message_graph(m), expected, tmp_path)
|
||
|
||
|
||
def test_state_machine_graph(tmp_path: Path) -> None:
|
||
s = StateMachine(
|
||
identifier="P::S",
|
||
states=[
|
||
State(
|
||
"START",
|
||
transitions=[
|
||
Transition(target=ID("STATE"), condition=Equal(Variable("Global"), TRUE)),
|
||
Transition(target=ID("null")),
|
||
],
|
||
exception_transition=Transition(target=ID("null")),
|
||
),
|
||
State(
|
||
"STATE",
|
||
transitions=[Transition(target=ID("IGNORED_1")), Transition(target=ID("null"))],
|
||
actions=[stmt.VariableAssignment("Global", FALSE), stmt.Reset("Local")],
|
||
declarations=[decl.VariableDeclaration("Local", "TLV::Message")],
|
||
),
|
||
State(
|
||
"IGNORED_1",
|
||
transitions=[Transition(target=ID("null"))],
|
||
),
|
||
],
|
||
declarations=[decl.VariableDeclaration("Global", "Boolean")],
|
||
parameters=[],
|
||
types=[BOOLEAN, models.tlv_message()],
|
||
)
|
||
|
||
expected_full = r"""
|
||
digraph "P::S" {
|
||
graph [bgcolor="#00000000", pad="0.1", ranksep="0.1 equally", splines=true,
|
||
truecolor=true];
|
||
edge [color="#6f6f6f", fontcolor="#6f6f6f", fontname="Fira Code", penwidth="2.5"];
|
||
node [color="#6f6f6f", fillcolor="#009641", fontcolor="#ffffff", fontname=Arimo,
|
||
shape=box, style="rounded,filled", width="1.5"];
|
||
START [fillcolor="#ffffff", fontcolor=black];
|
||
START -> STATE [minlen=3, tooltip="START → STATE\n\n[0] Global = True"];
|
||
START -> Final [minlen=3, tooltip=""];
|
||
STATE;
|
||
STATE -> IGNORED_1 [minlen=3, tooltip=""];
|
||
STATE -> Final [minlen=3, tooltip=""];
|
||
IGNORED_1;
|
||
IGNORED_1 -> Final [minlen=3, tooltip=""];
|
||
Final [fillcolor="#6f6f6f", label="", shape=circle, width="0.5"];
|
||
}
|
||
"""
|
||
|
||
assert_graph(create_state_machine_graph(s), expected_full, tmp_path)
|
||
|
||
expected_filtered = r"""
|
||
digraph "P::S" {
|
||
graph [bgcolor="#00000000", pad="0.1", ranksep="0.1 equally", splines=true,
|
||
truecolor=true];
|
||
edge [color="#6f6f6f", fontcolor="#6f6f6f", fontname="Fira Code", penwidth="2.5"];
|
||
node [color="#6f6f6f", fillcolor="#009641", fontcolor="#ffffff", fontname=Arimo,
|
||
shape=box, style="rounded,filled", width="1.5"];
|
||
START [fillcolor="#ffffff", fontcolor=black];
|
||
START -> STATE [minlen=3, tooltip="START → STATE\n\n[0] Global = True"];
|
||
START -> Final [minlen=3, tooltip=""];
|
||
STATE;
|
||
STATE -> Final [minlen=3, tooltip=""];
|
||
Final [fillcolor="#6f6f6f", label="", shape=circle, width="0.5"];
|
||
}
|
||
"""
|
||
|
||
assert_graph(create_state_machine_graph(s, ignore=[r"^IGNORED_"]), expected_filtered, tmp_path)
|
||
|
||
|
||
def test_missing_graphviz(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||
def write_mock(self: object, path: object, format: str = "") -> object: # noqa: ARG001, A002
|
||
raise InvocationException("GraphViz not found")
|
||
|
||
monkeypatch.setattr(Dot, "write", write_mock)
|
||
|
||
with pytest.raises(
|
||
RecordFluxError,
|
||
match=(
|
||
r"^"
|
||
r"error: GraphViz not found\n"
|
||
r"note: GraphViz is required for creating graphs"
|
||
r"$"
|
||
),
|
||
):
|
||
write_graph(Dot(""), tmp_path / "graph")
|