Coverage for /usr/local/lib/python3.7/site-packages/_pytest/main.py : 41%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1""" core implementation of testing process: init, session, runtest loop. """
2import enum
3import fnmatch
4import functools
5import importlib
6import os
7import sys
8from typing import Dict
10import attr
11import py
13import _pytest._code
14from _pytest import nodes
15from _pytest.config import directory_arg
16from _pytest.config import hookimpl
17from _pytest.config import UsageError
18from _pytest.fixtures import FixtureManager
19from _pytest.outcomes import exit
20from _pytest.runner import collect_one_node
21from _pytest.runner import SetupState
24class ExitCode(enum.IntEnum):
25 """
26 .. versionadded:: 5.0
28 Encodes the valid exit codes by pytest.
30 Currently users and plugins may supply other exit codes as well.
31 """
33 #: tests passed
34 OK = 0
35 #: tests failed
36 TESTS_FAILED = 1
37 #: pytest was interrupted
38 INTERRUPTED = 2
39 #: an internal error got in the way
40 INTERNAL_ERROR = 3
41 #: pytest was misused
42 USAGE_ERROR = 4
43 #: pytest couldn't find tests
44 NO_TESTS_COLLECTED = 5
47def pytest_addoption(parser):
48 parser.addini(
49 "norecursedirs",
50 "directory patterns to avoid for recursion",
51 type="args",
52 default=[".*", "build", "dist", "CVS", "_darcs", "{arch}", "*.egg", "venv"],
53 )
54 parser.addini(
55 "testpaths",
56 "directories to search for tests when no files or directories are given in the "
57 "command line.",
58 type="args",
59 default=[],
60 )
61 group = parser.getgroup("general", "running and selection options")
62 group._addoption(
63 "-x",
64 "--exitfirst",
65 action="store_const",
66 dest="maxfail",
67 const=1,
68 help="exit instantly on first error or failed test.",
69 ),
70 group._addoption(
71 "--maxfail",
72 metavar="num",
73 action="store",
74 type=int,
75 dest="maxfail",
76 default=0,
77 help="exit after first num failures or errors.",
78 )
79 group._addoption(
80 "--strict-markers",
81 "--strict",
82 action="store_true",
83 help="markers not registered in the `markers` section of the configuration file raise errors.",
84 )
85 group._addoption(
86 "-c",
87 metavar="file",
88 type=str,
89 dest="inifilename",
90 help="load configuration from `file` instead of trying to locate one of the implicit "
91 "configuration files.",
92 )
93 group._addoption(
94 "--continue-on-collection-errors",
95 action="store_true",
96 default=False,
97 dest="continue_on_collection_errors",
98 help="Force test execution even if collection errors occur.",
99 )
100 group._addoption(
101 "--rootdir",
102 action="store",
103 dest="rootdir",
104 help="Define root directory for tests. Can be relative path: 'root_dir', './root_dir', "
105 "'root_dir/another_dir/'; absolute path: '/home/user/root_dir'; path with variables: "
106 "'$HOME/root_dir'.",
107 )
109 group = parser.getgroup("collect", "collection")
110 group.addoption(
111 "--collectonly",
112 "--collect-only",
113 "--co",
114 action="store_true",
115 help="only collect tests, don't execute them.",
116 ),
117 group.addoption(
118 "--pyargs",
119 action="store_true",
120 help="try to interpret all arguments as python packages.",
121 )
122 group.addoption(
123 "--ignore",
124 action="append",
125 metavar="path",
126 help="ignore path during collection (multi-allowed).",
127 )
128 group.addoption(
129 "--ignore-glob",
130 action="append",
131 metavar="path",
132 help="ignore path pattern during collection (multi-allowed).",
133 )
134 group.addoption(
135 "--deselect",
136 action="append",
137 metavar="nodeid_prefix",
138 help="deselect item during collection (multi-allowed).",
139 )
140 # when changing this to --conf-cut-dir, config.py Conftest.setinitial
141 # needs upgrading as well
142 group.addoption(
143 "--confcutdir",
144 dest="confcutdir",
145 default=None,
146 metavar="dir",
147 type=functools.partial(directory_arg, optname="--confcutdir"),
148 help="only load conftest.py's relative to specified dir.",
149 )
150 group.addoption(
151 "--noconftest",
152 action="store_true",
153 dest="noconftest",
154 default=False,
155 help="Don't load any conftest.py files.",
156 )
157 group.addoption(
158 "--keepduplicates",
159 "--keep-duplicates",
160 action="store_true",
161 dest="keepduplicates",
162 default=False,
163 help="Keep duplicate tests.",
164 )
165 group.addoption(
166 "--collect-in-virtualenv",
167 action="store_true",
168 dest="collect_in_virtualenv",
169 default=False,
170 help="Don't ignore tests in a local virtualenv directory",
171 )
173 group = parser.getgroup("debugconfig", "test session debugging and configuration")
174 group.addoption(
175 "--basetemp",
176 dest="basetemp",
177 default=None,
178 metavar="dir",
179 help=(
180 "base temporary directory for this test run."
181 "(warning: this directory is removed if it exists)"
182 ),
183 )
186def wrap_session(config, doit):
187 """Skeleton command line program"""
188 session = Session(config)
189 session.exitstatus = ExitCode.OK
190 initstate = 0
191 try:
192 try:
193 config._do_configure()
194 initstate = 1
195 config.hook.pytest_sessionstart(session=session)
196 initstate = 2
197 session.exitstatus = doit(config, session) or 0
198 except UsageError:
199 session.exitstatus = ExitCode.USAGE_ERROR
200 raise
201 except Failed:
202 session.exitstatus = ExitCode.TESTS_FAILED
203 except (KeyboardInterrupt, exit.Exception):
204 excinfo = _pytest._code.ExceptionInfo.from_current()
205 exitstatus = ExitCode.INTERRUPTED
206 if isinstance(excinfo.value, exit.Exception):
207 if excinfo.value.returncode is not None:
208 exitstatus = excinfo.value.returncode
209 if initstate < 2:
210 sys.stderr.write(
211 "{}: {}\n".format(excinfo.typename, excinfo.value.msg)
212 )
213 config.hook.pytest_keyboard_interrupt(excinfo=excinfo)
214 session.exitstatus = exitstatus
215 except: # noqa
216 session.exitstatus = ExitCode.INTERNAL_ERROR
217 excinfo = _pytest._code.ExceptionInfo.from_current()
218 try:
219 config.notify_exception(excinfo, config.option)
220 except exit.Exception as exc:
221 if exc.returncode is not None:
222 session.exitstatus = exc.returncode
223 sys.stderr.write("{}: {}\n".format(type(exc).__name__, exc))
224 else:
225 if excinfo.errisinstance(SystemExit):
226 sys.stderr.write("mainloop: caught unexpected SystemExit!\n")
228 finally:
229 excinfo = None # Explicitly break reference cycle.
230 session.startdir.chdir()
231 if initstate >= 2:
232 config.hook.pytest_sessionfinish(
233 session=session, exitstatus=session.exitstatus
234 )
235 config._ensure_unconfigure()
236 return session.exitstatus
239def pytest_cmdline_main(config):
240 return wrap_session(config, _main)
243def _main(config, session):
244 """ default command line protocol for initialization, session,
245 running tests and reporting. """
246 config.hook.pytest_collection(session=session)
247 config.hook.pytest_runtestloop(session=session)
249 if session.testsfailed:
250 return ExitCode.TESTS_FAILED
251 elif session.testscollected == 0:
252 return ExitCode.NO_TESTS_COLLECTED
255def pytest_collection(session):
256 return session.perform_collect()
259def pytest_runtestloop(session):
260 if session.testsfailed and not session.config.option.continue_on_collection_errors:
261 raise session.Interrupted(
262 "%d error%s during collection"
263 % (session.testsfailed, "s" if session.testsfailed != 1 else "")
264 )
266 if session.config.option.collectonly:
267 return True
269 for i, item in enumerate(session.items):
270 nextitem = session.items[i + 1] if i + 1 < len(session.items) else None
271 item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem)
272 if session.shouldfail:
273 raise session.Failed(session.shouldfail)
274 if session.shouldstop:
275 raise session.Interrupted(session.shouldstop)
276 return True
279def _in_venv(path):
280 """Attempts to detect if ``path`` is the root of a Virtual Environment by
281 checking for the existence of the appropriate activate script"""
282 bindir = path.join("Scripts" if sys.platform.startswith("win") else "bin")
283 if not bindir.isdir():
284 return False
285 activates = (
286 "activate",
287 "activate.csh",
288 "activate.fish",
289 "Activate",
290 "Activate.bat",
291 "Activate.ps1",
292 )
293 return any([fname.basename in activates for fname in bindir.listdir()])
296def pytest_ignore_collect(path, config):
297 ignore_paths = config._getconftest_pathlist("collect_ignore", path=path.dirpath())
298 ignore_paths = ignore_paths or []
299 excludeopt = config.getoption("ignore")
300 if excludeopt:
301 ignore_paths.extend([py.path.local(x) for x in excludeopt])
303 if py.path.local(path) in ignore_paths:
304 return True
306 ignore_globs = config._getconftest_pathlist(
307 "collect_ignore_glob", path=path.dirpath()
308 )
309 ignore_globs = ignore_globs or []
310 excludeglobopt = config.getoption("ignore_glob")
311 if excludeglobopt:
312 ignore_globs.extend([py.path.local(x) for x in excludeglobopt])
314 if any(fnmatch.fnmatch(str(path), str(glob)) for glob in ignore_globs):
315 return True
317 allow_in_venv = config.getoption("collect_in_virtualenv")
318 if not allow_in_venv and _in_venv(path):
319 return True
321 return False
324def pytest_collection_modifyitems(items, config):
325 deselect_prefixes = tuple(config.getoption("deselect") or [])
326 if not deselect_prefixes:
327 return
329 remaining = []
330 deselected = []
331 for colitem in items:
332 if colitem.nodeid.startswith(deselect_prefixes):
333 deselected.append(colitem)
334 else:
335 remaining.append(colitem)
337 if deselected:
338 config.hook.pytest_deselected(items=deselected)
339 items[:] = remaining
342class FSHookProxy:
343 def __init__(self, fspath, pm, remove_mods):
344 self.fspath = fspath
345 self.pm = pm
346 self.remove_mods = remove_mods
348 def __getattr__(self, name):
349 x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods)
350 self.__dict__[name] = x
351 return x
354class NoMatch(Exception):
355 """ raised if matching cannot locate a matching names. """
358class Interrupted(KeyboardInterrupt):
359 """ signals an interrupted test run. """
361 __module__ = "builtins" # for py3
364class Failed(Exception):
365 """ signals a stop as failed test run. """
368@attr.s
369class _bestrelpath_cache(dict):
370 path = attr.ib()
372 def __missing__(self, path: str) -> str:
373 r = self.path.bestrelpath(path) # type: str
374 self[path] = r
375 return r
378class Session(nodes.FSCollector):
379 Interrupted = Interrupted
380 Failed = Failed
381 # Set on the session by runner.pytest_sessionstart.
382 _setupstate = None # type: SetupState
383 # Set on the session by fixtures.pytest_sessionstart.
384 _fixturemanager = None # type: FixtureManager
386 def __init__(self, config):
387 nodes.FSCollector.__init__(
388 self, config.rootdir, parent=None, config=config, session=self, nodeid=""
389 )
390 self.testsfailed = 0
391 self.testscollected = 0
392 self.shouldstop = False
393 self.shouldfail = False
394 self.trace = config.trace.root.get("collection")
395 self._norecursepatterns = config.getini("norecursedirs")
396 self.startdir = config.invocation_dir
397 self._initialpaths = frozenset()
398 # Keep track of any collected nodes in here, so we don't duplicate fixtures
399 self._node_cache = {}
400 self._bestrelpathcache = _bestrelpath_cache(
401 config.rootdir
402 ) # type: Dict[str, str]
403 # Dirnames of pkgs with dunder-init files.
404 self._pkg_roots = {}
406 self.config.pluginmanager.register(self, name="session")
408 def __repr__(self):
409 return "<%s %s exitstatus=%r testsfailed=%d testscollected=%d>" % (
410 self.__class__.__name__,
411 self.name,
412 getattr(self, "exitstatus", "<UNSET>"),
413 self.testsfailed,
414 self.testscollected,
415 )
417 def _node_location_to_relpath(self, node_path: str) -> str:
418 # bestrelpath is a quite slow function
419 return self._bestrelpathcache[node_path]
421 @hookimpl(tryfirst=True)
422 def pytest_collectstart(self):
423 if self.shouldfail:
424 raise self.Failed(self.shouldfail)
425 if self.shouldstop:
426 raise self.Interrupted(self.shouldstop)
428 @hookimpl(tryfirst=True)
429 def pytest_runtest_logreport(self, report):
430 if report.failed and not hasattr(report, "wasxfail"):
431 self.testsfailed += 1
432 maxfail = self.config.getvalue("maxfail")
433 if maxfail and self.testsfailed >= maxfail:
434 self.shouldfail = "stopping after %d failures" % (self.testsfailed)
436 pytest_collectreport = pytest_runtest_logreport
438 def isinitpath(self, path):
439 return path in self._initialpaths
441 def gethookproxy(self, fspath):
442 # check if we have the common case of running
443 # hooks with all conftest.py files
444 pm = self.config.pluginmanager
445 my_conftestmodules = pm._getconftestmodules(fspath)
446 remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
447 if remove_mods:
448 # one or more conftests are not in use at this fspath
449 proxy = FSHookProxy(fspath, pm, remove_mods)
450 else:
451 # all plugins are active for this fspath
452 proxy = self.config.hook
453 return proxy
455 def perform_collect(self, args=None, genitems=True):
456 hook = self.config.hook
457 try:
458 items = self._perform_collect(args, genitems)
459 self.config.pluginmanager.check_pending()
460 hook.pytest_collection_modifyitems(
461 session=self, config=self.config, items=items
462 )
463 finally:
464 hook.pytest_collection_finish(session=self)
465 self.testscollected = len(items)
466 return items
468 def _perform_collect(self, args, genitems):
469 if args is None:
470 args = self.config.args
471 self.trace("perform_collect", self, args)
472 self.trace.root.indent += 1
473 self._notfound = []
474 initialpaths = []
475 self._initialparts = []
476 self.items = items = []
477 for arg in args:
478 parts = self._parsearg(arg)
479 self._initialparts.append(parts)
480 initialpaths.append(parts[0])
481 self._initialpaths = frozenset(initialpaths)
482 rep = collect_one_node(self)
483 self.ihook.pytest_collectreport(report=rep)
484 self.trace.root.indent -= 1
485 if self._notfound:
486 errors = []
487 for arg, exc in self._notfound:
488 line = "(no name {!r} in any of {!r})".format(arg, exc.args[0])
489 errors.append("not found: {}\n{}".format(arg, line))
490 raise UsageError(*errors)
491 if not genitems:
492 return rep.result
493 else:
494 if rep.passed:
495 for node in rep.result:
496 self.items.extend(self.genitems(node))
497 return items
499 def collect(self):
500 for initialpart in self._initialparts:
501 self.trace("processing argument", initialpart)
502 self.trace.root.indent += 1
503 try:
504 yield from self._collect(initialpart)
505 except NoMatch:
506 report_arg = "::".join(map(str, initialpart))
507 # we are inside a make_report hook so
508 # we cannot directly pass through the exception
509 self._notfound.append((report_arg, sys.exc_info()[1]))
511 self.trace.root.indent -= 1
513 def _collect(self, arg):
514 from _pytest.python import Package
516 names = arg[:]
517 argpath = names.pop(0)
519 # Start with a Session root, and delve to argpath item (dir or file)
520 # and stack all Packages found on the way.
521 # No point in finding packages when collecting doctests
522 if not self.config.getoption("doctestmodules", False):
523 pm = self.config.pluginmanager
524 for parent in reversed(argpath.parts()):
525 if pm._confcutdir and pm._confcutdir.relto(parent):
526 break
528 if parent.isdir():
529 pkginit = parent.join("__init__.py")
530 if pkginit.isfile():
531 if pkginit not in self._node_cache:
532 col = self._collectfile(pkginit, handle_dupes=False)
533 if col:
534 if isinstance(col[0], Package):
535 self._pkg_roots[parent] = col[0]
536 # always store a list in the cache, matchnodes expects it
537 self._node_cache[col[0].fspath] = [col[0]]
539 # If it's a directory argument, recurse and look for any Subpackages.
540 # Let the Package collector deal with subnodes, don't collect here.
541 if argpath.check(dir=1):
542 assert not names, "invalid arg {!r}".format(arg)
544 seen_dirs = set()
545 for path in argpath.visit(
546 fil=self._visit_filter, rec=self._recurse, bf=True, sort=True
547 ):
548 dirpath = path.dirpath()
549 if dirpath not in seen_dirs:
550 # Collect packages first.
551 seen_dirs.add(dirpath)
552 pkginit = dirpath.join("__init__.py")
553 if pkginit.exists():
554 for x in self._collectfile(pkginit):
555 yield x
556 if isinstance(x, Package):
557 self._pkg_roots[dirpath] = x
558 if dirpath in self._pkg_roots:
559 # Do not collect packages here.
560 continue
562 for x in self._collectfile(path):
563 key = (type(x), x.fspath)
564 if key in self._node_cache:
565 yield self._node_cache[key]
566 else:
567 self._node_cache[key] = x
568 yield x
569 else:
570 assert argpath.check(file=1)
572 if argpath in self._node_cache:
573 col = self._node_cache[argpath]
574 else:
575 collect_root = self._pkg_roots.get(argpath.dirname, self)
576 col = collect_root._collectfile(argpath, handle_dupes=False)
577 if col:
578 self._node_cache[argpath] = col
579 m = self.matchnodes(col, names)
580 # If __init__.py was the only file requested, then the matched node will be
581 # the corresponding Package, and the first yielded item will be the __init__
582 # Module itself, so just use that. If this special case isn't taken, then all
583 # the files in the package will be yielded.
584 if argpath.basename == "__init__.py":
585 try:
586 yield next(m[0].collect())
587 except StopIteration:
588 # The package collects nothing with only an __init__.py
589 # file in it, which gets ignored by the default
590 # "python_files" option.
591 pass
592 return
593 yield from m
595 def _collectfile(self, path, handle_dupes=True):
596 assert (
597 path.isfile()
598 ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format(
599 path, path.isdir(), path.exists(), path.islink()
600 )
601 ihook = self.gethookproxy(path)
602 if not self.isinitpath(path):
603 if ihook.pytest_ignore_collect(path=path, config=self.config):
604 return ()
606 if handle_dupes:
607 keepduplicates = self.config.getoption("keepduplicates")
608 if not keepduplicates:
609 duplicate_paths = self.config.pluginmanager._duplicatepaths
610 if path in duplicate_paths:
611 return ()
612 else:
613 duplicate_paths.add(path)
615 return ihook.pytest_collect_file(path=path, parent=self)
617 def _recurse(self, dirpath):
618 if dirpath.basename == "__pycache__":
619 return False
620 ihook = self.gethookproxy(dirpath.dirpath())
621 if ihook.pytest_ignore_collect(path=dirpath, config=self.config):
622 return False
623 for pat in self._norecursepatterns:
624 if dirpath.check(fnmatch=pat):
625 return False
626 ihook = self.gethookproxy(dirpath)
627 ihook.pytest_collect_directory(path=dirpath, parent=self)
628 return True
630 @staticmethod
631 def _visit_filter(f):
632 return f.check(file=1)
634 def _tryconvertpyarg(self, x):
635 """Convert a dotted module name to path."""
636 try:
637 spec = importlib.util.find_spec(x)
638 # AttributeError: looks like package module, but actually filename
639 # ImportError: module does not exist
640 # ValueError: not a module name
641 except (AttributeError, ImportError, ValueError):
642 return x
643 if spec is None or spec.origin in {None, "namespace"}:
644 return x
645 elif spec.submodule_search_locations:
646 return os.path.dirname(spec.origin)
647 else:
648 return spec.origin
650 def _parsearg(self, arg):
651 """ return (fspath, names) tuple after checking the file exists. """
652 parts = str(arg).split("::")
653 if self.config.option.pyargs:
654 parts[0] = self._tryconvertpyarg(parts[0])
655 relpath = parts[0].replace("/", os.sep)
656 path = self.config.invocation_dir.join(relpath, abs=True)
657 if not path.check():
658 if self.config.option.pyargs:
659 raise UsageError(
660 "file or package not found: " + arg + " (missing __init__.py?)"
661 )
662 raise UsageError("file not found: " + arg)
663 parts[0] = path.realpath()
664 return parts
666 def matchnodes(self, matching, names):
667 self.trace("matchnodes", matching, names)
668 self.trace.root.indent += 1
669 nodes = self._matchnodes(matching, names)
670 num = len(nodes)
671 self.trace("matchnodes finished -> ", num, "nodes")
672 self.trace.root.indent -= 1
673 if num == 0:
674 raise NoMatch(matching, names[:1])
675 return nodes
677 def _matchnodes(self, matching, names):
678 if not matching or not names:
679 return matching
680 name = names[0]
681 assert name
682 nextnames = names[1:]
683 resultnodes = []
684 for node in matching:
685 if isinstance(node, nodes.Item):
686 if not names:
687 resultnodes.append(node)
688 continue
689 assert isinstance(node, nodes.Collector)
690 key = (type(node), node.nodeid)
691 if key in self._node_cache:
692 rep = self._node_cache[key]
693 else:
694 rep = collect_one_node(node)
695 self._node_cache[key] = rep
696 if rep.passed:
697 has_matched = False
698 for x in rep.result:
699 # TODO: remove parametrized workaround once collection structure contains parametrization
700 if x.name == name or x.name.split("[")[0] == name:
701 resultnodes.extend(self.matchnodes([x], nextnames))
702 has_matched = True
703 # XXX accept IDs that don't have "()" for class instances
704 if not has_matched and len(rep.result) == 1 and x.name == "()":
705 nextnames.insert(0, name)
706 resultnodes.extend(self.matchnodes([x], nextnames))
707 else:
708 # report collection failures here to avoid failing to run some test
709 # specified in the command line because the module could not be
710 # imported (#134)
711 node.ihook.pytest_collectreport(report=rep)
712 return resultnodes
714 def genitems(self, node):
715 self.trace("genitems", node)
716 if isinstance(node, nodes.Item):
717 node.ihook.pytest_itemcollected(item=node)
718 yield node
719 else:
720 assert isinstance(node, nodes.Collector)
721 rep = collect_one_node(node)
722 if rep.passed:
723 for subnode in rep.result:
724 yield from self.genitems(subnode)
725 node.ihook.pytest_collectreport(report=rep)