Ausgabe der neuen DB Einträge

This commit is contained in:
hubobel 2022-01-02 21:50:48 +01:00
parent bad48e1627
commit cfbbb9ee3d
2399 changed files with 843193 additions and 43 deletions

View file

@ -0,0 +1,86 @@
from .._core import Automaton, NoTransition
from unittest import TestCase
class CoreTests(TestCase):
"""
Tests for Automat's (currently private, implementation detail) core.
"""
def test_NoTransition(self):
"""
A L{NoTransition} exception describes the state and input symbol
that caused it.
"""
# NoTransition requires two arguments
with self.assertRaises(TypeError):
NoTransition()
state = "current-state"
symbol = "transitionless-symbol"
noTransitionException = NoTransition(state=state, symbol=symbol)
self.assertIs(noTransitionException.symbol, symbol)
self.assertIn(state, str(noTransitionException))
self.assertIn(symbol, str(noTransitionException))
def test_noOutputForInput(self):
"""
L{Automaton.outputForInput} raises L{NoTransition} if no
transition for that input is defined.
"""
a = Automaton()
self.assertRaises(NoTransition, a.outputForInput,
"no-state", "no-symbol")
def test_oneTransition(self):
"""
L{Automaton.addTransition} adds its input symbol to
L{Automaton.inputAlphabet}, all its outputs to
L{Automaton.outputAlphabet}, and causes L{Automaton.outputForInput} to
start returning the new state and output symbols.
"""
a = Automaton()
a.addTransition("beginning", "begin", "ending", ["end"])
self.assertEqual(a.inputAlphabet(), {"begin"})
self.assertEqual(a.outputAlphabet(), {"end"})
self.assertEqual(a.outputForInput("beginning", "begin"),
("ending", ["end"]))
self.assertEqual(a.states(), {"beginning", "ending"})
def test_oneTransition_nonIterableOutputs(self):
"""
L{Automaton.addTransition} raises a TypeError when given outputs
that aren't iterable and doesn't add any transitions.
"""
a = Automaton()
nonIterableOutputs = 1
self.assertRaises(
TypeError,
a.addTransition,
"fromState", "viaSymbol", "toState", nonIterableOutputs)
self.assertFalse(a.inputAlphabet())
self.assertFalse(a.outputAlphabet())
self.assertFalse(a.states())
self.assertFalse(a.allTransitions())
def test_initialState(self):
"""
L{Automaton.initialState} is a descriptor that sets the initial
state if it's not yet set, and raises L{ValueError} if it is.
"""
a = Automaton()
a.initialState = "a state"
self.assertEqual(a.initialState, "a state")
with self.assertRaises(ValueError):
a.initialState = "another state"
# FIXME: addTransition for transition that's been added before

View file

@ -0,0 +1,609 @@
import operator
import os
import shutil
import sys
import textwrap
import tempfile
from unittest import skipIf, TestCase
import six
def isTwistedInstalled():
try:
__import__('twisted')
except ImportError:
return False
else:
return True
class _WritesPythonModules(TestCase):
"""
A helper that enables generating Python module test fixtures.
"""
def setUp(self):
super(_WritesPythonModules, self).setUp()
from twisted.python.modules import getModule, PythonPath
from twisted.python.filepath import FilePath
self.getModule = getModule
self.PythonPath = PythonPath
self.FilePath = FilePath
self.originalSysModules = set(sys.modules.keys())
self.savedSysPath = sys.path[:]
self.pathDir = tempfile.mkdtemp()
self.makeImportable(self.pathDir)
def tearDown(self):
super(_WritesPythonModules, self).tearDown()
sys.path[:] = self.savedSysPath
modulesToDelete = six.viewkeys(sys.modules) - self.originalSysModules
for module in modulesToDelete:
del sys.modules[module]
shutil.rmtree(self.pathDir)
def makeImportable(self, path):
sys.path.append(path)
def writeSourceInto(self, source, path, moduleName):
directory = self.FilePath(path)
module = directory.child(moduleName)
# FilePath always opens a file in binary mode - but that will
# break on Python 3
with open(module.path, 'w') as f:
f.write(textwrap.dedent(source))
return self.PythonPath([directory.path])
def makeModule(self, source, path, moduleName):
pythonModuleName, _ = os.path.splitext(moduleName)
return self.writeSourceInto(source, path, moduleName)[pythonModuleName]
def attributesAsDict(self, hasIterAttributes):
return {attr.name: attr for attr in hasIterAttributes.iterAttributes()}
def loadModuleAsDict(self, module):
module.load()
return self.attributesAsDict(module)
def makeModuleAsDict(self, source, path, name):
return self.loadModuleAsDict(self.makeModule(source, path, name))
@skipIf(not isTwistedInstalled(), "Twisted is not installed.")
class OriginalLocationTests(_WritesPythonModules):
"""
Tests that L{isOriginalLocation} detects when a
L{PythonAttribute}'s FQPN refers to an object inside the module
where it was defined.
For example: A L{twisted.python.modules.PythonAttribute} with a
name of 'foo.bar' that refers to a 'bar' object defined in module
'baz' does *not* refer to bar's original location, while a
L{PythonAttribute} with a name of 'baz.bar' does.
"""
def setUp(self):
super(OriginalLocationTests, self).setUp()
from .._discover import isOriginalLocation
self.isOriginalLocation = isOriginalLocation
def test_failsWithNoModule(self):
"""
L{isOriginalLocation} returns False when the attribute refers to an
object whose source module cannot be determined.
"""
source = """\
class Fake(object):
pass
hasEmptyModule = Fake()
hasEmptyModule.__module__ = None
"""
moduleDict = self.makeModuleAsDict(source,
self.pathDir,
'empty_module_attr.py')
self.assertFalse(self.isOriginalLocation(
moduleDict['empty_module_attr.hasEmptyModule']))
def test_failsWithDifferentModule(self):
"""
L{isOriginalLocation} returns False when the attribute refers to
an object outside of the module where that object was defined.
"""
originalSource = """\
class ImportThisClass(object):
pass
importThisObject = ImportThisClass()
importThisNestingObject = ImportThisClass()
importThisNestingObject.nestedObject = ImportThisClass()
"""
importingSource = """\
from original import (ImportThisClass,
importThisObject,
importThisNestingObject)
"""
self.makeModule(originalSource, self.pathDir, 'original.py')
importingDict = self.makeModuleAsDict(importingSource,
self.pathDir,
'importing.py')
self.assertFalse(
self.isOriginalLocation(
importingDict['importing.ImportThisClass']))
self.assertFalse(
self.isOriginalLocation(
importingDict['importing.importThisObject']))
nestingObject = importingDict['importing.importThisNestingObject']
nestingObjectDict = self.attributesAsDict(nestingObject)
nestedObject = nestingObjectDict[
'importing.importThisNestingObject.nestedObject']
self.assertFalse(self.isOriginalLocation(nestedObject))
def test_succeedsWithSameModule(self):
"""
L{isOriginalLocation} returns True when the attribute refers to an
object inside the module where that object was defined.
"""
mSource = textwrap.dedent("""
class ThisClassWasDefinedHere(object):
pass
anObject = ThisClassWasDefinedHere()
aNestingObject = ThisClassWasDefinedHere()
aNestingObject.nestedObject = ThisClassWasDefinedHere()
""")
mDict = self.makeModuleAsDict(mSource, self.pathDir, 'm.py')
self.assertTrue(self.isOriginalLocation(
mDict['m.ThisClassWasDefinedHere']))
self.assertTrue(self.isOriginalLocation(mDict['m.aNestingObject']))
nestingObject = mDict['m.aNestingObject']
nestingObjectDict = self.attributesAsDict(nestingObject)
nestedObject = nestingObjectDict['m.aNestingObject.nestedObject']
self.assertTrue(self.isOriginalLocation(nestedObject))
@skipIf(not isTwistedInstalled(), "Twisted is not installed.")
class FindMachinesViaWrapperTests(_WritesPythonModules):
"""
L{findMachinesViaWrapper} recursively yields FQPN,
L{MethodicalMachine} pairs in and under a given
L{twisted.python.modules.PythonModule} or
L{twisted.python.modules.PythonAttribute}.
"""
TEST_MODULE_SOURCE = """
from automat import MethodicalMachine
class PythonClass(object):
_classMachine = MethodicalMachine()
class NestedClass(object):
_nestedClassMachine = MethodicalMachine()
ignoredAttribute = "I am ignored."
def ignoredMethod(self):
"I am also ignored."
rootLevelMachine = MethodicalMachine()
ignoredPythonObject = PythonClass()
anotherIgnoredPythonObject = "I am ignored."
"""
def setUp(self):
super(FindMachinesViaWrapperTests, self).setUp()
from .._discover import findMachinesViaWrapper
self.findMachinesViaWrapper = findMachinesViaWrapper
def test_yieldsMachine(self):
"""
When given a L{twisted.python.modules.PythonAttribute} that refers
directly to a L{MethodicalMachine}, L{findMachinesViaWrapper}
yields that machine and its FQPN.
"""
source = """\
from automat import MethodicalMachine
rootMachine = MethodicalMachine()
"""
moduleDict = self.makeModuleAsDict(source, self.pathDir, 'root.py')
rootMachine = moduleDict['root.rootMachine']
self.assertIn(('root.rootMachine', rootMachine.load()),
list(self.findMachinesViaWrapper(rootMachine)))
def test_yieldsMachineInClass(self):
"""
When given a L{twisted.python.modules.PythonAttribute} that refers
to a class that contains a L{MethodicalMachine} as a class
variable, L{findMachinesViaWrapper} yields that machine and
its FQPN.
"""
source = """\
from automat import MethodicalMachine
class PythonClass(object):
_classMachine = MethodicalMachine()
"""
moduleDict = self.makeModuleAsDict(source, self.pathDir, 'clsmod.py')
PythonClass = moduleDict['clsmod.PythonClass']
self.assertIn(('clsmod.PythonClass._classMachine',
PythonClass.load()._classMachine),
list(self.findMachinesViaWrapper(PythonClass)))
def test_yieldsMachineInNestedClass(self):
"""
When given a L{twisted.python.modules.PythonAttribute} that refers
to a nested class that contains a L{MethodicalMachine} as a
class variable, L{findMachinesViaWrapper} yields that machine
and its FQPN.
"""
source = """\
from automat import MethodicalMachine
class PythonClass(object):
class NestedClass(object):
_classMachine = MethodicalMachine()
"""
moduleDict = self.makeModuleAsDict(source,
self.pathDir,
'nestedcls.py')
PythonClass = moduleDict['nestedcls.PythonClass']
self.assertIn(('nestedcls.PythonClass.NestedClass._classMachine',
PythonClass.load().NestedClass._classMachine),
list(self.findMachinesViaWrapper(PythonClass)))
def test_yieldsMachineInModule(self):
"""
When given a L{twisted.python.modules.PythonModule} that refers to
a module that contains a L{MethodicalMachine},
L{findMachinesViaWrapper} yields that machine and its FQPN.
"""
source = """\
from automat import MethodicalMachine
rootMachine = MethodicalMachine()
"""
module = self.makeModule(source, self.pathDir, 'root.py')
rootMachine = self.loadModuleAsDict(module)['root.rootMachine'].load()
self.assertIn(('root.rootMachine', rootMachine),
list(self.findMachinesViaWrapper(module)))
def test_yieldsMachineInClassInModule(self):
"""
When given a L{twisted.python.modules.PythonModule} that refers to
the original module of a class containing a
L{MethodicalMachine}, L{findMachinesViaWrapper} yields that
machine and its FQPN.
"""
source = """\
from automat import MethodicalMachine
class PythonClass(object):
_classMachine = MethodicalMachine()
"""
module = self.makeModule(source, self.pathDir, 'clsmod.py')
PythonClass = self.loadModuleAsDict(
module)['clsmod.PythonClass'].load()
self.assertIn(('clsmod.PythonClass._classMachine',
PythonClass._classMachine),
list(self.findMachinesViaWrapper(module)))
def test_yieldsMachineInNestedClassInModule(self):
"""
When given a L{twisted.python.modules.PythonModule} that refers to
the original module of a nested class containing a
L{MethodicalMachine}, L{findMachinesViaWrapper} yields that
machine and its FQPN.
"""
source = """\
from automat import MethodicalMachine
class PythonClass(object):
class NestedClass(object):
_classMachine = MethodicalMachine()
"""
module = self.makeModule(source, self.pathDir, 'nestedcls.py')
PythonClass = self.loadModuleAsDict(
module)['nestedcls.PythonClass'].load()
self.assertIn(('nestedcls.PythonClass.NestedClass._classMachine',
PythonClass.NestedClass._classMachine),
list(self.findMachinesViaWrapper(module)))
def test_ignoresImportedClass(self):
"""
When given a L{twisted.python.modules.PythonAttribute} that refers
to a class imported from another module, any
L{MethodicalMachine}s on that class are ignored.
This behavior ensures that a machine is only discovered on a
class when visiting the module where that class was defined.
"""
originalSource = """
from automat import MethodicalMachine
class PythonClass(object):
_classMachine = MethodicalMachine()
"""
importingSource = """
from original import PythonClass
"""
self.makeModule(originalSource, self.pathDir, 'original.py')
importingModule = self.makeModule(importingSource,
self.pathDir,
'importing.py')
self.assertFalse(list(self.findMachinesViaWrapper(importingModule)))
def test_descendsIntoPackages(self):
"""
L{findMachinesViaWrapper} descends into packages to discover
machines.
"""
pythonPath = self.PythonPath([self.pathDir])
package = self.FilePath(self.pathDir).child("test_package")
package.makedirs()
package.child('__init__.py').touch()
source = """
from automat import MethodicalMachine
class PythonClass(object):
_classMachine = MethodicalMachine()
rootMachine = MethodicalMachine()
"""
self.makeModule(source, package.path, 'module.py')
test_package = pythonPath['test_package']
machines = sorted(self.findMachinesViaWrapper(test_package),
key=operator.itemgetter(0))
moduleDict = self.loadModuleAsDict(test_package['module'])
rootMachine = moduleDict['test_package.module.rootMachine'].load()
PythonClass = moduleDict['test_package.module.PythonClass'].load()
expectedMachines = sorted(
[('test_package.module.rootMachine',
rootMachine),
('test_package.module.PythonClass._classMachine',
PythonClass._classMachine)], key=operator.itemgetter(0))
self.assertEqual(expectedMachines, machines)
def test_infiniteLoop(self):
"""
L{findMachinesViaWrapper} ignores infinite loops.
Note this test can't fail - it can only run forever!
"""
source = """
class InfiniteLoop(object):
pass
InfiniteLoop.loop = InfiniteLoop
"""
module = self.makeModule(source, self.pathDir, 'loop.py')
self.assertFalse(list(self.findMachinesViaWrapper(module)))
@skipIf(not isTwistedInstalled(), "Twisted is not installed.")
class WrapFQPNTests(TestCase):
"""
Tests that ensure L{wrapFQPN} loads the
L{twisted.python.modules.PythonModule} or
L{twisted.python.modules.PythonAttribute} for a given FQPN.
"""
def setUp(self):
from twisted.python.modules import PythonModule, PythonAttribute
from .._discover import wrapFQPN, InvalidFQPN, NoModule, NoObject
self.PythonModule = PythonModule
self.PythonAttribute = PythonAttribute
self.wrapFQPN = wrapFQPN
self.InvalidFQPN = InvalidFQPN
self.NoModule = NoModule
self.NoObject = NoObject
def assertModuleWrapperRefersTo(self, moduleWrapper, module):
"""
Assert that a L{twisted.python.modules.PythonModule} refers to a
particular Python module.
"""
self.assertIsInstance(moduleWrapper, self.PythonModule)
self.assertEqual(moduleWrapper.name, module.__name__)
self.assertIs(moduleWrapper.load(), module)
def assertAttributeWrapperRefersTo(self, attributeWrapper, fqpn, obj):
"""
Assert that a L{twisted.python.modules.PythonAttribute} refers to a
particular Python object.
"""
self.assertIsInstance(attributeWrapper, self.PythonAttribute)
self.assertEqual(attributeWrapper.name, fqpn)
self.assertIs(attributeWrapper.load(), obj)
def test_failsWithEmptyFQPN(self):
"""
L{wrapFQPN} raises L{InvalidFQPN} when given an empty string.
"""
with self.assertRaises(self.InvalidFQPN):
self.wrapFQPN('')
def test_failsWithBadDotting(self):
""""
L{wrapFQPN} raises L{InvalidFQPN} when given a badly-dotted
FQPN. (e.g., x..y).
"""
for bad in ('.fails', 'fails.', 'this..fails'):
with self.assertRaises(self.InvalidFQPN):
self.wrapFQPN(bad)
def test_singleModule(self):
"""
L{wrapFQPN} returns a L{twisted.python.modules.PythonModule}
referring to the single module a dotless FQPN describes.
"""
import os
moduleWrapper = self.wrapFQPN('os')
self.assertIsInstance(moduleWrapper, self.PythonModule)
self.assertIs(moduleWrapper.load(), os)
def test_failsWithMissingSingleModuleOrPackage(self):
"""
L{wrapFQPN} raises L{NoModule} when given a dotless FQPN that does
not refer to a module or package.
"""
with self.assertRaises(self.NoModule):
self.wrapFQPN("this is not an acceptable name!")
def test_singlePackage(self):
"""
L{wrapFQPN} returns a L{twisted.python.modules.PythonModule}
referring to the single package a dotless FQPN describes.
"""
import xml
self.assertModuleWrapperRefersTo(self.wrapFQPN('xml'), xml)
def test_multiplePackages(self):
"""
L{wrapFQPN} returns a L{twisted.python.modules.PythonModule}
referring to the deepest package described by dotted FQPN.
"""
import xml.etree
self.assertModuleWrapperRefersTo(self.wrapFQPN('xml.etree'), xml.etree)
def test_multiplePackagesFinalModule(self):
"""
L{wrapFQPN} returns a L{twisted.python.modules.PythonModule}
referring to the deepest module described by dotted FQPN.
"""
import xml.etree.ElementTree
self.assertModuleWrapperRefersTo(
self.wrapFQPN('xml.etree.ElementTree'), xml.etree.ElementTree)
def test_singleModuleObject(self):
"""
L{wrapFQPN} returns a L{twisted.python.modules.PythonAttribute}
referring to the deepest object an FQPN names, traversing one module.
"""
import os
self.assertAttributeWrapperRefersTo(
self.wrapFQPN('os.path'), 'os.path', os.path)
def test_multiplePackagesObject(self):
"""
L{wrapFQPN} returns a L{twisted.python.modules.PythonAttribute}
referring to the deepest object described by an FQPN,
descending through several packages.
"""
import xml.etree.ElementTree
import automat
for fqpn, obj in [('xml.etree.ElementTree.fromstring',
xml.etree.ElementTree.fromstring),
('automat.MethodicalMachine.__doc__',
automat.MethodicalMachine.__doc__)]:
self.assertAttributeWrapperRefersTo(
self.wrapFQPN(fqpn), fqpn, obj)
def test_failsWithMultiplePackagesMissingModuleOrPackage(self):
"""
L{wrapFQPN} raises L{NoObject} when given an FQPN that contains a
missing attribute, module, or package.
"""
for bad in ('xml.etree.nope!',
'xml.etree.nope!.but.the.rest.is.believable'):
with self.assertRaises(self.NoObject):
self.wrapFQPN(bad)
@skipIf(not isTwistedInstalled(), "Twisted is not installed.")
class FindMachinesIntegrationTests(_WritesPythonModules):
"""
Integration tests to check that L{findMachines} yields all
machines discoverable at or below an FQPN.
"""
SOURCE = """
from automat import MethodicalMachine
class PythonClass(object):
_machine = MethodicalMachine()
ignored = "i am ignored"
rootLevel = MethodicalMachine()
ignored = "i am ignored"
"""
def setUp(self):
super(FindMachinesIntegrationTests, self).setUp()
from .._discover import findMachines
self.findMachines = findMachines
packageDir = self.FilePath(self.pathDir).child("test_package")
packageDir.makedirs()
self.pythonPath = self.PythonPath([self.pathDir])
self.writeSourceInto(self.SOURCE, packageDir.path, '__init__.py')
subPackageDir = packageDir.child('subpackage')
subPackageDir.makedirs()
subPackageDir.child('__init__.py').touch()
self.makeModule(self.SOURCE, subPackageDir.path, 'module.py')
self.packageDict = self.loadModuleAsDict(
self.pythonPath['test_package'])
self.moduleDict = self.loadModuleAsDict(
self.pythonPath['test_package']['subpackage']['module'])
def test_discoverAll(self):
"""
Given a top-level package FQPN, L{findMachines} discovers all
L{MethodicalMachine} instances in and below it.
"""
machines = sorted(self.findMachines('test_package'),
key=operator.itemgetter(0))
tpRootLevel = self.packageDict['test_package.rootLevel'].load()
tpPythonClass = self.packageDict['test_package.PythonClass'].load()
mRLAttr = self.moduleDict['test_package.subpackage.module.rootLevel']
mRootLevel = mRLAttr.load()
mPCAttr = self.moduleDict['test_package.subpackage.module.PythonClass']
mPythonClass = mPCAttr.load()
expectedMachines = sorted(
[('test_package.rootLevel', tpRootLevel),
('test_package.PythonClass._machine', tpPythonClass._machine),
('test_package.subpackage.module.rootLevel', mRootLevel),
('test_package.subpackage.module.PythonClass._machine',
mPythonClass._machine)],
key=operator.itemgetter(0))
self.assertEqual(expectedMachines, machines)

View file

@ -0,0 +1,587 @@
"""
Tests for the public interface of Automat.
"""
from functools import reduce
from unittest import TestCase
from automat._methodical import ArgSpec, _getArgNames, _getArgSpec, _filterArgs
from .. import MethodicalMachine, NoTransition
from .. import _methodical
class MethodicalTests(TestCase):
"""
Tests for L{MethodicalMachine}.
"""
def test_oneTransition(self):
"""
L{MethodicalMachine} provides a way for you to declare a state machine
with inputs, outputs, and states as methods. When you have declared an
input, an output, and a state, calling the input method in that state
will produce the specified output.
"""
class Machination(object):
machine = MethodicalMachine()
@machine.input()
def anInput(self):
"an input"
@machine.output()
def anOutput(self):
"an output"
return "an-output-value"
@machine.output()
def anotherOutput(self):
"another output"
return "another-output-value"
@machine.state(initial=True)
def anState(self):
"a state"
@machine.state()
def anotherState(self):
"another state"
anState.upon(anInput, enter=anotherState, outputs=[anOutput])
anotherState.upon(anInput, enter=anotherState,
outputs=[anotherOutput])
m = Machination()
self.assertEqual(m.anInput(), ["an-output-value"])
self.assertEqual(m.anInput(), ["another-output-value"])
def test_machineItselfIsPrivate(self):
"""
L{MethodicalMachine} is an implementation detail. If you attempt to
access it on an instance of your class, you will get an exception.
However, since tools may need to access it for the purposes of, for
example, visualization, you may access it on the class itself.
"""
expectedMachine = MethodicalMachine()
class Machination(object):
machine = expectedMachine
machination = Machination()
with self.assertRaises(AttributeError) as cm:
machination.machine
self.assertIn("MethodicalMachine is an implementation detail",
str(cm.exception))
self.assertIs(Machination.machine, expectedMachine)
def test_outputsArePrivate(self):
"""
One of the benefits of using a state machine is that your output method
implementations don't need to take invalid state transitions into
account - the methods simply won't be called. This property would be
broken if client code called output methods directly, so output methods
are not directly visible under their names.
"""
class Machination(object):
machine = MethodicalMachine()
counter = 0
@machine.input()
def anInput(self):
"an input"
@machine.output()
def anOutput(self):
self.counter += 1
@machine.state(initial=True)
def state(self):
"a machine state"
state.upon(anInput, enter=state, outputs=[anOutput])
mach1 = Machination()
mach1.anInput()
self.assertEqual(mach1.counter, 1)
mach2 = Machination()
with self.assertRaises(AttributeError) as cm:
mach2.anOutput
self.assertEqual(mach2.counter, 0)
self.assertIn(
"Machination.anOutput is a state-machine output method; to "
"produce this output, call an input method instead.",
str(cm.exception)
)
def test_multipleMachines(self):
"""
Two machines may co-exist happily on the same instance; they don't
interfere with each other.
"""
class MultiMach(object):
a = MethodicalMachine()
b = MethodicalMachine()
@a.input()
def inputA(self):
"input A"
@b.input()
def inputB(self):
"input B"
@a.state(initial=True)
def initialA(self):
"initial A"
@b.state(initial=True)
def initialB(self):
"initial B"
@a.output()
def outputA(self):
return "A"
@b.output()
def outputB(self):
return "B"
initialA.upon(inputA, initialA, [outputA])
initialB.upon(inputB, initialB, [outputB])
mm = MultiMach()
self.assertEqual(mm.inputA(), ["A"])
self.assertEqual(mm.inputB(), ["B"])
def test_collectOutputs(self):
"""
Outputs can be combined with the "collector" argument to "upon".
"""
import operator
class Machine(object):
m = MethodicalMachine()
@m.input()
def input(self):
"an input"
@m.output()
def outputA(self):
return "A"
@m.output()
def outputB(self):
return "B"
@m.state(initial=True)
def state(self):
"a state"
state.upon(input, state, [outputA, outputB],
collector=lambda x: reduce(operator.add, x))
m = Machine()
self.assertEqual(m.input(), "AB")
def test_methodName(self):
"""
Input methods preserve their declared names.
"""
class Mech(object):
m = MethodicalMachine()
@m.input()
def declaredInputName(self):
"an input"
@m.state(initial=True)
def aState(self):
"state"
m = Mech()
with self.assertRaises(TypeError) as cm:
m.declaredInputName("too", "many", "arguments")
self.assertIn("declaredInputName", str(cm.exception))
def test_inputWithArguments(self):
"""
If an input takes an argument, it will pass that along to its output.
"""
class Mechanism(object):
m = MethodicalMachine()
@m.input()
def input(self, x, y=1):
"an input"
@m.state(initial=True)
def state(self):
"a state"
@m.output()
def output(self, x, y=1):
self._x = x
return x + y
state.upon(input, state, [output])
m = Mechanism()
self.assertEqual(m.input(3), [4])
self.assertEqual(m._x, 3)
def test_outputWithSubsetOfArguments(self):
"""
Inputs pass arguments that output will accept.
"""
class Mechanism(object):
m = MethodicalMachine()
@m.input()
def input(self, x, y=1):
"an input"
@m.state(initial=True)
def state(self):
"a state"
@m.output()
def outputX(self, x):
self._x = x
return x
@m.output()
def outputY(self, y):
self._y = y
return y
@m.output()
def outputNoArgs(self):
return None
state.upon(input, state, [outputX, outputY, outputNoArgs])
m = Mechanism()
# Pass x as positional argument.
self.assertEqual(m.input(3), [3, 1, None])
self.assertEqual(m._x, 3)
self.assertEqual(m._y, 1)
# Pass x as key word argument.
self.assertEqual(m.input(x=4), [4, 1, None])
self.assertEqual(m._x, 4)
self.assertEqual(m._y, 1)
# Pass y as positional argument.
self.assertEqual(m.input(6, 3), [6, 3, None])
self.assertEqual(m._x, 6)
self.assertEqual(m._y, 3)
# Pass y as key word argument.
self.assertEqual(m.input(5, y=2), [5, 2, None])
self.assertEqual(m._x, 5)
self.assertEqual(m._y, 2)
def test_inputFunctionsMustBeEmpty(self):
"""
The wrapped input function must have an empty body.
"""
# input functions are executed to assert that the signature matches,
# but their body must be empty
_methodical._empty() # chase coverage
_methodical._docstring()
class Mechanism(object):
m = MethodicalMachine()
with self.assertRaises(ValueError) as cm:
@m.input()
def input(self):
"an input"
list() # pragma: no cover
self.assertEqual(str(cm.exception), "function body must be empty")
# all three of these cases should be valid. Functions/methods with
# docstrings produce slightly different bytecode than ones without.
class MechanismWithDocstring(object):
m = MethodicalMachine()
@m.input()
def input(self):
"an input"
@m.state(initial=True)
def start(self):
"starting state"
start.upon(input, enter=start, outputs=[])
MechanismWithDocstring().input()
class MechanismWithPass(object):
m = MethodicalMachine()
@m.input()
def input(self):
pass
@m.state(initial=True)
def start(self):
"starting state"
start.upon(input, enter=start, outputs=[])
MechanismWithPass().input()
class MechanismWithDocstringAndPass(object):
m = MethodicalMachine()
@m.input()
def input(self):
"an input"
pass
@m.state(initial=True)
def start(self):
"starting state"
start.upon(input, enter=start, outputs=[])
MechanismWithDocstringAndPass().input()
class MechanismReturnsNone(object):
m = MethodicalMachine()
@m.input()
def input(self):
return None
@m.state(initial=True)
def start(self):
"starting state"
start.upon(input, enter=start, outputs=[])
MechanismReturnsNone().input()
class MechanismWithDocstringAndReturnsNone(object):
m = MethodicalMachine()
@m.input()
def input(self):
"an input"
return None
@m.state(initial=True)
def start(self):
"starting state"
start.upon(input, enter=start, outputs=[])
MechanismWithDocstringAndReturnsNone().input()
def test_inputOutputMismatch(self):
"""
All the argument lists of the outputs for a given input must match; if
one does not the call to C{upon} will raise a C{TypeError}.
"""
class Mechanism(object):
m = MethodicalMachine()
@m.input()
def nameOfInput(self, a):
"an input"
@m.output()
def outputThatMatches(self, a):
"an output that matches"
@m.output()
def outputThatDoesntMatch(self, b):
"an output that doesn't match"
@m.state()
def state(self):
"a state"
with self.assertRaises(TypeError) as cm:
state.upon(nameOfInput, state, [outputThatMatches,
outputThatDoesntMatch])
self.assertIn("nameOfInput", str(cm.exception))
self.assertIn("outputThatDoesntMatch", str(cm.exception))
def test_getArgNames(self):
"""
Type annotations should be included in the set of
"""
spec = ArgSpec(
args=('a', 'b'),
varargs=None,
varkw=None,
defaults=None,
kwonlyargs=(),
kwonlydefaults=None,
annotations=(('a', int), ('b', str)),
)
self.assertEqual(
_getArgNames(spec),
{'a', 'b', ('a', int), ('b', str)},
)
def test_filterArgs(self):
"""
filterArgs() should not filter the `args` parameter
if outputSpec accepts `*args`.
"""
inputSpec = _getArgSpec(lambda *args, **kwargs: None)
outputSpec = _getArgSpec(lambda *args, **kwargs: None)
argsIn = ()
argsOut, _ = _filterArgs(argsIn, {}, inputSpec, outputSpec)
self.assertIs(argsIn, argsOut)
def test_multipleInitialStatesFailure(self):
"""
A L{MethodicalMachine} can only have one initial state.
"""
class WillFail(object):
m = MethodicalMachine()
@m.state(initial=True)
def firstInitialState(self):
"The first initial state -- this is OK."
with self.assertRaises(ValueError):
@m.state(initial=True)
def secondInitialState(self):
"The second initial state -- results in a ValueError."
def test_multipleTransitionsFailure(self):
"""
A L{MethodicalMachine} can only have one transition per start/event
pair.
"""
class WillFail(object):
m = MethodicalMachine()
@m.state(initial=True)
def start(self):
"We start here."
@m.state()
def end(self):
"Rainbows end."
@m.input()
def event(self):
"An event."
start.upon(event, enter=end, outputs=[])
with self.assertRaises(ValueError):
start.upon(event, enter=end, outputs=[])
def test_badTransitionForCurrentState(self):
"""
Calling any input method that lacks a transition for the machine's
current state raises an informative L{NoTransition}.
"""
class OnlyOnePath(object):
m = MethodicalMachine()
@m.state(initial=True)
def start(self):
"Start state."
@m.state()
def end(self):
"End state."
@m.input()
def advance(self):
"Move from start to end."
@m.input()
def deadEnd(self):
"A transition from nowhere to nowhere."
start.upon(advance, end, [])
machine = OnlyOnePath()
with self.assertRaises(NoTransition) as cm:
machine.deadEnd()
self.assertIn("deadEnd", str(cm.exception))
self.assertIn("start", str(cm.exception))
machine.advance()
with self.assertRaises(NoTransition) as cm:
machine.deadEnd()
self.assertIn("deadEnd", str(cm.exception))
self.assertIn("end", str(cm.exception))
def test_saveState(self):
"""
L{MethodicalMachine.serializer} is a decorator that modifies its
decoratee's signature to take a "state" object as its first argument,
which is the "serialized" argument to the L{MethodicalMachine.state}
decorator.
"""
class Mechanism(object):
m = MethodicalMachine()
def __init__(self):
self.value = 1
@m.state(serialized="first-state", initial=True)
def first(self):
"First state."
@m.state(serialized="second-state")
def second(self):
"Second state."
@m.serializer()
def save(self, state):
return {
'machine-state': state,
'some-value': self.value,
}
self.assertEqual(
Mechanism().save(),
{
"machine-state": "first-state",
"some-value": 1,
}
)
def test_restoreState(self):
"""
L{MethodicalMachine.unserializer} decorates a function that becomes a
machine-state unserializer; its return value is mapped to the
C{serialized} parameter to C{state}, and the L{MethodicalMachine}
associated with that instance's state is updated to that state.
"""
class Mechanism(object):
m = MethodicalMachine()
def __init__(self):
self.value = 1
self.ranOutput = False
@m.state(serialized="first-state", initial=True)
def first(self):
"First state."
@m.state(serialized="second-state")
def second(self):
"Second state."
@m.input()
def input(self):
"an input"
@m.output()
def output(self):
self.value = 2
self.ranOutput = True
return 1
@m.output()
def output2(self):
return 2
first.upon(input, second, [output],
collector=lambda x: list(x)[0])
second.upon(input, second, [output2],
collector=lambda x: list(x)[0])
@m.serializer()
def save(self, state):
return {
'machine-state': state,
'some-value': self.value,
}
@m.unserializer()
def _restore(self, blob):
self.value = blob['some-value']
return blob['machine-state']
@classmethod
def fromBlob(cls, blob):
self = cls()
self._restore(blob)
return self
m1 = Mechanism()
m1.input()
blob = m1.save()
m2 = Mechanism.fromBlob(blob)
self.assertEqual(m2.ranOutput, False)
self.assertEqual(m2.input(), 2)
self.assertEqual(
m2.save(),
{
'machine-state': 'second-state',
'some-value': 2,
}
)
# FIXME: error for wrong types on any call to _oneTransition
# FIXME: better public API for .upon; maybe a context manager?
# FIXME: when transitions are defined, validate that we can always get to
# terminal? do we care about this?
# FIXME: implementation (and use-case/example) for passing args from in to out
# FIXME: possibly these need some kind of support from core
# FIXME: wildcard state (in all states, when input X, emit Y and go to Z)
# FIXME: wildcard input (in state X, when any input, emit Y and go to Z)
# FIXME: combined wildcards (in any state for any input, emit Y go to Z)

View file

@ -0,0 +1,98 @@
from unittest import TestCase
from .._methodical import MethodicalMachine
class SampleObject(object):
mm = MethodicalMachine()
@mm.state(initial=True)
def begin(self):
"initial state"
@mm.state()
def middle(self):
"middle state"
@mm.state()
def end(self):
"end state"
@mm.input()
def go1(self):
"sample input"
@mm.input()
def go2(self):
"sample input"
@mm.input()
def back(self):
"sample input"
@mm.output()
def out(self):
"sample output"
setTrace = mm._setTrace
begin.upon(go1, middle, [out])
middle.upon(go2, end, [out])
end.upon(back, middle, [])
middle.upon(back, begin, [])
class TraceTests(TestCase):
def test_only_inputs(self):
traces = []
def tracer(old_state, input, new_state):
traces.append((old_state, input, new_state))
return None # "I only care about inputs, not outputs"
s = SampleObject()
s.setTrace(tracer)
s.go1()
self.assertEqual(traces, [("begin", "go1", "middle"),
])
s.go2()
self.assertEqual(traces, [("begin", "go1", "middle"),
("middle", "go2", "end"),
])
s.setTrace(None)
s.back()
self.assertEqual(traces, [("begin", "go1", "middle"),
("middle", "go2", "end"),
])
s.go2()
self.assertEqual(traces, [("begin", "go1", "middle"),
("middle", "go2", "end"),
])
def test_inputs_and_outputs(self):
traces = []
def tracer(old_state, input, new_state):
traces.append((old_state, input, new_state, None))
def trace_outputs(output):
traces.append((old_state, input, new_state, output))
return trace_outputs # "I care about outputs too"
s = SampleObject()
s.setTrace(tracer)
s.go1()
self.assertEqual(traces, [("begin", "go1", "middle", None),
("begin", "go1", "middle", "out"),
])
s.go2()
self.assertEqual(traces, [("begin", "go1", "middle", None),
("begin", "go1", "middle", "out"),
("middle", "go2", "end", None),
("middle", "go2", "end", "out"),
])
s.setTrace(None)
s.back()
self.assertEqual(traces, [("begin", "go1", "middle", None),
("begin", "go1", "middle", "out"),
("middle", "go2", "end", None),
("middle", "go2", "end", "out"),
])
s.go2()
self.assertEqual(traces, [("begin", "go1", "middle", None),
("begin", "go1", "middle", "out"),
("middle", "go2", "end", None),
("middle", "go2", "end", "out"),
])

View file

@ -0,0 +1,430 @@
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))