diff --git a/changelog/4046.bugfix.rst b/changelog/4046.bugfix.rst new file mode 100644 index 0000000000..2b0da70cd0 --- /dev/null +++ b/changelog/4046.bugfix.rst @@ -0,0 +1 @@ +Fix problems with running tests in package ``__init__.py`` files. diff --git a/src/_pytest/main.py b/src/_pytest/main.py index f27270f262..2bb4050812 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -281,15 +281,6 @@ def pytest_ignore_collect(path, config): if _in_venv(path) and not allow_in_venv: return True - # Skip duplicate paths. - keepduplicates = config.getoption("keepduplicates") - duplicate_paths = config.pluginmanager._duplicatepaths - if not keepduplicates: - if path in duplicate_paths: - return True - else: - duplicate_paths.add(path) - return False @@ -551,7 +542,15 @@ def _collect(self, arg): col = root._collectfile(argpath) if col: self._node_cache[argpath] = col - for y in self.matchnodes(col, names): + m = self.matchnodes(col, names) + # If __init__.py was the only file requested, then the matched node will be + # the corresponding Package, and the first yielded item will be the __init__ + # Module itself, so just use that. If this special case isn't taken, then all + # the files in the package will be yielded. + if argpath.basename == "__init__.py": + yield next(m[0].collect()) + return + for y in m: yield y def _collectfile(self, path): @@ -559,6 +558,16 @@ def _collectfile(self, path): if not self.isinitpath(path): if ihook.pytest_ignore_collect(path=path, config=self.config): return () + + # Skip duplicate paths. + keepduplicates = self.config.getoption("keepduplicates") + if not keepduplicates: + duplicate_paths = self.config.pluginmanager._duplicatepaths + if path in duplicate_paths: + return () + else: + duplicate_paths.add(path) + return ihook.pytest_collect_file(path=path, parent=self) def _recurse(self, path): diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 58f95034d2..414eabec6c 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -552,15 +552,6 @@ def isinitpath(self, path): return path in self.session._initialpaths def collect(self): - # XXX: HACK! - # Before starting to collect any files from this package we need - # to cleanup the duplicate paths added by the session's collect(). - # Proper fix is to not track these as duplicates in the first place. - for path in list(self.session.config.pluginmanager._duplicatepaths): - # if path.parts()[:len(self.fspath.dirpath().parts())] == self.fspath.dirpath().parts(): - if path.dirname.startswith(self.name): - self.session.config.pluginmanager._duplicatepaths.remove(path) - this_path = self.fspath.dirpath() init_module = this_path.join("__init__.py") if init_module.check(file=1) and path_matches_patterns( diff --git a/testing/test_collection.py b/testing/test_collection.py index 7f6791dae9..06f8d40ee4 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -957,6 +957,21 @@ def test_collect_init_tests(testdir): "*", ] ) + result = testdir.runpytest("./tests", "--collect-only") + result.stdout.fnmatch_lines( + [ + "*", + "*", + "*", + "*", + ] + ) + result = testdir.runpytest("./tests/test_foo.py", "--collect-only") + result.stdout.fnmatch_lines(["*", "*"]) + assert "test_init" not in result.stdout.str() + result = testdir.runpytest("./tests/__init__.py", "--collect-only") + result.stdout.fnmatch_lines(["*", "*"]) + assert "test_foo" not in result.stdout.str() def test_collect_invalid_signature_message(testdir): diff --git a/testing/test_session.py b/testing/test_session.py index 6225a2c0db..c1785b9166 100644 --- a/testing/test_session.py +++ b/testing/test_session.py @@ -219,7 +219,7 @@ class TestY(TestX): started = reprec.getcalls("pytest_collectstart") finished = reprec.getreports("pytest_collectreport") assert len(started) == len(finished) - assert len(started) == 7 # XXX extra TopCollector + assert len(started) == 8 colfail = [x for x in finished if x.failed] assert len(colfail) == 1