main.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664
  1. """ core implementation of testing process: init, session, runtest loop. """
  2. from __future__ import absolute_import, division, print_function
  3. import contextlib
  4. import functools
  5. import os
  6. import pkgutil
  7. import six
  8. import sys
  9. import _pytest
  10. from _pytest import nodes
  11. import _pytest._code
  12. import py
  13. from _pytest.config import directory_arg, UsageError, hookimpl
  14. from _pytest.outcomes import exit
  15. from _pytest.runner import collect_one_node
  16. # exitcodes for the command line
  17. EXIT_OK = 0
  18. EXIT_TESTSFAILED = 1
  19. EXIT_INTERRUPTED = 2
  20. EXIT_INTERNALERROR = 3
  21. EXIT_USAGEERROR = 4
  22. EXIT_NOTESTSCOLLECTED = 5
  23. def pytest_addoption(parser):
  24. parser.addini(
  25. "norecursedirs",
  26. "directory patterns to avoid for recursion",
  27. type="args",
  28. default=[".*", "build", "dist", "CVS", "_darcs", "{arch}", "*.egg", "venv"],
  29. )
  30. parser.addini(
  31. "testpaths",
  32. "directories to search for tests when no files or directories are given in the "
  33. "command line.",
  34. type="args",
  35. default=[],
  36. )
  37. # parser.addini("dirpatterns",
  38. # "patterns specifying possible locations of test files",
  39. # type="linelist", default=["**/test_*.txt",
  40. # "**/test_*.py", "**/*_test.py"]
  41. # )
  42. group = parser.getgroup("general", "running and selection options")
  43. group._addoption(
  44. "-x",
  45. "--exitfirst",
  46. action="store_const",
  47. dest="maxfail",
  48. const=1,
  49. help="exit instantly on first error or failed test.",
  50. ),
  51. group._addoption(
  52. "--maxfail",
  53. metavar="num",
  54. action="store",
  55. type=int,
  56. dest="maxfail",
  57. default=0,
  58. help="exit after first num failures or errors.",
  59. )
  60. group._addoption(
  61. "--strict",
  62. action="store_true",
  63. help="marks not registered in configuration file raise errors.",
  64. )
  65. group._addoption(
  66. "-c",
  67. metavar="file",
  68. type=str,
  69. dest="inifilename",
  70. help="load configuration from `file` instead of trying to locate one of the implicit "
  71. "configuration files.",
  72. )
  73. group._addoption(
  74. "--continue-on-collection-errors",
  75. action="store_true",
  76. default=False,
  77. dest="continue_on_collection_errors",
  78. help="Force test execution even if collection errors occur.",
  79. )
  80. group._addoption(
  81. "--rootdir",
  82. action="store",
  83. dest="rootdir",
  84. help="Define root directory for tests. Can be relative path: 'root_dir', './root_dir', "
  85. "'root_dir/another_dir/'; absolute path: '/home/user/root_dir'; path with variables: "
  86. "'$HOME/root_dir'.",
  87. )
  88. group = parser.getgroup("collect", "collection")
  89. group.addoption(
  90. "--collectonly",
  91. "--collect-only",
  92. action="store_true",
  93. help="only collect tests, don't execute them.",
  94. ),
  95. group.addoption(
  96. "--pyargs",
  97. action="store_true",
  98. help="try to interpret all arguments as python packages.",
  99. )
  100. group.addoption(
  101. "--ignore",
  102. action="append",
  103. metavar="path",
  104. help="ignore path during collection (multi-allowed).",
  105. )
  106. group.addoption(
  107. "--deselect",
  108. action="append",
  109. metavar="nodeid_prefix",
  110. help="deselect item during collection (multi-allowed).",
  111. )
  112. # when changing this to --conf-cut-dir, config.py Conftest.setinitial
  113. # needs upgrading as well
  114. group.addoption(
  115. "--confcutdir",
  116. dest="confcutdir",
  117. default=None,
  118. metavar="dir",
  119. type=functools.partial(directory_arg, optname="--confcutdir"),
  120. help="only load conftest.py's relative to specified dir.",
  121. )
  122. group.addoption(
  123. "--noconftest",
  124. action="store_true",
  125. dest="noconftest",
  126. default=False,
  127. help="Don't load any conftest.py files.",
  128. )
  129. group.addoption(
  130. "--keepduplicates",
  131. "--keep-duplicates",
  132. action="store_true",
  133. dest="keepduplicates",
  134. default=False,
  135. help="Keep duplicate tests.",
  136. )
  137. group.addoption(
  138. "--collect-in-virtualenv",
  139. action="store_true",
  140. dest="collect_in_virtualenv",
  141. default=False,
  142. help="Don't ignore tests in a local virtualenv directory",
  143. )
  144. group = parser.getgroup("debugconfig", "test session debugging and configuration")
  145. group.addoption(
  146. "--basetemp",
  147. dest="basetemp",
  148. default=None,
  149. metavar="dir",
  150. help="base temporary directory for this test run.",
  151. )
  152. def pytest_configure(config):
  153. __import__("pytest").config = config # compatibility
  154. def wrap_session(config, doit):
  155. """Skeleton command line program"""
  156. session = Session(config)
  157. session.exitstatus = EXIT_OK
  158. initstate = 0
  159. try:
  160. try:
  161. config._do_configure()
  162. initstate = 1
  163. config.hook.pytest_sessionstart(session=session)
  164. initstate = 2
  165. session.exitstatus = doit(config, session) or 0
  166. except UsageError:
  167. raise
  168. except Failed:
  169. session.exitstatus = EXIT_TESTSFAILED
  170. except KeyboardInterrupt:
  171. excinfo = _pytest._code.ExceptionInfo()
  172. if initstate < 2 and isinstance(excinfo.value, exit.Exception):
  173. sys.stderr.write("{}: {}\n".format(excinfo.typename, excinfo.value.msg))
  174. config.hook.pytest_keyboard_interrupt(excinfo=excinfo)
  175. session.exitstatus = EXIT_INTERRUPTED
  176. except: # noqa
  177. excinfo = _pytest._code.ExceptionInfo()
  178. config.notify_exception(excinfo, config.option)
  179. session.exitstatus = EXIT_INTERNALERROR
  180. if excinfo.errisinstance(SystemExit):
  181. sys.stderr.write("mainloop: caught Spurious SystemExit!\n")
  182. finally:
  183. excinfo = None # Explicitly break reference cycle.
  184. session.startdir.chdir()
  185. if initstate >= 2:
  186. config.hook.pytest_sessionfinish(
  187. session=session, exitstatus=session.exitstatus
  188. )
  189. config._ensure_unconfigure()
  190. return session.exitstatus
  191. def pytest_cmdline_main(config):
  192. return wrap_session(config, _main)
  193. def _main(config, session):
  194. """ default command line protocol for initialization, session,
  195. running tests and reporting. """
  196. config.hook.pytest_collection(session=session)
  197. config.hook.pytest_runtestloop(session=session)
  198. if session.testsfailed:
  199. return EXIT_TESTSFAILED
  200. elif session.testscollected == 0:
  201. return EXIT_NOTESTSCOLLECTED
  202. def pytest_collection(session):
  203. return session.perform_collect()
  204. def pytest_runtestloop(session):
  205. if session.testsfailed and not session.config.option.continue_on_collection_errors:
  206. raise session.Interrupted("%d errors during collection" % session.testsfailed)
  207. if session.config.option.collectonly:
  208. return True
  209. for i, item in enumerate(session.items):
  210. nextitem = session.items[i + 1] if i + 1 < len(session.items) else None
  211. item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem)
  212. if session.shouldfail:
  213. raise session.Failed(session.shouldfail)
  214. if session.shouldstop:
  215. raise session.Interrupted(session.shouldstop)
  216. return True
  217. def _in_venv(path):
  218. """Attempts to detect if ``path`` is the root of a Virtual Environment by
  219. checking for the existence of the appropriate activate script"""
  220. bindir = path.join("Scripts" if sys.platform.startswith("win") else "bin")
  221. if not bindir.isdir():
  222. return False
  223. activates = (
  224. "activate",
  225. "activate.csh",
  226. "activate.fish",
  227. "Activate",
  228. "Activate.bat",
  229. "Activate.ps1",
  230. )
  231. return any([fname.basename in activates for fname in bindir.listdir()])
  232. def pytest_ignore_collect(path, config):
  233. ignore_paths = config._getconftest_pathlist("collect_ignore", path=path.dirpath())
  234. ignore_paths = ignore_paths or []
  235. excludeopt = config.getoption("ignore")
  236. if excludeopt:
  237. ignore_paths.extend([py.path.local(x) for x in excludeopt])
  238. if py.path.local(path) in ignore_paths:
  239. return True
  240. allow_in_venv = config.getoption("collect_in_virtualenv")
  241. if _in_venv(path) and not allow_in_venv:
  242. return True
  243. # Skip duplicate paths.
  244. keepduplicates = config.getoption("keepduplicates")
  245. duplicate_paths = config.pluginmanager._duplicatepaths
  246. if not keepduplicates:
  247. if path in duplicate_paths:
  248. return True
  249. else:
  250. duplicate_paths.add(path)
  251. return False
  252. def pytest_collection_modifyitems(items, config):
  253. deselect_prefixes = tuple(config.getoption("deselect") or [])
  254. if not deselect_prefixes:
  255. return
  256. remaining = []
  257. deselected = []
  258. for colitem in items:
  259. if colitem.nodeid.startswith(deselect_prefixes):
  260. deselected.append(colitem)
  261. else:
  262. remaining.append(colitem)
  263. if deselected:
  264. config.hook.pytest_deselected(items=deselected)
  265. items[:] = remaining
  266. @contextlib.contextmanager
  267. def _patched_find_module():
  268. """Patch bug in pkgutil.ImpImporter.find_module
  269. When using pkgutil.find_loader on python<3.4 it removes symlinks
  270. from the path due to a call to os.path.realpath. This is not consistent
  271. with actually doing the import (in these versions, pkgutil and __import__
  272. did not share the same underlying code). This can break conftest
  273. discovery for pytest where symlinks are involved.
  274. The only supported python<3.4 by pytest is python 2.7.
  275. """
  276. if six.PY2: # python 3.4+ uses importlib instead
  277. def find_module_patched(self, fullname, path=None):
  278. # Note: we ignore 'path' argument since it is only used via meta_path
  279. subname = fullname.split(".")[-1]
  280. if subname != fullname and self.path is None:
  281. return None
  282. if self.path is None:
  283. path = None
  284. else:
  285. # original: path = [os.path.realpath(self.path)]
  286. path = [self.path]
  287. try:
  288. file, filename, etc = pkgutil.imp.find_module(subname, path)
  289. except ImportError:
  290. return None
  291. return pkgutil.ImpLoader(fullname, file, filename, etc)
  292. old_find_module = pkgutil.ImpImporter.find_module
  293. pkgutil.ImpImporter.find_module = find_module_patched
  294. try:
  295. yield
  296. finally:
  297. pkgutil.ImpImporter.find_module = old_find_module
  298. else:
  299. yield
  300. class FSHookProxy(object):
  301. def __init__(self, fspath, pm, remove_mods):
  302. self.fspath = fspath
  303. self.pm = pm
  304. self.remove_mods = remove_mods
  305. def __getattr__(self, name):
  306. x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods)
  307. self.__dict__[name] = x
  308. return x
  309. class NoMatch(Exception):
  310. """ raised if matching cannot locate a matching names. """
  311. class Interrupted(KeyboardInterrupt):
  312. """ signals an interrupted test run. """
  313. __module__ = "builtins" # for py3
  314. class Failed(Exception):
  315. """ signals a stop as failed test run. """
  316. class Session(nodes.FSCollector):
  317. Interrupted = Interrupted
  318. Failed = Failed
  319. def __init__(self, config):
  320. nodes.FSCollector.__init__(
  321. self, config.rootdir, parent=None, config=config, session=self, nodeid=""
  322. )
  323. self.testsfailed = 0
  324. self.testscollected = 0
  325. self.shouldstop = False
  326. self.shouldfail = False
  327. self.trace = config.trace.root.get("collection")
  328. self._norecursepatterns = config.getini("norecursedirs")
  329. self.startdir = py.path.local()
  330. # Keep track of any collected nodes in here, so we don't duplicate fixtures
  331. self._node_cache = {}
  332. self.config.pluginmanager.register(self, name="session")
  333. @hookimpl(tryfirst=True)
  334. def pytest_collectstart(self):
  335. if self.shouldfail:
  336. raise self.Failed(self.shouldfail)
  337. if self.shouldstop:
  338. raise self.Interrupted(self.shouldstop)
  339. @hookimpl(tryfirst=True)
  340. def pytest_runtest_logreport(self, report):
  341. if report.failed and not hasattr(report, "wasxfail"):
  342. self.testsfailed += 1
  343. maxfail = self.config.getvalue("maxfail")
  344. if maxfail and self.testsfailed >= maxfail:
  345. self.shouldfail = "stopping after %d failures" % (self.testsfailed)
  346. pytest_collectreport = pytest_runtest_logreport
  347. def isinitpath(self, path):
  348. return path in self._initialpaths
  349. def gethookproxy(self, fspath):
  350. # check if we have the common case of running
  351. # hooks with all conftest.py files
  352. pm = self.config.pluginmanager
  353. my_conftestmodules = pm._getconftestmodules(fspath)
  354. remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
  355. if remove_mods:
  356. # one or more conftests are not in use at this fspath
  357. proxy = FSHookProxy(fspath, pm, remove_mods)
  358. else:
  359. # all plugis are active for this fspath
  360. proxy = self.config.hook
  361. return proxy
  362. def perform_collect(self, args=None, genitems=True):
  363. hook = self.config.hook
  364. try:
  365. items = self._perform_collect(args, genitems)
  366. self.config.pluginmanager.check_pending()
  367. hook.pytest_collection_modifyitems(
  368. session=self, config=self.config, items=items
  369. )
  370. finally:
  371. hook.pytest_collection_finish(session=self)
  372. self.testscollected = len(items)
  373. return items
  374. def _perform_collect(self, args, genitems):
  375. if args is None:
  376. args = self.config.args
  377. self.trace("perform_collect", self, args)
  378. self.trace.root.indent += 1
  379. self._notfound = []
  380. self._initialpaths = set()
  381. self._initialparts = []
  382. self.items = items = []
  383. for arg in args:
  384. parts = self._parsearg(arg)
  385. self._initialparts.append(parts)
  386. self._initialpaths.add(parts[0])
  387. rep = collect_one_node(self)
  388. self.ihook.pytest_collectreport(report=rep)
  389. self.trace.root.indent -= 1
  390. if self._notfound:
  391. errors = []
  392. for arg, exc in self._notfound:
  393. line = "(no name %r in any of %r)" % (arg, exc.args[0])
  394. errors.append("not found: %s\n%s" % (arg, line))
  395. # XXX: test this
  396. raise UsageError(*errors)
  397. if not genitems:
  398. return rep.result
  399. else:
  400. if rep.passed:
  401. for node in rep.result:
  402. self.items.extend(self.genitems(node))
  403. return items
  404. def collect(self):
  405. for parts in self._initialparts:
  406. arg = "::".join(map(str, parts))
  407. self.trace("processing argument", arg)
  408. self.trace.root.indent += 1
  409. try:
  410. for x in self._collect(arg):
  411. yield x
  412. except NoMatch:
  413. # we are inside a make_report hook so
  414. # we cannot directly pass through the exception
  415. self._notfound.append((arg, sys.exc_info()[1]))
  416. self.trace.root.indent -= 1
  417. def _collect(self, arg):
  418. from _pytest.python import Package
  419. names = self._parsearg(arg)
  420. argpath = names.pop(0)
  421. paths = []
  422. root = self
  423. # Start with a Session root, and delve to argpath item (dir or file)
  424. # and stack all Packages found on the way.
  425. # No point in finding packages when collecting doctests
  426. if not self.config.option.doctestmodules:
  427. for parent in argpath.parts():
  428. pm = self.config.pluginmanager
  429. if pm._confcutdir and pm._confcutdir.relto(parent):
  430. continue
  431. if parent.isdir():
  432. pkginit = parent.join("__init__.py")
  433. if pkginit.isfile():
  434. if pkginit in self._node_cache:
  435. root = self._node_cache[pkginit]
  436. else:
  437. col = root._collectfile(pkginit)
  438. if col:
  439. if isinstance(col[0], Package):
  440. root = col[0]
  441. self._node_cache[root.fspath] = root
  442. # If it's a directory argument, recurse and look for any Subpackages.
  443. # Let the Package collector deal with subnodes, don't collect here.
  444. if argpath.check(dir=1):
  445. assert not names, "invalid arg %r" % (arg,)
  446. for path in argpath.visit(
  447. fil=lambda x: x.check(file=1), rec=self._recurse, bf=True, sort=True
  448. ):
  449. pkginit = path.dirpath().join("__init__.py")
  450. if pkginit.exists() and not any(x in pkginit.parts() for x in paths):
  451. for x in root._collectfile(pkginit):
  452. yield x
  453. paths.append(x.fspath.dirpath())
  454. if not any(x in path.parts() for x in paths):
  455. for x in root._collectfile(path):
  456. if (type(x), x.fspath) in self._node_cache:
  457. yield self._node_cache[(type(x), x.fspath)]
  458. else:
  459. yield x
  460. self._node_cache[(type(x), x.fspath)] = x
  461. else:
  462. assert argpath.check(file=1)
  463. if argpath in self._node_cache:
  464. col = self._node_cache[argpath]
  465. else:
  466. col = root._collectfile(argpath)
  467. if col:
  468. self._node_cache[argpath] = col
  469. for y in self.matchnodes(col, names):
  470. yield y
  471. def _collectfile(self, path):
  472. ihook = self.gethookproxy(path)
  473. if not self.isinitpath(path):
  474. if ihook.pytest_ignore_collect(path=path, config=self.config):
  475. return ()
  476. return ihook.pytest_collect_file(path=path, parent=self)
  477. def _recurse(self, path):
  478. ihook = self.gethookproxy(path.dirpath())
  479. if ihook.pytest_ignore_collect(path=path, config=self.config):
  480. return
  481. for pat in self._norecursepatterns:
  482. if path.check(fnmatch=pat):
  483. return False
  484. ihook = self.gethookproxy(path)
  485. ihook.pytest_collect_directory(path=path, parent=self)
  486. return True
  487. def _tryconvertpyarg(self, x):
  488. """Convert a dotted module name to path.
  489. """
  490. try:
  491. with _patched_find_module():
  492. loader = pkgutil.find_loader(x)
  493. except ImportError:
  494. return x
  495. if loader is None:
  496. return x
  497. # This method is sometimes invoked when AssertionRewritingHook, which
  498. # does not define a get_filename method, is already in place:
  499. try:
  500. with _patched_find_module():
  501. path = loader.get_filename(x)
  502. except AttributeError:
  503. # Retrieve path from AssertionRewritingHook:
  504. path = loader.modules[x][0].co_filename
  505. if loader.is_package(x):
  506. path = os.path.dirname(path)
  507. return path
  508. def _parsearg(self, arg):
  509. """ return (fspath, names) tuple after checking the file exists. """
  510. parts = str(arg).split("::")
  511. if self.config.option.pyargs:
  512. parts[0] = self._tryconvertpyarg(parts[0])
  513. relpath = parts[0].replace("/", os.sep)
  514. path = self.config.invocation_dir.join(relpath, abs=True)
  515. if not path.check():
  516. if self.config.option.pyargs:
  517. raise UsageError(
  518. "file or package not found: " + arg + " (missing __init__.py?)"
  519. )
  520. else:
  521. raise UsageError("file not found: " + arg)
  522. parts[0] = path
  523. return parts
  524. def matchnodes(self, matching, names):
  525. self.trace("matchnodes", matching, names)
  526. self.trace.root.indent += 1
  527. nodes = self._matchnodes(matching, names)
  528. num = len(nodes)
  529. self.trace("matchnodes finished -> ", num, "nodes")
  530. self.trace.root.indent -= 1
  531. if num == 0:
  532. raise NoMatch(matching, names[:1])
  533. return nodes
  534. def _matchnodes(self, matching, names):
  535. if not matching or not names:
  536. return matching
  537. name = names[0]
  538. assert name
  539. nextnames = names[1:]
  540. resultnodes = []
  541. for node in matching:
  542. if isinstance(node, nodes.Item):
  543. if not names:
  544. resultnodes.append(node)
  545. continue
  546. assert isinstance(node, nodes.Collector)
  547. key = (type(node), node.nodeid)
  548. if key in self._node_cache:
  549. rep = self._node_cache[key]
  550. else:
  551. rep = collect_one_node(node)
  552. self._node_cache[key] = rep
  553. if rep.passed:
  554. has_matched = False
  555. for x in rep.result:
  556. # TODO: remove parametrized workaround once collection structure contains parametrization
  557. if x.name == name or x.name.split("[")[0] == name:
  558. resultnodes.extend(self.matchnodes([x], nextnames))
  559. has_matched = True
  560. # XXX accept IDs that don't have "()" for class instances
  561. if not has_matched and len(rep.result) == 1 and x.name == "()":
  562. nextnames.insert(0, name)
  563. resultnodes.extend(self.matchnodes([x], nextnames))
  564. else:
  565. # report collection failures here to avoid failing to run some test
  566. # specified in the command line because the module could not be
  567. # imported (#134)
  568. node.ihook.pytest_collectreport(report=rep)
  569. return resultnodes
  570. def genitems(self, node):
  571. self.trace("genitems", node)
  572. if isinstance(node, nodes.Item):
  573. node.ihook.pytest_itemcollected(item=node)
  574. yield node
  575. else:
  576. assert isinstance(node, nodes.Collector)
  577. rep = collect_one_node(node)
  578. if rep.passed:
  579. for subnode in rep.result:
  580. for x in self.genitems(subnode):
  581. yield x
  582. node.ihook.pytest_collectreport(report=rep)