未验证 提交 ee224c35 编写于 作者: L Lukáš Doktor

Merging pull request 2061

Signed-off-by: NLukáš Doktor <ldoktor@redhat.com>

* https://github.com/avocado-framework/avocado:
  loader: recursive test discovery
  loader: add '__init__.py' to path when path is a dir
......@@ -594,12 +594,14 @@ class FileLoader(TestLoader):
tests.extend(self._make_tests(pth, which_tests))
return tests
def _find_avocado_tests(self, path):
def _find_avocado_tests(self, path, class_name=None):
"""
Attempts to find Avocado instrumented tests from Python source files
:param path: path to a Python source code file
:type path: str
:param class_name: the specific class to be found
:type path: str
:returns: dict with class name and additional info such as method names
and tags
:rtype: dict
......@@ -615,6 +617,9 @@ class FileLoader(TestLoader):
# The resulting test classes
result = {}
if os.path.isdir(path):
path = os.path.join(path, "__init__.py")
mod = ast.parse(open(path).read(), path)
for statement in mod.body:
......@@ -643,18 +648,99 @@ class FileLoader(TestLoader):
# Looking for a 'class Anything(anything):'
elif isinstance(statement, ast.ClassDef):
# class_name will exist only under recursion. In that
# case, we will only process the class if it has the
# expected class_name.
if class_name is not None and class_name != statement.name:
continue
docstring = ast.get_docstring(statement)
# Looking for a class that has in the docstring either
# ":avocado: enable" or ":avocado: disable
if safeloader.check_docstring_directive(docstring, 'disable'):
if (safeloader.check_docstring_directive(docstring, 'disable')
and class_name is None):
continue
cl_tags = safeloader.get_docstring_directives_tags(docstring)
if safeloader.check_docstring_directive(docstring, 'enable'):
info = self._get_methods_info(statement.body,
cl_tags)
if (safeloader.check_docstring_directive(docstring, 'enable')
and class_name is None):
info = self._get_methods_info(statement.body, cl_tags)
result[statement.name] = info
continue
# Looking for the 'recursive' docstring or a 'class_name'
# (meaning we are under recursion)
if (safeloader.check_docstring_directive(docstring, 'recursive')
or class_name is not None):
info = self._get_methods_info(statement.body, cl_tags)
result[statement.name] = info
# Getting the list of parents of the current class
parents = statement.bases
# Searching the parents in the same module
for parent in parents[:]:
# Looking for a 'class FooTest(module.Parent)'
if isinstance(parent, ast.Attribute):
parent_class = parent.attr
# Looking for a 'class FooTest(Parent)'
else:
parent_class = parent.id
res = self._find_avocado_tests(path, parent_class)
if res:
parents.remove(parent)
for cls in res:
info.extend(res[cls])
# If there are parents left to be discovered, they
# might be in a different module.
for parent in parents:
if isinstance(parent, ast.Attribute):
# Looking for a 'class FooTest(module.Parent)'
parent_module = parent.value.id
parent_class = parent.attr
else:
# Looking for a 'class FooTest(Parent)'
parent_module = None
parent_class = parent.id
for node in mod.body:
reference = None
# Looking for 'from parent import class'
if isinstance(node, ast.ImportFrom):
reference = parent_class
# Looking for 'import parent'
elif isinstance(node, ast.Import):
reference = parent_module
if reference is None:
continue
for artifact in node.names:
# Looking for a class alias
# ('from parent import class as alias')
if artifact.asname is not None:
parent_class = reference = artifact.name
# If the parent class or the parent module
# is found in the imports, discover the
# parent module path and find the parent
# class there
if artifact.name == reference:
modules_paths = [os.path.dirname(path)]
modules_paths.extend(sys.path)
if parent_module is None:
parent_module = node.module
_, ppath, _ = imp.find_module(parent_module,
modules_paths)
res = self._find_avocado_tests(ppath,
parent_class)
if res:
for cls in res:
info.extend(res[cls])
continue
if test_import:
......@@ -676,6 +762,7 @@ class FileLoader(TestLoader):
info = self._get_methods_info(statement.body,
cl_tags)
result[statement.name] = info
continue
return result
......
......@@ -1251,6 +1251,68 @@ The docstring ``:avocado: disable`` is evaluated first by Avocado,
meaning that if both ``:avocado: disable`` and ``:avocado: enable`` are
present in the same docstring, the test will not be listed.
Recursively Discovering Tests
-----------------------------
In addition to the ``:avocado: enable`` and ``:avocado: disable``
docstring directives, Avocado has support for the ``:avocado: recursive``
directive. It is intended to be used in inherited classes when you want
to tell Avocado to also discover the ancestor classes.
The ``:avocado: recursive`` directive will direct Avocado to evaluate all
the ancestors of the class until the base class, the one derived from
from `avocado.Test`.
Example:
File `/usr/share/avocado/tests/test_base_class.py`::
from avocado import Test
class BaseClass(Test):
def test_basic(self):
pass
File `/usr/share/avocado/tests/test_first_child.py`::
from test_base_class import BaseClass
class FirstChild(BaseClass):
def test_first_child(self):
pass
File `/usr/share/avocado/tests/test_second_child.py`::
from test_first_child import FirstChild
class SecondChild(FirstChild):
"""
:avocado: recursive
"""
def test_second_child(self):
pass
Using only `test_second_child.py` as a test reference will result in::
$ avocado list test_second_child.py
INSTRUMENTED test_second_child.py:SecondChild.test_second_child
INSTRUMENTED test_second_child.py:SecondChild.test_first_child
INSTRUMENTED test_second_child.py:SecondChild.test_basic
Notice that the `:avocado: disable` docstring will be ignored in
ancestors during the recursive discovery. What means that even if an
ancestor contains the docstring `:avocado: disable`, that ancestor will
still be included in the results.
.. _categorizing-tests:
Categorizing tests
......
import os
import shutil
import stat
import sys
import multiprocessing
import tempfile
import unittest
......@@ -205,6 +207,37 @@ class MyClass(Test):
pass
'''
RECURSIVE_DISCOVERY_TEST1 = """
from avocado import Test
class BaseClass(Test):
def test_basic(self):
pass
class FirstChild(BaseClass):
def test_first_child(self):
pass
class SecondChild(FirstChild):
'''
:avocado: disable
'''
def test_second_child(self):
pass
"""
RECURSIVE_DISCOVERY_TEST2 = """
from avocado import Test
from recursive_discovery_test1 import SecondChild
class ThirdChild(Test, SecondChild):
'''
:avocado: recursive
'''
def test_third_child(self):
pass
"""
class LoaderTest(unittest.TestCase):
......@@ -485,6 +518,24 @@ class LoaderTest(unittest.TestCase):
self.assertEqual(expected_order, methods)
avocado_keep_methods_order.remove()
def test_recursive_discovery(self):
avocado_recursive_discovery_test1 = script.TemporaryScript(
'recursive_discovery_test1.py',
RECURSIVE_DISCOVERY_TEST1)
avocado_recursive_discovery_test1.save()
avocado_recursive_discovery_test2 = script.TemporaryScript(
'recursive_discovery_test2.py',
RECURSIVE_DISCOVERY_TEST2)
avocado_recursive_discovery_test2.save()
sys.path.append(os.path.dirname(avocado_recursive_discovery_test1.path))
tests = self.loader._find_avocado_tests(avocado_recursive_discovery_test2.path)
expected = {'ThirdChild': [('test_third_child', set([])),
('test_second_child', set([])),
('test_first_child', set([])),
('test_basic', set([]))]}
self.assertEqual(expected, tests)
def tearDown(self):
shutil.rmtree(self.tmpdir)
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册