1# -*- coding: utf-8 -*- 2""" core implementation of testing process: init, session, runtest loop. """ 3from __future__ import absolute_import 4from __future__ import division 5from __future__ import print_function 6 7import contextlib 8import fnmatch 9import functools 10import os 11import pkgutil 12import sys 13import warnings 14 15import attr 16import py 17import six 18 19import _pytest._code 20from _pytest import nodes 21from _pytest.config import directory_arg 22from _pytest.config import hookimpl 23from _pytest.config import UsageError 24from _pytest.deprecated import PYTEST_CONFIG_GLOBAL 25from _pytest.outcomes import exit 26from _pytest.runner import collect_one_node 27 28# exitcodes for the command line 29EXIT_OK = 0 30EXIT_TESTSFAILED = 1 31EXIT_INTERRUPTED = 2 32EXIT_INTERNALERROR = 3 33EXIT_USAGEERROR = 4 34EXIT_NOTESTSCOLLECTED = 5 35 36 37def pytest_addoption(parser): 38 parser.addini( 39 "norecursedirs", 40 "directory patterns to avoid for recursion", 41 type="args", 42 default=[".*", "build", "dist", "CVS", "_darcs", "{arch}", "*.egg", "venv"], 43 ) 44 parser.addini( 45 "testpaths", 46 "directories to search for tests when no files or directories are given in the " 47 "command line.", 48 type="args", 49 default=[], 50 ) 51 group = parser.getgroup("general", "running and selection options") 52 group._addoption( 53 "-x", 54 "--exitfirst", 55 action="store_const", 56 dest="maxfail", 57 const=1, 58 help="exit instantly on first error or failed test.", 59 ), 60 group._addoption( 61 "--maxfail", 62 metavar="num", 63 action="store", 64 type=int, 65 dest="maxfail", 66 default=0, 67 help="exit after first num failures or errors.", 68 ) 69 group._addoption( 70 "--strict-markers", 71 "--strict", 72 action="store_true", 73 help="markers not registered in the `markers` section of the configuration file raise errors.", 74 ) 75 group._addoption( 76 "-c", 77 metavar="file", 78 type=str, 79 dest="inifilename", 80 help="load configuration from `file` instead of trying to locate one of the implicit " 81 "configuration files.", 82 ) 83 group._addoption( 84 "--continue-on-collection-errors", 85 action="store_true", 86 default=False, 87 dest="continue_on_collection_errors", 88 help="Force test execution even if collection errors occur.", 89 ) 90 group._addoption( 91 "--rootdir", 92 action="store", 93 dest="rootdir", 94 help="Define root directory for tests. Can be relative path: 'root_dir', './root_dir', " 95 "'root_dir/another_dir/'; absolute path: '/home/user/root_dir'; path with variables: " 96 "'$HOME/root_dir'.", 97 ) 98 99 group = parser.getgroup("collect", "collection") 100 group.addoption( 101 "--collectonly", 102 "--collect-only", 103 action="store_true", 104 help="only collect tests, don't execute them.", 105 ), 106 group.addoption( 107 "--pyargs", 108 action="store_true", 109 help="try to interpret all arguments as python packages.", 110 ) 111 group.addoption( 112 "--ignore", 113 action="append", 114 metavar="path", 115 help="ignore path during collection (multi-allowed).", 116 ) 117 group.addoption( 118 "--ignore-glob", 119 action="append", 120 metavar="path", 121 help="ignore path pattern during collection (multi-allowed).", 122 ) 123 group.addoption( 124 "--deselect", 125 action="append", 126 metavar="nodeid_prefix", 127 help="deselect item during collection (multi-allowed).", 128 ) 129 # when changing this to --conf-cut-dir, config.py Conftest.setinitial 130 # needs upgrading as well 131 group.addoption( 132 "--confcutdir", 133 dest="confcutdir", 134 default=None, 135 metavar="dir", 136 type=functools.partial(directory_arg, optname="--confcutdir"), 137 help="only load conftest.py's relative to specified dir.", 138 ) 139 group.addoption( 140 "--noconftest", 141 action="store_true", 142 dest="noconftest", 143 default=False, 144 help="Don't load any conftest.py files.", 145 ) 146 group.addoption( 147 "--keepduplicates", 148 "--keep-duplicates", 149 action="store_true", 150 dest="keepduplicates", 151 default=False, 152 help="Keep duplicate tests.", 153 ) 154 group.addoption( 155 "--collect-in-virtualenv", 156 action="store_true", 157 dest="collect_in_virtualenv", 158 default=False, 159 help="Don't ignore tests in a local virtualenv directory", 160 ) 161 162 group = parser.getgroup("debugconfig", "test session debugging and configuration") 163 group.addoption( 164 "--basetemp", 165 dest="basetemp", 166 default=None, 167 metavar="dir", 168 help=( 169 "base temporary directory for this test run." 170 "(warning: this directory is removed if it exists)" 171 ), 172 ) 173 174 175class _ConfigDeprecated(object): 176 def __init__(self, config): 177 self.__dict__["_config"] = config 178 179 def __getattr__(self, attr): 180 warnings.warn(PYTEST_CONFIG_GLOBAL, stacklevel=2) 181 return getattr(self._config, attr) 182 183 def __setattr__(self, attr, val): 184 warnings.warn(PYTEST_CONFIG_GLOBAL, stacklevel=2) 185 return setattr(self._config, attr, val) 186 187 def __repr__(self): 188 return "{}({!r})".format(type(self).__name__, self._config) 189 190 191def pytest_configure(config): 192 __import__("pytest").config = _ConfigDeprecated(config) # compatibility 193 194 195def wrap_session(config, doit): 196 """Skeleton command line program""" 197 session = Session(config) 198 session.exitstatus = EXIT_OK 199 initstate = 0 200 try: 201 try: 202 config._do_configure() 203 initstate = 1 204 config.hook.pytest_sessionstart(session=session) 205 initstate = 2 206 session.exitstatus = doit(config, session) or 0 207 except UsageError: 208 session.exitstatus = EXIT_USAGEERROR 209 raise 210 except Failed: 211 session.exitstatus = EXIT_TESTSFAILED 212 except (KeyboardInterrupt, exit.Exception): 213 excinfo = _pytest._code.ExceptionInfo.from_current() 214 exitstatus = EXIT_INTERRUPTED 215 if isinstance(excinfo.value, exit.Exception): 216 if excinfo.value.returncode is not None: 217 exitstatus = excinfo.value.returncode 218 if initstate < 2: 219 sys.stderr.write( 220 "{}: {}\n".format(excinfo.typename, excinfo.value.msg) 221 ) 222 config.hook.pytest_keyboard_interrupt(excinfo=excinfo) 223 session.exitstatus = exitstatus 224 except: # noqa 225 excinfo = _pytest._code.ExceptionInfo.from_current() 226 config.notify_exception(excinfo, config.option) 227 session.exitstatus = EXIT_INTERNALERROR 228 if excinfo.errisinstance(SystemExit): 229 sys.stderr.write("mainloop: caught unexpected SystemExit!\n") 230 231 finally: 232 excinfo = None # Explicitly break reference cycle. 233 session.startdir.chdir() 234 if initstate >= 2: 235 config.hook.pytest_sessionfinish( 236 session=session, exitstatus=session.exitstatus 237 ) 238 config._ensure_unconfigure() 239 return session.exitstatus 240 241 242def pytest_cmdline_main(config): 243 return wrap_session(config, _main) 244 245 246def _main(config, session): 247 """ default command line protocol for initialization, session, 248 running tests and reporting. """ 249 config.hook.pytest_collection(session=session) 250 config.hook.pytest_runtestloop(session=session) 251 252 if session.testsfailed: 253 return EXIT_TESTSFAILED 254 elif session.testscollected == 0: 255 return EXIT_NOTESTSCOLLECTED 256 257 258def pytest_collection(session): 259 return session.perform_collect() 260 261 262def pytest_runtestloop(session): 263 if session.testsfailed and not session.config.option.continue_on_collection_errors: 264 raise session.Interrupted("%d errors during collection" % session.testsfailed) 265 266 if session.config.option.collectonly: 267 return True 268 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 277 278 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()]) 294 295 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]) 302 303 if py.path.local(path) in ignore_paths: 304 return True 305 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]) 313 314 if any( 315 fnmatch.fnmatch(six.text_type(path), six.text_type(glob)) 316 for glob in ignore_globs 317 ): 318 return True 319 320 allow_in_venv = config.getoption("collect_in_virtualenv") 321 if not allow_in_venv and _in_venv(path): 322 return True 323 324 return False 325 326 327def pytest_collection_modifyitems(items, config): 328 deselect_prefixes = tuple(config.getoption("deselect") or []) 329 if not deselect_prefixes: 330 return 331 332 remaining = [] 333 deselected = [] 334 for colitem in items: 335 if colitem.nodeid.startswith(deselect_prefixes): 336 deselected.append(colitem) 337 else: 338 remaining.append(colitem) 339 340 if deselected: 341 config.hook.pytest_deselected(items=deselected) 342 items[:] = remaining 343 344 345@contextlib.contextmanager 346def _patched_find_module(): 347 """Patch bug in pkgutil.ImpImporter.find_module 348 349 When using pkgutil.find_loader on python<3.4 it removes symlinks 350 from the path due to a call to os.path.realpath. This is not consistent 351 with actually doing the import (in these versions, pkgutil and __import__ 352 did not share the same underlying code). This can break conftest 353 discovery for pytest where symlinks are involved. 354 355 The only supported python<3.4 by pytest is python 2.7. 356 """ 357 if six.PY2: # python 3.4+ uses importlib instead 358 359 def find_module_patched(self, fullname, path=None): 360 # Note: we ignore 'path' argument since it is only used via meta_path 361 subname = fullname.split(".")[-1] 362 if subname != fullname and self.path is None: 363 return None 364 if self.path is None: 365 path = None 366 else: 367 # original: path = [os.path.realpath(self.path)] 368 path = [self.path] 369 try: 370 file, filename, etc = pkgutil.imp.find_module(subname, path) 371 except ImportError: 372 return None 373 return pkgutil.ImpLoader(fullname, file, filename, etc) 374 375 old_find_module = pkgutil.ImpImporter.find_module 376 pkgutil.ImpImporter.find_module = find_module_patched 377 try: 378 yield 379 finally: 380 pkgutil.ImpImporter.find_module = old_find_module 381 else: 382 yield 383 384 385class FSHookProxy(object): 386 def __init__(self, fspath, pm, remove_mods): 387 self.fspath = fspath 388 self.pm = pm 389 self.remove_mods = remove_mods 390 391 def __getattr__(self, name): 392 x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods) 393 self.__dict__[name] = x 394 return x 395 396 397class NoMatch(Exception): 398 """ raised if matching cannot locate a matching names. """ 399 400 401class Interrupted(KeyboardInterrupt): 402 """ signals an interrupted test run. """ 403 404 __module__ = "builtins" # for py3 405 406 407class Failed(Exception): 408 """ signals a stop as failed test run. """ 409 410 411@attr.s 412class _bestrelpath_cache(dict): 413 path = attr.ib() 414 415 def __missing__(self, path): 416 r = self.path.bestrelpath(path) 417 self[path] = r 418 return r 419 420 421class Session(nodes.FSCollector): 422 Interrupted = Interrupted 423 Failed = Failed 424 425 def __init__(self, config): 426 nodes.FSCollector.__init__( 427 self, config.rootdir, parent=None, config=config, session=self, nodeid="" 428 ) 429 self.testsfailed = 0 430 self.testscollected = 0 431 self.shouldstop = False 432 self.shouldfail = False 433 self.trace = config.trace.root.get("collection") 434 self._norecursepatterns = config.getini("norecursedirs") 435 self.startdir = config.invocation_dir 436 self._initialpaths = frozenset() 437 # Keep track of any collected nodes in here, so we don't duplicate fixtures 438 self._node_cache = {} 439 self._bestrelpathcache = _bestrelpath_cache(config.rootdir) 440 # Dirnames of pkgs with dunder-init files. 441 self._pkg_roots = {} 442 443 self.config.pluginmanager.register(self, name="session") 444 445 def __repr__(self): 446 return "<%s %s exitstatus=%r testsfailed=%d testscollected=%d>" % ( 447 self.__class__.__name__, 448 self.name, 449 getattr(self, "exitstatus", "<UNSET>"), 450 self.testsfailed, 451 self.testscollected, 452 ) 453 454 def _node_location_to_relpath(self, node_path): 455 # bestrelpath is a quite slow function 456 return self._bestrelpathcache[node_path] 457 458 @hookimpl(tryfirst=True) 459 def pytest_collectstart(self): 460 if self.shouldfail: 461 raise self.Failed(self.shouldfail) 462 if self.shouldstop: 463 raise self.Interrupted(self.shouldstop) 464 465 @hookimpl(tryfirst=True) 466 def pytest_runtest_logreport(self, report): 467 if report.failed and not hasattr(report, "wasxfail"): 468 self.testsfailed += 1 469 maxfail = self.config.getvalue("maxfail") 470 if maxfail and self.testsfailed >= maxfail: 471 self.shouldfail = "stopping after %d failures" % (self.testsfailed) 472 473 pytest_collectreport = pytest_runtest_logreport 474 475 def isinitpath(self, path): 476 return path in self._initialpaths 477 478 def gethookproxy(self, fspath): 479 # check if we have the common case of running 480 # hooks with all conftest.py files 481 pm = self.config.pluginmanager 482 my_conftestmodules = pm._getconftestmodules(fspath) 483 remove_mods = pm._conftest_plugins.difference(my_conftestmodules) 484 if remove_mods: 485 # one or more conftests are not in use at this fspath 486 proxy = FSHookProxy(fspath, pm, remove_mods) 487 else: 488 # all plugis are active for this fspath 489 proxy = self.config.hook 490 return proxy 491 492 def perform_collect(self, args=None, genitems=True): 493 hook = self.config.hook 494 try: 495 items = self._perform_collect(args, genitems) 496 self.config.pluginmanager.check_pending() 497 hook.pytest_collection_modifyitems( 498 session=self, config=self.config, items=items 499 ) 500 finally: 501 hook.pytest_collection_finish(session=self) 502 self.testscollected = len(items) 503 return items 504 505 def _perform_collect(self, args, genitems): 506 if args is None: 507 args = self.config.args 508 self.trace("perform_collect", self, args) 509 self.trace.root.indent += 1 510 self._notfound = [] 511 initialpaths = [] 512 self._initialparts = [] 513 self.items = items = [] 514 for arg in args: 515 parts = self._parsearg(arg) 516 self._initialparts.append(parts) 517 initialpaths.append(parts[0]) 518 self._initialpaths = frozenset(initialpaths) 519 rep = collect_one_node(self) 520 self.ihook.pytest_collectreport(report=rep) 521 self.trace.root.indent -= 1 522 if self._notfound: 523 errors = [] 524 for arg, exc in self._notfound: 525 line = "(no name %r in any of %r)" % (arg, exc.args[0]) 526 errors.append("not found: %s\n%s" % (arg, line)) 527 # XXX: test this 528 raise UsageError(*errors) 529 if not genitems: 530 return rep.result 531 else: 532 if rep.passed: 533 for node in rep.result: 534 self.items.extend(self.genitems(node)) 535 return items 536 537 def collect(self): 538 for initialpart in self._initialparts: 539 arg = "::".join(map(str, initialpart)) 540 self.trace("processing argument", arg) 541 self.trace.root.indent += 1 542 try: 543 for x in self._collect(arg): 544 yield x 545 except NoMatch: 546 # we are inside a make_report hook so 547 # we cannot directly pass through the exception 548 self._notfound.append((arg, sys.exc_info()[1])) 549 550 self.trace.root.indent -= 1 551 552 def _collect(self, arg): 553 from _pytest.python import Package 554 555 names = self._parsearg(arg) 556 argpath = names.pop(0) 557 558 # Start with a Session root, and delve to argpath item (dir or file) 559 # and stack all Packages found on the way. 560 # No point in finding packages when collecting doctests 561 if not self.config.getoption("doctestmodules", False): 562 pm = self.config.pluginmanager 563 for parent in reversed(argpath.parts()): 564 if pm._confcutdir and pm._confcutdir.relto(parent): 565 break 566 567 if parent.isdir(): 568 pkginit = parent.join("__init__.py") 569 if pkginit.isfile(): 570 if pkginit not in self._node_cache: 571 col = self._collectfile(pkginit, handle_dupes=False) 572 if col: 573 if isinstance(col[0], Package): 574 self._pkg_roots[parent] = col[0] 575 # always store a list in the cache, matchnodes expects it 576 self._node_cache[col[0].fspath] = [col[0]] 577 578 # If it's a directory argument, recurse and look for any Subpackages. 579 # Let the Package collector deal with subnodes, don't collect here. 580 if argpath.check(dir=1): 581 assert not names, "invalid arg %r" % (arg,) 582 583 seen_dirs = set() 584 for path in argpath.visit( 585 fil=self._visit_filter, rec=self._recurse, bf=True, sort=True 586 ): 587 dirpath = path.dirpath() 588 if dirpath not in seen_dirs: 589 # Collect packages first. 590 seen_dirs.add(dirpath) 591 pkginit = dirpath.join("__init__.py") 592 if pkginit.exists(): 593 for x in self._collectfile(pkginit): 594 yield x 595 if isinstance(x, Package): 596 self._pkg_roots[dirpath] = x 597 if dirpath in self._pkg_roots: 598 # Do not collect packages here. 599 continue 600 601 for x in self._collectfile(path): 602 key = (type(x), x.fspath) 603 if key in self._node_cache: 604 yield self._node_cache[key] 605 else: 606 self._node_cache[key] = x 607 yield x 608 else: 609 assert argpath.check(file=1) 610 611 if argpath in self._node_cache: 612 col = self._node_cache[argpath] 613 else: 614 collect_root = self._pkg_roots.get(argpath.dirname, self) 615 col = collect_root._collectfile(argpath, handle_dupes=False) 616 if col: 617 self._node_cache[argpath] = col 618 m = self.matchnodes(col, names) 619 # If __init__.py was the only file requested, then the matched node will be 620 # the corresponding Package, and the first yielded item will be the __init__ 621 # Module itself, so just use that. If this special case isn't taken, then all 622 # the files in the package will be yielded. 623 if argpath.basename == "__init__.py": 624 try: 625 yield next(m[0].collect()) 626 except StopIteration: 627 # The package collects nothing with only an __init__.py 628 # file in it, which gets ignored by the default 629 # "python_files" option. 630 pass 631 return 632 for y in m: 633 yield y 634 635 def _collectfile(self, path, handle_dupes=True): 636 assert path.isfile(), "%r is not a file (isdir=%r, exists=%r, islink=%r)" % ( 637 path, 638 path.isdir(), 639 path.exists(), 640 path.islink(), 641 ) 642 ihook = self.gethookproxy(path) 643 if not self.isinitpath(path): 644 if ihook.pytest_ignore_collect(path=path, config=self.config): 645 return () 646 647 if handle_dupes: 648 keepduplicates = self.config.getoption("keepduplicates") 649 if not keepduplicates: 650 duplicate_paths = self.config.pluginmanager._duplicatepaths 651 if path in duplicate_paths: 652 return () 653 else: 654 duplicate_paths.add(path) 655 656 return ihook.pytest_collect_file(path=path, parent=self) 657 658 def _recurse(self, dirpath): 659 if dirpath.basename == "__pycache__": 660 return False 661 ihook = self.gethookproxy(dirpath.dirpath()) 662 if ihook.pytest_ignore_collect(path=dirpath, config=self.config): 663 return False 664 for pat in self._norecursepatterns: 665 if dirpath.check(fnmatch=pat): 666 return False 667 ihook = self.gethookproxy(dirpath) 668 ihook.pytest_collect_directory(path=dirpath, parent=self) 669 return True 670 671 if six.PY2: 672 673 @staticmethod 674 def _visit_filter(f): 675 return f.check(file=1) and not f.strpath.endswith("*.pyc") 676 677 else: 678 679 @staticmethod 680 def _visit_filter(f): 681 return f.check(file=1) 682 683 def _tryconvertpyarg(self, x): 684 """Convert a dotted module name to path.""" 685 try: 686 with _patched_find_module(): 687 loader = pkgutil.find_loader(x) 688 except ImportError: 689 return x 690 if loader is None: 691 return x 692 # This method is sometimes invoked when AssertionRewritingHook, which 693 # does not define a get_filename method, is already in place: 694 try: 695 with _patched_find_module(): 696 path = loader.get_filename(x) 697 except AttributeError: 698 # Retrieve path from AssertionRewritingHook: 699 path = loader.modules[x][0].co_filename 700 if loader.is_package(x): 701 path = os.path.dirname(path) 702 return path 703 704 def _parsearg(self, arg): 705 """ return (fspath, names) tuple after checking the file exists. """ 706 parts = str(arg).split("::") 707 if self.config.option.pyargs: 708 parts[0] = self._tryconvertpyarg(parts[0]) 709 relpath = parts[0].replace("/", os.sep) 710 path = self.config.invocation_dir.join(relpath, abs=True) 711 if not path.check(): 712 if self.config.option.pyargs: 713 raise UsageError( 714 "file or package not found: " + arg + " (missing __init__.py?)" 715 ) 716 raise UsageError("file not found: " + arg) 717 parts[0] = path.realpath() 718 return parts 719 720 def matchnodes(self, matching, names): 721 self.trace("matchnodes", matching, names) 722 self.trace.root.indent += 1 723 nodes = self._matchnodes(matching, names) 724 num = len(nodes) 725 self.trace("matchnodes finished -> ", num, "nodes") 726 self.trace.root.indent -= 1 727 if num == 0: 728 raise NoMatch(matching, names[:1]) 729 return nodes 730 731 def _matchnodes(self, matching, names): 732 if not matching or not names: 733 return matching 734 name = names[0] 735 assert name 736 nextnames = names[1:] 737 resultnodes = [] 738 for node in matching: 739 if isinstance(node, nodes.Item): 740 if not names: 741 resultnodes.append(node) 742 continue 743 assert isinstance(node, nodes.Collector) 744 key = (type(node), node.nodeid) 745 if key in self._node_cache: 746 rep = self._node_cache[key] 747 else: 748 rep = collect_one_node(node) 749 self._node_cache[key] = rep 750 if rep.passed: 751 has_matched = False 752 for x in rep.result: 753 # TODO: remove parametrized workaround once collection structure contains parametrization 754 if x.name == name or x.name.split("[")[0] == name: 755 resultnodes.extend(self.matchnodes([x], nextnames)) 756 has_matched = True 757 # XXX accept IDs that don't have "()" for class instances 758 if not has_matched and len(rep.result) == 1 and x.name == "()": 759 nextnames.insert(0, name) 760 resultnodes.extend(self.matchnodes([x], nextnames)) 761 else: 762 # report collection failures here to avoid failing to run some test 763 # specified in the command line because the module could not be 764 # imported (#134) 765 node.ihook.pytest_collectreport(report=rep) 766 return resultnodes 767 768 def genitems(self, node): 769 self.trace("genitems", node) 770 if isinstance(node, nodes.Item): 771 node.ihook.pytest_itemcollected(item=node) 772 yield node 773 else: 774 assert isinstance(node, nodes.Collector) 775 rep = collect_one_node(node) 776 if rep.passed: 777 for subnode in rep.result: 778 for x in self.genitems(subnode): 779 yield x 780 node.ihook.pytest_collectreport(report=rep) 781