My dotfiles
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

404 строки
13 KiB

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