Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix test collection from packages mixed with directories. #3768 and #3789 #3802

Merged
merged 3 commits into from Aug 16, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/3768.bugfix.rst
@@ -0,0 +1 @@
Fix test collection from packages mixed with normal directories.
1 change: 1 addition & 0 deletions changelog/3789.bugfix.rst
@@ -0,0 +1 @@
Fix test collection from packages mixed with normal directories.
25 changes: 10 additions & 15 deletions src/_pytest/python.py
Expand Up @@ -216,18 +216,6 @@ def pytest_pycollect_makemodule(path, parent):
return Module(path, parent)


def pytest_ignore_collect(path, config):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this does not really seem necessary, main.pytest_ignore_collect already seems to do the exactly same thing anyway.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially I copy/pasted the Package class from Session because they both serve very similar functions, and then realized not everything was needed.

# Skip duplicate packages.
keepduplicates = config.getoption("keepduplicates")
if keepduplicates:
duplicate_paths = config.pluginmanager._duplicatepaths
if path.basename == "__init__.py":
if path in duplicate_paths:
return True
else:
duplicate_paths.add(path)


@hookimpl(hookwrapper=True)
def pytest_pycollect_makeitem(collector, name, obj):
outcome = yield
Expand Down Expand Up @@ -554,9 +542,7 @@ def __init__(self, fspath, parent=None, config=None, session=None, nodeid=None):
self.name = fspath.dirname
self.trace = session.trace
self._norecursepatterns = session._norecursepatterns
for path in list(session.config.pluginmanager._duplicatepaths):
if path.dirname == fspath.dirname and path != fspath:
session.config.pluginmanager._duplicatepaths.remove(path)
self.fspath = fspath

def _recurse(self, path):
ihook = self.gethookproxy(path.dirpath())
Expand Down Expand Up @@ -594,6 +580,15 @@ def isinitpath(self, path):
return path in self.session._initialpaths

def collect(self):
# XXX: HACK!
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strange, I expect main.pytest_ignore_collect to handle this properly already:

pytest/src/_pytest/main.py

Lines 261 to 284 in 4d8903f

def pytest_ignore_collect(path, config):
ignore_paths = config._getconftest_pathlist("collect_ignore", path=path.dirpath())
ignore_paths = ignore_paths or []
excludeopt = config.getoption("ignore")
if excludeopt:
ignore_paths.extend([py.path.local(x) for x in excludeopt])
if py.path.local(path) in ignore_paths:
return True
allow_in_venv = config.getoption("collect_in_virtualenv")
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

If pytest_ignore_collect sees the same path twice, it should ignore the collection when see the path the second time, which would not trigger a new Package item to be collected.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is it sees that some modules were already "collected" (at Session level) via the recurse() method. Then when the package goes and tries to collect its children and turn them into nodes it can't because they look like duplicates. Wrong! My first reaction was to try to stop marking them as duplicates at Session level, but everything is so tangled up that I can't find a cleaner way.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JFI: I've moved it in #4241.

# 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()
pkg_prefix = None
for path in this_path.visit(rec=self._recurse, bf=True, sort=True):
Expand Down
40 changes: 40 additions & 0 deletions testing/python/collect.py
Expand Up @@ -1583,3 +1583,43 @@ def test_package_collection_infinite_recursion(testdir):
testdir.copy_example("collect/package_infinite_recursion")
result = testdir.runpytest()
result.stdout.fnmatch_lines("*1 passed*")


def test_package_with_modules(testdir):
"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just FYI, you could have created the actual files in testing/example_scripts and copied the whole directory here like the code above this function does.

The idea is that it is easier to execute the test using pytest as normal during testing/debugging. 😉

This is fine as is, just thought I would mention this for the future. 👍

.
└── root
├── __init__.py
├── sub1
│ ├── __init__.py
│ └── sub1_1
│ ├── __init__.py
│ └── test_in_sub1.py
└── sub2
└── test
└── test_in_sub2.py

"""
root = testdir.mkpydir("root")
sub1 = root.mkdir("sub1")
sub1.ensure("__init__.py")
sub1_test = sub1.mkdir("sub1_1")
sub1_test.ensure("__init__.py")
sub2 = root.mkdir("sub2")
sub2_test = sub2.mkdir("sub2")

sub1_test.join("test_in_sub1.py").write("def test_1(): pass")
sub2_test.join("test_in_sub2.py").write("def test_2(): pass")

# Execute from .
result = testdir.runpytest("-v", "-s")
result.assert_outcomes(passed=2)

# Execute from . with one argument "root"
result = testdir.runpytest("-v", "-s", "root")
result.assert_outcomes(passed=2)

# Chdir into package's root and execute with no args
root.chdir()
result = testdir.runpytest("-v", "-s")
result.assert_outcomes(passed=2)