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