-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
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 handling recursive symlinks #7956
Conversation
Looks like the test fails on Windows - I guess it should probably be skipped there instead. |
f98502c
to
a2884aa
Compare
Something is not entirely correct with codecov, I'm not sure if my patch caused that drop in coverage, I've rebased my commit to master and it still appears. Could you please look at it? Thanks. |
The If you can reasonably write a test to cover it, then that would fix it, but sometimes it's not worth the effort, in which case we can override it and merge anyway. (As for the change itself, I need to look into it a bit before giving a proper review, which I'll do soon) |
Thanks! When I looked at codecov it resulted me a really huge diff about many many lines of difference, but now it seems trivial. Maybe I wasn't paying attention to every detail. Anyway, I did some research on how I could add a test for that bare Reading further,
So it seems that it is implementation specific. I ran out of ideas adding coverage for that |
4abf898
to
d3db71f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great finding.
Also please add yourself to AUTHORS
@csernazs! 👍
src/_pytest/main.py
Outdated
if not direntry.is_file(): | ||
continue | ||
except OSError as err: | ||
# 1921: ERROR_CANT_RESOLVE_FILENAME - fix for broken symlink pointing to itself |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
# 1921: ERROR_CANT_RESOLVE_FILENAME - fix for broken symlink pointing to itself | |
# 1921: ERROR_CANT_RESOLVE_FILENAME - fix for broken symlink pointing to itself (#7951) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @csernazs,
I think this approach is good.
There is one more place which calls visit()
and would require a similar treatment. Also, while looking at this I discovered #7981 and submitted PR #7982 to fix it, but that PR would add yet another place which can trigger this exception. I think that adding the try-catch code in every instance would be too annoying, therefore I suggest the following:
-
Copy
_ignore_error()
from pathlib into_pytest/pathlib.py
(with a comment indicating it was copied from pathlib as of Python 3.9). -
Modify
_pytest.pathlib.visit()
to do a pre-iteration of theos.scandir
results, runningentry.is_file()
(while ignoring the result) and skipping the entry if the error should be ignored.
This way, we can localize the try-catch in one place and know that it doesn't happen at later stages.
WDYT?
I completely agree with moving the code to pathlib.py (I also wanted to do it but I couldn't find a good place for it, but pathlib.py is perfect) |
I've pushed the suggested changes. A few remarks:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@csernazs Perfect! I left a suggestion and a comment.
|
||
entries = [] | ||
for entry in os.scandir(path): | ||
try: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would add a comment here, something like this:
Skip entries with symlink loops and other brokenness, so the caller doesn't
have to deal with it.
entry.is_file() | ||
except OSError as err: | ||
if _ignore_error(err): | ||
continue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
continue | |
continue | |
raise |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In such case, when the directory have n entries where 1 entry is broken, the caller will not able to see any entries but the error itself. In such case, the caller won't be able to ignore or somehow workaround the error.
On the other hand, raising the error makes sense as if is_file() raises some error, probably the following open() or other file-related call will fail and it is better to report the error as soon as possible.
So made the suggested change, just wanted to comment on it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with both the first and second part.
testing/test_collection.py
Outdated
def test_foo(): assert True | ||
""" | ||
) | ||
os.symlink("recursive", str(testdir.tmpdir.join("recursive"))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another comment: should use symlink_or_skip
here (already imported), for platforms which don't support symlinks.
testing/test_pathlib.py
Outdated
tmpdir.join("foo").write_binary(b"") | ||
tmpdir.join("bar").write_binary(b"") | ||
|
||
os.symlink("recursive", str(tmpdir.join("recursive"))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, looks like it's here too...
entry.is_file() | ||
except OSError as err: | ||
if _ignore_error(err): | ||
continue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with both the first and second part.
When pytest was run on a directory containing a recursive symlink it failed with ELOOP as the library was not able to determine the type of the direntry: src/_pytest/main.py:685: in collect if not direntry.is_file(): E OSError: [Errno 40] Too many levels of symbolic links: '/home/florian/proj/pytest/tests/recursive' This is fixed by handling ELOOP and other errors in the visit function in pathlib.py, so the entries whose is_file() call raises an OSError with the pre-defined list of error numbers will be exluded from the result. The _ignore_errors function was copied from Lib/pathlib.py of cpython 3.9. Fixes pytest-dev#7951
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @csernazs! Will also backport this to 6.1.x.
Fix handling recursive symlinks (cherry picked from commit 7fb0ea3)
Thank you guys for all the help! |
Fix handling recursive symlinks (cherry picked from commit 7fb0ea3) Adjusted to pass str(path) for Python 3.5 support.
When pytest was run on a directory containing a recursive symlink it failed
with ELOOP as the library was not able to determine the type of the
direntry:
src/_pytest/main.py:685: in collect
if not direntry.is_file():
E OSError: [Errno 40] Too many levels of symbolic links: '/home/florian/proj/pytest/tests/recursive'
This is fixed now by handling ELOOP.
Fixes #7951