未验证 提交 ac035ebb 编写于 作者: C Cleber Rosa

Merge remote-tracking branch 'ldoktor/mux-plugin4'

......@@ -446,14 +446,15 @@ class Job(object):
"command.")
raise exceptions.OptionValidationError(e_msg)
if isinstance(getattr(self.args, 'multiplex_files', None),
multiplexer.Mux):
mux = self.args.multiplex_files # pylint: disable=E1101
else:
mux = getattr(self.args, "mux", None)
if mux is None:
mux = multiplexer.Mux()
if not mux.is_parsed(): # Mux not yet parsed, apply args
try:
mux = multiplexer.Mux(self.args)
mux.parse(self.args)
except (IOError, ValueError) as details:
raise exceptions.OptionValidationError(details)
raise exceptions.OptionValidationError("Unable to parse mux: "
"%s" % details)
self.args.test_result_total = mux.get_number_of_tests(self.test_suite)
self._make_test_result()
......
......@@ -100,7 +100,7 @@ def retrieve_urls(resultsdir):
def retrieve_mux(resultsdir):
"""
Retrieves the job multiplex from the results directory.
Retrieves the job Mux object from the results directory.
"""
recorded_mux = _retrieve(resultsdir, MUX_FILENAME)
if recorded_mux is None:
......
......@@ -26,8 +26,6 @@ import re
from . import tree
MULTIPLEX_CAPABLE = tree.MULTIPLEX_CAPABLE
class MuxTree(object):
......@@ -80,18 +78,6 @@ class MuxTree(object):
yield list(itertools.chain(*pools.next()))
def yaml2tree(input_yamls, filter_only=None, filter_out=None,
debug=False):
if filter_only is None:
filter_only = []
if filter_out is None:
filter_out = []
input_tree = tree.create_from_yaml(input_yamls, debug)
# TODO: Process filters and multiplex simultaneously
final_tree = tree.apply_filters(input_tree, filter_only, filter_out)
return final_tree
# TODO: Create multiplexer plugin and split these functions into multiple files
class NoMatchError(KeyError):
pass
......@@ -377,28 +363,103 @@ class AvocadoParam(object):
yield (leaf.environment_origin[key].path, key, value)
def _report_mux_already_parsed(self, *args, **kwargs):
"""
Raises exception describing that `self.data` alteration is restricted
"""
raise RuntimeError("Mux already parsed, altering is restricted. %s %s"
% (args, kwargs))
class Mux(object):
"""
This is a multiplex object which multiplexes the test_suite.
"""
def __init__(self, args):
def __init__(self, debug=False):
"""
:param debug: Store whether this instance should debug the mux
:note: people need to check whether mux uses debug and reflect that
in order to provide the right results.
"""
self._has_multiple_variants = None
mux_files = getattr(args, 'multiplex_files', None)
self.variants = None
self.debug = debug
self.data = tree.TreeNodeDebug() if debug else tree.TreeNode()
self._mux_path = None
def parse(self, args):
"""
Apply options defined on the cmdline
:param args: Parsed cmdline arguments
"""
filter_only = getattr(args, 'filter_only', None)
filter_out = getattr(args, 'filter_out', None)
if mux_files:
mux_tree = yaml2tree(mux_files)
else: # no variants
mux_tree = tree.TreeNode()
if getattr(args, 'default_avocado_params', None):
mux_tree.merge(args.default_avocado_params)
mux_tree = tree.apply_filters(mux_tree, filter_only, filter_out)
self._parse_basic_injects(args)
mux_tree = tree.apply_filters(self.data, filter_only, filter_out)
self.variants = MuxTree(mux_tree)
self._mux_path = getattr(args, 'mux_path', None)
if self._mux_path is None:
self._mux_path = ['/run/*']
# disable data alteration (and remove data as they are not useful)
self.data = None
self.data_inject = _report_mux_already_parsed
self.data_merge = _report_mux_already_parsed
def _parse_basic_injects(self, args):
"""
Inject data from the basic injects defined by Mux
:param args: Parsed cmdline arguments
"""
# FIXME: Backward compatibility params, to be removed when 36 LTS is
# discontinued
if (not getattr(args, "mux_skip_defaults", False) and
hasattr(args, "default_avocado_params")):
self.data_merge(args.default_avocado_params)
# Extend default multiplex tree of --mux-inject values
for inject in getattr(args, "mux_inject", []):
entry = inject.split(':', 3)
if len(entry) < 2:
raise ValueError("key:entry pairs required, found only %s"
% (entry))
elif len(entry) == 2: # key, entry
self.data_inject(*entry)
else: # path, key, entry
self.data_inject(key=entry[1], value=entry[2], path=entry[0])
def is_parsed(self):
"""
Reports whether the tree was already multiplexed
"""
return self.variants is not None
def data_inject(self, key, value, path=None):
"""
Inject entry to the mux tree (params database)
:param key: Key to which we'd like to assign the value
:param value: The key's value
:param path: Optional path to the node to which we assign the value,
by default '/'.
"""
if path:
node = self.data.get_node(path, True)
else:
node = self.data
node.value[key] = value
def data_merge(self, tree):
"""
Merge tree into the mux tree (params database)
:param tree: Tree to be merged into this database.
:type tree: :class:`avocado.core.tree.TreeNode`
"""
self.data.merge(tree)
def get_number_of_tests(self, test_suite):
"""
......
......@@ -12,7 +12,6 @@
# Copyright: Red Hat Inc. 2013-2014
# Author: Ruda Moura <rmoura@redhat.com>
"""
Avocado application command line parsing.
"""
......@@ -21,8 +20,9 @@ import argparse
import logging
from . import exit_codes
from . import tree
from . import multiplexer
from . import settings
from . import tree
from .output import BUILTIN_STREAMS, BUILTIN_STREAM_SETS
from .version import VERSION
......@@ -125,6 +125,9 @@ class Parser(object):
dest='subcommand')
# Allow overriding default params by plugins
self.args.mux = multiplexer.Mux(getattr(self.args, "mux-debug", False))
# FIXME: Backward compatibility params, to be removed when 36 LTS is
# discontinued
self.args.default_avocado_params = tree.TreeNode()
def finish(self):
......
......@@ -80,7 +80,7 @@ class RemoteTestRunner(TestRunner):
test_data = path + '.data'
if os.path.isdir(test_data):
self.remote.send_files(test_data, os.path.dirname(rpath))
for mux_file in getattr(self.job.args, 'multiplex_files') or []:
for mux_file in getattr(self.job.args, 'multiplex') or []:
rpath = os.path.join(self.remote_test_dir, mux_file)
self.remote.makedir(os.path.dirname(rpath))
self.remote.send_files(mux_file, rpath)
......@@ -181,9 +181,9 @@ class RemoteTestRunner(TestRunner):
extra_params = []
mux_files = [os.path.join(self.remote_test_dir, mux_file)
for mux_file in getattr(self.job.args,
'multiplex_files') or []]
'multiplex') or []]
if mux_files:
extra_params.append("--multiplex %s" % " ".join(mux_files))
extra_params.append("-m %s" % " ".join(mux_files))
if getattr(self.job.args, "dry_run", False):
extra_params.append("--dry-run")
......
......@@ -39,28 +39,12 @@ import locale
import os
import re
try:
import yaml
except ImportError:
MULTIPLEX_CAPABLE = False
else:
MULTIPLEX_CAPABLE = True
try:
from yaml import CLoader as Loader
except ImportError:
from yaml import Loader
from . import output
# Mapping for yaml flags
YAML_INCLUDE = 0
YAML_USING = 1
YAML_REMOVE_NODE = 2
YAML_REMOVE_VALUE = 3
YAML_MUX = 4
__RE_FILE_SPLIT = re.compile(r'(?<!\\):') # split by ':' but not '\\:'
__RE_FILE_SUBS = re.compile(r'(?<!\\)\\:') # substitute '\\:' but not '\\\\:'
# Tags to remove node/value
REMOVE_NODE = 0
REMOVE_VALUE = 1
class Control(object): # Few methods pylint: disable=R0903
......@@ -144,7 +128,7 @@ class TreeNode(object):
"""
for ctrl in other.ctrl:
if isinstance(ctrl, Control):
if ctrl.code == YAML_REMOVE_NODE:
if ctrl.code == REMOVE_NODE:
remove = []
regexp = re.compile(ctrl.value)
for child in self.children:
......@@ -152,7 +136,7 @@ class TreeNode(object):
remove.append(child)
for child in remove:
self.children.remove(child)
elif ctrl.code == YAML_REMOVE_VALUE:
elif ctrl.code == REMOVE_VALUE:
remove = []
regexp = re.compile(ctrl.value)
for key in self.value.iterkeys():
......@@ -304,181 +288,6 @@ class TreeNode(object):
return self
class Value(tuple): # Few methods pylint: disable=R0903
""" Used to mark values to simplify checking for node vs. value """
pass
class ListOfNodeObjects(list): # Few methods pylint: disable=R0903
"""
Used to mark list as list of objects from whose node is going to be created
"""
pass
def _create_from_yaml(path, cls_node=TreeNode):
""" Create tree structure from yaml stream """
def tree_node_from_values(name, values):
""" Create `name` node and add values """
node = cls_node(str(name))
using = ''
for value in values:
if isinstance(value, TreeNode):
node.add_child(value)
elif isinstance(value[0], Control):
if value[0].code == YAML_INCLUDE:
# Include file
ypath = value[1]
if not os.path.isabs(ypath):
ypath = os.path.join(os.path.dirname(path), ypath)
if not os.path.exists(ypath):
raise ValueError("File '%s' included from '%s' does not "
"exist." % (ypath, path))
node.merge(_create_from_yaml('/:' + ypath, cls_node))
elif value[0].code == YAML_USING:
if using:
raise ValueError("!using can be used only once per "
"node! (%s:%s)" % (path, name))
using = value[1]
if using[0] == '/':
using = using[1:]
if using[-1] == '/':
using = using[:-1]
elif value[0].code == YAML_REMOVE_NODE:
value[0].value = value[1] # set the name
node.ctrl.append(value[0]) # add "blue pill" of death
elif value[0].code == YAML_REMOVE_VALUE:
value[0].value = value[1] # set the name
node.ctrl.append(value[0])
elif value[0].code == YAML_MUX:
node.multiplex = True
else:
node.value[value[0]] = value[1]
if using:
if name is not '':
for name in using.split('/')[::-1]:
node = cls_node(name, children=[node])
else:
using = using.split('/')[::-1]
node.name = using.pop()
while True:
if not using:
break
name = using.pop() # 'using' is list pylint: disable=E1101
node = cls_node(name, children=[node])
node = cls_node('', children=[node])
return node
def mapping_to_tree_loader(loader, node):
""" Maps yaml mapping tag to TreeNode structure """
_value = []
for key_node, value_node in node.value:
if key_node.tag.startswith('!'): # reflect tags everywhere
key = loader.construct_object(key_node)
else:
key = loader.construct_python_str(key_node)
value = loader.construct_object(value_node)
_value.append((key, value))
objects = ListOfNodeObjects()
for name, values in _value:
if isinstance(values, ListOfNodeObjects): # New node from list
objects.append(tree_node_from_values(name, values))
elif values is None: # Empty node
objects.append(cls_node(str(name)))
else: # Values
objects.append(Value((name, values)))
return objects
def mux_loader(loader, obj):
"""
Special !mux loader which allows to tag node as 'multiplex = True'.
"""
if not isinstance(obj, yaml.ScalarNode):
objects = mapping_to_tree_loader(loader, obj)
else: # This means it's empty node. Don't call mapping_to_tree_loader
objects = ListOfNodeObjects()
objects.append((Control(YAML_MUX), None))
return objects
Loader.add_constructor(u'!include',
lambda loader, node: Control(YAML_INCLUDE))
Loader.add_constructor(u'!using',
lambda loader, node: Control(YAML_USING))
Loader.add_constructor(u'!remove_node',
lambda loader, node: Control(YAML_REMOVE_NODE))
Loader.add_constructor(u'!remove_value',
lambda loader, node: Control(YAML_REMOVE_VALUE))
Loader.add_constructor(u'!mux', mux_loader)
Loader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
mapping_to_tree_loader)
# Parse file name ([$using:]$path)
path = __RE_FILE_SPLIT.split(path, 1)
if len(path) == 1:
path = __RE_FILE_SUBS.sub(':', path[0])
using = ["run"]
else:
nodes = __RE_FILE_SUBS.sub(':', path[0]).strip('/').split('/')
using = [node for node in nodes if node]
if not path[0].startswith('/'): # relative path, put into /run
using.insert(0, 'run')
path = __RE_FILE_SUBS.sub(':', path[1])
# Load the tree
with open(path) as stream:
loaded_tree = yaml.load(stream, Loader)
loaded_tree = tree_node_from_values('', loaded_tree)
# Add prefix
if using:
loaded_tree.name = using.pop()
while True:
if not using:
break
loaded_tree = cls_node(using.pop(), children=[loaded_tree])
loaded_tree = cls_node('', children=[loaded_tree])
return loaded_tree
def create_from_yaml(paths, debug=False):
"""
Create tree structure from yaml-like file
:param fileobj: File object to be processed
:raise SyntaxError: When yaml-file is corrupted
:return: Root of the created tree structure
"""
def _merge(data, path):
""" Normal run """
data.merge(_create_from_yaml(path))
def _merge_debug(data, path):
""" Use NamedTreeNodeDebug magic """
node_cls = get_named_tree_cls(path)
data.merge(_create_from_yaml(path, node_cls))
if not debug:
data = TreeNode()
merge = _merge
else:
data = TreeNodeDebug()
merge = _merge_debug
path = None
try:
for path in paths:
merge(data, path)
# Yaml can raise IndexError on some files
except (yaml.YAMLError, IndexError) as details:
if 'mapping values are not allowed in this context' in str(details):
details = ("%s\nMake sure !tags and colons are separated by a "
"space (eg. !include :)" % details)
msg = "Invalid multiplex file '%s': %s" % (path, details)
raise IOError(2, msg, path)
return data
def path_parent(path):
"""
From a given path, return its parent path.
......
......@@ -16,7 +16,6 @@ import logging
import sys
from avocado.core import exit_codes, output
from avocado.core import multiplexer
from avocado.core import tree
from avocado.core.plugin_interfaces import CLICmd
from avocado.core.settings import settings
......@@ -29,26 +28,21 @@ class Multiplex(CLICmd):
"""
name = 'multiplex'
description = 'Generate a list of dictionaries with params from a multiplex file'
description = "Tool to analyze and visualize test variants and params"
def __init__(self, *args, **kwargs):
super(Multiplex, self).__init__(*args, **kwargs)
self._from_args_tree = tree.TreeNode()
def configure(self, parser):
if multiplexer.MULTIPLEX_CAPABLE is False:
return
parser = super(Multiplex, self).configure(parser)
parser.add_argument('multiplex_files', nargs='+',
help='Path(s) to a multiplex file(s)')
parser.add_argument('--filter-only', nargs='*', default=[],
help='Filter only path(s) from multiplexing')
parser.add_argument('--filter-out', nargs='*', default=[],
help='Filter out path(s) from multiplexing')
parser.add_argument('--system-wide', action='store_true',
parser.add_argument('--system-wide', action='store_false',
default=True, dest="mux-skip-defaults",
help="Combine the files with the default "
"tree.")
parser.add_argument('-c', '--contents', action='store_true',
......@@ -59,8 +53,8 @@ class Multiplex(CLICmd):
"the final multiplex tree.")
env_parser = parser.add_argument_group("environment view options")
env_parser.add_argument('-d', '--debug', action='store_true',
default=False, help="Debug multiplexed "
"files.")
dest="mux_debug", default=False,
help="Debug the multiplex tree.")
tree_parser = parser.add_argument_group("tree view options")
tree_parser.add_argument('-t', '--tree', action='store_true',
default=False, help='Shows the multiplex '
......@@ -68,40 +62,22 @@ class Multiplex(CLICmd):
tree_parser.add_argument('-i', '--inherit', action="store_true",
help="Show the inherited values")
def _activate(self, args):
# Extend default multiplex tree of --env values
for value in getattr(args, "mux_inject", []):
value = value.split(':', 2)
if len(value) < 2:
raise ValueError("key:value pairs required, found only %s"
% (value))
elif len(value) == 2:
self._from_args_tree.value[value[0]] = value[1]
else:
node = self._from_args_tree.get_node(value[0], True)
node.value[value[1]] = value[2]
def run(self, args):
self._activate(args)
log = logging.getLogger("avocado.app")
err = None
if args.tree and args.debug:
if args.tree and args.mux_debug:
err = "Option --tree is incompatible with --debug."
elif not args.tree and args.inherit:
err = "Option --inherit can be only used with --tree"
if err:
log.error(err)
sys.exit(exit_codes.AVOCADO_FAIL)
mux = args.mux
try:
mux_tree = multiplexer.yaml2tree(args.multiplex_files,
args.filter_only, args.filter_out,
args.debug)
except IOError as details:
log.error(details.strerror)
mux.parse(args)
except (IOError, ValueError) as details:
log.error("Unable to parse mux: %s", details)
sys.exit(exit_codes.AVOCADO_JOB_FAIL)
if args.system_wide:
mux_tree.merge(args.default_avocado_params)
mux_tree.merge(self._from_args_tree)
if args.tree:
if args.contents:
verbose = 1
......@@ -111,13 +87,12 @@ class Multiplex(CLICmd):
verbose += 2
use_utf8 = settings.get_value("runner.output", "utf8",
key_type=bool, default=None)
log.debug(tree.tree_view(mux_tree, verbose, use_utf8))
log.debug(tree.tree_view(mux.variants.root, verbose, use_utf8))
sys.exit(exit_codes.AVOCADO_ALL_OK)
variants = multiplexer.MuxTree(mux_tree)
log.info('Variants generated:')
for (index, tpl) in enumerate(variants):
if not args.debug:
for (index, tpl) in enumerate(mux.variants):
if not args.mux_debug:
paths = ', '.join([x.path for x in tpl])
else:
color = output.TERM_SUPPORT.LOWLIGHT
......
......@@ -27,6 +27,12 @@ from avocado.core.settings import settings
from avocado.core.test import ReplaySkipTest
def ignore_call(*args, **kwargs):
"""
Accepts anything and does nothing
"""
class Replay(CLI):
"""
......@@ -57,7 +63,7 @@ class Replay(CLI):
replay_parser.add_argument('--replay-ignore',
dest='replay_ignore',
type=self._valid_ignore,
default=None,
default=[],
help='Ignore multiplex (mux) and/or '
'configuration (config) from the '
'source job')
......@@ -125,9 +131,9 @@ class Replay(CLI):
log = logging.getLogger("avocado.app")
err = None
if args.replay_teststatus and args.multiplex_files:
err = ("Option --replay-test-status is incompatible with "
"--multiplex.")
if args.replay_teststatus and 'mux' in args.replay_ignore:
err = ("Option `--replay-test-status` is incompatible with "
"`--replay-ignore mux`.")
elif args.replay_teststatus and args.url:
err = ("Option --replay-test-status is incompatible with "
"test URLs given on the command line.")
......@@ -195,29 +201,31 @@ class Replay(CLI):
else:
setattr(args, 'url', urls)
if args.replay_ignore and 'config' in args.replay_ignore:
if 'config' in args.replay_ignore:
log.warn("Ignoring configuration from source job with "
"--replay-ignore.")
else:
self.load_config(resultsdir)
if args.replay_ignore and 'mux' in args.replay_ignore:
if 'mux' in args.replay_ignore:
log.warn("Ignoring multiplex from source job with "
"--replay-ignore.")
else:
if getattr(args, 'multiplex_files', None) is not None:
log.warn('Overriding the replay multiplex with '
'--multiplex-file.')
# Use absolute paths to avoid problems with os.chdir
args.multiplex_files = [os.path.abspath(_)
for _ in args.multiplex_files]
mux = jobdata.retrieve_mux(resultsdir)
if mux is None:
log.error('Source job multiplex data not found. Aborting.')
sys.exit(exit_codes.AVOCADO_JOB_FAIL)
else:
mux = jobdata.retrieve_mux(resultsdir)
if mux is None:
log.error('Source job multiplex data not found. Aborting.')
sys.exit(exit_codes.AVOCADO_JOB_FAIL)
else:
setattr(args, "multiplex_files", mux)
# Ignore data manipulation. This is necessary, because
# we replaced the unparsed object with parsed one. There
# are other plugins running before/after this which might
# want to alter the mux object.
if len(args.mux.data) or args.mux.data.environment:
log.warning("Using src job Mux data only, use `--replay-"
"ignore mux` to override them.")
setattr(args, "mux", mux)
mux.data_merge = ignore_call
mux.data_inject = ignore_call
if args.replay_teststatus:
replay_map = self._create_replay_map(resultsdir,
......
......@@ -23,7 +23,6 @@ import sys
from avocado.core import exit_codes
from avocado.core import job
from avocado.core import loader
from avocado.core import multiplexer
from avocado.core.plugin_interfaces import CLICmd
from avocado.core.dispatcher import ResultDispatcher
from avocado.core.settings import settings
......@@ -37,7 +36,8 @@ class Run(CLICmd):
"""
name = 'run'
description = 'Run one or more tests (native test, test alias, binary or script)'
description = ("Runs one or more tests (native test, test alias, binary"
"or script)")
def configure(self, parser):
"""
......@@ -55,15 +55,16 @@ class Run(CLICmd):
help="Instead of running the test only "
"list them and log their params.")
parser.add_argument('-z', '--archive', action='store_true', default=False,
help='Archive (ZIP) files generated by tests')
parser.add_argument('-z', '--archive', action='store_true',
default=False, help='Archive (ZIP) files generated'
' by tests')
parser.add_argument('--force-job-id', dest='unique_job_id',
type=str, default=None,
help=('Forces the use of a particular job ID. Used '
'internally when interacting with an avocado '
'server. You should not use this option '
'unless you know exactly what you\'re doing'))
help='Forces the use of a particular job ID. Used '
'internally when interacting with an avocado '
'server. You should not use this option '
'unless you know exactly what you\'re doing')
parser.add_argument('--job-results-dir', action='store',
dest='logdir', default=None, metavar='DIRECTORY',
......@@ -72,37 +73,37 @@ class Run(CLICmd):
parser.add_argument('--job-timeout', action='store',
default=None, metavar='SECONDS',
help=('Set the maximum amount of time (in SECONDS) that '
'tests are allowed to execute. '
'Values <= zero means "no timeout". '
'You can also use suffixes, like: '
' s (seconds), m (minutes), h (hours). '))
help='Set the maximum amount of time (in SECONDS) '
'that tests are allowed to execute. '
'Values <= zero means "no timeout". '
'You can also use suffixes, like: '
' s (seconds), m (minutes), h (hours). ')
parser.add_argument('--failfast', choices=('on', 'off'),
help='Enable or disable the job interruption on '
'first failed test.')
'first failed test.')
sysinfo_default = settings.get_value('sysinfo.collect',
'enabled',
key_type='bool',
default=True)
sysinfo_default = 'on' if sysinfo_default is True else 'off'
parser.add_argument('--sysinfo', choices=('on', 'off'), default=sysinfo_default,
help=('Enable or disable system information '
'(hardware details, profilers, etc.). '
'Current: %(default)s'))
parser.add_argument('--sysinfo', choices=('on', 'off'),
default=sysinfo_default, help="Enable or disable "
"system information (hardware details, profilers, "
"etc.). Current: %(default)s")
parser.output = parser.add_argument_group('output and result format')
parser.output.add_argument(
'-s', '--silent', action="store_true", default=argparse.SUPPRESS,
help='Silence stdout')
parser.output.add_argument('-s', '--silent', action="store_true",
default=argparse.SUPPRESS,
help='Silence stdout')
parser.output.add_argument(
'--show-job-log', action='store_true', default=False,
help=('Display only the job log on stdout. Useful '
'for test debugging purposes. No output will '
'be displayed if you also specify --silent'))
parser.output.add_argument('--show-job-log', action='store_true',
default=False, help="Display only the job "
"log on stdout. Useful for test debugging "
"purposes. No output will be displayed if "
"you also specify --silent")
parser.output.add_argument("--store-logging-stream", nargs="*",
default=[], metavar="STREAM[:LEVEL]",
......@@ -114,31 +115,24 @@ class Run(CLICmd):
out_check.add_argument('--output-check-record',
choices=('none', 'all', 'stdout', 'stderr'),
default='none',
help=('Record output streams of your tests '
'to reference files (valid options: '
'none (do not record output streams), '
'all (record both stdout and stderr), '
'stdout (record only stderr), '
'stderr (record only stderr). '
'Current: %(default)s'))
help="Record output streams of your tests "
"to reference files (valid options: none (do "
"not record output streams), all (record both "
"stdout and stderr), stdout (record only "
"stderr), stderr (record only stderr). "
'Current: %(default)s')
out_check.add_argument('--output-check', choices=('on', 'off'),
default='on',
help=('Enable or disable test output (stdout/stderr) check. '
'If this option is off, no output will '
'be checked, even if there are reference files '
'present for the test. '
'Current: on (output check enabled)'))
help="Enable or disable test output (stdout/"
"stderr) check. If this option is off, no "
"output will be checked, even if there are "
"reference files present for the test. "
"Current: on (output check enabled)")
loader.add_loader_options(parser)
mux = parser.add_argument_group('test parameters')
if multiplexer.MULTIPLEX_CAPABLE:
mux.add_argument('-m', '--multiplex', nargs='*',
dest='multiplex_files',
default=None, metavar='FILE',
help='Location of one or more Avocado multiplex '
'(.yaml) FILE(s) (order dependent)')
mux.add_argument('--filter-only', nargs='*', default=[],
help='Filter only path(s) from multiplexing')
mux.add_argument('--filter-out', nargs='*', default=[],
......@@ -150,19 +144,6 @@ class Run(CLICmd):
help="Inject [path:]key:node values into the "
"final multiplex tree.")
def _activate(self, args):
# Extend default multiplex tree of --mux_inject values
for value in getattr(args, "mux_inject", []):
value = value.split(':', 2)
if len(value) < 2:
raise ValueError("key:value pairs required, found only %s"
% (value))
elif len(value) == 2:
args.default_avocado_params.value[value[0]] = value[1]
else:
node = args.default_avocado_params.get_node(value[0], True)
node.value[value[1]] = value[2]
def run(self, args):
"""
Run test modules or simple tests.
......@@ -170,7 +151,6 @@ class Run(CLICmd):
:param args: Command line args received from the run subparser.
"""
log = logging.getLogger("avocado.app")
self._activate(args)
if args.unique_job_id is not None:
try:
int(args.unique_job_id, 16)
......@@ -188,9 +168,9 @@ class Run(CLICmd):
job_run = job_instance.run()
result_dispatcher = ResultDispatcher()
if result_dispatcher.extensions:
# At this point job_instance doesn't have a single results attribute
# which is the end goal. For now, we pick any of the plugin classes
# added to the result proxy.
# At this point job_instance doesn't have a single results
# attribute which is the end goal. For now, we pick any of the
# plugin classes added to the result proxy.
if len(job_instance.result_proxy.output_plugins) > 0:
result = job_instance.result_proxy.output_plugins[0]
result_dispatcher.map_method('render', result, job_instance)
......
# 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 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See LICENSE for more details.
#
# Copyright: Red Hat Inc. 2016
# Author: Lukas Doktor <ldoktor@redhat.com>
"""Multiplexer plugin to parse yaml files to params"""
import logging
import os
import re
import sys
from avocado.core import tree, exit_codes
from avocado.core.plugin_interfaces import CLI
try:
import yaml
except ImportError:
MULTIPLEX_CAPABLE = False
else:
MULTIPLEX_CAPABLE = True
try:
from yaml import CLoader as Loader
except ImportError:
from yaml import Loader
# Mapping for yaml flags
YAML_INCLUDE = 100
YAML_USING = 101
YAML_REMOVE_NODE = tree.REMOVE_NODE
YAML_REMOVE_VALUE = tree.REMOVE_VALUE
YAML_MUX = 102
__RE_FILE_SPLIT = re.compile(r'(?<!\\):') # split by ':' but not '\\:'
__RE_FILE_SUBS = re.compile(r'(?<!\\)\\:') # substitute '\\:' but not '\\\\:'
class Value(tuple): # Few methods pylint: disable=R0903
""" Used to mark values to simplify checking for node vs. value """
pass
class ListOfNodeObjects(list): # Few methods pylint: disable=R0903
"""
Used to mark list as list of objects from whose node is going to be created
"""
pass
def _create_from_yaml(path, cls_node=tree.TreeNode):
""" Create tree structure from yaml stream """
def tree_node_from_values(name, values):
""" Create `name` node and add values """
node = cls_node(str(name))
using = ''
for value in values:
if isinstance(value, tree.TreeNode):
node.add_child(value)
elif isinstance(value[0], tree.Control):
if value[0].code == YAML_INCLUDE:
# Include file
ypath = value[1]
if not os.path.isabs(ypath):
ypath = os.path.join(os.path.dirname(path), ypath)
if not os.path.exists(ypath):
raise ValueError("File '%s' included from '%s' does not "
"exist." % (ypath, path))
node.merge(_create_from_yaml('/:' + ypath, cls_node))
elif value[0].code == YAML_USING:
if using:
raise ValueError("!using can be used only once per "
"node! (%s:%s)" % (path, name))
using = value[1]
if using[0] == '/':
using = using[1:]
if using[-1] == '/':
using = using[:-1]
elif value[0].code == YAML_REMOVE_NODE:
value[0].value = value[1] # set the name
node.ctrl.append(value[0]) # add "blue pill" of death
elif value[0].code == YAML_REMOVE_VALUE:
value[0].value = value[1] # set the name
node.ctrl.append(value[0])
elif value[0].code == YAML_MUX:
node.multiplex = True
else:
node.value[value[0]] = value[1]
if using:
if name is not '':
for name in using.split('/')[::-1]:
node = cls_node(name, children=[node])
else:
using = using.split('/')[::-1]
node.name = using.pop()
while True:
if not using:
break
name = using.pop() # 'using' is list pylint: disable=E1101
node = cls_node(name, children=[node])
node = cls_node('', children=[node])
return node
def mapping_to_tree_loader(loader, node):
""" Maps yaml mapping tag to TreeNode structure """
_value = []
for key_node, value_node in node.value:
if key_node.tag.startswith('!'): # reflect tags everywhere
key = loader.construct_object(key_node)
else:
key = loader.construct_python_str(key_node)
value = loader.construct_object(value_node)
_value.append((key, value))
objects = ListOfNodeObjects()
for name, values in _value:
if isinstance(values, ListOfNodeObjects): # New node from list
objects.append(tree_node_from_values(name, values))
elif values is None: # Empty node
objects.append(cls_node(str(name)))
else: # Values
objects.append(Value((name, values)))
return objects
def mux_loader(loader, obj):
"""
Special !mux loader which allows to tag node as 'multiplex = True'.
"""
if not isinstance(obj, yaml.ScalarNode):
objects = mapping_to_tree_loader(loader, obj)
else: # This means it's empty node. Don't call mapping_to_tree_loader
objects = ListOfNodeObjects()
objects.append((tree.Control(YAML_MUX), None))
return objects
Loader.add_constructor(u'!include',
lambda loader, node: tree.Control(YAML_INCLUDE))
Loader.add_constructor(u'!using',
lambda loader, node: tree.Control(YAML_USING))
Loader.add_constructor(u'!remove_node',
lambda loader, node: tree.Control(YAML_REMOVE_NODE))
Loader.add_constructor(u'!remove_value',
lambda loader, node: tree.Control(YAML_REMOVE_VALUE))
Loader.add_constructor(u'!mux', mux_loader)
Loader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
mapping_to_tree_loader)
# Parse file name ([$using:]$path)
path = __RE_FILE_SPLIT.split(path, 1)
if len(path) == 1:
path = __RE_FILE_SUBS.sub(':', path[0])
using = ["run"]
else:
nodes = __RE_FILE_SUBS.sub(':', path[0]).strip('/').split('/')
using = [node for node in nodes if node]
if not path[0].startswith('/'): # relative path, put into /run
using.insert(0, 'run')
path = __RE_FILE_SUBS.sub(':', path[1])
# Load the tree
with open(path) as stream:
loaded_tree = yaml.load(stream, Loader)
if loaded_tree is None:
return
loaded_tree = tree_node_from_values('', loaded_tree)
# Add prefix
if using:
loaded_tree.name = using.pop()
while True:
if not using:
break
loaded_tree = cls_node(using.pop(), children=[loaded_tree])
loaded_tree = cls_node('', children=[loaded_tree])
return loaded_tree
def create_from_yaml(paths, debug=False):
"""
Create tree structure from yaml-like file
:param fileobj: File object to be processed
:raise SyntaxError: When yaml-file is corrupted
:return: Root of the created tree structure
"""
def _merge(data, path):
""" Normal run """
tmp = _create_from_yaml(path)
if tmp:
data.merge(tmp)
def _merge_debug(data, path):
""" Use NamedTreeNodeDebug magic """
node_cls = tree.get_named_tree_cls(path)
tmp = _create_from_yaml(path, node_cls)
if tmp:
data.merge(tmp)
if not debug:
data = tree.TreeNode()
merge = _merge
else:
data = tree.TreeNodeDebug()
merge = _merge_debug
path = None
try:
for path in paths:
merge(data, path)
# Yaml can raise IndexError on some files
except (yaml.YAMLError, IndexError) as details:
if 'mapping values are not allowed in this context' in str(details):
details = ("%s\nMake sure !tags and colons are separated by a "
"space (eg. !include :)" % details)
msg = "Invalid multiplex file '%s': %s" % (path, details)
raise IOError(2, msg, path)
return data
class YamlToMux(CLI):
"""
Registers callback to inject params from yaml file to the
"""
name = 'yaml_to_mux'
description = "YamlToMux options for the 'run' subcommand"
def configure(self, parser):
"""
Configures "run" and "multiplex" subparsers
"""
if not MULTIPLEX_CAPABLE:
return
for name in ("run", "multiplex"):
subparser = parser.subcommands.choices.get(name, None)
if subparser is None:
continue
mux = subparser.add_argument_group("yaml to mux options")
mux.add_argument("-m", "--mux-yaml", nargs='*', metavar="FILE",
help="Location of one or more Avocado"
" multiplex (.yaml) FILE(s) (order dependent)")
mux.add_argument("--multiplex", nargs='*',
default=None, metavar="FILE",
help="DEPRECATED: Location of one or more Avocado"
" multiplex (.yaml) FILE(s) (order dependent)")
def run(self, args):
# Merge the multiplex
multiplex_files = getattr(args, "mux_yaml", None)
if multiplex_files:
debug = getattr(args, "mux_debug", False)
try:
args.mux.data_merge(create_from_yaml(multiplex_files, debug))
except IOError as details:
logging.getLogger("avocado.app").error(details.strerror)
sys.exit(exit_codes.AVOCADO_JOB_FAIL)
# Deprecated --multiplex option
multiplex_files = getattr(args, "multiplex", None)
if multiplex_files:
msg = ("The use of `--multiplex` is deprecated, use `--mux-yaml` "
"instead.")
logging.getLogger("avocado.test").warning(msg)
debug = getattr(args, "mux_debug", False)
try:
args.mux.data_merge(create_from_yaml(multiplex_files, debug))
except IOError as details:
logging.getLogger("avocado.app").error(details.strerror)
sys.exit(exit_codes.AVOCADO_JOB_FAIL)
......@@ -69,7 +69,7 @@ place, the test notifies you and you can investigate the problem. This is
demonstrated in ``examples/tests/doublefree_nasty.py`` test. To unveil the
power of Avocado, run this test using::
avocado run --gdb-run-bin=doublefree: examples/tests/doublefree_nasty.py --gdb-prerun-commands examples/tests/doublefree_nasty.py.data/gdb_pre --multiplex examples/tests/doublefree_nasty.py.data/iterations.yaml
avocado run --gdb-run-bin=doublefree: examples/tests/doublefree_nasty.py --gdb-prerun-commands examples/tests/doublefree_nasty.py.data/gdb_pre --mux-yaml examples/tests/doublefree_nasty.py.data/iterations.yaml
which executes 100 iterations of this test while setting all breakpoints from
the ``examples/tests/doublefree_nasty.py.data/gdb_pre`` file (you can specify
......
.. _multiplex_configuration:
.. _mux:
=======================
Multiplex Configuration
=======================
===================
Test variants - Mux
===================
The ``Mux`` is a special mechanism to produce multiple variants of the same
test with different parameters. This is essential in order to get a decent
coverage and avocado allows several ways to define those parameters from
simple enumeration of key/value pairs to complex trees which allows in simple
manner define test matrices with all possible variants.
This sounds similar to sparse matrix jobs in Jenkins, but the difference is
that instead of filters, which are available too, avocado allows specifying
so called ``mux domains``, which is a nicer way to represent data.
As the data is represented in trees it creates all possible variants
per domain and then all combinations of these. It sounds complicated, but
in reality it follows the way people are used to define dependencies,
therefor it's very simple to use and clear even in complex cases.
The best explanation comes usually from examples, so feel free to scroll down
to `yaml_to_mux plugin`_ section, which uses the default mux plugin to feed
the Mux.
Mux internals
-------------
The ``Mux`` is a core part of avocado and one can see it as a ``multiplexed``
database, which contains key/value pairs associated to given paths and
as we are talking about a tree of those, we call the paths ``Nodes``.
Mux allows iterating through all possible combinations which are stored in
the database, which is called ``multiplexation``. Mux yields ``variants``,
which are lists of leaf nodes with their values, which are then processed
into ``AvocadoParams``. Those params are available in tests as
``self.params`` and one can query for the current parameters::
self.params.get(key="my_key", path="/some/location/*",
default="default_value")
Let's get back to Mux for a while. As mentioned earlier, it's a database
which allows storing multiple variants of test parameters. To fill the
database, you can use several commands.
1. ``--mux-inject`` - injects directly [path:]key:node values from the
cmdline (see ``avocado multiplex -h``)
2. ``yaml_to_mux plugin`` - allows parsing ``yaml`` files into the Mux
database (see `yaml_to_mux plugin`_)
3. Custom plugin using the simple ``Mux`` API (see `mux_api`_)
.. _mux_api:
Mux API
-------
.. warning:: This API is internal, we might change it at any moment. On the
other hand we maintain ``avocado-virt`` plugin which uses this
API so in such case we'd provide a patch there demonstrating
the necessary changes.
The ``Mux`` object is defined in ``avocado/core/multiplexer.py``, is always
instantiated in ``avocado.core.parser.py`` and always available in
``args.mux``. The basic workflow is:
1. Initialize ``Mux`` in ``args.mux``
2. Fill it with data (``plugins`` or ``job``)
3. Multiplex it (in ``job``)
4. Iterate through all variants on all job's tests
Once the ``Mux`` object is multiplexed (3), it's restricted to alter the
data (2) to avoid changing the already produced data.
The main API needed for your plugins, which we are going to try keeping as
stable as possible is:
* mux.is_parsed() - to find out whether the object was already parsed
* data_inject(key, value, path=None) - to inject key/value pairs optionaly
to a given path (by default '/')
* data_merge(tree) - to merge ``avocado.core.tree.TreeNode``-like tree
into the database.
Given these you should be able to implement any kind of parser or params
feeder, should you require one. We favor ``yaml`` and therefor we implemented
a ``yaml_to_mux`` plugin which can be found in
``avocado/plugins/yaml_to_mux.py`` and on it we also describe the way
``Mux`` works: `yaml_to_mux plugin`_
Yaml_to_mux plugin
==================
In order to get a good coverage one always needs to execute the same test
with different parameters or in various environments. Avocado uses the
term ``Multiplexation`` to generate multiple variants of the same test with
different values. To define these variants and values
term ``Multiplexation`` or ``Mux`` to generate multiple variants of the same
test with different values. To define these variants and values
`YAML <http://www.yaml.org/>`_ files are used. The benefit of using YAML
file is the visible separation of different scopes. Even very advanced setups
are still human readable, unlike traditional sparse, multi-dimensional-matrices
......@@ -48,7 +135,7 @@ flags (lines 2, 9, 14, 19) which modifies the behavior.
Nodes
=====
-----
They define context of the key=>value pairs allowing us to easily identify
for what this values might be used for and also it makes possible to define
......@@ -59,7 +146,7 @@ is disabled, so the value of node name is always as written in the yaml
file (unlike values, where `yes` converts to `True` and such).
Nodes are organized in parent-child relationship and together they create
a tree. To view this structure use ``avocado multiplex --tree <file>``::
a tree. To view this structure use ``avocado multiplex --tree -m <file>``::
┗━━ run
┣━━ hw
......@@ -85,7 +172,7 @@ parameters available in tests.
Keys and Values
===============
---------------
Every value other than dict (4,6,8,11) is used as value of the antecedent
node.
......@@ -127,7 +214,7 @@ This means that the value can be of any YAML supported value, eg. bool, None,
list or custom type, while the key is always string.
Variants
========
--------
In the end all leaves are gathered and turned into parameters, more specifically into
``AvocadoParams``::
......@@ -182,7 +269,7 @@ Results in::
Resolution order
================
----------------
You can see that only leaves are part of the test parameters. It might happen
that some of these leaves contain different values of the same key. Then
......@@ -225,11 +312,11 @@ relative paths (the ones starting with ``*``)
Injecting files
===============
---------------
You can run any test with any YAML file by::
avocado run sleeptest.py --multiplex file.yaml
avocado run sleeptest.py --mux-yaml file.yaml
This puts the content of ``file.yaml`` into ``/run``
location, which as mentioned in previous section, is the default ``mux-path``
......@@ -241,7 +328,7 @@ when you have two files and you don't want the content to be merged into
a single place becomming effectively a single blob, you can do that by
giving a name to your yaml file::
avocado run sleeptest.py --multiplex duration:duration.yaml
avocado run sleeptest.py --mux-yaml duration:duration.yaml
The content of ``duration.yaml`` is injected into ``/run/duration``. Still when
keys from other files don't clash, you can use ``params.get(key)`` and retrieve
......@@ -253,7 +340,7 @@ multiple files by using the same or different name, or even a complex
Last but not least, advanced users can inject the file into whatever location
they prefer by::
avocado run sleeptest.py --multiplex /my/variants/duration:duration.yaml
avocado run sleeptest.py --mux-yaml /my/variants/duration:duration.yaml
Simple ``params.get(key)`` won't look in this location, which might be the
intention of the test writer. There are several ways to access the values:
......@@ -272,7 +359,7 @@ parameters.
Multiple files
==============
--------------
You can provide multiple files. In such scenario final tree is a combination
of the provided files where later nodes with the same name override values of
......@@ -318,7 +405,7 @@ Whole file is **merged** into the node where it's defined.
Advanced YAML tags
==================
------------------
There are additional features related to YAML files. Most of them require values
separated by ``":"``. Again, in all such cases it's mandatory to add a white space
......@@ -391,7 +478,7 @@ child, etc. Example is in section `Variants`_
Complete example
================
----------------
Let's take a second look at the first example::
......@@ -422,7 +509,7 @@ Let's take a second look at the first example::
After filters are applied (simply removes non-matching variants), leaves
are gathered and all variants are generated::
$ avocado multiplex examples/mux-environment.yaml
$ avocado multiplex -m examples/mux-environment.yaml
Variants generated:
Variant 1: /hw/cpu/intel, /hw/disk/scsi, /distro/fedora, /env/debug
Variant 2: /hw/cpu/intel, /hw/disk/scsi, /distro/fedora, /env/prod
......
......@@ -40,7 +40,7 @@ The replay feature will retrieve the original job urls, the multiplex
tree and the configuration. Let's see another example, now using
multiplex file::
$ avocado run /bin/true /bin/false --multiplex mux-environment.yaml
$ avocado run /bin/true /bin/false --mux-yaml mux-environment.yaml
JOB ID : bd6aa3b852d4290637b5e771b371537541043d1d
JOB LOG : $HOME/avocado/job-results/job-2016-01-11T21.56-bd6aa3b/job.log
TESTS : 48
......
......@@ -55,7 +55,7 @@ Note that the test class provides you with a number of convenience attributes:
of ``self.log``. It lets you log debug, info, error and warning messages.
* A parameter passing system (and fetching system) that can be accessed by
means of ``self.params``. This is hooked to the Multiplexer, about which
you can find that more information at :doc:`MultiplexConfig`.
you can find that more information at :doc:`Mux`.
Saving test generated (custom) data
===================================
......@@ -88,7 +88,7 @@ Accessing test parameters
Each test has a set of parameters that can be accessed through
``self.params.get($name, $path=None, $default=None)``.
Avocado finds and populates ``self.params`` with all parameters you define on
a Multiplex Config file (see :doc:`MultiplexConfig`). As an example, consider
a Multiplex Config file (see :doc:`Mux`). As an example, consider
the following multiplex file for sleeptest::
sleeptest:
......@@ -101,9 +101,9 @@ the following multiplex file for sleeptest::
long:
sleep_length: 5
When running this example by ``avocado run $test --multiplex $file.yaml``
When running this example by ``avocado run $test --mux-yaml $file.yaml``
three variants are executed and the content is injected into ``/run`` namespace
(see :doc:`MultiplexConfig` for details). Every variant contains variables
(see :doc:`Mux` for details). Every variant contains variables
"type" and "sleep_length". To obtain the current value, you need the name
("sleep_length") and its path. The path differs for each variant so it's
needed to use the most suitable portion of the path, in this example:
......@@ -146,7 +146,7 @@ simply inject the values elsewhere (eg. `/run/sleeptest` =>
default path, which won't generate clash, but would return their values
instead. Then you need to clarify the path (eg. `'*'` => `sleeptest/*`)
More details on that are in :doc:`MultiplexConfig`
More details on that are in :doc:`Mux`
Using a multiplex file
======================
......@@ -154,7 +154,7 @@ Using a multiplex file
You may use the Avocado runner with a multiplex file to provide params and matrix
generation for sleeptest just like::
$ avocado run sleeptest.py --multiplex examples/tests/sleeptest.py.data/sleeptest.yaml
$ avocado run sleeptest.py --mux-yaml examples/tests/sleeptest.py.data/sleeptest.yaml
JOB ID : d565e8dec576d6040f894841f32a836c751f968f
JOB LOG : $HOME/avocado/job-results/job-2014-08-12T15.44-d565e8de/job.log
TESTS : 3
......@@ -165,8 +165,8 @@ generation for sleeptest just like::
TESTS TIME : 6.50 s
JOB HTML : $HOME/avocado/job-results/job-2014-08-12T15.44-d565e8de/html/results.html
The ``--multiplex`` accepts either only ``$FILE_LOCATION`` or ``$INJECT_TO:$FILE_LOCATION``.
As explained in :doc:`MultiplexConfig` without any path the content gets
The ``--mux-yaml`` accepts either only ``$FILE_LOCATION`` or ``$INJECT_TO:$FILE_LOCATION``.
As explained in :doc:`Mux` without any path the content gets
injected into ``/run`` in order to be in the default relative path location.
The ``$INJECT_TO`` can be either relative path, then it's injected into
``/run/$INJECT_TO`` location, or absolute path (starting with ``'/'``), then
......@@ -174,19 +174,19 @@ it's injected directly into the specified path and it's up to the test/framework
developer to get the value from this location (using path or adding the path to
``mux-path``). To understand the difference execute those commands::
$ avocado multiplex -t examples/tests/sleeptest.py.data/sleeptest.yaml
$ avocado multiplex -t duration:examples/tests/sleeptest.py.data/sleeptest.yaml
$ avocado multiplex -t /my/location:examples/tests/sleeptest.py.data/sleeptest.yaml
$ avocado multiplex -t -m examples/tests/sleeptest.py.data/sleeptest.yaml
$ avocado multiplex -t -m duration:examples/tests/sleeptest.py.data/sleeptest.yaml
$ avocado multiplex -t -m /my/location:examples/tests/sleeptest.py.data/sleeptest.yaml
Note that, as your multiplex file specifies all parameters for sleeptest, you
can't leave the test ID empty::
$ scripts/avocado run --multiplex examples/tests/sleeptest/sleeptest.yaml
$ scripts/avocado run --mux-yaml examples/tests/sleeptest/sleeptest.yaml
Empty test ID. A test path or alias must be provided
You can also execute multiple tests with the same multiplex file::
$ avocado run sleeptest.py synctest.py --multiplex examples/tests/sleeptest.py.data/sleeptest.yaml
$ avocado run sleeptest.py synctest.py --mux-yaml examples/tests/sleeptest.py.data/sleeptest.yaml
JOB ID : cd20fc8d1714da6d4791c19322374686da68c45c
JOB LOG : $HOME/avocado/job-results/job-2016-05-04T09.25-cd20fc8/job.log
TESTS : 8
......@@ -775,7 +775,7 @@ impact your test grid. You can account for that possibility and set up a
::
$ avocado run sleeptest.py --multiplex /tmp/sleeptest-example.yaml
$ avocado run sleeptest.py --mux-yaml /tmp/sleeptest-example.yaml
JOB ID : 6d5a2ff16bb92395100fbc3945b8d253308728c9
JOB LOG : $HOME/avocado/job-results/job-2014-08-12T15.52-6d5a2ff1/job.log
TESTS : 1
......@@ -1099,7 +1099,7 @@ Here are the current variables that Avocado exports to the tests:
+-------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------+
| AVOCADO_TEST_SYSINFODIR | The system information directory | $HOME/logs/job-results/job-2014-09-16T14.38-ac332e6/test-results/$HOME/my_test.sh.1/sysinfo |
+-------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------+
| * | All variables from --multiplex-file | TIMEOUT=60; IO_WORKERS=10; VM_BYTES=512M; ... |
| * | All variables from --mux-yaml | TIMEOUT=60; IO_WORKERS=10; VM_BYTES=512M; ... |
+-------------------------+---------------------------------------+-----------------------------------------------------------------------------------------------------+
......
......@@ -14,7 +14,7 @@ Contents:
Configuration
Loaders
LoggingSystem
MultiplexConfig
Mux
Replay
Diff
RunningTestsRemotely
......
......@@ -34,27 +34,32 @@ class MultiplexTests(unittest.TestCase):
def setUp(self):
self.tmpdir = tempfile.mkdtemp(prefix='avocado_' + __name__)
def run_and_check(self, cmd_line, expected_rc):
def run_and_check(self, cmd_line, expected_rc, tests=None):
os.chdir(basedir)
result = process.run(cmd_line, ignore_status=True)
self.assertEqual(result.exit_status, expected_rc,
"Command %s did not return rc "
"%d:\n%s" % (cmd_line, expected_rc, result))
if tests:
exp = ("PASS %s | ERROR 0 | FAIL %s | SKIP 0 | WARN 0 | "
"INTERRUPT 0" % tests)
self.assertIn(exp, result.stdout, "%s not in stdout:\n%s"
% (exp, result))
return result
def test_mplex_plugin(self):
cmd_line = './scripts/avocado multiplex examples/tests/sleeptest.py.data/sleeptest.yaml'
cmd_line = './scripts/avocado multiplex -m examples/tests/sleeptest.py.data/sleeptest.yaml'
expected_rc = exit_codes.AVOCADO_ALL_OK
self.run_and_check(cmd_line, expected_rc)
def test_mplex_plugin_nonexistent(self):
cmd_line = './scripts/avocado multiplex nonexist'
cmd_line = './scripts/avocado multiplex -m nonexist'
expected_rc = exit_codes.AVOCADO_JOB_FAIL
result = self.run_and_check(cmd_line, expected_rc)
self.assertIn('No such file or directory', result.stderr)
def test_mplex_debug(self):
cmd_line = ('./scripts/avocado multiplex -c -d '
cmd_line = ('./scripts/avocado multiplex -c -d -m '
'/:examples/mux-selftest.yaml '
'/:examples/mux-environment.yaml '
'/:examples/mux-selftest.yaml '
......@@ -65,41 +70,47 @@ class MultiplexTests(unittest.TestCase):
def test_run_mplex_noid(self):
cmd_line = ('./scripts/avocado run --job-results-dir %s --sysinfo=off '
'--multiplex examples/tests/sleeptest.py.data/sleeptest.yaml' % self.tmpdir)
'-m examples/tests/sleeptest.py.data/sleeptest.yaml' % self.tmpdir)
expected_rc = exit_codes.AVOCADO_JOB_FAIL
self.run_and_check(cmd_line, expected_rc)
def test_run_mplex_passtest(self):
cmd_line = ('./scripts/avocado run --job-results-dir %s --sysinfo=off '
'passtest.py --multiplex '
'passtest.py -m '
'examples/tests/sleeptest.py.data/sleeptest.yaml'
% self.tmpdir)
expected_rc = exit_codes.AVOCADO_ALL_OK
self.run_and_check(cmd_line, expected_rc)
self.run_and_check(cmd_line, expected_rc, (4, 0))
def test_run_mplex_doublepass(self):
cmd_line = ('./scripts/avocado run --job-results-dir %s --sysinfo=off '
'passtest.py passtest.py --multiplex '
'passtest.py passtest.py -m '
'examples/tests/sleeptest.py.data/sleeptest.yaml'
% self.tmpdir)
self.run_and_check(cmd_line, expected_rc=0)
self.run_and_check(cmd_line, exit_codes.AVOCADO_ALL_OK, (8, 0))
def test_run_mplex_failtest(self):
cmd_line = ('./scripts/avocado run --job-results-dir %s --sysinfo=off '
'passtest.py failtest.py --multiplex '
'passtest.py failtest.py -m '
'examples/tests/sleeptest.py.data/sleeptest.yaml'
% self.tmpdir)
expected_rc = exit_codes.AVOCADO_TESTS_FAIL
self.run_and_check(cmd_line, expected_rc)
self.run_and_check(cmd_line, expected_rc, (4, 4))
def test_run_double_mplex(self):
cmd_line = ('./scripts/avocado run --job-results-dir %s --sysinfo=off '
'passtest.py --multiplex '
'passtest.py -m '
'examples/tests/sleeptest.py.data/sleeptest.yaml '
'examples/tests/sleeptest.py.data/sleeptest.yaml'
% self.tmpdir)
expected_rc = exit_codes.AVOCADO_ALL_OK
self.run_and_check(cmd_line, expected_rc)
self.run_and_check(cmd_line, expected_rc, (4, 0))
def test_empty_file(self):
cmd_line = ("./scripts/avocado run -m selftests/.data/empty_file "
"-- passtest.py")
result = self.run_and_check(cmd_line, exit_codes.AVOCADO_ALL_OK,
(1, 0))
def test_run_mplex_params(self):
for variant_msg in (('/run/short', 'A'),
......@@ -107,7 +118,7 @@ class MultiplexTests(unittest.TestCase):
('/run/long', 'This is very long\nmultiline\ntext.')):
variant, msg = variant_msg
cmd_line = ('./scripts/avocado run --job-results-dir %s --sysinfo=off examples/tests/env_variables.sh '
'--multiplex examples/tests/env_variables.sh.data/env_variables.yaml '
'-m examples/tests/env_variables.sh.data/env_variables.yaml '
'--filter-only %s --show-job-log' % (self.tmpdir, variant))
expected_rc = exit_codes.AVOCADO_ALL_OK
result = self.run_and_check(cmd_line, expected_rc)
......
......@@ -24,8 +24,7 @@ class ReplayTests(unittest.TestCase):
def setUp(self):
self.tmpdir = tempfile.mkdtemp(prefix='avocado_' + __name__)
cmd_line = ('./scripts/avocado run passtest.py '
'--multiplex '
'examples/tests/sleeptest.py.data/sleeptest.yaml '
'-m examples/tests/sleeptest.py.data/sleeptest.yaml '
'--job-results-dir %s --sysinfo=off --json -' %
self.tmpdir)
expected_rc = exit_codes.AVOCADO_ALL_OK
......@@ -170,14 +169,14 @@ class ReplayTests(unittest.TestCase):
"""
Runs a replay job with custom a mux and using '--replay-test-status'
"""
cmd_line = ('./scripts/avocado run --replay %s --multiplex '
'examples/mux-environment.yaml --replay-test-status FAIL '
cmd_line = ('./scripts/avocado run --replay %s --replay-ignore mux '
'--replay-test-status FAIL '
'--job-results-dir %s --replay-data-dir %s '
'--sysinfo=off' % (self.jobid, self.tmpdir, self.jobdir))
expected_rc = exit_codes.AVOCADO_FAIL
result = self.run_and_check(cmd_line, expected_rc)
msg = "Option --replay-test-status is incompatible with "\
"--multiplex."
msg = ("Option `--replay-test-status` is incompatible with "
"`--replay-ignore mux`")
self.assertIn(msg, result.stderr)
def test_run_replay_status_and_urls(self):
......
......@@ -26,8 +26,7 @@ class ReplayExtRunnerTests(unittest.TestCase):
self.tmpdir = tempfile.mkdtemp(prefix='avocado_' + __name__)
test = script.make_script(os.path.join(self.tmpdir, 'test'), 'exit 0')
cmd_line = ('./scripts/avocado run %s '
'--multiplex '
'examples/tests/sleeptest.py.data/sleeptest.yaml '
'-m examples/tests/sleeptest.py.data/sleeptest.yaml '
'--external-runner /bin/bash '
'--job-results-dir %s --sysinfo=off --json -' %
(test, self.tmpdir))
......
......@@ -4,6 +4,7 @@ import sys
from avocado.core import multiplexer
from avocado.core import tree
from avocado.plugins import yaml_to_mux
if sys.version_info[:2] == (2, 6):
import unittest2 as unittest
......@@ -24,54 +25,48 @@ def combine(leaves_pools):
class TestMultiplex(unittest.TestCase):
@unittest.skipIf(not yaml_to_mux.MULTIPLEX_CAPABLE,
"Not multiplex capable")
def setUp(self):
self.mux_tree = tree.create_from_yaml(['/:' + PATH_PREFIX +
'examples/mux-selftest.yaml'])
self.mux_tree = yaml_to_mux.create_from_yaml(['/:' + PATH_PREFIX +
'examples/mux-selftest.'
'yaml'])
self.mux_full = tuple(multiplexer.MuxTree(self.mux_tree))
@unittest.skipIf(not tree.MULTIPLEX_CAPABLE, "Not multiplex capable")
def test_empty(self):
act = tuple(multiplexer.MuxTree(tree.TreeNode()))
self.assertEqual(act, (['', ],))
@unittest.skipIf(not tree.MULTIPLEX_CAPABLE, "Not multiplex capable")
def test_partial(self):
exp = (['intel', 'scsi'], ['intel', 'virtio'], ['amd', 'scsi'],
['amd', 'virtio'], ['arm', 'scsi'], ['arm', 'virtio'])
act = tuple(multiplexer.MuxTree(self.mux_tree.children[0]))
self.assertEqual(act, exp)
@unittest.skipIf(not tree.MULTIPLEX_CAPABLE, "Not multiplex capable")
def test_full(self):
self.assertEqual(len(self.mux_full), 12)
@unittest.skipIf(not tree.MULTIPLEX_CAPABLE, "Not multiplex capable")
def test_create_variants(self):
from_file = multiplexer.yaml2tree(
from_file = yaml_to_mux.create_from_yaml(
["/:" + PATH_PREFIX + 'examples/mux-selftest.yaml'])
from_file = multiplexer.MuxTree(from_file)
self.assertEqual(self.mux_full, tuple(from_file))
# Filters are tested in tree_unittests, only verify `multiplex_yamls` calls
@unittest.skipIf(not tree.MULTIPLEX_CAPABLE, "Not multiplex capable")
def test_filter_only(self):
exp = (['intel', 'scsi'], ['intel', 'virtio'])
act = multiplexer.yaml2tree(["/:" + PATH_PREFIX +
'examples/mux-selftest.yaml'],
('/hw/cpu/intel',
'/distro/fedora',
'/hw'))
act = yaml_to_mux.create_from_yaml(["/:" + PATH_PREFIX +
'examples/mux-selftest.yaml'])
act = tree.apply_filters(act, ('/hw/cpu/intel', '/distro/fedora',
'/hw'))
act = tuple(multiplexer.MuxTree(act))
self.assertEqual(act, exp)
@unittest.skipIf(not tree.MULTIPLEX_CAPABLE, "Not multiplex capable")
def test_filter_out(self):
act = multiplexer.yaml2tree(["/:" + PATH_PREFIX +
'examples/mux-selftest.yaml'],
None,
('/hw/cpu/intel',
'/distro/fedora',
'/distro'))
act = yaml_to_mux.create_from_yaml(["/:" + PATH_PREFIX +
'examples/mux-selftest.yaml'])
act = tree.apply_filters(act, None, ('/hw/cpu/intel', '/distro/fedora',
'/distro'))
act = tuple(multiplexer.MuxTree(act))
self.assertEqual(len(act), 4)
self.assertEqual(len(act[0]), 3)
......@@ -85,8 +80,8 @@ class TestMultiplex(unittest.TestCase):
class TestAvocadoParams(unittest.TestCase):
def setUp(self):
yamls = multiplexer.yaml2tree(["/:" + PATH_PREFIX +
'examples/mux-selftest-params.yaml'])
yamls = yaml_to_mux.create_from_yaml(["/:" + PATH_PREFIX +
'examples/mux-selftest-params.yaml'])
self.yamls = iter(multiplexer.MuxTree(yamls))
self.params1 = multiplexer.AvocadoParams(self.yamls.next(), 'Unittest1',
['/ch0/*', '/ch1/*'], {})
......@@ -95,13 +90,13 @@ class TestAvocadoParams(unittest.TestCase):
self.params2 = multiplexer.AvocadoParams(self.yamls.next(), 'Unittest2',
['/ch1/*', '/ch0/*'], {})
@unittest.skipIf(not tree.MULTIPLEX_CAPABLE, "Not multiplex capable")
@unittest.skipIf(not yaml_to_mux.MULTIPLEX_CAPABLE, "Not multiplex capable")
def test_pickle(self):
params = pickle.dumps(self.params1, 2) # protocol == 2
params = pickle.loads(params)
self.assertEqual(self.params1, params)
@unittest.skipIf(not tree.MULTIPLEX_CAPABLE, "Not multiplex capable")
@unittest.skipIf(not yaml_to_mux.MULTIPLEX_CAPABLE, "Not multiplex capable")
def test_basic(self):
self.assertEqual(self.params1, self.params1)
self.assertNotEqual(self.params1, self.params2)
......@@ -110,7 +105,7 @@ class TestAvocadoParams(unittest.TestCase):
str(multiplexer.AvocadoParams([], 'Unittest', [], {}))
self.assertEqual(15, sum([1 for _ in self.params1.iteritems()]))
@unittest.skipIf(not tree.MULTIPLEX_CAPABLE, "Not multiplex capable")
@unittest.skipIf(not yaml_to_mux.MULTIPLEX_CAPABLE, "Not multiplex capable")
def test_unhashable(self):
""" Verifies that unhashable arguments can be passed to params.get """
self.assertEqual(self.params1.get("root", "/ch0/", ["foo"]), ["foo"])
......@@ -118,7 +113,7 @@ class TestAvocadoParams(unittest.TestCase):
'/ch0/ch0.1/ch0.1.1/ch0.1.1.1/',
['bar']), 'unique1')
@unittest.skipIf(not tree.MULTIPLEX_CAPABLE, "Not multiplex capable")
@unittest.skipIf(not yaml_to_mux.MULTIPLEX_CAPABLE, "Not multiplex capable")
def test_get_abs_path(self):
# /ch0/ is not leaf thus it's not queryable
self.assertEqual(self.params1.get('root', '/ch0/', 'bbb'), 'bbb')
......@@ -140,7 +135,7 @@ class TestAvocadoParams(unittest.TestCase):
'/ch0/ch0.1/ch0.1.1/ch0.1.1.1/',
'hhh'), 'hhh')
@unittest.skipIf(not tree.MULTIPLEX_CAPABLE, "Not multiplex capable")
@unittest.skipIf(not yaml_to_mux.MULTIPLEX_CAPABLE, "Not multiplex capable")
def test_get_greedy_path(self):
self.assertEqual(self.params1.get('unique1', '/*/*/*/ch0.1.1.1/',
111), 'unique1')
......@@ -164,7 +159,7 @@ class TestAvocadoParams(unittest.TestCase):
# path matches nothing
self.assertEqual(self.params1.get('root', '', 999), 999)
@unittest.skipIf(not tree.MULTIPLEX_CAPABLE, "Not multiplex capable")
@unittest.skipIf(not yaml_to_mux.MULTIPLEX_CAPABLE, "Not multiplex capable")
def test_get_rel_path(self):
self.assertEqual(self.params1.get('root', default='iii'), 'root')
self.assertEqual(self.params1.get('unique1', '*', 'jjj'), 'unique1')
......@@ -179,7 +174,7 @@ class TestAvocadoParams(unittest.TestCase):
self.assertEqual(self.params2.get('unique1', '*/ch0.1.1.1/', 'ooo'),
'ooo')
@unittest.skipIf(not tree.MULTIPLEX_CAPABLE, "Not multiplex capable")
@unittest.skipIf(not yaml_to_mux.MULTIPLEX_CAPABLE, "Not multiplex capable")
def test_get_clashes(self):
# One inherited, the other is new
self.assertRaisesRegexp(ValueError, r"'clash1'.* \['/ch0/ch0.1/ch0.1.1"
......
......@@ -38,7 +38,7 @@ class RemoteTestRunnerTest(unittest.TestCase):
remote_no_copy=False,
remote_timeout=60,
show_job_log=False,
multiplex_files=['foo.yaml', 'bar/baz.yaml'],
multiplex=['foo.yaml', 'bar/baz.yaml'],
dry_run=True,
env_keep=None)
log = flexmock()
......@@ -105,7 +105,7 @@ _=/usr/bin/env''', exit_status=0)
args = ("cd ~/avocado/tests; avocado run --force-job-id 1-sleeptest;0 "
"--json - --archive /tests/sleeptest /tests/other/test "
"passtest --multiplex ~/avocado/tests/foo.yaml "
"passtest -m ~/avocado/tests/foo.yaml "
"~/avocado/tests/bar/baz.yaml --dry-run")
(Remote.should_receive('run')
.with_args(args, timeout=61, ignore_status=True)
......@@ -113,7 +113,7 @@ _=/usr/bin/env''', exit_status=0)
Results = flexmock(remote=Remote, urls=['sleeptest'],
stream=stream, timeout=None,
args=flexmock(show_job_log=False,
multiplex_files=['foo.yaml', 'bar/baz.yaml'],
multiplex=['foo.yaml', 'bar/baz.yaml'],
dry_run=True))
Results.should_receive('start_tests').once().ordered()
args = {'status': u'PASS', 'whiteboard': '', 'time_start': 0,
......
......@@ -7,6 +7,7 @@ else:
import unittest
from avocado.core import tree
from avocado.plugins import yaml_to_mux
if __name__ == "__main__":
PATH_PREFIX = "../../../../"
......@@ -16,8 +17,8 @@ else:
class TestTree(unittest.TestCase):
# Share tree with all tests
tree = tree.create_from_yaml(['/:' + PATH_PREFIX +
'examples/mux-selftest.yaml'])
tree = yaml_to_mux.create_from_yaml(['/:' + PATH_PREFIX +
'examples/mux-selftest.yaml'])
def test_node_order(self):
self.assertIsInstance(self.tree, tree.TreeNode)
......@@ -160,8 +161,9 @@ class TestTree(unittest.TestCase):
tree2.children[0].children[2].children[1].value)
def test_advanced_yaml(self):
tree2 = tree.create_from_yaml(['/:' + PATH_PREFIX + 'examples/mux-'
'selftest-advanced.yaml'])
tree2 = yaml_to_mux.create_from_yaml(['/:' + PATH_PREFIX +
'examples/mux-selftest-advanced.'
'yaml'])
exp = ['intel', 'amd', 'arm', 'scsi', 'virtio', 'fedora', '6',
'7', 'gentoo', 'mint', 'prod', 'new_node', 'on']
act = tree2.get_leaves()
......
......@@ -138,6 +138,7 @@ if __name__ == '__main__':
'tap = avocado.plugins.tap:TAP',
'vm = avocado.plugins.vm:VM',
'docker = avocado.plugins.docker:Docker',
'yaml_to_mux = avocado.plugins.yaml_to_mux:YamlToMux',
],
'avocado.plugins.cli.cmd': [
'config = avocado.plugins.config:Config',
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册