# -*- coding: utf-8 -*-
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import os
import sys
import textwrap
import types

import attr
import importlib_metadata
import py
import six

import pytest
from _pytest.main import EXIT_NOTESTSCOLLECTED
from _pytest.main import EXIT_USAGEERROR
from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG


def prepend_pythonpath(*dirs):
    cur = os.getenv("PYTHONPATH")
    if cur:
        dirs += (cur,)
    return os.pathsep.join(str(p) for p in dirs)


class TestGeneralUsage(object):
    def test_config_error(self, testdir):
        testdir.copy_example("conftest_usageerror/conftest.py")
        result = testdir.runpytest(testdir.tmpdir)
        assert result.ret == EXIT_USAGEERROR
        result.stderr.fnmatch_lines(["*ERROR: hello"])
        result.stdout.fnmatch_lines(["*pytest_unconfigure_called"])

    def test_root_conftest_syntax_error(self, testdir):
        testdir.makepyfile(conftest="raise SyntaxError\n")
        result = testdir.runpytest()
        result.stderr.fnmatch_lines(["*raise SyntaxError*"])
        assert result.ret != 0

    def test_early_hook_error_issue38_1(self, testdir):
        testdir.makeconftest(
            """
            def pytest_sessionstart():
                0 / 0
        """
        )
        result = testdir.runpytest(testdir.tmpdir)
        assert result.ret != 0
        # tracestyle is native by default for hook failures
        result.stdout.fnmatch_lines(
            ["*INTERNALERROR*File*conftest.py*line 2*", "*0 / 0*"]
        )
        result = testdir.runpytest(testdir.tmpdir, "--fulltrace")
        assert result.ret != 0
        # tracestyle is native by default for hook failures
        result.stdout.fnmatch_lines(
            ["*INTERNALERROR*def pytest_sessionstart():*", "*INTERNALERROR*0 / 0*"]
        )

    def test_early_hook_configure_error_issue38(self, testdir):
        testdir.makeconftest(
            """
            def pytest_configure():
                0 / 0
        """
        )
        result = testdir.runpytest(testdir.tmpdir)
        assert result.ret != 0
        # here we get it on stderr
        result.stderr.fnmatch_lines(
            ["*INTERNALERROR*File*conftest.py*line 2*", "*0 / 0*"]
        )

    def test_file_not_found(self, testdir):
        result = testdir.runpytest("asd")
        assert result.ret != 0
        result.stderr.fnmatch_lines(["ERROR: file not found*asd"])

    def test_file_not_found_unconfigure_issue143(self, testdir):
        testdir.makeconftest(
            """
            def pytest_configure():
                print("---configure")
            def pytest_unconfigure():
                print("---unconfigure")
        """
        )
        result = testdir.runpytest("-s", "asd")
        assert result.ret == 4  # EXIT_USAGEERROR
        result.stderr.fnmatch_lines(["ERROR: file not found*asd"])
        result.stdout.fnmatch_lines(["*---configure", "*---unconfigure"])

    def test_config_preparse_plugin_option(self, testdir):
        testdir.makepyfile(
            pytest_xyz="""
            def pytest_addoption(parser):
                parser.addoption("--xyz", dest="xyz", action="store")
        """
        )
        testdir.makepyfile(
            test_one="""
            def test_option(pytestconfig):
                assert pytestconfig.option.xyz == "123"
        """
        )
        result = testdir.runpytest("-p", "pytest_xyz", "--xyz=123", syspathinsert=True)
        assert result.ret == 0
        result.stdout.fnmatch_lines(["*1 passed*"])

    @pytest.mark.parametrize("load_cov_early", [True, False])
    def test_early_load_setuptools_name(self, testdir, monkeypatch, load_cov_early):
        testdir.makepyfile(mytestplugin1_module="")
        testdir.makepyfile(mytestplugin2_module="")
        testdir.makepyfile(mycov_module="")
        testdir.syspathinsert()

        loaded = []

        @attr.s
        class DummyEntryPoint(object):
            name = attr.ib()
            module = attr.ib()
            group = "pytest11"

            def load(self):
                __import__(self.module)
                loaded.append(self.name)
                return sys.modules[self.module]

        entry_points = [
            DummyEntryPoint("myplugin1", "mytestplugin1_module"),
            DummyEntryPoint("myplugin2", "mytestplugin2_module"),
            DummyEntryPoint("mycov", "mycov_module"),
        ]

        @attr.s
        class DummyDist(object):
            entry_points = attr.ib()
            files = ()

        def my_dists():
            return (DummyDist(entry_points),)

        monkeypatch.setattr(importlib_metadata, "distributions", my_dists)
        params = ("-p", "mycov") if load_cov_early else ()
        testdir.runpytest_inprocess(*params)
        if load_cov_early:
            assert loaded == ["mycov", "myplugin1", "myplugin2"]
        else:
            assert loaded == ["myplugin1", "myplugin2", "mycov"]

    def test_assertion_magic(self, testdir):
        p = testdir.makepyfile(
            """
            def test_this():
                x = 0
                assert x
        """
        )
        result = testdir.runpytest(p)
        result.stdout.fnmatch_lines([">       assert x", "E       assert 0"])
        assert result.ret == 1

    def test_nested_import_error(self, testdir):
        p = testdir.makepyfile(
            """
                import import_fails
                def test_this():
                    assert import_fails.a == 1
        """
        )
        testdir.makepyfile(import_fails="import does_not_work")
        result = testdir.runpytest(p)
        result.stdout.fnmatch_lines(
            [
                # XXX on jython this fails:  ">   import import_fails",
                "ImportError while importing test module*",
                "*No module named *does_not_work*",
            ]
        )
        assert result.ret == 2

    def test_not_collectable_arguments(self, testdir):
        p1 = testdir.makepyfile("")
        p2 = testdir.makefile(".pyc", "123")
        result = testdir.runpytest(p1, p2)
        assert result.ret
        result.stderr.fnmatch_lines(["*ERROR: not found:*{}".format(p2.basename)])

    @pytest.mark.filterwarnings("default")
    def test_better_reporting_on_conftest_load_failure(self, testdir, request):
        """Show a user-friendly traceback on conftest import failures (#486, #3332)"""
        testdir.makepyfile("")
        testdir.makeconftest(
            """
            def foo():
                import qwerty
            foo()
        """
        )
        result = testdir.runpytest("--help")
        result.stdout.fnmatch_lines(
            """
            *--version*
            *warning*conftest.py*
        """
        )
        result = testdir.runpytest()
        dirname = request.node.name + "0"
        exc_name = (
            "ModuleNotFoundError" if sys.version_info >= (3, 6) else "ImportError"
        )
        result.stderr.fnmatch_lines(
            [
                "ImportError while loading conftest '*{sep}{dirname}{sep}conftest.py'.".format(
                    dirname=dirname, sep=os.sep
                ),
                "conftest.py:3: in <module>",
                "    foo()",
                "conftest.py:2: in foo",
                "    import qwerty",
                "E   {}: No module named {q}qwerty{q}".format(
                    exc_name, q="'" if six.PY3 else ""
                ),
            ]
        )

    def test_early_skip(self, testdir):
        testdir.mkdir("xyz")
        testdir.makeconftest(
            """
            import pytest
            def pytest_collect_directory():
                pytest.skip("early")
        """
        )
        result = testdir.runpytest()
        assert result.ret == EXIT_NOTESTSCOLLECTED
        result.stdout.fnmatch_lines(["*1 skip*"])

    def test_issue88_initial_file_multinodes(self, testdir):
        testdir.copy_example("issue88_initial_file_multinodes")
        p = testdir.makepyfile("def test_hello(): pass")
        result = testdir.runpytest(p, "--collect-only")
        result.stdout.fnmatch_lines(["*MyFile*test_issue88*", "*Module*test_issue88*"])

    def test_issue93_initialnode_importing_capturing(self, testdir):
        testdir.makeconftest(
            """
            import sys
            print("should not be seen")
            sys.stderr.write("stder42\\n")
        """
        )
        result = testdir.runpytest()
        assert result.ret == EXIT_NOTESTSCOLLECTED
        assert "should not be seen" not in result.stdout.str()
        assert "stderr42" not in result.stderr.str()

    def test_conftest_printing_shows_if_error(self, testdir):
        testdir.makeconftest(
            """
            print("should be seen")
            assert 0
        """
        )
        result = testdir.runpytest()
        assert result.ret != 0
        assert "should be seen" in result.stdout.str()

    @pytest.mark.skipif(
        not hasattr(py.path.local, "mksymlinkto"),
        reason="symlink not available on this platform",
    )
    def test_chdir(self, testdir):
        testdir.tmpdir.join("py").mksymlinkto(py._pydir)
        p = testdir.tmpdir.join("main.py")
        p.write(
            textwrap.dedent(
                """\
                import sys, os
                sys.path.insert(0, '')
                import py
                print(py.__file__)
                print(py.__path__)
                os.chdir(os.path.dirname(os.getcwd()))
                print(py.log)
                """
            )
        )
        result = testdir.runpython(p)
        assert not result.ret

    def test_issue109_sibling_conftests_not_loaded(self, testdir):
        sub1 = testdir.mkdir("sub1")
        sub2 = testdir.mkdir("sub2")
        sub1.join("conftest.py").write("assert 0")
        result = testdir.runpytest(sub2)
        assert result.ret == EXIT_NOTESTSCOLLECTED
        sub2.ensure("__init__.py")
        p = sub2.ensure("test_hello.py")
        result = testdir.runpytest(p)
        assert result.ret == EXIT_NOTESTSCOLLECTED
        result = testdir.runpytest(sub1)
        assert result.ret == EXIT_USAGEERROR

    def test_directory_skipped(self, testdir):
        testdir.makeconftest(
            """
            import pytest
            def pytest_ignore_collect():
                pytest.skip("intentional")
        """
        )
        testdir.makepyfile("def test_hello(): pass")
        result = testdir.runpytest()
        assert result.ret == EXIT_NOTESTSCOLLECTED
        result.stdout.fnmatch_lines(["*1 skipped*"])

    def test_multiple_items_per_collector_byid(self, testdir):
        c = testdir.makeconftest(
            """
            import pytest
            class MyItem(pytest.Item):
                def runtest(self):
                    pass
            class MyCollector(pytest.File):
                def collect(self):
                    return [MyItem(name="xyz", parent=self)]
            def pytest_collect_file(path, parent):
                if path.basename.startswith("conftest"):
                    return MyCollector(path, parent)
        """
        )
        result = testdir.runpytest(c.basename + "::" + "xyz")
        assert result.ret == 0
        result.stdout.fnmatch_lines(["*1 pass*"])

    def test_skip_on_generated_funcarg_id(self, testdir):
        testdir.makeconftest(
            """
            import pytest
            def pytest_generate_tests(metafunc):
                metafunc.parametrize('x', [3], ids=['hello-123'])
            def pytest_runtest_setup(item):
                print(item.keywords)
                if 'hello-123' in item.keywords:
                    pytest.skip("hello")
                assert 0
        """
        )
        p = testdir.makepyfile("""def test_func(x): pass""")
        res = testdir.runpytest(p, SHOW_PYTEST_WARNINGS_ARG)
        assert res.ret == 0
        res.stdout.fnmatch_lines(["*1 skipped*"])

    def test_direct_addressing_selects(self, testdir):
        p = testdir.makepyfile(
            """
            def pytest_generate_tests(metafunc):
                metafunc.parametrize('i', [1, 2], ids=["1", "2"])
            def test_func(i):
                pass
        """
        )
        res = testdir.runpytest(
            p.basename + "::" + "test_func[1]", SHOW_PYTEST_WARNINGS_ARG
        )
        assert res.ret == 0
        res.stdout.fnmatch_lines(["*1 passed*"])

    def test_direct_addressing_notfound(self, testdir):
        p = testdir.makepyfile(
            """
            def test_func():
                pass
        """
        )
        res = testdir.runpytest(p.basename + "::" + "test_notfound")
        assert res.ret
        res.stderr.fnmatch_lines(["*ERROR*not found*"])

    def test_docstring_on_hookspec(self):
        from _pytest import hookspec

        for name, value in vars(hookspec).items():
            if name.startswith("pytest_"):
                assert value.__doc__, "no docstring for %s" % name

    def test_initialization_error_issue49(self, testdir):
        testdir.makeconftest(
            """
            def pytest_configure():
                x
        """
        )
        result = testdir.runpytest()
        assert result.ret == 3  # internal error
        result.stderr.fnmatch_lines(["INTERNAL*pytest_configure*", "INTERNAL*x*"])
        assert "sessionstarttime" not in result.stderr.str()

    @pytest.mark.parametrize("lookfor", ["test_fun.py::test_a"])
    def test_issue134_report_error_when_collecting_member(self, testdir, lookfor):
        testdir.makepyfile(
            test_fun="""
            def test_a():
                pass
            def"""
        )
        result = testdir.runpytest(lookfor)
        result.stdout.fnmatch_lines(["*SyntaxError*"])
        if "::" in lookfor:
            result.stderr.fnmatch_lines(["*ERROR*"])
            assert result.ret == 4  # usage error only if item not found

    def test_report_all_failed_collections_initargs(self, testdir):
        testdir.makeconftest(
            """
            from _pytest.main import EXIT_USAGEERROR

            def pytest_sessionfinish(exitstatus):
                assert exitstatus == EXIT_USAGEERROR
                print("pytest_sessionfinish_called")
            """
        )
        testdir.makepyfile(test_a="def", test_b="def")
        result = testdir.runpytest("test_a.py::a", "test_b.py::b")
        result.stderr.fnmatch_lines(["*ERROR*test_a.py::a*", "*ERROR*test_b.py::b*"])
        result.stdout.fnmatch_lines(["pytest_sessionfinish_called"])
        assert result.ret == EXIT_USAGEERROR

    @pytest.mark.usefixtures("recwarn")
    def test_namespace_import_doesnt_confuse_import_hook(self, testdir):
        """
        Ref #383. Python 3.3's namespace package messed with our import hooks
        Importing a module that didn't exist, even if the ImportError was
        gracefully handled, would make our test crash.

        Use recwarn here to silence this warning in Python 2.7:
            ImportWarning: Not importing directory '...\not_a_package': missing __init__.py
        """
        testdir.mkdir("not_a_package")
        p = testdir.makepyfile(
            """
            try:
                from not_a_package import doesnt_exist
            except ImportError:
                # We handle the import error gracefully here
                pass

            def test_whatever():
                pass
        """
        )
        res = testdir.runpytest(p.basename)
        assert res.ret == 0

    def test_unknown_option(self, testdir):
        result = testdir.runpytest("--qwlkej")
        result.stderr.fnmatch_lines(
            """
            *unrecognized*
        """
        )

    def test_getsourcelines_error_issue553(self, testdir, monkeypatch):
        monkeypatch.setattr("inspect.getsourcelines", None)
        p = testdir.makepyfile(
            """
            def raise_error(obj):
                raise IOError('source code not available')

            import inspect
            inspect.getsourcelines = raise_error

            def test_foo(invalid_fixture):
                pass
        """
        )
        res = testdir.runpytest(p)
        res.stdout.fnmatch_lines(
            ["*source code not available*", "E*fixture 'invalid_fixture' not found"]
        )

    def test_plugins_given_as_strings(self, tmpdir, monkeypatch, _sys_snapshot):
        """test that str values passed to main() as `plugins` arg
        are interpreted as module names to be imported and registered.
        #855.
        """
        with pytest.raises(ImportError) as excinfo:
            pytest.main([str(tmpdir)], plugins=["invalid.module"])
        assert "invalid" in str(excinfo.value)

        p = tmpdir.join("test_test_plugins_given_as_strings.py")
        p.write("def test_foo(): pass")
        mod = types.ModuleType("myplugin")
        monkeypatch.setitem(sys.modules, "myplugin", mod)
        assert pytest.main(args=[str(tmpdir)], plugins=["myplugin"]) == 0

    def test_parametrized_with_bytes_regex(self, testdir):
        p = testdir.makepyfile(
            """
            import re
            import pytest
            @pytest.mark.parametrize('r', [re.compile(b'foo')])
            def test_stuff(r):
                pass
        """
        )
        res = testdir.runpytest(p)
        res.stdout.fnmatch_lines(["*1 passed*"])

    def test_parametrized_with_null_bytes(self, testdir):
        """Test parametrization with values that contain null bytes and unicode characters (#2644, #2957)"""
        p = testdir.makepyfile(
            u"""
            # encoding: UTF-8
            import pytest

            @pytest.mark.parametrize("data", [b"\\x00", "\\x00", u'ação'])
            def test_foo(data):
                assert data
        """
        )
        res = testdir.runpytest(p)
        res.assert_outcomes(passed=3)


class TestInvocationVariants(object):
    def test_earlyinit(self, testdir):
        p = testdir.makepyfile(
            """
            import pytest
            assert hasattr(pytest, 'mark')
        """
        )
        result = testdir.runpython(p)
        assert result.ret == 0

    @pytest.mark.xfail("sys.platform.startswith('java')")
    def test_pydoc(self, testdir):
        for name in ("py.test", "pytest"):
            result = testdir.runpython_c("import {};help({})".format(name, name))
            assert result.ret == 0
            s = result.stdout.str()
            assert "MarkGenerator" in s

    def test_import_star_py_dot_test(self, testdir):
        p = testdir.makepyfile(
            """
            from py.test import *
            #collect
            #cmdline
            #Item
            # assert collect.Item is Item
            # assert collect.Collector is Collector
            main
            skip
            xfail
        """
        )
        result = testdir.runpython(p)
        assert result.ret == 0

    def test_import_star_pytest(self, testdir):
        p = testdir.makepyfile(
            """
            from pytest import *
            #Item
            #File
            main
            skip
            xfail
        """
        )
        result = testdir.runpython(p)
        assert result.ret == 0

    def test_double_pytestcmdline(self, testdir):
        p = testdir.makepyfile(
            run="""
            import pytest
            pytest.main()
            pytest.main()
        """
        )
        testdir.makepyfile(
            """
            def test_hello():
                pass
        """
        )
        result = testdir.runpython(p)
        result.stdout.fnmatch_lines(["*1 passed*", "*1 passed*"])

    def test_python_minus_m_invocation_ok(self, testdir):
        p1 = testdir.makepyfile("def test_hello(): pass")
        res = testdir.run(sys.executable, "-m", "pytest", str(p1))
        assert res.ret == 0

    def test_python_minus_m_invocation_fail(self, testdir):
        p1 = testdir.makepyfile("def test_fail(): 0/0")
        res = testdir.run(sys.executable, "-m", "pytest", str(p1))
        assert res.ret == 1

    def test_python_pytest_package(self, testdir):
        p1 = testdir.makepyfile("def test_pass(): pass")
        res = testdir.run(sys.executable, "-m", "pytest", str(p1))
        assert res.ret == 0
        res.stdout.fnmatch_lines(["*1 passed*"])

    def test_equivalence_pytest_pytest(self):
        assert pytest.main == py.test.cmdline.main

    def test_invoke_with_invalid_type(self, capsys):
        with pytest.raises(
            TypeError, match="expected to be a list or tuple of strings, got: '-h'"
        ):
            pytest.main("-h")

    def test_invoke_with_path(self, tmpdir, capsys):
        retcode = pytest.main(tmpdir)
        assert retcode == EXIT_NOTESTSCOLLECTED
        out, err = capsys.readouterr()

    def test_invoke_plugin_api(self, testdir, capsys):
        class MyPlugin(object):
            def pytest_addoption(self, parser):
                parser.addoption("--myopt")

        pytest.main(["-h"], plugins=[MyPlugin()])
        out, err = capsys.readouterr()
        assert "--myopt" in out

    def test_pyargs_importerror(self, testdir, monkeypatch):
        monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", False)
        path = testdir.mkpydir("tpkg")
        path.join("test_hello.py").write("raise ImportError")

        result = testdir.runpytest("--pyargs", "tpkg.test_hello", syspathinsert=True)
        assert result.ret != 0

        result.stdout.fnmatch_lines(["collected*0*items*/*1*errors"])

    def test_cmdline_python_package(self, testdir, monkeypatch):
        import warnings

        monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", False)
        path = testdir.mkpydir("tpkg")
        path.join("test_hello.py").write("def test_hello(): pass")
        path.join("test_world.py").write("def test_world(): pass")
        result = testdir.runpytest("--pyargs", "tpkg")
        assert result.ret == 0
        result.stdout.fnmatch_lines(["*2 passed*"])
        result = testdir.runpytest("--pyargs", "tpkg.test_hello", syspathinsert=True)
        assert result.ret == 0
        result.stdout.fnmatch_lines(["*1 passed*"])

        empty_package = testdir.mkpydir("empty_package")
        monkeypatch.setenv("PYTHONPATH", str(empty_package), prepend=os.pathsep)
        # the path which is not a package raises a warning on pypy;
        # no idea why only pypy and not normal python warn about it here
        with warnings.catch_warnings():
            warnings.simplefilter("ignore", ImportWarning)
            result = testdir.runpytest("--pyargs", ".")
        assert result.ret == 0
        result.stdout.fnmatch_lines(["*2 passed*"])

        monkeypatch.setenv("PYTHONPATH", str(testdir), prepend=os.pathsep)
        result = testdir.runpytest("--pyargs", "tpkg.test_missing", syspathinsert=True)
        assert result.ret != 0
        result.stderr.fnmatch_lines(["*not*found*test_missing*"])

    def test_cmdline_python_namespace_package(self, testdir, monkeypatch):
        """
        test --pyargs option with namespace packages (#1567)

        Ref: https://packaging.python.org/guides/packaging-namespace-packages/
        """
        monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False)

        search_path = []
        for dirname in "hello", "world":
            d = testdir.mkdir(dirname)
            search_path.append(d)
            ns = d.mkdir("ns_pkg")
            ns.join("__init__.py").write(
                "__import__('pkg_resources').declare_namespace(__name__)"
            )
            lib = ns.mkdir(dirname)
            lib.ensure("__init__.py")
            lib.join("test_{}.py".format(dirname)).write(
                "def test_{}(): pass\ndef test_other():pass".format(dirname)
            )

        # The structure of the test directory is now:
        # .
        # ├── hello
        # │   └── ns_pkg
        # │       ├── __init__.py
        # │       └── hello
        # │           ├── __init__.py
        # │           └── test_hello.py
        # └── world
        #     └── ns_pkg
        #         ├── __init__.py
        #         └── world
        #             ├── __init__.py
        #             └── test_world.py

        # NOTE: the different/reversed ordering is intentional here.
        monkeypatch.setenv("PYTHONPATH", prepend_pythonpath(*search_path))
        for p in search_path:
            monkeypatch.syspath_prepend(p)

        # mixed module and filenames:
        monkeypatch.chdir("world")
        result = testdir.runpytest("--pyargs", "-v", "ns_pkg.hello", "ns_pkg/world")
        assert result.ret == 0
        result.stdout.fnmatch_lines(
            [
                "test_hello.py::test_hello*PASSED*",
                "test_hello.py::test_other*PASSED*",
                "ns_pkg/world/test_world.py::test_world*PASSED*",
                "ns_pkg/world/test_world.py::test_other*PASSED*",
                "*4 passed in*",
            ]
        )

        # specify tests within a module
        testdir.chdir()
        result = testdir.runpytest(
            "--pyargs", "-v", "ns_pkg.world.test_world::test_other"
        )
        assert result.ret == 0
        result.stdout.fnmatch_lines(
            ["*test_world.py::test_other*PASSED*", "*1 passed*"]
        )

    def test_invoke_test_and_doctestmodules(self, testdir):
        p = testdir.makepyfile(
            """
            def test():
                pass
        """
        )
        result = testdir.runpytest(str(p) + "::test", "--doctest-modules")
        result.stdout.fnmatch_lines(["*1 passed*"])

    @pytest.mark.skipif(not hasattr(os, "symlink"), reason="requires symlinks")
    def test_cmdline_python_package_symlink(self, testdir, monkeypatch):
        """
        test --pyargs option with packages with path containing symlink can
        have conftest.py in their package (#2985)
        """
        # dummy check that we can actually create symlinks: on Windows `os.symlink` is available,
        # but normal users require special admin privileges to create symlinks.
        if sys.platform == "win32":
            try:
                os.symlink(
                    str(testdir.tmpdir.ensure("tmpfile")),
                    str(testdir.tmpdir.join("tmpfile2")),
                )
            except OSError as e:
                pytest.skip(six.text_type(e.args[0]))
        monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False)

        dirname = "lib"
        d = testdir.mkdir(dirname)
        foo = d.mkdir("foo")
        foo.ensure("__init__.py")
        lib = foo.mkdir("bar")
        lib.ensure("__init__.py")
        lib.join("test_bar.py").write(
            "def test_bar(): pass\ndef test_other(a_fixture):pass"
        )
        lib.join("conftest.py").write(
            "import pytest\n@pytest.fixture\ndef a_fixture():pass"
        )

        d_local = testdir.mkdir("local")
        symlink_location = os.path.join(str(d_local), "lib")
        if six.PY2:
            os.symlink(str(d), symlink_location)
        else:
            os.symlink(str(d), symlink_location, target_is_directory=True)

        # The structure of the test directory is now:
        # .
        # ├── local
        # │   └── lib -> ../lib
        # └── lib
        #     └── foo
        #         ├── __init__.py
        #         └── bar
        #             ├── __init__.py
        #             ├── conftest.py
        #             └── test_bar.py

        # NOTE: the different/reversed ordering is intentional here.
        search_path = ["lib", os.path.join("local", "lib")]
        monkeypatch.setenv("PYTHONPATH", prepend_pythonpath(*search_path))
        for p in search_path:
            monkeypatch.syspath_prepend(p)

        # module picked up in symlink-ed directory:
        # It picks up local/lib/foo/bar (symlink) via sys.path.
        result = testdir.runpytest("--pyargs", "-v", "foo.bar")
        testdir.chdir()
        assert result.ret == 0
        if hasattr(py.path.local, "mksymlinkto"):
            result.stdout.fnmatch_lines(
                [
                    "lib/foo/bar/test_bar.py::test_bar PASSED*",
                    "lib/foo/bar/test_bar.py::test_other PASSED*",
                    "*2 passed*",
                ]
            )
        else:
            result.stdout.fnmatch_lines(
                [
                    "*lib/foo/bar/test_bar.py::test_bar PASSED*",
                    "*lib/foo/bar/test_bar.py::test_other PASSED*",
                    "*2 passed*",
                ]
            )

    def test_cmdline_python_package_not_exists(self, testdir):
        result = testdir.runpytest("--pyargs", "tpkgwhatv")
        assert result.ret
        result.stderr.fnmatch_lines(["ERROR*file*or*package*not*found*"])

    @pytest.mark.xfail(reason="decide: feature or bug")
    def test_noclass_discovery_if_not_testcase(self, testdir):
        testpath = testdir.makepyfile(
            """
            import unittest
            class TestHello(object):
                def test_hello(self):
                    assert self.attr

            class RealTest(unittest.TestCase, TestHello):
                attr = 42
        """
        )
        reprec = testdir.inline_run(testpath)
        reprec.assertoutcome(passed=1)

    def test_doctest_id(self, testdir):
        testdir.makefile(
            ".txt",
            """
            >>> x=3
            >>> x
            4
        """,
        )
        result = testdir.runpytest("-rf")
        lines = result.stdout.str().splitlines()
        for line in lines:
            if line.startswith(("FAIL ", "FAILED ")):
                _fail, _sep, testid = line.partition(" ")
                break
        result = testdir.runpytest(testid, "-rf")
        result.stdout.fnmatch_lines(
            ["FAILED test_doctest_id.txt::test_doctest_id.txt", "*1 failed*"]
        )

    def test_core_backward_compatibility(self):
        """Test backward compatibility for get_plugin_manager function. See #787."""
        import _pytest.config

        assert (
            type(_pytest.config.get_plugin_manager())
            is _pytest.config.PytestPluginManager
        )

    def test_has_plugin(self, request):
        """Test hasplugin function of the plugin manager (#932)."""
        assert request.config.pluginmanager.hasplugin("python")


class TestDurations(object):
    source = """
        import time
        frag = 0.002
        def test_something():
            pass
        def test_2():
            time.sleep(frag*5)
        def test_1():
            time.sleep(frag)
        def test_3():
            time.sleep(frag*10)
    """

    def test_calls(self, testdir):
        testdir.makepyfile(self.source)
        result = testdir.runpytest("--durations=10")
        assert result.ret == 0
        result.stdout.fnmatch_lines_random(
            ["*durations*", "*call*test_3*", "*call*test_2*"]
        )
        result.stdout.fnmatch_lines(
            ["(0.00 durations hidden.  Use -vv to show these durations.)"]
        )

    def test_calls_show_2(self, testdir):
        testdir.makepyfile(self.source)
        result = testdir.runpytest("--durations=2")
        assert result.ret == 0
        lines = result.stdout.get_lines_after("*slowest*durations*")
        assert "4 passed" in lines[2]

    def test_calls_showall(self, testdir):
        testdir.makepyfile(self.source)
        result = testdir.runpytest("--durations=0")
        assert result.ret == 0
        for x in "23":
            for y in ("call",):  # 'setup', 'call', 'teardown':
                for line in result.stdout.lines:
                    if ("test_%s" % x) in line and y in line:
                        break
                else:
                    raise AssertionError("not found {} {}".format(x, y))

    def test_calls_showall_verbose(self, testdir):
        testdir.makepyfile(self.source)
        result = testdir.runpytest("--durations=0", "-vv")
        assert result.ret == 0
        for x in "123":
            for y in ("call",):  # 'setup', 'call', 'teardown':
                for line in result.stdout.lines:
                    if ("test_%s" % x) in line and y in line:
                        break
                else:
                    raise AssertionError("not found {} {}".format(x, y))

    def test_with_deselected(self, testdir):
        testdir.makepyfile(self.source)
        result = testdir.runpytest("--durations=2", "-k test_2")
        assert result.ret == 0
        result.stdout.fnmatch_lines(["*durations*", "*call*test_2*"])

    def test_with_failing_collection(self, testdir):
        testdir.makepyfile(self.source)
        testdir.makepyfile(test_collecterror="""xyz""")
        result = testdir.runpytest("--durations=2", "-k test_1")
        assert result.ret == 2
        result.stdout.fnmatch_lines(["*Interrupted: 1 errors during collection*"])
        # Collection errors abort test execution, therefore no duration is
        # output
        assert "duration" not in result.stdout.str()

    def test_with_not(self, testdir):
        testdir.makepyfile(self.source)
        result = testdir.runpytest("-k not 1")
        assert result.ret == 0


class TestDurationWithFixture(object):
    source = """
        import pytest
        import time
        frag = 0.01

        @pytest.fixture
        def setup_fixt():
            time.sleep(frag)

        def test_1(setup_fixt):
            time.sleep(frag)
    """

    def test_setup_function(self, testdir):
        testdir.makepyfile(self.source)
        result = testdir.runpytest("--durations=10")
        assert result.ret == 0

        result.stdout.fnmatch_lines_random(
            """
            *durations*
            * setup *test_1*
            * call *test_1*
        """
        )


def test_zipimport_hook(testdir, tmpdir):
    """Test package loader is being used correctly (see #1837)."""
    zipapp = pytest.importorskip("zipapp")
    testdir.tmpdir.join("app").ensure(dir=1)
    testdir.makepyfile(
        **{
            "app/foo.py": """
            import pytest
            def main():
                pytest.main(['--pyarg', 'foo'])
        """
        }
    )
    target = tmpdir.join("foo.zip")
    zipapp.create_archive(str(testdir.tmpdir.join("app")), str(target), main="foo:main")
    result = testdir.runpython(target)
    assert result.ret == 0
    result.stderr.fnmatch_lines(["*not found*foo*"])
    assert "INTERNALERROR>" not in result.stdout.str()


def test_import_plugin_unicode_name(testdir):
    testdir.makepyfile(myplugin="")
    testdir.makepyfile(
        """
        def test(): pass
    """
    )
    testdir.makeconftest(
        """
        pytest_plugins = [u'myplugin']
    """
    )
    r = testdir.runpytest()
    assert r.ret == 0


def test_pytest_plugins_as_module(testdir):
    """Do not raise an error if pytest_plugins attribute is a module (#3899)"""
    testdir.makepyfile(
        **{
            "__init__.py": "",
            "pytest_plugins.py": "",
            "conftest.py": "from . import pytest_plugins",
            "test_foo.py": "def test(): pass",
        }
    )
    result = testdir.runpytest()
    result.stdout.fnmatch_lines(["* 1 passed in *"])


def test_deferred_hook_checking(testdir):
    """
    Check hooks as late as possible (#1821).
    """
    testdir.syspathinsert()
    testdir.makepyfile(
        **{
            "plugin.py": """
        class Hooks(object):
            def pytest_my_hook(self, config):
                pass

        def pytest_configure(config):
            config.pluginmanager.add_hookspecs(Hooks)
        """,
            "conftest.py": """
            pytest_plugins = ['plugin']
            def pytest_my_hook(config):
                return 40
        """,
            "test_foo.py": """
            def test(request):
                assert request.config.hook.pytest_my_hook(config=request.config) == [40]
        """,
        }
    )
    result = testdir.runpytest()
    result.stdout.fnmatch_lines(["* 1 passed *"])


def test_fixture_values_leak(testdir):
    """Ensure that fixture objects are properly destroyed by the garbage collector at the end of their expected
    life-times (#2981).
    """
    testdir.makepyfile(
        """
        import attr
        import gc
        import pytest
        import weakref

        @attr.s
        class SomeObj(object):
            name = attr.ib()

        fix_of_test1_ref = None
        session_ref = None

        @pytest.fixture(scope='session')
        def session_fix():
            global session_ref
            obj = SomeObj(name='session-fixture')
            session_ref = weakref.ref(obj)
            return obj

        @pytest.fixture
        def fix(session_fix):
            global fix_of_test1_ref
            obj = SomeObj(name='local-fixture')
            fix_of_test1_ref = weakref.ref(obj)
            return obj

        def test1(fix):
            assert fix_of_test1_ref() is fix

        def test2():
            gc.collect()
            # fixture "fix" created during test1 must have been destroyed by now
            assert fix_of_test1_ref() is None
    """
    )
    result = testdir.runpytest()
    result.stdout.fnmatch_lines(["* 2 passed *"])


def test_fixture_order_respects_scope(testdir):
    """Ensure that fixtures are created according to scope order, regression test for #2405
    """
    testdir.makepyfile(
        """
        import pytest

        data = {}

        @pytest.fixture(scope='module')
        def clean_data():
            data.clear()

        @pytest.fixture(autouse=True)
        def add_data():
            data.update(value=True)

        @pytest.mark.usefixtures('clean_data')
        def test_value():
            assert data.get('value')
    """
    )
    result = testdir.runpytest()
    assert result.ret == 0


def test_frame_leak_on_failing_test(testdir):
    """pytest would leak garbage referencing the frames of tests that failed that could never be reclaimed (#2798)

    Unfortunately it was not possible to remove the actual circles because most of them
    are made of traceback objects which cannot be weakly referenced. Those objects at least
    can be eventually claimed by the garbage collector.
    """
    testdir.makepyfile(
        """
        import gc
        import weakref

        class Obj:
            pass

        ref = None

        def test1():
            obj = Obj()
            global ref
            ref = weakref.ref(obj)
            assert 0

        def test2():
            gc.collect()
            assert ref() is None
    """
    )
    result = testdir.runpytest_subprocess()
    result.stdout.fnmatch_lines(["*1 failed, 1 passed in*"])


def test_fixture_mock_integration(testdir):
    """Test that decorators applied to fixture are left working (#3774)"""
    p = testdir.copy_example("acceptance/fixture_mock_integration.py")
    result = testdir.runpytest(p)
    result.stdout.fnmatch_lines(["*1 passed*"])


def test_usage_error_code(testdir):
    result = testdir.runpytest("-unknown-option-")
    assert result.ret == EXIT_USAGEERROR


@pytest.mark.skipif(
    sys.version_info[:2] < (3, 5), reason="async def syntax python 3.5+ only"
)
@pytest.mark.filterwarnings("default")
def test_warn_on_async_function(testdir):
    testdir.makepyfile(
        test_async="""
        async def test_1():
            pass
        async def test_2():
            pass
    """
    )
    result = testdir.runpytest()
    result.stdout.fnmatch_lines(
        [
            "test_async.py::test_1",
            "test_async.py::test_2",
            "*Coroutine functions are not natively supported*",
            "*2 skipped, 2 warnings in*",
        ]
    )
    # ensure our warning message appears only once
    assert (
        result.stdout.str().count("Coroutine functions are not natively supported") == 1
    )
