• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# pytest custom collection adapter for legacy pyyaml unit tests/data files; surfaces each
2# legacy test case as a pyyaml item
3
4import os
5import pathlib
6import pytest
7import warnings
8
9from test_appliance import find_test_filenames, DATA
10
11try:
12    from yaml import _yaml
13    HAS_LIBYAML_EXT = True
14    del _yaml
15except ImportError:
16    HAS_LIBYAML_EXT = False
17
18
19_test_filenames = find_test_filenames(DATA)
20
21# ignore all datafiles
22collect_ignore_glob = ['data/*']
23
24
25class PyYAMLItem(pytest.Item):
26    def __init__(self, parent=None, config=None, session=None, nodeid=None, function=None, filenames=None, **kwargs):
27        self._function = function
28        self._fargs = filenames or []
29
30        super().__init__(os.path.basename(filenames[0]) if filenames else parent.name, parent, config, session, nodeid)
31        # this is gnarly since the type of fspath is private; fixed in pytest 7 to use pathlib on the `path` attr
32        if filenames:  # pass the data file location as the test path
33            self.fspath = parent.fspath.__class__(filenames[0])
34            self.lineno = 1
35        else:  # pass the function location in the code
36            self.fspath = parent.fspath.__class__(function.__code__.co_filename)
37            self.lineno = function.__code__.co_firstlineno
38
39    def runtest(self):
40        self._function(verbose=True, *self._fargs)
41
42    def reportinfo(self):
43        return self.fspath, self.lineno, ''
44
45
46class PyYAMLCollector(pytest.Collector):
47    def __init__(self, name, parent=None, function=None, **kwargs):
48        self._function = function
49        self.fspath = parent.fspath.__class__(function.__code__.co_filename)
50        self.lineno = function.__code__.co_firstlineno
51
52        # avoid fspath deprecation warnings on pytest < 7
53        if hasattr(self, 'path') and 'fspath' in kwargs:
54            del kwargs['fspath']
55
56        super().__init__(name=name, parent=parent, **kwargs)
57
58    def collect(self):
59        items = []
60
61        unittest = getattr(self._function, 'unittest', None)
62
63        if unittest is True:  # no filenames
64            items.append(PyYAMLItem.from_parent(parent=self, function=self._function, filenames=None))
65        else:
66            for base, exts in _test_filenames:
67                filenames = []
68                for ext in unittest:
69                    if ext not in exts:
70                        break
71                    filenames.append(os.path.join(DATA, base + ext))
72                else:
73                    skip_exts = getattr(self._function, 'skip', [])
74                    for skip_ext in skip_exts:
75                        if skip_ext in exts:
76                            break
77                    else:
78                        items.append(PyYAMLItem.from_parent(parent=self, function=self._function, filenames=filenames))
79
80        return items or None
81
82    def reportinfo(self):
83        return self.fspath, self.lineno, ''
84
85    @classmethod
86    def from_parent(cls, parent, fspath, **kwargs):
87        return super().from_parent(parent=parent, fspath=fspath, **kwargs)
88
89
90@pytest.hookimpl(hookwrapper=True, trylast=True)
91def pytest_pycollect_makeitem(collector, name: str, obj: object):
92    outcome = yield
93    outcome.get_result()
94    if not callable(obj):
95        outcome.force_result(None)
96        return
97    unittest = getattr(obj, 'unittest', None)
98
99    if not unittest:
100        outcome.force_result(None)
101        return
102
103    if unittest is True:  # no file list to run against, just return a test item instead of a collector
104        outcome.force_result(PyYAMLItem.from_parent(name=name, parent=collector, fspath=collector.fspath, function=obj))
105        return
106
107    # there's a file list; return a collector to create individual items for each
108    outcome.force_result(PyYAMLCollector.from_parent(name=name, parent=collector, fspath=collector.fspath, function=obj))
109    return
110
111
112def pytest_collection_modifyitems(session, config, items):
113    pass
114
115
116def pytest_ignore_collect(collection_path: pathlib.Path):
117    basename = collection_path.name
118    # ignore all Python files in this subtree for normal pytest collection
119    if basename not in ['test_yaml.py', 'test_yaml_ext.py']:
120        return True
121
122    # ignore extension tests (depending on config)
123    if basename == 'test_yaml_ext.py':
124        require_libyaml = os.environ.get('PYYAML_FORCE_LIBYAML', None)
125        if require_libyaml == '1' and not HAS_LIBYAML_EXT:
126            raise RuntimeError('PYYAML_FORCE_LIBYAML envvar is set, but libyaml extension is not available')
127        if require_libyaml == '0':
128            return True
129        if not HAS_LIBYAML_EXT:
130            warnings.warn('libyaml extension is not available, skipping libyaml tests')
131            return True
132
133