Skip to content

Commit

Permalink
Merge pull request #23 from markrwilliams/visualizer
Browse files Browse the repository at this point in the history
Visualizer
  • Loading branch information
glyph committed May 25, 2016
2 parents bf63a63 + 9bd18e9 commit 07ec3f5
Show file tree
Hide file tree
Showing 7 changed files with 272 additions and 65 deletions.
12 changes: 8 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
language: python
python: 2.7
env:
- TOX_ENV=py27
- TOX_ENV=py33
- TOX_ENV=py34
- TOX_ENV=pypy
- TOX_ENV=py27-extras
- TOX_ENV=py27-noextras
- TOX_ENV=pypy-extras
- TOX_ENV=pypy-noextras
- TOX_ENV=py33-extras
- TOX_ENV=py33-noextras
- TOX_ENV=py34-extras
- TOX_ENV=py34-noextras

install:
- sudo apt-get install graphviz
Expand Down
27 changes: 14 additions & 13 deletions automat/_methodical.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,19 +279,20 @@ def unserialize(oself, *args, **kwargs):
return decorator


def graphviz(self):
def asDigraph(self):
"""
Visualize this state machine using graphviz.
Generate a L{graphviz.Digraph} that represents this machine's
states and transitions.
@return: L{graphviz.Digraph} object; for more information, please
see the documentation for
U{graphviz<https://graphviz.readthedocs.io/>}
@return: an iterable of lines of graphviz-format data suitable for
feeding to C{dot} or C{neato} which visualizes the state machine
described by this L{MethodicalMachine}.
"""
from ._visualize import graphviz
for line in graphviz(
self._automaton,
stateAsString=lambda state: state.method.__name__,
inputAsString=lambda input: input.method.__name__,
outputAsString=lambda output: output.method.__name__,
):
yield line
from ._visualize import makeDigraph
return makeDigraph(
self._automaton,
stateAsString=lambda state: state.method.__name__,
inputAsString=lambda input: input.method.__name__,
outputAsString=lambda output: output.method.__name__,
)
2 changes: 1 addition & 1 deletion automat/_test/test_methodical.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ def secondInitialState(self):
def test_badTransitionForCurrentState(self):
"""
Calling any input method that lacks a transition for the machine's
current state raises an informative C{NoTransition}.
current state raises an informative L{NoTransition}.
"""

class OnlyOnePath(object):
Expand Down
176 changes: 170 additions & 6 deletions automat/_test/test_visualize.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,31 @@

from __future__ import unicode_literals
import functools

import os
import subprocess
from unittest import TestCase, skipIf

from characteristic import attributes

from .._methodical import MethodicalMachine


def isGraphvizModuleInstalled():
"""
Is the graphviz Python module installed?
"""
try:
__import__("graphviz")
except ImportError:
return False
else:
return True


def isGraphvizInstalled():
"""
Is graphviz installed?
Are the graphviz tools installed?
"""

r, w = os.pipe()
os.close(w)
try:
Expand Down Expand Up @@ -44,8 +59,156 @@ def out(self):
return mm


@skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.")
class ElementMakerTests(TestCase):
"""
L{elementMaker} generates HTML representing the specified element.
"""

def setUp(self):
from .._visualize import elementMaker
self.elementMaker = elementMaker

def test_sortsAttrs(self):
"""
L{elementMaker} orders HTML attributes lexicographically.
"""
expected = r'<div a="1" b="2" c="3"></div>'
self.assertEqual(expected,
self.elementMaker("div",
b='2',
a='1',
c='3'))

def test_quotesAttrs(self):
"""
L{elementMaker} quotes HTML attributes according to DOT's quoting rule.
See U{http://www.graphviz.org/doc/info/lang.html}, footnote 1.
"""
expected = r'<div a="1" b="a \" quote" c="a string"></div>'
self.assertEqual(expected,
self.elementMaker("div",
b='a " quote',
a=1,
c="a string"))

def test_noAttrs(self):
"""
L{elementMaker} should render an element with no attributes.
"""
expected = r'<div ></div>'
self.assertEqual(expected, self.elementMaker("div"))


@attributes(['name', 'children', 'attrs'])
class HTMLElement(object):
"""Holds an HTML element, as created by elementMaker."""


def findElements(element, predicate):
"""
Recursively collect all elements in an L{HTMLElement} tree that
match the optional predicate.
"""
if predicate(element):
return [element]
elif isLeaf(element):
return []

return [result
for child in element.children
for result in findElements(child, predicate)]


def isLeaf(element):
"""
This HTML element is actually leaf node.
"""
return not isinstance(element, HTMLElement)


@skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.")
class TableMakerTests(TestCase):
"""
Tests that ensure L{tableMaker} generates HTML tables usable as
labels in DOT graphs.
For more information, read the "HTML-Like Labels" section of
U{http://www.graphviz.org/doc/info/shapes.html}.
"""

def fakeElementMaker(self, name, *children, **attrs):
return HTMLElement(name=name, children=children, attrs=attrs)

def setUp(self):
from .._visualize import tableMaker

self.inputLabel = "input label"
self.port = "the port"
self.tableMaker = functools.partial(tableMaker,
_E=self.fakeElementMaker)

def test_inputLabelRow(self):
"""
The table returned by L{tableMaker} always contains the input
symbol label in its first row, and that row contains one cell
with a port attribute set to the provided port.
"""

def hasPort(element):
return (not isLeaf(element)
and element.attrs.get("port") == self.port)

for outputLabels in ([], ["an output label"]):
table = self.tableMaker(self.inputLabel, outputLabels,
port=self.port)
self.assertGreater(len(table.children), 0)
inputLabelRow = table.children[0]

portCandidates = findElements(table, hasPort)

self.assertEqual(len(portCandidates), 1)
self.assertEqual(portCandidates[0].name, "td")
self.assertEqual(findElements(inputLabelRow, isLeaf),
[self.inputLabel])

def test_noOutputLabels(self):
"""
L{tableMaker} does not add a colspan attribute to the input
label's cell or a second row if there no output labels.
"""
table = self.tableMaker("input label", (), port=self.port)
self.assertEqual(len(table.children), 1)
(inputLabelRow,) = table.children
self.assertNotIn("colspan", inputLabelRow.attrs)

def test_withOutputLabels(self):
"""
L{tableMaker} adds a colspan attribute to the input label's cell
equal to the number of output labels and a second row that
contains the output labels.
"""
table = self.tableMaker(self.inputLabel, ("output label 1",
"output label 2"),
port=self.port)

self.assertEqual(len(table.children), 2)
inputRow, outputRow = table.children

def hasCorrectColspan(element):
return (not isLeaf(element)
and element.name == "td"
and element.attrs.get('colspan') == "2")

self.assertEqual(len(findElements(inputRow, hasCorrectColspan)),
1)
self.assertEqual(findElements(outputRow, isLeaf), ["output label 1",
"output label 2"])


@skipIf(not isGraphvizInstalled(), "Graphviz is not installed.")
@skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.")
@skipIf(not isGraphvizInstalled(), "Graphviz tools are not installed.")
class IntegrationTests(TestCase):
"""
Tests which make sure Graphviz can understand the output produced by
Expand All @@ -58,11 +221,12 @@ def test_validGraphviz(self):
"""
p = subprocess.Popen("dot", stdin=subprocess.PIPE,
stdout=subprocess.PIPE)
out, err = p.communicate("".join(sampleMachine().graphviz())
out, err = p.communicate("".join(sampleMachine().asDigraph())
.encode("utf-8"))
self.assertEqual(p.returncode, 0)


@skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.")
class SpotChecks(TestCase):
"""
Tests to make sure that the output contains salient features of the machine
Expand All @@ -74,7 +238,7 @@ def test_containsMachineFeatures(self):
The output of L{graphviz} should contain the names of the states,
inputs, outputs in the state machine.
"""
gvout = "".join(sampleMachine().graphviz())
gvout = "".join(sampleMachine().asDigraph())
self.assertIn("begin", gvout)
self.assertIn("end", gvout)
self.assertIn("go", gvout)
Expand Down
Loading

0 comments on commit 07ec3f5

Please sign in to comment.