123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430 |
- from __future__ import print_function
- import functools
-
- import os
- import subprocess
- from unittest import TestCase, skipIf
-
- import attr
-
- from .._methodical import MethodicalMachine
-
- from .test_discover import isTwistedInstalled
-
-
- def isGraphvizModuleInstalled():
- """
- Is the graphviz Python module installed?
- """
- try:
- __import__("graphviz")
- except ImportError:
- return False
- else:
- return True
-
-
- def isGraphvizInstalled():
- """
- Are the graphviz tools installed?
- """
- r, w = os.pipe()
- os.close(w)
- try:
- return not subprocess.call("dot", stdin=r, shell=True)
- finally:
- os.close(r)
-
-
-
- def sampleMachine():
- """
- Create a sample L{MethodicalMachine} with some sample states.
- """
- mm = MethodicalMachine()
- class SampleObject(object):
- @mm.state(initial=True)
- def begin(self):
- "initial state"
- @mm.state()
- def end(self):
- "end state"
- @mm.input()
- def go(self):
- "sample input"
- @mm.output()
- def out(self):
- "sample output"
- begin.upon(go, end, [out])
- so = SampleObject()
- so.go()
- 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"))
-
-
- @attr.s
- class HTMLElement(object):
- """Holds an HTML element, as created by elementMaker."""
- name = attr.ib()
- children = attr.ib()
- attributes = attr.ib()
-
-
- 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, **attributes):
- return HTMLElement(name=name, children=children, attributes=attributes)
-
- 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.attributes.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.attributes)
-
- 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.attributes.get('colspan') == "2")
-
- self.assertEqual(len(findElements(inputRow, hasCorrectColspan)),
- 1)
- self.assertEqual(findElements(outputRow, isLeaf), ["output label 1",
- "output label 2"])
-
-
- @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
- Automat.
- """
-
- def test_validGraphviz(self):
- """
- L{graphviz} emits valid graphviz data.
- """
- p = subprocess.Popen("dot", stdin=subprocess.PIPE,
- stdout=subprocess.PIPE)
- 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
- being generated.
- """
-
- 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().asDigraph())
- self.assertIn("begin", gvout)
- self.assertIn("end", gvout)
- self.assertIn("go", gvout)
- self.assertIn("out", gvout)
-
-
- class RecordsDigraphActions(object):
- """
- Records calls made to L{FakeDigraph}.
- """
-
- def __init__(self):
- self.reset()
-
- def reset(self):
- self.renderCalls = []
- self.saveCalls = []
-
-
- class FakeDigraph(object):
- """
- A fake L{graphviz.Digraph}. Instantiate it with a
- L{RecordsDigraphActions}.
- """
-
- def __init__(self, recorder):
- self._recorder = recorder
-
- def render(self, **kwargs):
- self._recorder.renderCalls.append(kwargs)
-
- def save(self, **kwargs):
- self._recorder.saveCalls.append(kwargs)
-
-
- class FakeMethodicalMachine(object):
- """
- A fake L{MethodicalMachine}. Instantiate it with a L{FakeDigraph}
- """
-
- def __init__(self, digraph):
- self._digraph = digraph
-
- def asDigraph(self):
- return self._digraph
-
-
- @skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.")
- @skipIf(not isGraphvizInstalled(), "Graphviz tools are not installed.")
- @skipIf(not isTwistedInstalled(), "Twisted is not installed.")
- class VisualizeToolTests(TestCase):
-
- def setUp(self):
- self.digraphRecorder = RecordsDigraphActions()
- self.fakeDigraph = FakeDigraph(self.digraphRecorder)
-
- self.fakeProgname = 'tool-test'
- self.fakeSysPath = ['ignored']
- self.collectedOutput = []
- self.fakeFQPN = 'fake.fqpn'
-
- def collectPrints(self, *args):
- self.collectedOutput.append(' '.join(args))
-
- def fakeFindMachines(self, fqpn):
- yield fqpn, FakeMethodicalMachine(self.fakeDigraph)
-
- def tool(self,
- progname=None,
- argv=None,
- syspath=None,
- findMachines=None,
- print=None):
- from .._visualize import tool
- return tool(
- _progname=progname or self.fakeProgname,
- _argv=argv or [self.fakeFQPN],
- _syspath=syspath or self.fakeSysPath,
- _findMachines=findMachines or self.fakeFindMachines,
- _print=print or self.collectPrints)
-
- def test_checksCurrentDirectory(self):
- """
- L{tool} adds '' to sys.path to ensure
- L{automat._discover.findMachines} searches the current
- directory.
- """
- self.tool(argv=[self.fakeFQPN])
- self.assertEqual(self.fakeSysPath[0], '')
-
- def test_quietHidesOutput(self):
- """
- Passing -q/--quiet hides all output.
- """
- self.tool(argv=[self.fakeFQPN, '--quiet'])
- self.assertFalse(self.collectedOutput)
- self.tool(argv=[self.fakeFQPN, '-q'])
- self.assertFalse(self.collectedOutput)
-
- def test_onlySaveDot(self):
- """
- Passing an empty string for --image-directory/-i disables
- rendering images.
- """
- for arg in ('--image-directory', '-i'):
- self.digraphRecorder.reset()
- self.collectedOutput = []
-
- self.tool(argv=[self.fakeFQPN, arg, ''])
- self.assertFalse(any("image" in line
- for line in self.collectedOutput))
-
- self.assertEqual(len(self.digraphRecorder.saveCalls), 1)
- (call,) = self.digraphRecorder.saveCalls
- self.assertEqual("{}.dot".format(self.fakeFQPN),
- call['filename'])
-
- self.assertFalse(self.digraphRecorder.renderCalls)
-
- def test_saveOnlyImage(self):
- """
- Passing an empty string for --dot-directory/-d disables saving dot
- files.
- """
- for arg in ('--dot-directory', '-d'):
- self.digraphRecorder.reset()
- self.collectedOutput = []
- self.tool(argv=[self.fakeFQPN, arg, ''])
-
- self.assertFalse(any("dot" in line
- for line in self.collectedOutput))
-
- self.assertEqual(len(self.digraphRecorder.renderCalls), 1)
- (call,) = self.digraphRecorder.renderCalls
- self.assertEqual("{}.dot".format(self.fakeFQPN),
- call['filename'])
- self.assertTrue(call['cleanup'])
-
- self.assertFalse(self.digraphRecorder.saveCalls)
-
- def test_saveDotAndImagesInDifferentDirectories(self):
- """
- Passing different directories to --image-directory and --dot-directory
- writes images and dot files to those directories.
- """
- imageDirectory = 'image'
- dotDirectory = 'dot'
- self.tool(argv=[self.fakeFQPN,
- '--image-directory', imageDirectory,
- '--dot-directory', dotDirectory])
-
- self.assertTrue(any("image" in line
- for line in self.collectedOutput))
- self.assertTrue(any("dot" in line
- for line in self.collectedOutput))
-
- self.assertEqual(len(self.digraphRecorder.renderCalls), 1)
- (renderCall,) = self.digraphRecorder.renderCalls
- self.assertEqual(renderCall["directory"], imageDirectory)
- self.assertTrue(renderCall['cleanup'])
-
- self.assertEqual(len(self.digraphRecorder.saveCalls), 1)
- (saveCall,) = self.digraphRecorder.saveCalls
- self.assertEqual(saveCall["directory"], dotDirectory)
-
- def test_saveDotAndImagesInSameDirectory(self):
- """
- Passing the same directory to --image-directory and --dot-directory
- writes images and dot files to that one directory.
- """
- directory = 'imagesAndDot'
- self.tool(argv=[self.fakeFQPN,
- '--image-directory', directory,
- '--dot-directory', directory])
-
- self.assertTrue(any("image and dot" in line
- for line in self.collectedOutput))
-
- self.assertEqual(len(self.digraphRecorder.renderCalls), 1)
- (renderCall,) = self.digraphRecorder.renderCalls
- self.assertEqual(renderCall["directory"], directory)
- self.assertFalse(renderCall['cleanup'])
-
- self.assertFalse(len(self.digraphRecorder.saveCalls))
|