[a69d8cd] | 1 | """ |
---|
[b3af1c2] | 2 | pytest hooks for sasmodels |
---|
[a69d8cd] | 3 | |
---|
[b3af1c2] | 4 | Hooks for running sasmodels tests via pytest. |
---|
[a69d8cd] | 5 | |
---|
| 6 | *pytest_collection_modifyitems* adds the test description to the end of |
---|
| 7 | the test name. This is needed for the generated list of tests is sasmodels, |
---|
| 8 | where 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 |
---|
| 10 | leave the "::[3]" in the name since that is the way you can indicate this |
---|
[b3af1c2] | 11 | test specifically from the pytest command line. [This is perhaps because |
---|
[a69d8cd] | 12 | the modifyitems hook is only called after test selection.] |
---|
[bb4ff2a] | 13 | |
---|
| 14 | *pytest_ignore_collect* skips kernelcl.py if pyopencl cannot be imported. |
---|
[a69d8cd] | 15 | """ |
---|
| 16 | from __future__ import print_function |
---|
| 17 | |
---|
[bb4ff2a] | 18 | import os.path |
---|
[f6fd413] | 19 | import inspect |
---|
[bb4ff2a] | 20 | |
---|
[a69d8cd] | 21 | import pytest |
---|
| 22 | from _pytest.unittest import TestCaseFunction |
---|
[335271e] | 23 | |
---|
| 24 | def 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] | 58 | def 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] | 69 | def 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 | |
---|
| 80 | USE_DOCSTRING_AS_DESCRIPTION = True |
---|
| 81 | def 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 |
---|