# -*- coding: utf-8 -*-
#
# MONK automated test framework
#
# Copyright (C) 2013 DResearch Fahrzeugelektronik GmbH
# Written and maintained by MONK Developers <project-monk@dresearch-fe.de>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version
# 3 of the License, or (at your option) any later version.
#
"""
Instead of creating :py:class:`~monk_tf.dev.Device` and
:py:class:`~monk_tf.conn.AConnection` objects by yourself, you can also choose
to put corresponding data in a separate file and let this layer handle the
object concstruction and destruction for you. Doing this will probably make
your test code look more clean, keep the number of places where you need to
change something as small as possible, and lets you reuse data that you already
have described.
A hello world test with it looks like this::
import nose
from monk_tf import fixture
def test_hello():
''' say hello
'''
# set up
h = fixture.Fixture('target_device.cfg')
expected_out = "hello"
# execute
out = h.devs[0].cmd('echo "hello"')
# assert
nose.tools.eq_(expected_out, out)
# tear down
h.tear_down()
When using this layer setting up a device only takes one line of code. The rest
of the information is in the ``target_device.cfg`` file. :term:`MONK` currently
comes with one text format parser predefined, which is the
:py:class:`~monk_tf.fixture.XiniParser`. ``Xini`` is short for
:term:`extended INI`. You may, however, use any data format you want, if you
extend the :py:class:`~monk_tf.fixture.AParser` class accordingly.
An example ``Xini`` data file might look like this::
[device1]
type=Device
[[serial1]]
type=SerialConnection
port=/dev/ttyUSB1
user=example
password=secret
As you can see it looks like an :term:`INI` file. There are sections,
consisting of a title enclosed in squared brackets (``[]``) and lists of
properties, consisting of key-value pairs separated by equality signs (``=``).
The unusual part is that the section *serial1* is surrounded by two pairs of
squared brackets (``[]``). This is the specialty of this format indicating that
*serial1* is a subsection of *device1* and therefore is a nested section. This
nesting can be done unlimited, by surrounding a section with more and more
pairs of squared brackets (``[]``) according to the level of nesting intended.
In this example *serial1* belongs to *device1* and the types indicate the
corresponding :term:`MONK` object to be created.
Classes
-------
"""
import os
import sys
import logging
import configobj as config
import conn
import dev
logger = logging.getLogger(__name__)
############
#
# Exceptions
#
############
[docs]class AFixtureException(Exception):
""" Base class for exceptions of the fixture layer.
If you want to make sure that you catch all exceptions that are related
to this layer, you should catch *AFixtureExceptions*. This also means
that if you extend this list of exceptions you should inherit from this
exception and not from :py:exc:`~exceptions.Exception`.
"""
pass
[docs]class AParseException(AFixtureException):
""" Base class for exceptions concerning parsing errors.
"""
pass
[docs]class NotXiniException(AParseException):
"""
is raised when a :term:`fixture file` could not be parsed as
:term:`extended INI`.
"""
pass
[docs]class CantParseException(AFixtureException):
""" is raised when a Fixture cannot parse a given file.
"""
pass
[docs]class NoDeviceException(AFixtureException):
""" is raised when a :py:clas:`~monk_tf.fixture.Fixture` requires a device but has none.
"""
######################################################
#
# Parsers - Read a text file and be used like a dict()
#
######################################################
[docs]class AParser(dict):
""" Base class for all parsers.
Do not instantiate this class! This basically just provides the :term:`API`
that is needed by :py:class:`~monk_tf.fixture.Fixture` to interact with the
data that is parsed. Each child class should make sure that it always
provides its parsed data like a :py:class:`dict` would. If you require your
own parser, you can extend this. :py:class:`~monk_tf.fixture.XiniParser`
provides a very basic example.
"""
pass
[docs]class XiniParser(config.ConfigObj, AParser):
""" Reads config files in :term:`extended INI` format.
"""
def _load(self, infile, configspec):
""" Changes exception type raised.
Overwrites method from :py:class:`~configobj.ConfigObj` to raise a
:py:class:`~monk_tf.fixture.NotXiniException` instead of a
:py:class:`~configobj.ConfigObjError`.
"""
try:
self.file_error = True
super(XiniParser, self)._load(infile, configspec)
except config.ConfigObjError as e:
t, val, traceback = sys.exc_info()
raise NotXiniException, e.message, traceback
##############################################################
#
# Fixture Classes - creates MONK objects based on dictionaries
#
##############################################################
[docs]class Fixture(object):
""" Creates :term:`MONK` objects based on dictionary like objects.
This is the class that provides the fundamental feature of this layer. It
reads data files by trying to parse them via its list of known parsers and
if it succeeds, it creates :term:`MONK` objects based on the configuration
given by the data file. Most likely these objects are one or more
:py:class:`~monk_tf.dev.Device` objects that have at least one
:py:class:`~monk_tf.conn.AConnection` object each. If more than one
:term:`fixture file` is read containing the same name on the highest level,
then the latest data gets used. This does not work on lower levels of
nesting, though. If you attempt to overwrite lower levels of nesting, what
actually happens is that the highest layer gets overwritten and you lose
the data that was stored in the older objects. This is simply how
:py:meth:`set.update` works.
One source of data (either a file name or a child class of
:py:class:`~monk_tf.fixture.AParser`) can be given to an object of this
class by its constructer, others can be added afterwards with the
:py:meth:`~monk_tf.fixture.Fixture.read` method. An example looks like
this::
import monk_tf.fixture as mf
fixture = mf.Fixture('/etc/monk_tf/default_devices.cfg')
.read('~/.monk/default_devices.cfg')
# can also be a parser object
.read(XiniParser('~/testsuite12345/suite_devices.cfg'))
"""
_DEFAULT_CLASSES = {
"Device" : dev.Device,
"HydraDevice" : dev.Hydra,
"DevelDevice" : dev.DevelDevice,
"SerialConnection" : conn.SerialConn,
"SshConnection" : conn.SshConn,
}
_DEFAULT_PARSERS = [
XiniParser,
]
_DEFAULT_DEBUG_SOURCE = "MONK_DEBUG_SOURCE"
def __init__(self, source=None, name=None, parsers=None, classes=None,
lookfordbgsrc=True):
"""
:param source: The :term:`fixture file` or
:py:class:`~monk_tf.fixture.AParser` object to be read.
:param name: The name of this object.
:param parsers: An :python:term:`iterable` of
:py:class:`~monk_tf.fixture.AParser` classes to be used
for parsing a given
:py:attr:`~monk_tf.fixture.Fixture.source`.
:param classes: A :py:class:`dict` of classes to class names. Used for
parsing the type attribute in
:term:`fixture files<fixture file>`.
:param lookfordbgsrc: If True an environment variable is looked for to
read a local debug config. If False it won't be
looked for.
"""
self.name = name or self.__class__.__name__
self._logger = logging.getLogger("{}:{}".format(__name__, self.name))
self.devs = []
self.parsers = parsers or self._DEFAULT_PARSERS
self.classes = classes or self._DEFAULT_CLASSES
self.props = {}
if source:
self._update_props(
self._parse(source))
if lookfordbgsrc and self._DEFAULT_DEBUG_SOURCE in os.environ:
self._logger.debug("load debug source from {}".format(
self._DEFAULT_DEBUG_SOURCE))
self._update_props(
self._parse(os.environ[self._DEFAULT_DEBUG_SOURCE]))
else:
self._logger.debug("no debug source file found")
self._initialize()
[docs] def read(self, source):
""" Read more data, either as a file name or as a parser.
:param source: the data source; either a file name or a
:py:class:`~monk_tf.fixture.AParser` child class
instance.
:return: self
"""
self._logger.debug("read: " + str(source))
props = self._parse(source)
self._update_props(props)
self._initialize()
return self
def _parse(self, source):
""" Parse data file.
:param source: the data source; either a file name or a
:py:class:`~monk_tf.fixture.AParser` child class
instance.
:return: Returns a :py:class:`~monk_tf.fixture.AParser` instance.
:raises: :py:class:`~monk_tf.fixture.CantParseException`
"""
self._logger.debug("parse: " + str(source))
if isinstance(source, AParser):
return source
else:
for parser in self.parsers:
try:
return parser(source)
except AParseException as e:
self._logger.exception(e)
continue
raise CantParseException()
def _update_props(self, props):
""" Updates the properties with a dictionary-like object.
This basically uses :py:meth:`dict.update` to update
:py:attr:`self.props <~monk_tf.fixture.Fixture.props>` with a new set
of data.
:param props: object that can be used with :py:meth:`dict.update`
"""
self._logger.debug("add props: " + str(props))
self.props.update(props)
self._logger.debug("final props: " + str(props))
def _initialize(self):
""" Create :term:`MONK` objects based on self's properties.
"""
self._logger.debug("initialize with props: " + str(self.props))
self.devs = []
for dname in self.props.keys():
dconf = dict(self.props[dname])
dclass = self.classes[dconf.pop("type")]
dconns = []
for cname in dconf.keys():
cconf = dict(dconf[cname])
cclass = self.classes[cconf.pop("type")]
cconf['name'] = cname
dconns.append(cclass(**cconf))
self.devs.append(dclass(name=dname, conns=dconns))
[docs] def cmd(self, msg):
""" call :py:meth:`cmd` from first :py:class:`~monk_tf.device.Device`
"""
try:
return self.devs[0].cmd(msg)
except IndexError:
raise NoDeviceException("this fixture has no device loaded")
[docs] def tear_down(self):
""" Can be used for explicit destruction of managed objects.
This should be called in every :term:`test case` as the last step.
"""
for device in self.devs:
try:
del device
except Exception as e:
logger.exception(e)
self.devs = []
def __str__(self):
return "{cls}.devs:{devs}".format(
cls=self.__class__.__name__,
devs=[str(d) for d in self.devs],
)