source: sasmodels/conftest.py @ b3f4831

ticket_1156
Last change on this file since b3f4831 was f6fd413, checked in by Paul Kienzle <pkienzle@…>, 7 years ago

pytest: is_generator is not part of public api, so reimplement it in conftest

  • Property mode set to 100644
File size: 4.6 KB
RevLine 
[a69d8cd]1"""
[b3af1c2]2pytest hooks for sasmodels
[a69d8cd]3
[b3af1c2]4Hooks for running sasmodels tests via pytest.
[a69d8cd]5
6*pytest_collection_modifyitems* adds the test description to the end of
7the test name.  This is needed for the generated list of tests is sasmodels,
8where each test has a description giving the name of the model.  For example
9"model_tests::[3]" becomes "model_tests::[3]::bcc_paracrystal-dll".  Need to
10leave the "::[3]" in the name since that is the way you can indicate this
[b3af1c2]11test specifically from the pytest command line. [This is perhaps because
[a69d8cd]12the modifyitems hook is only called after test selection.]
[bb4ff2a]13
14*pytest_ignore_collect* skips kernelcl.py if pyopencl cannot be imported.
[a69d8cd]15"""
16from __future__ import print_function
17
[bb4ff2a]18import os.path
[f6fd413]19import inspect
[bb4ff2a]20
[a69d8cd]21import pytest
22from _pytest.unittest import TestCaseFunction
[335271e]23
24def pytest_pycollect_makeitem(collector, name, obj):
25    """
26    Convert test generator into list of function tests so that pytest doesn't
27    complain about deprecated yield tests.
28
29    Note that unlike nose, the tests are generated and saved instead of run
30    immediately.  This means that any dynamic context, such as a for-loop
31    variable, must be captured by wrapping the yield result in a function call.
32
33    For example::
34
35        for value in 1, 2, 3:
36            for test in test_cases:
37                yield test, value
38
39    will need to be changed to::
40
41        def build_test(test, value):
42            return test, value
43        for value in 1, 2, 3:
44            for test in test_cases:
45                yield build_test(test, value)
46
47    This allows the context (test and value) to be captured by lexical closure
48    in build_test. See https://stackoverflow.com/a/233835/6195051.
49    """
50    if collector.istestfunction(obj, name) and is_generator(obj):
[0e7611b]51        tests = []
52        for number, yielded in enumerate(obj()):
53            index, call, args = split_yielded_test(yielded, number)
54            test = pytest.Function(name+index, collector, args=args, callobj=call)
55            tests.append(test)
[335271e]56        return tests
57
[f6fd413]58def is_generator(func):
59    """
60    Returns True if function has yield.
61    """
62    # Cribbed from _pytest.compat is_generator and iscoroutinefunction; these
63    # may not be available as part of pytest 4.
64    coroutine = (getattr(func, '_is_coroutine', False) or
65                 getattr(inspect, 'iscoroutinefunction', lambda f: False)(func))
66    generator = inspect.isgeneratorfunction(func)
67    return generator and not coroutine
68
[0e7611b]69def split_yielded_test(obj, number):
70    if not isinstance(obj, (tuple, list)):
71        obj = (obj,)
72    if not callable(obj[0]):
73        index = "['%s']"%obj[0]
74        obj = obj[1:]
75    else:
76        index = "[%d]"%number
77    call, args = obj[0], obj[1:]
78    return index, call, args
[a69d8cd]79
80USE_DOCSTRING_AS_DESCRIPTION = True
81def pytest_collection_modifyitems(session, config, items):
82    """
83    Add description to the test node id if item is a function and function
84    has a description attribute or __doc__ attribute.
85    """
86    for item in items:
87        if isinstance(item, pytest.Function):
88            if isinstance(item, TestCaseFunction):
89                # TestCase uses item.name to find the method so skip
90                continue
91            function = item.obj
92
93            # If the test case provides a "description" attribute then use it
94            # as an extended description.  If there is no description attribute,
95            # then perhaps use the test docstring.
96            if USE_DOCSTRING_AS_DESCRIPTION:
97                description = getattr(function, 'description', function.__doc__)
98            else:
99                description = getattr(function, 'description', "")
100
101            # If description is not supplied but yield args are, then use the
102            # yield args for the description
103            if not description and getattr(item, '_args', ()):
[b3af1c2]104                description = (str(item._args) if len(item._args) > 1
105                               else str(item._args[0]))
[a69d8cd]106
[b3af1c2]107            # Set the description as part of the node identifier.
[a69d8cd]108            if description:
109                # Strip spaces from start and end and strip dots from end
110                # pytest converts '.' to '::' on output for some reason.
111                description = description.strip().rstrip('.')
112                # Join multi-line descriptions into a single line
113                if '\n' in description:
[b3af1c2]114                    description = " ".join(line.strip()
115                                           for line in description.split('\n'))
[a69d8cd]116
117                # Note: leave the current name mostly as-is since the prefix
118                # is needed to specify the nth test from a list of tests.
119                item.name += "::" + description
Note: See TracBrowser for help on using the repository browser.