import os import pprint import shutil import sys import textwrap from pathlib import Path from typing import List import py.path import pytest from _pytest.config import ExitCode from _pytest.fixtures import FixtureRequest from _pytest.main import _in_venv from _pytest.main import Session from _pytest.monkeypatch import MonkeyPatch from _pytest.nodes import Item from _pytest.pathlib import symlink_or_skip from _pytest.pytester import HookRecorder from _pytest.pytester import Pytester def ensure_file(file_path: Path) -> Path: """Ensure that file exists""" file_path.parent.mkdir(parents=True, exist_ok=True) file_path.touch(exist_ok=True) return file_path class TestCollector: def test_collect_versus_item(self) -> None: from pytest import Collector from pytest import Item assert not issubclass(Collector, Item) assert not issubclass(Item, Collector) def test_check_equality(self, pytester: Pytester) -> None: modcol = pytester.getmodulecol( """ def test_pass(): pass def test_fail(): assert 0 """ ) fn1 = pytester.collect_by_name(modcol, "test_pass") assert isinstance(fn1, pytest.Function) fn2 = pytester.collect_by_name(modcol, "test_pass") assert isinstance(fn2, pytest.Function) assert fn1 == fn2 assert fn1 != modcol assert hash(fn1) == hash(fn2) fn3 = pytester.collect_by_name(modcol, "test_fail") assert isinstance(fn3, pytest.Function) assert not (fn1 == fn3) assert fn1 != fn3 for fn in fn1, fn2, fn3: assert isinstance(fn, pytest.Function) assert fn != 3 # type: ignore[comparison-overlap] assert fn != modcol assert fn != [1, 2, 3] # type: ignore[comparison-overlap] assert [1, 2, 3] != fn # type: ignore[comparison-overlap] assert modcol != fn assert pytester.collect_by_name(modcol, "doesnotexist") is None def test_getparent(self, pytester: Pytester) -> None: modcol = pytester.getmodulecol( """ class TestClass: def test_foo(self): pass """ ) cls = pytester.collect_by_name(modcol, "TestClass") assert isinstance(cls, pytest.Class) instance = pytester.collect_by_name(cls, "()") assert isinstance(instance, pytest.Instance) fn = pytester.collect_by_name(instance, "test_foo") assert isinstance(fn, pytest.Function) module_parent = fn.getparent(pytest.Module) assert module_parent is modcol function_parent = fn.getparent(pytest.Function) assert function_parent is fn class_parent = fn.getparent(pytest.Class) assert class_parent is cls def test_getcustomfile_roundtrip(self, pytester: Pytester) -> None: hello = pytester.makefile(".xxx", hello="world") pytester.makepyfile( conftest=""" import pytest class CustomFile(pytest.File): pass def pytest_collect_file(path, parent): if path.ext == ".xxx": return CustomFile.from_parent(fspath=path, parent=parent) """ ) node = pytester.getpathnode(hello) assert isinstance(node, pytest.File) assert node.name == "hello.xxx" nodes = node.session.perform_collect([node.nodeid], genitems=False) assert len(nodes) == 1 assert isinstance(nodes[0], pytest.File) def test_can_skip_class_with_test_attr(self, pytester: Pytester) -> None: """Assure test class is skipped when using `__test__=False` (See #2007).""" pytester.makepyfile( """ class TestFoo(object): __test__ = False def __init__(self): pass def test_foo(): assert True """ ) result = pytester.runpytest() result.stdout.fnmatch_lines(["collected 0 items", "*no tests ran in*"]) class TestCollectFS: def test_ignored_certain_directories(self, pytester: Pytester) -> None: tmpdir = pytester.path ensure_file(tmpdir / "build" / "test_notfound.py") ensure_file(tmpdir / "dist" / "test_notfound.py") ensure_file(tmpdir / "_darcs" / "test_notfound.py") ensure_file(tmpdir / "CVS" / "test_notfound.py") ensure_file(tmpdir / "{arch}" / "test_notfound.py") ensure_file(tmpdir / ".whatever" / "test_notfound.py") ensure_file(tmpdir / ".bzr" / "test_notfound.py") ensure_file(tmpdir / "normal" / "test_found.py") for x in Path(str(tmpdir)).rglob("test_*.py"): x.write_text("def test_hello(): pass", "utf-8") result = pytester.runpytest("--collect-only") s = result.stdout.str() assert "test_notfound" not in s assert "test_found" in s @pytest.mark.parametrize( "fname", ( "activate", "activate.csh", "activate.fish", "Activate", "Activate.bat", "Activate.ps1", ), ) def test_ignored_virtualenvs(self, pytester: Pytester, fname: str) -> None: bindir = "Scripts" if sys.platform.startswith("win") else "bin" ensure_file(pytester.path / "virtual" / bindir / fname) testfile = ensure_file(pytester.path / "virtual" / "test_invenv.py") testfile.write_text("def test_hello(): pass") # by default, ignore tests inside a virtualenv result = pytester.runpytest() result.stdout.no_fnmatch_line("*test_invenv*") # allow test collection if user insists result = pytester.runpytest("--collect-in-virtualenv") assert "test_invenv" in result.stdout.str() # allow test collection if user directly passes in the directory result = pytester.runpytest("virtual") assert "test_invenv" in result.stdout.str() @pytest.mark.parametrize( "fname", ( "activate", "activate.csh", "activate.fish", "Activate", "Activate.bat", "Activate.ps1", ), ) def test_ignored_virtualenvs_norecursedirs_precedence( self, pytester: Pytester, fname: str ) -> None: bindir = "Scripts" if sys.platform.startswith("win") else "bin" # norecursedirs takes priority ensure_file(pytester.path / ".virtual" / bindir / fname) testfile = ensure_file(pytester.path / ".virtual" / "test_invenv.py") testfile.write_text("def test_hello(): pass") result = pytester.runpytest("--collect-in-virtualenv") result.stdout.no_fnmatch_line("*test_invenv*") # ...unless the virtualenv is explicitly given on the CLI result = pytester.runpytest("--collect-in-virtualenv", ".virtual") assert "test_invenv" in result.stdout.str() @pytest.mark.parametrize( "fname", ( "activate", "activate.csh", "activate.fish", "Activate", "Activate.bat", "Activate.ps1", ), ) def test__in_venv(self, pytester: Pytester, fname: str) -> None: """Directly test the virtual env detection function""" bindir = "Scripts" if sys.platform.startswith("win") else "bin" # no bin/activate, not a virtualenv base_path = pytester.mkdir("venv") assert _in_venv(base_path) is False # with bin/activate, totally a virtualenv bin_path = base_path.joinpath(bindir) bin_path.mkdir() bin_path.joinpath(fname).touch() assert _in_venv(base_path) is True def test_custom_norecursedirs(self, pytester: Pytester) -> None: pytester.makeini( """ [pytest] norecursedirs = mydir xyz* """ ) tmpdir = pytester.path ensure_file(tmpdir / "mydir" / "test_hello.py").write_text("def test_1(): pass") ensure_file(tmpdir / "xyz123" / "test_2.py").write_text("def test_2(): 0/0") ensure_file(tmpdir / "xy" / "test_ok.py").write_text("def test_3(): pass") rec = pytester.inline_run() rec.assertoutcome(passed=1) rec = pytester.inline_run("xyz123/test_2.py") rec.assertoutcome(failed=1) def test_testpaths_ini(self, pytester: Pytester, monkeypatch: MonkeyPatch) -> None: pytester.makeini( """ [pytest] testpaths = gui uts """ ) tmpdir = pytester.path ensure_file(tmpdir / "env" / "test_1.py").write_text("def test_env(): pass") ensure_file(tmpdir / "gui" / "test_2.py").write_text("def test_gui(): pass") ensure_file(tmpdir / "uts" / "test_3.py").write_text("def test_uts(): pass") # executing from rootdir only tests from `testpaths` directories # are collected items, reprec = pytester.inline_genitems("-v") assert [x.name for x in items] == ["test_gui", "test_uts"] # check that explicitly passing directories in the command-line # collects the tests for dirname in ("env", "gui", "uts"): items, reprec = pytester.inline_genitems(tmpdir.joinpath(dirname)) assert [x.name for x in items] == ["test_%s" % dirname] # changing cwd to each subdirectory and running pytest without # arguments collects the tests in that directory normally for dirname in ("env", "gui", "uts"): monkeypatch.chdir(pytester.path.joinpath(dirname)) items, reprec = pytester.inline_genitems() assert [x.name for x in items] == ["test_%s" % dirname] class TestCollectPluginHookRelay: def test_pytest_collect_file(self, pytester: Pytester) -> None: wascalled = [] class Plugin: def pytest_collect_file(self, path): if not path.basename.startswith("."): # Ignore hidden files, e.g. .testmondata. wascalled.append(path) pytester.makefile(".abc", "xyz") pytest.main(pytester.path, plugins=[Plugin()]) assert len(wascalled) == 1 assert wascalled[0].ext == ".abc" class TestPrunetraceback: def test_custom_repr_failure(self, pytester: Pytester) -> None: p = pytester.makepyfile( """ import not_exists """ ) pytester.makeconftest( """ import pytest def pytest_collect_file(path, parent): return MyFile.from_parent(fspath=path, parent=parent) class MyError(Exception): pass class MyFile(pytest.File): def collect(self): raise MyError() def repr_failure(self, excinfo): if excinfo.errisinstance(MyError): return "hello world" return pytest.File.repr_failure(self, excinfo) """ ) result = pytester.runpytest(p) result.stdout.fnmatch_lines(["*ERROR collecting*", "*hello world*"]) @pytest.mark.xfail(reason="other mechanism for adding to reporting needed") def test_collect_report_postprocessing(self, pytester: Pytester) -> None: p = pytester.makepyfile( """ import not_exists """ ) pytester.makeconftest( """ import pytest @pytest.hookimpl(hookwrapper=True) def pytest_make_collect_report(): outcome = yield rep = outcome.get_result() rep.headerlines += ["header1"] outcome.force_result(rep) """ ) result = pytester.runpytest(p) result.stdout.fnmatch_lines(["*ERROR collecting*", "*header1*"]) class TestCustomConftests: def test_ignore_collect_path(self, pytester: Pytester) -> None: pytester.makeconftest( """ def pytest_ignore_collect(path, config): return path.basename.startswith("x") or \ path.basename == "test_one.py" """ ) sub = pytester.mkdir("xy123") ensure_file(sub / "test_hello.py").write_text("syntax error") sub.joinpath("conftest.py").write_text("syntax error") pytester.makepyfile("def test_hello(): pass") pytester.makepyfile(test_one="syntax error") result = pytester.runpytest("--fulltrace") assert result.ret == 0 result.stdout.fnmatch_lines(["*1 passed*"]) def test_ignore_collect_not_called_on_argument(self, pytester: Pytester) -> None: pytester.makeconftest( """ def pytest_ignore_collect(path, config): return True """ ) p = pytester.makepyfile("def test_hello(): pass") result = pytester.runpytest(p) assert result.ret == 0 result.stdout.fnmatch_lines(["*1 passed*"]) result = pytester.runpytest() assert result.ret == ExitCode.NO_TESTS_COLLECTED result.stdout.fnmatch_lines(["*collected 0 items*"]) def test_collectignore_exclude_on_option(self, pytester: Pytester) -> None: pytester.makeconftest( """ import py from pathlib import Path collect_ignore = [py.path.local('hello'), 'test_world.py', Path('bye')] def pytest_addoption(parser): parser.addoption("--XX", action="store_true", default=False) def pytest_configure(config): if config.getvalue("XX"): collect_ignore[:] = [] """ ) pytester.mkdir("hello") pytester.makepyfile(test_world="def test_hello(): pass") result = pytester.runpytest() assert result.ret == ExitCode.NO_TESTS_COLLECTED result.stdout.no_fnmatch_line("*passed*") result = pytester.runpytest("--XX") assert result.ret == 0 assert "passed" in result.stdout.str() def test_collectignoreglob_exclude_on_option(self, pytester: Pytester) -> None: pytester.makeconftest( """ collect_ignore_glob = ['*w*l[dt]*'] def pytest_addoption(parser): parser.addoption("--XX", action="store_true", default=False) def pytest_configure(config): if config.getvalue("XX"): collect_ignore_glob[:] = [] """ ) pytester.makepyfile(test_world="def test_hello(): pass") pytester.makepyfile(test_welt="def test_hallo(): pass") result = pytester.runpytest() assert result.ret == ExitCode.NO_TESTS_COLLECTED result.stdout.fnmatch_lines(["*collected 0 items*"]) result = pytester.runpytest("--XX") assert result.ret == 0 result.stdout.fnmatch_lines(["*2 passed*"]) def test_pytest_fs_collect_hooks_are_seen(self, pytester: Pytester) -> None: pytester.makeconftest( """ import pytest class MyModule(pytest.Module): pass def pytest_collect_file(path, parent): if path.ext == ".py": return MyModule.from_parent(fspath=path, parent=parent) """ ) pytester.mkdir("sub") pytester.makepyfile("def test_x(): pass") result = pytester.runpytest("--co") result.stdout.fnmatch_lines(["*MyModule*", "*test_x*"]) def test_pytest_collect_file_from_sister_dir(self, pytester: Pytester) -> None: sub1 = pytester.mkpydir("sub1") sub2 = pytester.mkpydir("sub2") conf1 = pytester.makeconftest( """ import pytest class MyModule1(pytest.Module): pass def pytest_collect_file(path, parent): if path.ext == ".py": return MyModule1.from_parent(fspath=path, parent=parent) """ ) conf1.replace(sub1.joinpath(conf1.name)) conf2 = pytester.makeconftest( """ import pytest class MyModule2(pytest.Module): pass def pytest_collect_file(path, parent): if path.ext == ".py": return MyModule2.from_parent(fspath=path, parent=parent) """ ) conf2.replace(sub2.joinpath(conf2.name)) p = pytester.makepyfile("def test_x(): pass") shutil.copy(p, sub1.joinpath(p.name)) shutil.copy(p, sub2.joinpath(p.name)) result = pytester.runpytest("--co") result.stdout.fnmatch_lines(["*MyModule1*", "*MyModule2*", "*test_x*"]) class TestSession: def test_collect_topdir(self, pytester: Pytester) -> None: p = pytester.makepyfile("def test_func(): pass") id = "::".join([p.name, "test_func"]) # XXX migrate to collectonly? (see below) config = pytester.parseconfig(id) topdir = pytester.path rcol = Session.from_config(config) assert topdir == rcol.fspath # rootid = rcol.nodeid # root2 = rcol.perform_collect([rcol.nodeid], genitems=False)[0] # assert root2 == rcol, rootid colitems = rcol.perform_collect([rcol.nodeid], genitems=False) assert len(colitems) == 1 assert colitems[0].fspath == p def get_reported_items(self, hookrec: HookRecorder) -> List[Item]: """Return pytest.Item instances reported by the pytest_collectreport hook""" calls = hookrec.getcalls("pytest_collectreport") return [ x for call in calls for x in call.report.result if isinstance(x, pytest.Item) ] def test_collect_protocol_single_function(self, pytester: Pytester) -> None: p = pytester.makepyfile("def test_func(): pass") id = "::".join([p.name, "test_func"]) items, hookrec = pytester.inline_genitems(id) (item,) = items assert item.name == "test_func" newid = item.nodeid assert newid == id pprint.pprint(hookrec.calls) topdir = pytester.path # noqa hookrec.assert_contains( [ ("pytest_collectstart", "collector.fspath == topdir"), ("pytest_make_collect_report", "collector.fspath == topdir"), ("pytest_collectstart", "collector.fspath == p"), ("pytest_make_collect_report", "collector.fspath == p"), ("pytest_pycollect_makeitem", "name == 'test_func'"), ("pytest_collectreport", "report.result[0].name == 'test_func'"), ] ) # ensure we are reporting the collection of the single test item (#2464) assert [x.name for x in self.get_reported_items(hookrec)] == ["test_func"] def test_collect_protocol_method(self, pytester: Pytester) -> None: p = pytester.makepyfile( """ class TestClass(object): def test_method(self): pass """ ) normid = p.name + "::TestClass::test_method" for id in [p.name, p.name + "::TestClass", normid]: items, hookrec = pytester.inline_genitems(id) assert len(items) == 1 assert items[0].name == "test_method" newid = items[0].nodeid assert newid == normid # ensure we are reporting the collection of the single test item (#2464) assert [x.name for x in self.get_reported_items(hookrec)] == ["test_method"] def test_collect_custom_nodes_multi_id(self, pytester: Pytester) -> None: p = pytester.makepyfile("def test_func(): pass") pytester.makeconftest( """ import pytest class SpecialItem(pytest.Item): def runtest(self): return # ok class SpecialFile(pytest.File): def collect(self): return [SpecialItem.from_parent(name="check", parent=self)] def pytest_collect_file(path, parent): if path.basename == %r: return SpecialFile.from_parent(fspath=path, parent=parent) """ % p.name ) id = p.name items, hookrec = pytester.inline_genitems(id) pprint.pprint(hookrec.calls) assert len(items) == 2 hookrec.assert_contains( [ ("pytest_collectstart", "collector.fspath == collector.session.fspath"), ( "pytest_collectstart", "collector.__class__.__name__ == 'SpecialFile'", ), ("pytest_collectstart", "collector.__class__.__name__ == 'Module'"), ("pytest_pycollect_makeitem", "name == 'test_func'"), ("pytest_collectreport", "report.nodeid.startswith(p.name)"), ] ) assert len(self.get_reported_items(hookrec)) == 2 def test_collect_subdir_event_ordering(self, pytester: Pytester) -> None: p = pytester.makepyfile("def test_func(): pass") aaa = pytester.mkpydir("aaa") test_aaa = aaa.joinpath("test_aaa.py") p.replace(test_aaa) items, hookrec = pytester.inline_genitems() assert len(items) == 1 pprint.pprint(hookrec.calls) hookrec.assert_contains( [ ("pytest_collectstart", "collector.fspath == test_aaa"), ("pytest_pycollect_makeitem", "name == 'test_func'"), ("pytest_collectreport", "report.nodeid.startswith('aaa/test_aaa.py')"), ] ) def test_collect_two_commandline_args(self, pytester: Pytester) -> None: p = pytester.makepyfile("def test_func(): pass") aaa = pytester.mkpydir("aaa") bbb = pytester.mkpydir("bbb") test_aaa = aaa.joinpath("test_aaa.py") shutil.copy(p, test_aaa) test_bbb = bbb.joinpath("test_bbb.py") p.replace(test_bbb) id = "." items, hookrec = pytester.inline_genitems(id) assert len(items) == 2 pprint.pprint(hookrec.calls) hookrec.assert_contains( [ ("pytest_collectstart", "collector.fspath == test_aaa"), ("pytest_pycollect_makeitem", "name == 'test_func'"), ("pytest_collectreport", "report.nodeid == 'aaa/test_aaa.py'"), ("pytest_collectstart", "collector.fspath == test_bbb"), ("pytest_pycollect_makeitem", "name == 'test_func'"), ("pytest_collectreport", "report.nodeid == 'bbb/test_bbb.py'"), ] ) def test_serialization_byid(self, pytester: Pytester) -> None: pytester.makepyfile("def test_func(): pass") items, hookrec = pytester.inline_genitems() assert len(items) == 1 (item,) = items items2, hookrec = pytester.inline_genitems(item.nodeid) (item2,) = items2 assert item2.name == item.name assert item2.fspath == item.fspath def test_find_byid_without_instance_parents(self, pytester: Pytester) -> None: p = pytester.makepyfile( """ class TestClass(object): def test_method(self): pass """ ) arg = p.name + "::TestClass::test_method" items, hookrec = pytester.inline_genitems(arg) assert len(items) == 1 (item,) = items assert item.nodeid.endswith("TestClass::test_method") # ensure we are reporting the collection of the single test item (#2464) assert [x.name for x in self.get_reported_items(hookrec)] == ["test_method"] class Test_getinitialnodes: def test_global_file(self, pytester: Pytester) -> None: tmpdir = pytester.path x = ensure_file(tmpdir / "x.py") with tmpdir.cwd(): config = pytester.parseconfigure(x) col = pytester.getnode(config, x) assert isinstance(col, pytest.Module) assert col.name == "x.py" assert col.parent is not None assert col.parent.parent is None for parent in col.listchain(): assert parent.config is config def test_pkgfile(self, pytester: Pytester) -> None: """Verify nesting when a module is within a package. The parent chain should match: Module -> Package -> Session. Session's parent should always be None. """ tmpdir = pytester.path subdir = tmpdir.joinpath("subdir") x = ensure_file(subdir / "x.py") ensure_file(subdir / "__init__.py") with subdir.cwd(): config = pytester.parseconfigure(x) col = pytester.getnode(config, x) assert col is not None assert col.name == "x.py" assert isinstance(col, pytest.Module) assert isinstance(col.parent, pytest.Package) assert isinstance(col.parent.parent, pytest.Session) # session is batman (has no parents) assert col.parent.parent.parent is None for parent in col.listchain(): assert parent.config is config class Test_genitems: def test_check_collect_hashes(self, pytester: Pytester) -> None: p = pytester.makepyfile( """ def test_1(): pass def test_2(): pass """ ) shutil.copy(p, p.parent / (p.stem + "2" + ".py")) items, reprec = pytester.inline_genitems(p.parent) assert len(items) == 4 for numi, i in enumerate(items): for numj, j in enumerate(items): if numj != numi: assert hash(i) != hash(j) assert i != j def test_example_items1(self, pytester: Pytester) -> None: p = pytester.makepyfile( """ import pytest def testone(): pass class TestX(object): def testmethod_one(self): pass class TestY(TestX): @pytest.mark.parametrize("arg0", [".["]) def testmethod_two(self, arg0): pass """ ) items, reprec = pytester.inline_genitems(p) assert len(items) == 4 assert items[0].name == "testone" assert items[1].name == "testmethod_one" assert items[2].name == "testmethod_one" assert items[3].name == "testmethod_two[.[]" # let's also test getmodpath here assert items[0].getmodpath() == "testone" # type: ignore[attr-defined] assert items[1].getmodpath() == "TestX.testmethod_one" # type: ignore[attr-defined] assert items[2].getmodpath() == "TestY.testmethod_one" # type: ignore[attr-defined] # PR #6202: Fix incorrect result of getmodpath method. (Resolves issue #6189) assert items[3].getmodpath() == "TestY.testmethod_two[.[]" # type: ignore[attr-defined] s = items[0].getmodpath(stopatmodule=False) # type: ignore[attr-defined] assert s.endswith("test_example_items1.testone") print(s) def test_class_and_functions_discovery_using_glob(self, pytester: Pytester) -> None: """Test that Python_classes and Python_functions config options work as prefixes and glob-like patterns (#600).""" pytester.makeini( """ [pytest] python_classes = *Suite Test python_functions = *_test test """ ) p = pytester.makepyfile( """ class MyTestSuite(object): def x_test(self): pass class TestCase(object): def test_y(self): pass """ ) items, reprec = pytester.inline_genitems(p) ids = [x.getmodpath() for x in items] # type: ignore[attr-defined] assert ids == ["MyTestSuite.x_test", "TestCase.test_y"] def test_matchnodes_two_collections_same_file(pytester: Pytester) -> None: pytester.makeconftest( """ import pytest def pytest_configure(config): config.pluginmanager.register(Plugin2()) class Plugin2(object): def pytest_collect_file(self, path, parent): if path.ext == ".abc": return MyFile2.from_parent(fspath=path, parent=parent) def pytest_collect_file(path, parent): if path.ext == ".abc": return MyFile1.from_parent(fspath=path, parent=parent) class MyFile1(pytest.File): def collect(self): yield Item1.from_parent(name="item1", parent=self) class MyFile2(pytest.File): def collect(self): yield Item2.from_parent(name="item2", parent=self) class Item1(pytest.Item): def runtest(self): pass class Item2(pytest.Item): def runtest(self): pass """ ) p = pytester.makefile(".abc", "") result = pytester.runpytest() assert result.ret == 0 result.stdout.fnmatch_lines(["*2 passed*"]) res = pytester.runpytest("%s::item2" % p.name) res.stdout.fnmatch_lines(["*1 passed*"]) class TestNodekeywords: def test_no_under(self, pytester: Pytester) -> None: modcol = pytester.getmodulecol( """ def test_pass(): pass def test_fail(): assert 0 """ ) values = list(modcol.keywords) assert modcol.name in values for x in values: assert not x.startswith("_") assert modcol.name in repr(modcol.keywords) def test_issue345(self, pytester: Pytester) -> None: pytester.makepyfile( """ def test_should_not_be_selected(): assert False, 'I should not have been selected to run' def test___repr__(): pass """ ) reprec = pytester.inline_run("-k repr") reprec.assertoutcome(passed=1, failed=0) def test_keyword_matching_is_case_insensitive_by_default( self, pytester: Pytester ) -> None: """Check that selection via -k EXPRESSION is case-insensitive. Since markers are also added to the node keywords, they too can be matched without having to think about case sensitivity. """ pytester.makepyfile( """ import pytest def test_sPeCiFiCToPiC_1(): assert True class TestSpecificTopic_2: def test(self): assert True @pytest.mark.sPeCiFiCToPic_3 def test(): assert True @pytest.mark.sPeCiFiCToPic_4 class Test: def test(self): assert True def test_failing_5(): assert False, "This should not match" """ ) num_matching_tests = 4 for expression in ("specifictopic", "SPECIFICTOPIC", "SpecificTopic"): reprec = pytester.inline_run("-k " + expression) reprec.assertoutcome(passed=num_matching_tests, failed=0) COLLECTION_ERROR_PY_FILES = dict( test_01_failure=""" def test_1(): assert False """, test_02_import_error=""" import asdfasdfasdf def test_2(): assert True """, test_03_import_error=""" import asdfasdfasdf def test_3(): assert True """, test_04_success=""" def test_4(): assert True """, ) def test_exit_on_collection_error(pytester: Pytester) -> None: """Verify that all collection errors are collected and no tests executed""" pytester.makepyfile(**COLLECTION_ERROR_PY_FILES) res = pytester.runpytest() assert res.ret == 2 res.stdout.fnmatch_lines( [ "collected 2 items / 2 errors", "*ERROR collecting test_02_import_error.py*", "*No module named *asdfa*", "*ERROR collecting test_03_import_error.py*", "*No module named *asdfa*", ] ) def test_exit_on_collection_with_maxfail_smaller_than_n_errors( pytester: Pytester, ) -> None: """ Verify collection is aborted once maxfail errors are encountered ignoring further modules which would cause more collection errors. """ pytester.makepyfile(**COLLECTION_ERROR_PY_FILES) res = pytester.runpytest("--maxfail=1") assert res.ret == 1 res.stdout.fnmatch_lines( [ "collected 1 item / 1 error", "*ERROR collecting test_02_import_error.py*", "*No module named *asdfa*", "*! stopping after 1 failures !*", "*= 1 error in *", ] ) res.stdout.no_fnmatch_line("*test_03*") def test_exit_on_collection_with_maxfail_bigger_than_n_errors( pytester: Pytester, ) -> None: """ Verify the test run aborts due to collection errors even if maxfail count of errors was not reached. """ pytester.makepyfile(**COLLECTION_ERROR_PY_FILES) res = pytester.runpytest("--maxfail=4") assert res.ret == 2 res.stdout.fnmatch_lines( [ "collected 2 items / 2 errors", "*ERROR collecting test_02_import_error.py*", "*No module named *asdfa*", "*ERROR collecting test_03_import_error.py*", "*No module named *asdfa*", "*! Interrupted: 2 errors during collection !*", "*= 2 errors in *", ] ) def test_continue_on_collection_errors(pytester: Pytester) -> None: """ Verify tests are executed even when collection errors occur when the --continue-on-collection-errors flag is set """ pytester.makepyfile(**COLLECTION_ERROR_PY_FILES) res = pytester.runpytest("--continue-on-collection-errors") assert res.ret == 1 res.stdout.fnmatch_lines( ["collected 2 items / 2 errors", "*1 failed, 1 passed, 2 errors*"] ) def test_continue_on_collection_errors_maxfail(pytester: Pytester) -> None: """ Verify tests are executed even when collection errors occur and that maxfail is honoured (including the collection error count). 4 tests: 2 collection errors + 1 failure + 1 success test_4 is never executed because the test run is with --maxfail=3 which means it is interrupted after the 2 collection errors + 1 failure. """ pytester.makepyfile(**COLLECTION_ERROR_PY_FILES) res = pytester.runpytest("--continue-on-collection-errors", "--maxfail=3") assert res.ret == 1 res.stdout.fnmatch_lines(["collected 2 items / 2 errors", "*1 failed, 2 errors*"]) def test_fixture_scope_sibling_conftests(pytester: Pytester) -> None: """Regression test case for https://github.com/pytest-dev/pytest/issues/2836""" foo_path = pytester.mkdir("foo") foo_path.joinpath("conftest.py").write_text( textwrap.dedent( """\ import pytest @pytest.fixture def fix(): return 1 """ ) ) foo_path.joinpath("test_foo.py").write_text("def test_foo(fix): assert fix == 1") # Tests in `food/` should not see the conftest fixture from `foo/` food_path = pytester.mkpydir("food") food_path.joinpath("test_food.py").write_text("def test_food(fix): assert fix == 1") res = pytester.runpytest() assert res.ret == 1 res.stdout.fnmatch_lines( [ "*ERROR at setup of test_food*", "E*fixture 'fix' not found", "*1 passed, 1 error*", ] ) def test_collect_init_tests(pytester: Pytester) -> None: """Check that we collect files from __init__.py files when they patch the 'python_files' (#3773)""" p = pytester.copy_example("collect/collect_init_tests") result = pytester.runpytest(p, "--collect-only") result.stdout.fnmatch_lines( [ "collected 2 items", "", " ", " ", " ", " ", ] ) result = pytester.runpytest("./tests", "--collect-only") result.stdout.fnmatch_lines( [ "collected 2 items", "", " ", " ", " ", " ", ] ) # Ignores duplicates with "." and pkginit (#4310). result = pytester.runpytest("./tests", ".", "--collect-only") result.stdout.fnmatch_lines( [ "collected 2 items", "", " ", " ", " ", " ", ] ) # Same as before, but different order. result = pytester.runpytest(".", "tests", "--collect-only") result.stdout.fnmatch_lines( [ "collected 2 items", "", " ", " ", " ", " ", ] ) result = pytester.runpytest("./tests/test_foo.py", "--collect-only") result.stdout.fnmatch_lines( ["", " ", " "] ) result.stdout.no_fnmatch_line("*test_init*") result = pytester.runpytest("./tests/__init__.py", "--collect-only") result.stdout.fnmatch_lines( ["", " ", " "] ) result.stdout.no_fnmatch_line("*test_foo*") def test_collect_invalid_signature_message(pytester: Pytester) -> None: """Check that we issue a proper message when we can't determine the signature of a test function (#4026). """ pytester.makepyfile( """ import pytest class TestCase: @pytest.fixture def fix(): pass """ ) result = pytester.runpytest() result.stdout.fnmatch_lines( ["Could not determine arguments of *.fix *: invalid method signature"] ) def test_collect_handles_raising_on_dunder_class(pytester: Pytester) -> None: """Handle proxy classes like Django's LazySettings that might raise on ``isinstance`` (#4266). """ pytester.makepyfile( """ class ImproperlyConfigured(Exception): pass class RaisesOnGetAttr(object): def raises(self): raise ImproperlyConfigured __class__ = property(raises) raises = RaisesOnGetAttr() def test_1(): pass """ ) result = pytester.runpytest() result.stdout.fnmatch_lines(["*1 passed in*"]) assert result.ret == 0 def test_collect_with_chdir_during_import(pytester: Pytester) -> None: subdir = pytester.mkdir("sub") pytester.path.joinpath("conftest.py").write_text( textwrap.dedent( """ import os os.chdir(%r) """ % (str(subdir),) ) ) pytester.makepyfile( """ def test_1(): import os assert os.getcwd() == %r """ % (str(subdir),) ) with pytester.path.cwd(): result = pytester.runpytest() result.stdout.fnmatch_lines(["*1 passed in*"]) assert result.ret == 0 # Handles relative testpaths. pytester.makeini( """ [pytest] testpaths = . """ ) with pytester.path.cwd(): result = pytester.runpytest("--collect-only") result.stdout.fnmatch_lines(["collected 1 item"]) def test_collect_pyargs_with_testpaths( pytester: Pytester, monkeypatch: MonkeyPatch ) -> None: testmod = pytester.mkdir("testmod") # NOTE: __init__.py is not collected since it does not match python_files. testmod.joinpath("__init__.py").write_text("def test_func(): pass") testmod.joinpath("test_file.py").write_text("def test_func(): pass") root = pytester.mkdir("root") root.joinpath("pytest.ini").write_text( textwrap.dedent( """ [pytest] addopts = --pyargs testpaths = testmod """ ) ) monkeypatch.setenv("PYTHONPATH", str(pytester.path), prepend=os.pathsep) with root.cwd(): result = pytester.runpytest_subprocess() result.stdout.fnmatch_lines(["*1 passed in*"]) def test_collect_symlink_file_arg(pytester: Pytester) -> None: """Collect a direct symlink works even if it does not match python_files (#4325).""" real = pytester.makepyfile( real=""" def test_nodeid(request): assert request.node.nodeid == "symlink.py::test_nodeid" """ ) symlink = pytester.path.joinpath("symlink.py") symlink_or_skip(real, symlink) result = pytester.runpytest("-v", symlink) result.stdout.fnmatch_lines(["symlink.py::test_nodeid PASSED*", "*1 passed in*"]) assert result.ret == 0 def test_collect_symlink_out_of_tree(pytester: Pytester) -> None: """Test collection of symlink via out-of-tree rootdir.""" sub = pytester.mkdir("sub") real = sub.joinpath("test_real.py") real.write_text( textwrap.dedent( """ def test_nodeid(request): # Should not contain sub/ prefix. assert request.node.nodeid == "test_real.py::test_nodeid" """ ), ) out_of_tree = pytester.mkdir("out_of_tree") symlink_to_sub = out_of_tree.joinpath("symlink_to_sub") symlink_or_skip(sub, symlink_to_sub) os.chdir(sub) result = pytester.runpytest("-vs", "--rootdir=%s" % sub, symlink_to_sub) result.stdout.fnmatch_lines( [ # Should not contain "sub/"! "test_real.py::test_nodeid PASSED" ] ) assert result.ret == 0 def test_collect_symlink_dir(pytester: Pytester) -> None: """A symlinked directory is collected.""" dir = pytester.mkdir("dir") dir.joinpath("test_it.py").write_text("def test_it(): pass", "utf-8") pytester.path.joinpath("symlink_dir").symlink_to(dir) result = pytester.runpytest() result.assert_outcomes(passed=2) def test_collectignore_via_conftest(pytester: Pytester) -> None: """collect_ignore in parent conftest skips importing child (issue #4592).""" tests = pytester.mkpydir("tests") tests.joinpath("conftest.py").write_text("collect_ignore = ['ignore_me']") ignore_me = tests.joinpath("ignore_me") ignore_me.mkdir() ignore_me.joinpath("__init__.py").touch() ignore_me.joinpath("conftest.py").write_text("assert 0, 'should_not_be_called'") result = pytester.runpytest() assert result.ret == ExitCode.NO_TESTS_COLLECTED def test_collect_pkg_init_and_file_in_args(pytester: Pytester) -> None: subdir = pytester.mkdir("sub") init = subdir.joinpath("__init__.py") init.write_text("def test_init(): pass") p = subdir.joinpath("test_file.py") p.write_text("def test_file(): pass") # NOTE: without "-o python_files=*.py" this collects test_file.py twice. # This changed/broke with "Add package scoped fixtures #2283" (2b1410895) # initially (causing a RecursionError). result = pytester.runpytest("-v", str(init), str(p)) result.stdout.fnmatch_lines( [ "sub/test_file.py::test_file PASSED*", "sub/test_file.py::test_file PASSED*", "*2 passed in*", ] ) result = pytester.runpytest("-v", "-o", "python_files=*.py", str(init), str(p)) result.stdout.fnmatch_lines( [ "sub/__init__.py::test_init PASSED*", "sub/test_file.py::test_file PASSED*", "*2 passed in*", ] ) def test_collect_pkg_init_only(pytester: Pytester) -> None: subdir = pytester.mkdir("sub") init = subdir.joinpath("__init__.py") init.write_text("def test_init(): pass") result = pytester.runpytest(str(init)) result.stdout.fnmatch_lines(["*no tests ran in*"]) result = pytester.runpytest("-v", "-o", "python_files=*.py", str(init)) result.stdout.fnmatch_lines(["sub/__init__.py::test_init PASSED*", "*1 passed in*"]) @pytest.mark.parametrize("use_pkg", (True, False)) def test_collect_sub_with_symlinks(use_pkg: bool, pytester: Pytester) -> None: """Collection works with symlinked files and broken symlinks""" sub = pytester.mkdir("sub") if use_pkg: sub.joinpath("__init__.py").touch() sub.joinpath("test_file.py").write_text("def test_file(): pass") # Create a broken symlink. symlink_or_skip("test_doesnotexist.py", sub.joinpath("test_broken.py")) # Symlink that gets collected. symlink_or_skip("test_file.py", sub.joinpath("test_symlink.py")) result = pytester.runpytest("-v", str(sub)) result.stdout.fnmatch_lines( [ "sub/test_file.py::test_file PASSED*", "sub/test_symlink.py::test_file PASSED*", "*2 passed in*", ] ) def test_collector_respects_tbstyle(pytester: Pytester) -> None: p1 = pytester.makepyfile("assert 0") result = pytester.runpytest(p1, "--tb=native") assert result.ret == ExitCode.INTERRUPTED result.stdout.fnmatch_lines( [ "*_ ERROR collecting test_collector_respects_tbstyle.py _*", "Traceback (most recent call last):", ' File "*/test_collector_respects_tbstyle.py", line 1, in ', " assert 0", "AssertionError: assert 0", "*! Interrupted: 1 error during collection !*", "*= 1 error in *", ] ) def test_does_not_eagerly_collect_packages(pytester: Pytester) -> None: pytester.makepyfile("def test(): pass") pydir = pytester.mkpydir("foopkg") pydir.joinpath("__init__.py").write_text("assert False") result = pytester.runpytest() assert result.ret == ExitCode.OK def test_does_not_put_src_on_path(pytester: Pytester) -> None: # `src` is not on sys.path so it should not be importable ensure_file(pytester.path / "src/nope/__init__.py") pytester.makepyfile( "import pytest\n" "def test():\n" " with pytest.raises(ImportError):\n" " import nope\n" ) result = pytester.runpytest() assert result.ret == ExitCode.OK def test_fscollector_from_parent(pytester: Pytester, request: FixtureRequest) -> None: """Ensure File.from_parent can forward custom arguments to the constructor. Context: https://github.com/pytest-dev/pytest-cpp/pull/47 """ class MyCollector(pytest.File): def __init__(self, fspath, parent, x): super().__init__(fspath, parent) self.x = x @classmethod def from_parent(cls, parent, *, fspath, x): return super().from_parent(parent=parent, fspath=fspath, x=x) collector = MyCollector.from_parent( parent=request.session, fspath=py.path.local(pytester.path) / "foo", x=10 ) assert collector.x == 10 def test_class_from_parent(pytester: Pytester, request: FixtureRequest) -> None: """Ensure Class.from_parent can forward custom arguments to the constructor.""" class MyCollector(pytest.Class): def __init__(self, name, parent, x): super().__init__(name, parent) self.x = x @classmethod def from_parent(cls, parent, *, name, x): return super().from_parent(parent=parent, name=name, x=x) collector = MyCollector.from_parent(parent=request.session, name="foo", x=10) assert collector.x == 10 class TestImportModeImportlib: def test_collect_duplicate_names(self, pytester: Pytester) -> None: """--import-mode=importlib can import modules with same names that are not in packages.""" pytester.makepyfile( **{ "tests_a/test_foo.py": "def test_foo1(): pass", "tests_b/test_foo.py": "def test_foo2(): pass", } ) result = pytester.runpytest("-v", "--import-mode=importlib") result.stdout.fnmatch_lines( [ "tests_a/test_foo.py::test_foo1 *", "tests_b/test_foo.py::test_foo2 *", "* 2 passed in *", ] ) def test_conftest(self, pytester: Pytester) -> None: """Directory containing conftest modules are not put in sys.path as a side-effect of importing them.""" tests_dir = pytester.path.joinpath("tests") pytester.makepyfile( **{ "tests/conftest.py": "", "tests/test_foo.py": """ import sys def test_check(): assert r"{tests_dir}" not in sys.path """.format( tests_dir=tests_dir ), } ) result = pytester.runpytest("-v", "--import-mode=importlib") result.stdout.fnmatch_lines(["* 1 passed in *"]) def setup_conftest_and_foo(self, pytester: Pytester) -> None: """Setup a tests folder to be used to test if modules in that folder can be imported due to side-effects of --import-mode or not.""" pytester.makepyfile( **{ "tests/conftest.py": "", "tests/foo.py": """ def foo(): return 42 """, "tests/test_foo.py": """ def test_check(): from foo import foo assert foo() == 42 """, } ) def test_modules_importable_as_side_effect(self, pytester: Pytester) -> None: """In import-modes `prepend` and `append`, we are able to import modules from folders containing conftest.py files due to the side effect of changing sys.path.""" self.setup_conftest_and_foo(pytester) result = pytester.runpytest("-v", "--import-mode=prepend") result.stdout.fnmatch_lines(["* 1 passed in *"]) def test_modules_not_importable_as_side_effect(self, pytester: Pytester) -> None: """In import-mode `importlib`, modules in folders containing conftest.py are not importable, as don't change sys.path or sys.modules as side effect of importing the conftest.py file. """ self.setup_conftest_and_foo(pytester) result = pytester.runpytest("-v", "--import-mode=importlib") result.stdout.fnmatch_lines( [ "*ModuleNotFoundError: No module named 'foo'", "tests?test_foo.py:2: ModuleNotFoundError", "* 1 failed in *", ] ) def test_does_not_crash_on_error_from_decorated_function(pytester: Pytester) -> None: """Regression test for an issue around bad exception formatting due to assertion rewriting mangling lineno's (#4984).""" pytester.makepyfile( """ @pytest.fixture def a(): return 4 """ ) result = pytester.runpytest() # Not INTERNAL_ERROR assert result.ret == ExitCode.INTERRUPTED def test_does_not_crash_on_recursive_symlink(pytester: Pytester) -> None: """Regression test for an issue around recursive symlinks (#7951).""" symlink_or_skip("recursive", pytester.path.joinpath("recursive")) pytester.makepyfile( """ def test_foo(): assert True """ ) result = pytester.runpytest() assert result.ret == ExitCode.OK assert result.parseoutcomes() == {"passed": 1}