提交 94cb27dc 编写于 作者: L Lucas Meneghel Rodrigues

Merge pull request #315 from ldoktor/multiplexer

avocado.multiplexer: Support for debug run [v3]
......@@ -112,6 +112,7 @@ class TermSupport(object):
COLOR_GREEN = '\033[92m'
COLOR_YELLOW = '\033[93m'
COLOR_RED = '\033[91m'
COLOR_DARKGREY = '\033[90m'
CONTROL_END = '\033[0m'
......@@ -138,6 +139,7 @@ class TermSupport(object):
self.WARN = self.COLOR_YELLOW
self.PARTIAL = self.COLOR_YELLOW
self.ENDC = self.CONTROL_END
self.LOWLIGHT = self.COLOR_DARKGREY
term = os.environ.get("TERM")
if (not os.isatty(1)) or (term not in self.allowed_terms):
self.disable()
......@@ -155,6 +157,7 @@ class TermSupport(object):
self.WARN = ''
self.PARTIAL = ''
self.ENDC = ''
self.LOWLIGHT = ''
def header_str(self, msg):
"""
......
......@@ -46,6 +46,10 @@ except ImportError:
class TreeNode(object):
"""
Class for bounding nodes into tree-structure.
"""
def __init__(self, name='', value=None, parent=None, children=None):
if value is None:
value = {}
......@@ -55,6 +59,7 @@ class TreeNode(object):
self.value = value
self.parent = parent
self.children = []
self._environment = None
for child in children:
self.add_child(child)
......@@ -66,25 +71,30 @@ class TreeNode(object):
return '%s: %s' % (self.path, ', '.join(variables))
def __len__(self):
""" Return number of descended leaf nodes """
return len(tuple(self.iter_leaves()))
def __iter__(self):
""" Iterate through descended leaf nodes """
return self.iter_leaves()
def __eq__(self, other):
""" Compares node to other node or string to name of this node """
if isinstance(other, str): # Compare names
if self.name == other:
return True
elif isinstance(other, self.__class__):
first = self.__dict__.copy()
first.pop('parent')
second = other.__dict__.copy()
second.pop('parent')
return first == second
return False
else:
for attr in ('name', 'value', 'children'):
if getattr(self, attr) != getattr(other, attr):
return False
return True
def add_child(self, node):
if isinstance(node, self.__class__):
"""
Append node as child. Nodes with the same name gets merged into the
existing position.
"""
if isinstance(node, TreeNode):
if node.name in self.children:
self.children[self.children.index(node.name)].merge(node)
else:
......@@ -94,26 +104,36 @@ class TreeNode(object):
raise ValueError('Bad node type.')
def merge(self, other):
""" Merges $other node into this one (doesn't check the name) """
"""
Merges `other` node into this one without checking the name of the
other node. New values are appended, existing values overwritten
and unaffected ones are kept. Then all other node children are
added as children (recursively they get either appended at the end
or merged into existing node in the previous position.
"""
self.value.update(other.value)
for child in other.children:
self.add_child(child)
@property
def is_leaf(self):
return len(self.children) == 0
""" Is this a leaf node? """
return not self.children
@property
def root(self):
""" Root of this tree """
return self.get_root()
def get_root(self):
""" Get root of this tree """
root = None
for root in self.iter_parents():
pass
return root
def iter_parents(self):
""" Iterate through parent nodes to root """
node = self.parent
while True:
if node is None:
......@@ -123,16 +143,20 @@ class TreeNode(object):
@property
def parents(self):
""" List of parent nodes """
return self.get_parents()
def get_parents(self):
""" Get list of parent nodes """
return list(self.iter_parents())
@property
def path(self):
""" Node path """
return self.get_path()
def get_path(self, sep='/'):
""" Get node path """
path = [str(self.name)]
for node in self.iter_parents():
path.append(str(node.name))
......@@ -140,86 +164,114 @@ class TreeNode(object):
@property
def environment(self):
""" Node environment (values + preceding envs) """
return self.get_environment()
def get_environment(self):
def update_or_extend(target, source):
for k, _ in source.items():
if k in target and isinstance(target[k], list):
target[k].extend(source[k])
else:
if isinstance(source[k], list):
target[k] = source[k][:]
""" Get node environment (values + preceding envs) """
if self._environment is None:
self._environment = (self.parent.environment.copy()
if self.parent else {})
for key, value in self.value.iteritems():
if isinstance(value, list):
if (key in self._environment
and isinstance(self._environment[key], list)):
self._environment[key] = self._environment[key] + value
else:
target[k] = source[k]
env = {}
rev_parents = reversed(self.get_parents())
for parent in rev_parents:
update_or_extend(env, parent.value)
update_or_extend(env, self.value)
return env
def iter_children_preorder(self, node=None):
q = collections.deque()
self._environment[key] = value
else:
self._environment[key] = value
return self._environment
def set_environment_dirty(self):
"""
Set the environment cache dirty. You should call this always when
you query for the environment and then change the value or structure.
Otherwise you'll get the old environment instead.
"""
for child in self.children:
child.set_environment_dirty()
self._environment = None
def iter_children_preorder(self):
""" Iterate through children """
queue = collections.deque()
node = self
while node is not None:
yield node
q.extendleft(reversed(node.children))
queue.extendleft(reversed(node.children))
try:
node = q.popleft()
except:
node = queue.popleft()
except IndexError:
node = None
def iter_leaves(self):
""" Iterate throuh leaf nodes """
for node in self.iter_children_preorder():
if node.is_leaf:
yield node
def get_leaves(self):
""" Get list of leaf nodes """
return list(self.iter_leaves())
def get_ascii(self, show_internal=True, compact=False, attributes=None):
(lines, _) = self._ascii_art(show_internal=show_internal,
compact=compact, attributes=attributes)
"""
Get ascii-art tree structure
:param show_internal: Show intermediary nodes
:param compact: Compress the tree vertically
:param attributes: List of node attributes to be printed out ['name']
:return: string
"""
(lines, _) = self.ascii_art(show_internal=show_internal,
compact=compact, attributes=attributes)
return '\n' + '\n'.join(lines)
def _ascii_art(self, char1='-', show_internal=True, compact=False,
attributes=None):
def ascii_art(self, char1='-', show_internal=True, compact=False,
attributes=None):
"""
Generate ascii-art for this node
:param char1: Incomming path character [-]
:param show_internal: Show intermediary nodes
:param compact: Compress the tree vertically
:param attributes: List of node attributes to be printed out ['name']
:return: list of strings
"""
if attributes is None:
attributes = ["name"]
node_name = ', '.join(map(str, [getattr(self, v)
for v in attributes
if hasattr(self, v)]))
LEN = max(3, len(node_name)
if not self.children or show_internal else 3)
PAD = ' ' * LEN
PA = ' ' * (LEN - 1)
length = max(3, len(node_name)
if not self.children or show_internal else 3)
pad = ' ' * length
_pad = ' ' * (length - 1)
if not self.is_leaf:
mids = []
result = []
for c in self.children:
for char in self.children:
if len(self.children) == 1:
char2 = '/'
elif c is self.children[0]:
elif char is self.children[0]:
char2 = '/'
elif c is self.children[-1]:
elif char is self.children[-1]:
char2 = '\\'
else:
char2 = '-'
(clines, mid) = c._ascii_art(char2, show_internal, compact,
attributes)
(clines, mid) = char.ascii_art(char2, show_internal, compact,
attributes)
mids.append(mid + len(result))
result.extend(clines)
if not compact:
result.append('')
if not compact:
result.pop()
(lo, hi, end) = (mids[0], mids[-1], len(result))
prefixes = ([PAD] * (lo + 1) + [PA + '|'] * (hi - lo - 1)
+ [PAD] * (end - hi))
mid = (lo + hi) / 2
prefixes[mid] = char1 + '-' * (LEN - 2) + prefixes[mid][-1]
(low, high, end) = (mids[0], mids[-1], len(result))
prefixes = ([pad] * (low + 1) + [_pad + '|'] * (high - low - 1)
+ [pad] * (end - high))
mid = (low + high) / 2
prefixes[mid] = char1 + '-' * (length - 2) + prefixes[mid][-1]
result = [p + l for (p, l) in zip(prefixes, result)]
if show_internal:
stem = result[mid]
......@@ -229,21 +281,23 @@ class TreeNode(object):
return [char1 + '-' + node_name], 0
def detach(self):
""" Detach this node from parent """
if self.parent:
self.parent.children.remove(self)
self.parent = None
return self
def _create_from_yaml(stream):
""" Create tree structure from yaml stream """
class Value(tuple):
class Value(tuple): # Few methods pylint: disable=R0903
""" Used to mark values to simplify checking for node vs. value """
pass
""" Used to mark values to simplify checking for node vs. value """
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 """
""" Create `name` node and add values """
node_children = []
node_values = []
for value in values:
......@@ -251,10 +305,12 @@ def _create_from_yaml(stream):
node_children.append(value)
else:
node_values.append(value)
return TreeNode(name, dict(node_values), children=node_children)
return cls_node(name, dict(node_values), children=node_children)
def mapping_to_tree_loader(loader, node):
""" Maps yaml mapping tag to TreeNode structure """
def is_node(values):
""" Whether these values represent node or just random values """
if (isinstance(values, list) and values
and isinstance(values[0], (Value, TreeNode))):
# When any value is TreeNode or Value, all of them are already
......@@ -267,26 +323,44 @@ def _create_from_yaml(stream):
if is_node(values): # New node
objects.append(tree_node_from_values(name, values))
elif values is None: # Empty node
objects.append(TreeNode(name))
objects.append(cls_node(name))
else: # Values
objects.append(Value((name, values)))
return objects
Loader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
mapping_to_tree_loader)
return tree_node_from_values('', yaml.load(stream, Loader))
with open(path) as stream:
return tree_node_from_values('', yaml.load(stream, Loader))
def create_from_yaml(paths):
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
"""
data = TreeNode()
def _merge(data, path):
""" Normal run """
data.merge(_create_from_yaml(path))
def _merge_debug(data, path):
""" Use NamedTreeNodeDebug magic """
node_cls = tree_debug.get_named_tree_cls(path)
data.merge(_create_from_yaml(path, node_cls))
if not debug:
data = TreeNode()
merge = _merge
else:
from avocado.core import tree_debug
data = tree_debug.TreeNodeDebug()
merge = _merge_debug
try:
for path in paths:
data.merge(_create_from_yaml(open(path).read()))
merge(data, path)
except (yaml.scanner.ScannerError, yaml.parser.ParserError) as err:
raise SyntaxError(err)
return data
......
"""
Debug version of the avocado.core.tree.TreeNode with additional utils.
:license: GPLv2
:copyright: Red Hat, Inc. 2014
:author: Lukas Doktor <ldoktor@redhat.com>
"""
import itertools
import os
from avocado.core import output
from avocado.core.tree import TreeNode
class OutputValue(object): # only container pylint: disable=R0903
""" Ordinary value with some debug info """
def __init__(self, value, node, srcyaml):
self.value = value
self.node = node
self.yaml = srcyaml
def __str__(self):
return "%s%s@%s:%s%s" % (self.value,
output.term_support.LOWLIGHT,
self.yaml, self.node.path,
output.term_support.ENDC)
class OutputList(list): # only container pylint: disable=R0903
""" List with some debug info """
def __init__(self, values, nodes, yamls):
super(OutputList, self).__init__(values)
self.nodes = nodes
self.yamls = yamls
def __add__(self, other):
""" Keep attrs separate in order to print the origins """
value = super(OutputList, self).__add__(other)
return OutputList(value,
self.nodes + other.nodes,
self.yamls + other.yamls)
def __str__(self):
color = output.term_support.LOWLIGHT
cend = output.term_support.ENDC
return ' + '.join("%s%s@%s:%s%s"
% (_[0], color, _[1], _[2].path, cend)
for _ in itertools.izip(self, self.yamls,
self.nodes))
class ValueDict(dict): # only container pylint: disable=R0903
""" Dict which stores the origin of the items """
def __init__(self, srcyaml, node, values):
super(ValueDict, self).__init__()
self.yaml = srcyaml
self.node = node
self.yaml_per_key = {}
for key, value in values.iteritems():
self[key] = value
def __setitem__(self, key, value):
""" Store yaml_per_key and value """
# Merge is responsible to set `self.yaml` to current file
self.yaml_per_key[key] = self.yaml
return super(ValueDict, self).__setitem__(key, value)
def __getitem__(self, key):
"""
This is debug run. Fake the results and return either
OutputValue (let's call it string) and OutputList. These
overrides the `__str__` and return string with origin.
:warning: Returned values are unusable in tests!
"""
value = super(ValueDict, self).__getitem__(key)
origin = self.yaml_per_key.get(key)
if isinstance(value, list):
value = OutputList([value], [self.node], [origin])
else:
value = OutputValue(value, self.node, origin)
return value
def iteritems(self):
""" Slower implementation with the use of __getitem__ """
for key in self.iterkeys():
yield key, self[key]
raise StopIteration
class TreeNodeDebug(TreeNode): # only container pylint: disable=R0903
"""
Debug version of TreeNodeDebug
:warning: Origin of the value is appended to all values thus it's not
suitable for running tests.
"""
def __init__(self, name='', value=None, parent=None, children=None,
srcyaml=None):
if value is None:
value = {}
if srcyaml:
srcyaml = os.path.relpath(srcyaml)
super(TreeNodeDebug, self).__init__(name,
ValueDict(srcyaml, self, value),
parent, children)
self.yaml = srcyaml
def merge(self, other):
"""
Override origin with the one from other tree. Updated/Newly set values
are going to use this location as origin.
"""
if hasattr(other, 'yaml'):
srcyaml = os.path.relpath(other.yaml)
# when we use TreeNodeDebug, value is always ValueDict
self.value.yaml_per_key.update(other.value.yaml_per_key) # pylint: disable=E1101
else:
srcyaml = "Unknown"
self.yaml = srcyaml
self.value.yaml = srcyaml
return super(TreeNodeDebug, self).merge(other)
def get_named_tree_cls(path):
""" Return TreeNodeDebug class with hardcoded yaml path """
class NamedTreeNodeDebug(TreeNodeDebug): # pylint: disable=R0903
""" Fake class with hardcoded yaml path """
def __init__(self, name='', value=None, parent=None,
children=None):
super(NamedTreeNodeDebug, self).__init__(name, value, parent,
children, path)
return NamedTreeNodeDebug
......@@ -77,12 +77,13 @@ def multiplex(*args):
yield tuple(prod)
def multiplex_yamls(input_yamls, filter_only=None, filter_out=None):
def multiplex_yamls(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)
input_tree = tree.create_from_yaml(input_yamls, debug)
final_tree = tree.apply_filters(input_tree, filter_only, filter_out)
leaves = (x for x in final_tree.iter_leaves() if x.parent is not None)
variants = multiplex(leaves)
......
......@@ -50,6 +50,9 @@ class Multiplexer(plugin.Plugin):
self.parser.add_argument('-c', '--contents', action='store_true', default=False,
help="Shows the variant's content (variables)")
self.parser.add_argument('-d', '--debug', action='store_true',
default=False, help="Debug multiplexed "
"files.")
super(Multiplexer, self).configure(self.parser)
def run(self, args):
......@@ -71,15 +74,22 @@ class Multiplexer(plugin.Plugin):
variants = multiplexer.multiplex_yamls(multiplex_files,
args.filter_only,
args.filter_out)
args.filter_out,
args.debug)
view.notify(event='message', msg='Variants generated:')
for (index, tpl) in enumerate(variants):
paths = ', '.join([x.path for x in tpl])
view.notify(event='minor', msg='Variant %s: %s' %
if not args.debug:
paths = ', '.join([x.path for x in tpl])
else:
color = output.term_support.LOWLIGHT
cend = output.term_support.ENDC
paths = ', '.join(["%s%s@%s%s" % (_.name, color, _.yaml, cend)
for _ in tpl])
view.notify(event='minor', msg='\nVariant %s: %s' %
(index + 1, paths))
if args.contents:
env = collections.OrderedDict()
env = {}
for node in tpl:
env.update(node.environment)
for k in sorted(env.keys()):
......
# Special values
# joinlist: list which gets combined while getting environment
# corruptlist: list which is overwritten with string and again by list
# /distro: should be merged from two separated trees into the position of
# the first one. PS: Don't do this in production, it works but
# it's not nice and readable... here it simulates the use of
# multiple files and checks that the node ordering works fine.
# /env/opt_CFLAGS: Should be present in merged node
# /env/prod/opt_CFLAGS: value should be overridden by latter node
hw:
cpu:
test_value:
- a
joinlist:
- first_item
intel:
cpu_CFLAGS: '-march=core2'
amd:
test_value:
- b
- c
joinlist: ['second', 'third']
cpu_CFLAGS: '-march=athlon64'
arm:
cpu_CFLAGS: '-mabi=apcs-gnu -march=armv8-a -mtune=arm8'
disk:
disk_type: 'virtio'
corruptlist: 'nonlist'
scsi:
corruptlist: ['againlist']
disk_type: 'scsi'
virtio:
test_value: 42
corruptlist: ['upper_node_list']
distro:
fedora:
init: 'systemd'
mint:
init: 'systemv'
env:
opt_CFLAGS: '-Os'
prod:
opt_CFLAGS: 'THIS SHOULD GET OVERWRITTEN'
env:
prod:
opt_CFLAGS: '-O2'
distro:
mint:
init: 'systemv'
......@@ -13,6 +13,15 @@ if os.path.isdir(os.path.join(basedir, 'avocado')):
from avocado.utils import process
DEBUG_OUT = """Variant 16: amd@examples/mux-environment.yaml, virtio@examples/mux-environment.yaml, mint@examples/mux-environment.yaml, debug@examples/mux-environment.yaml
corruptlist: nonlist@examples/mux-selftest.yaml:/hw/disk
cpu_CFLAGS: -march=athlon64@examples/mux-environment.yaml:/hw/cpu/amd
disk_type: virtio@examples/mux-environment.yaml:/hw/disk/virtio
init: systemv@examples/mux-environment.yaml:/distro/mint
joinlist: ['first_item']@examples/mux-selftest.yaml:/hw/cpu + ['second', 'third']@examples/mux-selftest.yaml:/hw/cpu/amd
opt_CFLAGS: -O0 -g@examples/mux-environment.yaml:/env/debug
"""
class MultiplexTests(unittest.TestCase):
......@@ -51,6 +60,14 @@ class MultiplexTests(unittest.TestCase):
expected_rc = 2
self.run_and_check(cmd_line, expected_rc)
def test_mplex_debug(self):
cmd_line = ('./scripts/avocado multiplex -c -d '
'examples/mux-selftest.yaml examples/mux-environment.yaml '
'examples/mux-selftest.yaml examples/mux-environment.yaml')
expected_rc = 0
out = self.run_and_check(cmd_line, expected_rc)
self.assertIn(DEBUG_OUT, out)
def test_run_mplex_noid(self):
cmd_line = './scripts/avocado run --multiplex examples/tests/sleeptest.py.data/sleeptest.yaml'
expected_rc = 2
......
......@@ -17,7 +17,8 @@ class TestTree(unittest.TestCase):
self.tree.children[0].children[0].children[0].value)
disk = self.tree.children[0].children[1]
self.assertEqual('scsi', disk.children[0])
self.assertEqual({'disk_type': 'scsi'}, disk.children[0].value)
self.assertEqual({'disk_type': 'scsi', 'corruptlist': ['againlist']},
disk.children[0].value)
self.assertEqual('virtio', disk.children[1])
self.assertEqual({}, disk.children[1].value)
self.assertEqual('distro', self.tree.children[1])
......@@ -54,6 +55,11 @@ class TestTree(unittest.TestCase):
# Add_child incorrect class
self.assertRaises(ValueError, tree3.add_child, 'probably_bad_type')
def test_links(self):
""" Verify child->parent links """
for leaf in self.tree:
self.assertEqual(leaf.root, self.tree)
def test_basic_functions(self):
# repr
self.assertEqual("TreeNode(name='hw')", repr(self.tree.children[0]))
......@@ -70,18 +76,36 @@ class TestTree(unittest.TestCase):
)
# .parents
self.assertEqual(['hw', ''], self.tree.children[0].children[0].parents)
# environment
# environment / (root)
self.assertEqual({}, self.tree.environment)
self.assertEqual({'test_value': 42},
# environment /hw (nodes first)
self.assertEqual({'corruptlist': ['upper_node_list']},
self.tree.children[0].environment)
cpu = self.tree.children[0].children[0]
self.assertEqual({'test_value': ['a']},
# environment /hw/cpu (mixed env)
self.assertEqual({'corruptlist': ['upper_node_list'],
'joinlist': ['first_item']},
cpu.environment)
vals = {'test_value': ['a', 'b', 'c'], 'cpu_CFLAGS': '-march=athlon64'}
# environment /hw/cpu/amd (list extension)
vals = {'corruptlist': ['upper_node_list'],
'cpu_CFLAGS': '-march=athlon64',
'joinlist': ['first_item', 'second', 'third']}
self.assertEqual(vals, cpu.children[1].environment)
vals = {'test_value': ['a'], 'cpu_CFLAGS': '-mabi=apcs-gnu '
# environment /hw/cpu/arm (deep env)
vals = {'corruptlist': ['upper_node_list'], 'joinlist': ['first_item'],
'cpu_CFLAGS': '-mabi=apcs-gnu '
'-march=armv8-a -mtune=arm8'}
self.assertEqual(vals, cpu.children[2].environment)
# environment /hw/disk (list -> string)
vals = {'corruptlist': 'nonlist', 'disk_type': 'virtio'}
disk = self.tree.children[0].children[1]
self.assertEqual(vals, disk.environment)
# environment /hw/disk/scsi (string -> list)
vals = {'corruptlist': ['againlist'], 'disk_type': 'scsi'}
self.assertEqual(vals, disk.children[0].environment)
# environment /env
vals = {'opt_CFLAGS': '-Os'}
self.assertEqual(vals, self.tree.children[2].environment)
# leaves order
leaves = ['intel', 'amd', 'arm', 'scsi', 'virtio', 'fedora', 'mint',
'prod']
......@@ -119,9 +143,10 @@ class TestTree(unittest.TestCase):
exp = ['intel', 'amd', 'arm', 'scsi', 'virtio', 'default', 'virtio',
'fedora', 'mint', 'prod']
self.assertEqual(exp, tree2.get_leaves())
self.assertEqual({'test_value': 42, 'another_value': 'bbb'},
self.assertEqual({'corruptlist': ['upper_node_list'],
'another_value': 'bbb'},
tree2.children[0].value)
self.assertEqual({'test_value': ['z']},
self.assertEqual({'joinlist': ['first_item'], 'test_value': ['z']},
tree2.children[0].children[0].value)
self.assertFalse(tree2.children[0].children[2].children[0].value)
self.assertEqual({'nic': 'virtio'},
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册