minimal adjustments
[dotfiles/.git] / .config / coc / extensions / node_modules / coc-python / pythonFiles / testing_tools / adapter / pytest.py
1 # Copyright (c) Microsoft Corporation. All rights reserved.
2 # Licensed under the MIT License.
3
4 from __future__ import absolute_import
5
6 import os.path
7
8 import pytest
9
10 from . import util
11 from .errors import UnsupportedCommandError
12 from .info import TestInfo, TestPath, ParentInfo
13
14
15 def add_cli_subparser(cmd, name, parent):
16     """Add a new subparser to the given parent and add args to it."""
17     parser = parent.add_parser(name)
18     if cmd == 'discover':
19         # For now we don't have any tool-specific CLI options to add.
20         pass
21     else:
22         raise UnsupportedCommandError(cmd)
23     return parser
24
25
26 def discover(pytestargs=None, hidestdio=False,
27              _pytest_main=pytest.main, _plugin=None, **_ignored):
28     """Return the results of test discovery."""
29     if _plugin is None:
30         _plugin = TestCollector()
31
32     pytestargs = _adjust_pytest_args(pytestargs)
33     # We use this helper rather than "-pno:terminal" due to possible
34     # platform-dependent issues.
35     with util.hide_stdio() if hidestdio else util.noop_cm():
36         ec = _pytest_main(pytestargs, [_plugin])
37     if ec != 0:
38         raise Exception('pytest discovery failed (exit code {})'.format(ec))
39     if not _plugin._started:
40         raise Exception('pytest discovery did not start')
41     return (
42             _plugin._tests.parents,
43             #[p._replace(
44             #    id=p.id.lstrip('.' + os.path.sep),
45             #    parentid=p.parentid.lstrip('.' + os.path.sep),
46             #    )
47             # for p in _plugin._tests.parents],
48             list(_plugin._tests),
49             )
50
51
52 def _adjust_pytest_args(pytestargs):
53     pytestargs = list(pytestargs) if pytestargs else []
54     # Duplicate entries should be okay.
55     pytestargs.insert(0, '--collect-only')
56     # TODO: pull in code from:
57     #  src/client/unittests/pytest/services/discoveryService.ts
58     #  src/client/unittests/pytest/services/argsService.ts
59     return pytestargs
60
61
62 class TestCollector(object):
63     """This is a pytest plugin that collects the discovered tests."""
64
65     NORMCASE = staticmethod(os.path.normcase)
66     PATHSEP = os.path.sep
67
68     def __init__(self, tests=None):
69         if tests is None:
70             tests = DiscoveredTests()
71         self._tests = tests
72         self._started = False
73
74     # Relevant plugin hooks:
75     #  https://docs.pytest.org/en/latest/reference.html#collection-hooks
76
77     def pytest_collection_modifyitems(self, session, config, items):
78         self._started = True
79         self._tests.reset()
80         for item in items:
81             test, suiteids = _parse_item(item, self.NORMCASE, self.PATHSEP)
82             self._tests.add_test(test, suiteids)
83
84     # This hook is not specified in the docs, so we also provide
85     # the "modifyitems" hook just in case.
86     def pytest_collection_finish(self, session):
87         self._started = True
88         try:
89             items = session.items
90         except AttributeError:
91             # TODO: Is there an alternative?
92             return
93         self._tests.reset()
94         for item in items:
95             test, suiteids = _parse_item(item, self.NORMCASE, self.PATHSEP)
96             self._tests.add_test(test, suiteids)
97
98
99 class DiscoveredTests(object):
100
101     def __init__(self):
102         self.reset()
103
104     def __len__(self):
105         return len(self._tests)
106
107     def __getitem__(self, index):
108         return self._tests[index]
109
110     @property
111     def parents(self):
112         return sorted(self._parents.values(), key=lambda v: (v.root or v.name, v.id))
113
114     def reset(self):
115         self._parents = {}
116         self._tests = []
117
118     def add_test(self, test, suiteids):
119         parentid = self._ensure_parent(test.path, test.parentid, suiteids)
120         test = test._replace(parentid=parentid)
121         if not test.id.startswith('.' + os.path.sep):
122             test = test._replace(id=os.path.join('.', test.id))
123         self._tests.append(test)
124
125     def _ensure_parent(self, path, parentid, suiteids):
126         if not parentid.startswith('.' + os.path.sep):
127             parentid = os.path.join('.', parentid)
128         fileid = self._ensure_file(path.root, path.relfile)
129         rootdir = path.root
130
131         if not path.func:
132             return parentid
133
134         fullsuite, _, funcname = path.func.rpartition('.')
135         suiteid = self._ensure_suites(fullsuite, rootdir, fileid, suiteids)
136         parent = suiteid if suiteid else fileid
137
138         if path.sub:
139             if (rootdir, parentid) not in self._parents:
140                 funcinfo = ParentInfo(parentid, 'function', funcname,
141                                       rootdir, parent)
142                 self._parents[(rootdir, parentid)] = funcinfo
143         elif parent != parentid:
144             # TODO: What to do?
145             raise NotImplementedError
146         return parentid
147
148     def _ensure_file(self, rootdir, relfile):
149         if (rootdir, '.') not in self._parents:
150             self._parents[(rootdir, '.')] = ParentInfo('.', 'folder', rootdir)
151         if relfile.startswith('.' + os.path.sep):
152             fileid = relfile
153         else:
154             fileid = relfile = os.path.join('.', relfile)
155
156         if (rootdir, fileid) not in self._parents:
157             folderid, filebase = os.path.split(fileid)
158             fileinfo = ParentInfo(fileid, 'file', filebase, rootdir, folderid)
159             self._parents[(rootdir, fileid)] = fileinfo
160
161             while folderid != '.' and (rootdir, folderid) not in self._parents:
162                 parentid, name = os.path.split(folderid)
163                 folderinfo = ParentInfo(folderid, 'folder', name, rootdir, parentid)
164                 self._parents[(rootdir, folderid)] = folderinfo
165                 folderid = parentid
166         return relfile
167
168     def _ensure_suites(self, fullsuite, rootdir, fileid, suiteids):
169         if not fullsuite:
170             if suiteids:
171                 # TODO: What to do?
172                 raise NotImplementedError
173             return None
174         if len(suiteids) != fullsuite.count('.') + 1:
175             # TODO: What to do?
176             raise NotImplementedError
177
178         suiteid = suiteids.pop()
179         if not suiteid.startswith('.' + os.path.sep):
180             suiteid = os.path.join('.', suiteid)
181         final = suiteid
182         while '.' in fullsuite and (rootdir, suiteid) not in self._parents:
183             parentid = suiteids.pop()
184             if not parentid.startswith('.' + os.path.sep):
185                 parentid = os.path.join('.', parentid)
186             fullsuite, _, name = fullsuite.rpartition('.')
187             suiteinfo = ParentInfo(suiteid, 'suite', name, rootdir, parentid)
188             self._parents[(rootdir, suiteid)] = suiteinfo
189
190             suiteid = parentid
191         else:
192             name = fullsuite
193             suiteinfo = ParentInfo(suiteid, 'suite', name, rootdir, fileid)
194             if (rootdir, suiteid) not in self._parents:
195                 self._parents[(rootdir, suiteid)] = suiteinfo
196         return final
197
198
199 def _parse_item(item, _normcase, _pathsep):
200     """
201     (pytest.Collector)
202         pytest.Session
203         pytest.Package
204         pytest.Module
205         pytest.Class
206         (pytest.File)
207     (pytest.Item)
208         pytest.Function
209     """
210     #_debug_item(item, showsummary=True)
211     kind, _ = _get_item_kind(item)
212     # Figure out the func, suites, and subs.
213     (fileid, suiteids, suites, funcid, basename, parameterized
214      ) = _parse_node_id(item.nodeid, kind)
215     if kind == 'function':
216         funcname = basename
217         if funcid and item.function.__name__ != funcname:
218             # TODO: What to do?
219             raise NotImplementedError
220         if suites:
221             testfunc = '.'.join(suites) + '.' + funcname
222         else:
223             testfunc = funcname
224     elif kind == 'doctest':
225         testfunc = None
226         funcname = None
227
228     # Figure out the file.
229     fspath = str(item.fspath)
230     if not fspath.endswith(_pathsep + fileid):
231         raise NotImplementedError
232     filename = fspath[-len(fileid):]
233     testroot = str(item.fspath)[:-len(fileid)].rstrip(_pathsep)
234     if _pathsep in filename:
235         relfile = filename
236     else:
237         relfile = '.' + _pathsep + filename
238     srcfile, lineno, fullname = item.location
239     if srcfile != fileid:
240         # pytest supports discovery of tests imported from other
241         # modules.  This is reflected by a different filename
242         # in item.location.
243         if _normcase(fileid) == _normcase(srcfile):
244             srcfile = fileid
245     else:
246         srcfile = relfile
247     location = '{}:{}'.format(srcfile, lineno)
248     if kind == 'function':
249         if testfunc and fullname != testfunc + parameterized:
250             print(fullname, testfunc)
251             # TODO: What to do?
252             raise NotImplementedError
253     elif kind == 'doctest':
254         if testfunc and fullname != testfunc + parameterized:
255             print(fullname, testfunc)
256             # TODO: What to do?
257             raise NotImplementedError
258
259     # Sort out the parent.
260     if parameterized:
261         parentid = funcid
262     elif suites:
263         parentid = suiteids[-1]
264     else:
265         parentid = fileid
266
267     # Sort out markers.
268     #  See: https://docs.pytest.org/en/latest/reference.html#marks
269     markers = set()
270     for marker in item.own_markers:
271         if marker.name == 'parameterize':
272             # We've already covered these.
273             continue
274         elif marker.name == 'skip':
275             markers.add('skip')
276         elif marker.name == 'skipif':
277             markers.add('skip-if')
278         elif marker.name == 'xfail':
279             markers.add('expected-failure')
280         # TODO: Support other markers?
281
282     test = TestInfo(
283         id=item.nodeid,
284         name=item.name,
285         path=TestPath(
286             root=testroot,
287             relfile=relfile,
288             func=testfunc,
289             sub=[parameterized] if parameterized else None,
290             ),
291         source=location,
292         markers=sorted(markers) if markers else None,
293         parentid=parentid,
294         )
295     return test, suiteids
296
297
298 def _parse_node_id(nodeid, kind='function'):
299     if kind == 'doctest':
300         try:
301             parentid, name = nodeid.split('::')
302         except ValueError:
303             # TODO: Unexpected!  What to do?
304             raise NotImplementedError
305         funcid = None
306         parameterized = ''
307     else:
308         parameterized = ''
309         if nodeid.endswith(']'):
310             funcid, sep, parameterized = nodeid.partition('[')
311             if not sep:
312                 # TODO: Unexpected!  What to do?
313                 raise NotImplementedError
314             parameterized = sep + parameterized
315         else:
316             funcid = nodeid
317
318         parentid, _, name = funcid.rpartition('::')
319         if not name:
320             # TODO: What to do?  We expect at least a filename and a function
321             raise NotImplementedError
322
323     suites = []
324     suiteids = []
325     while '::' in parentid:
326         suiteids.insert(0, parentid)
327         parentid, _, suitename = parentid.rpartition('::')
328         suites.insert(0, suitename)
329     fileid = parentid
330
331     return fileid, suiteids, suites, funcid, name, parameterized
332
333
334 def _get_item_kind(item):
335     """Return (kind, isunittest) for the given item."""
336     try:
337         itemtype = item.kind
338     except AttributeError:
339         itemtype = item.__class__.__name__
340
341     if itemtype == 'DoctestItem':
342         return 'doctest', False
343     elif itemtype == 'Function':
344         return 'function', False
345     elif itemtype == 'TestCaseFunction':
346         return 'function', True
347     elif item.hasattr('function'):
348         return 'function', False
349     else:
350         return None, False
351
352
353 #############################
354 # useful for debugging
355
356 def _debug_item(item, showsummary=False):
357     item._debugging = True
358     try:
359         # TODO: Make a PytestTest class to wrap the item?
360         summary = {
361                 'id': item.nodeid,
362                 'kind': _get_item_kind(item),
363                 'class': item.__class__.__name__,
364                 'name': item.name,
365                 'fspath': item.fspath,
366                 'location': item.location,
367                 'func': getattr(item, 'function', None),
368                 'markers': item.own_markers,
369                 #'markers': list(item.iter_markers()),
370                 'props': item.user_properties,
371                 'attrnames': dir(item),
372                 }
373     finally:
374         item._debugging = False
375
376     if showsummary:
377         print(item.nodeid)
378         for key in ('kind', 'class', 'name', 'fspath', 'location', 'func',
379                     'markers', 'props'):
380             print('  {:12} {}'.format(key, summary[key]))
381         print()
382
383     return summary
384
385
386 def _group_attr_names(attrnames):
387     grouped = {
388             'dunder': [n for n in attrnames
389                        if n.startswith('__') and n.endswith('__')],
390             'private': [n for n in attrnames if n.startswith('_')],
391             'constants': [n for n in attrnames if n.isupper()],
392             'classes': [n for n in attrnames
393                         if n == n.capitalize() and not n.isupper()],
394             'vars': [n for n in attrnames if n.islower()],
395             }
396     grouped['other'] = [n for n in attrnames
397                           if n not in grouped['dunder']
398                           and n not in grouped['private']
399                           and n not in grouped['constants']
400                           and n not in grouped['classes']
401                           and n not in grouped['vars']
402                           ]
403     return grouped