diff --git a/.travis.yml b/.travis.yml
index 66d06fccd..9010ae1c2 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,4 +1,3 @@
-sudo: false
language: python
dist: xenial
stages:
@@ -26,7 +25,7 @@ env:
matrix:
allow_failures:
- python: '3.8-dev'
- env: TOXENV=py38
+ env: TOXENV=py38-xdist
jobs:
include:
@@ -34,14 +33,12 @@ jobs:
- env: TOXENV=pypy PYTEST_NO_COVERAGE=1
python: 'pypy-5.4'
dist: trusty
- - env: TOXENV=py34
+ - env: TOXENV=py34-xdist
python: '3.4'
- - env: TOXENV=py35
+ - env: TOXENV=py35-xdist
python: '3.5'
- - env: TOXENV=py36
+ - env: TOXENV=py36-xdist
python: '3.6'
- - env: TOXENV=py38
- python: '3.8-dev'
- env: TOXENV=py37
- &test-macos
language: generic
@@ -50,15 +47,20 @@ jobs:
sudo: required
install:
- python -m pip install --pre tox
- env: TOXENV=py27
+ env: TOXENV=py27-xdist
- <<: *test-macos
- env: TOXENV=py37
+ env: TOXENV=py37-xdist
before_install:
- brew update
- brew upgrade python
- brew unlink python
- brew link python
+ # Jobs only run via Travis cron jobs (currently daily).
+ - env: TOXENV=py38-xdist
+ python: '3.8-dev'
+ if: type = cron
+
- stage: baseline
env: TOXENV=py27-pexpect,py27-trial,py27-numpy
- env: TOXENV=py37-xdist
diff --git a/changelog/4782.bugfix.rst b/changelog/4782.bugfix.rst
new file mode 100644
index 000000000..12e08d00c
--- /dev/null
+++ b/changelog/4782.bugfix.rst
@@ -0,0 +1 @@
+Fix ``AssertionError`` with collection of broken symlinks with packages.
diff --git a/doc/en/assert.rst b/doc/en/assert.rst
index 186e12853..b119adcf0 100644
--- a/doc/en/assert.rst
+++ b/doc/en/assert.rst
@@ -88,23 +88,30 @@ and if you need to have access to the actual exception info you may use::
the actual exception raised. The main attributes of interest are
``.type``, ``.value`` and ``.traceback``.
-.. versionchanged:: 3.0
+You can pass a ``match`` keyword parameter to the context-manager to test
+that a regular expression matches on the string representation of an exception
+(similar to the ``TestCase.assertRaisesRegexp`` method from ``unittest``)::
-In the context manager form you may use the keyword argument
-``message`` to specify a custom failure message::
+ import pytest
- >>> with raises(ZeroDivisionError, message="Expecting ZeroDivisionError"):
- ... pass
- ... Failed: Expecting ZeroDivisionError
+ def myfunc():
+ raise ValueError("Exception 123 raised")
-If you want to write test code that works on Python 2.4 as well,
-you may also use two other ways to test for an expected exception::
+ def test_match():
+ with pytest.raises(ValueError, match=r'.* 123 .*'):
+ myfunc()
+
+The regexp parameter of the ``match`` method is matched with the ``re.search``
+function, so in the above example ``match='123'`` would have worked as
+well.
+
+There's an alternate form of the ``pytest.raises`` function where you pass
+a function that will be executed with the given ``*args`` and ``**kwargs`` and
+assert that the given exception is raised::
pytest.raises(ExpectedException, func, *args, **kwargs)
-which will execute the specified function with args and kwargs and
-assert that the given ``ExpectedException`` is raised. The reporter will
-provide you with helpful output in case of failures such as *no
+The reporter will provide you with helpful output in case of failures such as *no
exception* or *wrong exception*.
Note that it is also possible to specify a "raises" argument to
@@ -121,23 +128,6 @@ exceptions your own code is deliberately raising, whereas using
like documenting unfixed bugs (where the test describes what "should" happen)
or bugs in dependencies.
-Also, the context manager form accepts a ``match`` keyword parameter to test
-that a regular expression matches on the string representation of an exception
-(like the ``TestCase.assertRaisesRegexp`` method from ``unittest``)::
-
- import pytest
-
- def myfunc():
- raise ValueError("Exception 123 raised")
-
- def test_match():
- with pytest.raises(ValueError, match=r'.* 123 .*'):
- myfunc()
-
-The regexp parameter of the ``match`` method is matched with the ``re.search``
-function. So in the above example ``match='123'`` would have worked as
-well.
-
.. _`assertwarns`:
diff --git a/doc/en/talks.rst b/doc/en/talks.rst
index aa1fb00e7..be0c6fb9f 100644
--- a/doc/en/talks.rst
+++ b/doc/en/talks.rst
@@ -25,6 +25,9 @@ Talks and blog postings
- pytest: recommendations, basic packages for testing in Python and Django, Andreu Vallbona, PyconES 2017 (`slides in english `_, `video in spanish `_)
+- `pytest advanced, Andrew Svetlov (Russian, PyCon Russia, 2016)
+ `_.
+
- `Pythonic testing, Igor Starikov (Russian, PyNsk, November 2016)
`_.
diff --git a/src/_pytest/main.py b/src/_pytest/main.py
index b49de86fb..6e639d872 100644
--- a/src/_pytest/main.py
+++ b/src/_pytest/main.py
@@ -618,7 +618,12 @@ class Session(nodes.FSCollector):
yield y
def _collectfile(self, path, handle_dupes=True):
- assert path.isfile()
+ assert path.isfile(), "%r is not a file (isdir=%r, exists=%r, islink=%r)" % (
+ path,
+ path.isdir(),
+ path.exists(),
+ path.islink(),
+ )
ihook = self.gethookproxy(path)
if not self.isinitpath(path):
if ihook.pytest_ignore_collect(path=path, config=self.config):
diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py
index 00ec80894..ce02e70cd 100644
--- a/src/_pytest/nodes.py
+++ b/src/_pytest/nodes.py
@@ -113,11 +113,12 @@ class Node(object):
:raise ValueError: if ``warning`` instance is not a subclass of PytestWarning.
- Example usage::
+ Example usage:
.. code-block:: python
node.warn(PytestWarning("some message"))
+
"""
from _pytest.warning_types import PytestWarning
diff --git a/src/_pytest/python.py b/src/_pytest/python.py
index 48962d137..215015d27 100644
--- a/src/_pytest/python.py
+++ b/src/_pytest/python.py
@@ -599,7 +599,12 @@ class Package(Module):
return proxy
def _collectfile(self, path, handle_dupes=True):
- assert path.isfile()
+ assert path.isfile(), "%r is not a file (isdir=%r, exists=%r, islink=%r)" % (
+ path,
+ path.isdir(),
+ path.exists(),
+ path.islink(),
+ )
ihook = self.gethookproxy(path)
if not self.isinitpath(path):
if ihook.pytest_ignore_collect(path=path, config=self.config):
@@ -632,7 +637,8 @@ class Package(Module):
pkg_prefixes = set()
for path in this_path.visit(rec=self._recurse, bf=True, sort=True):
# We will visit our own __init__.py file, in which case we skip it.
- if path.isfile():
+ is_file = path.isfile()
+ if is_file:
if path.basename == "__init__.py" and path.dirpath() == this_path:
continue
@@ -643,12 +649,14 @@ class Package(Module):
):
continue
- if path.isdir():
- if path.join("__init__.py").check(file=1):
- pkg_prefixes.add(path)
- else:
+ if is_file:
for x in self._collectfile(path):
yield x
+ elif not path.isdir():
+ # Broken symlink or invalid/missing file.
+ continue
+ elif path.join("__init__.py").check(file=1):
+ pkg_prefixes.add(path)
def _get_xunit_setup_teardown(holder, attr_name, param_obj=None):
diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py
index b2e418030..eda0c0905 100644
--- a/src/_pytest/terminal.py
+++ b/src/_pytest/terminal.py
@@ -280,7 +280,9 @@ class TerminalReporter(object):
def write_fspath_result(self, nodeid, res, **markup):
fspath = self.config.rootdir.join(nodeid.split("::")[0])
- if fspath != self.currentfspath:
+ # NOTE: explicitly check for None to work around py bug, and for less
+ # overhead in general (https://github.com/pytest-dev/py/pull/207).
+ if self.currentfspath is None or fspath != self.currentfspath:
if self.currentfspath is not None and self._show_progress_info:
self._write_progress_information_filling_space()
self.currentfspath = fspath
diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py
index 843c8ca37..4d109cc2b 100644
--- a/src/_pytest/tmpdir.py
+++ b/src/_pytest/tmpdir.py
@@ -31,7 +31,7 @@ class TempPathFactory(object):
# using os.path.abspath() to get absolute path instead of resolve() as it
# does not work the same in all platforms (see #4427)
# Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012)
- convert=attr.converters.optional(
+ converter=attr.converters.optional(
lambda p: Path(os.path.abspath(six.text_type(p)))
)
)
diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py
index a852277cc..bfb6acfab 100644
--- a/testing/test_assertrewrite.py
+++ b/testing/test_assertrewrite.py
@@ -1308,10 +1308,17 @@ class TestEarlyRewriteBailout(object):
@pytest.mark.skipif(
sys.platform.startswith("win32"), reason="cannot remove cwd on Windows"
)
- def test_cwd_changed(self, testdir):
+ def test_cwd_changed(self, testdir, monkeypatch):
+ # Setup conditions for py's fspath trying to import pathlib on py34
+ # always (previously triggered via xdist only).
+ # Ref: https://github.com/pytest-dev/py/pull/207
+ monkeypatch.setattr(sys, "path", [""] + sys.path)
+ if "pathlib" in sys.modules:
+ del sys.modules["pathlib"]
+
testdir.makepyfile(
**{
- "test_bar.py": """
+ "test_setup_nonexisting_cwd.py": """
import os
import shutil
import tempfile
@@ -1320,7 +1327,7 @@ class TestEarlyRewriteBailout(object):
os.chdir(d)
shutil.rmtree(d)
""",
- "test_foo.py": """
+ "test_test.py": """
def test():
pass
""",
diff --git a/testing/test_collection.py b/testing/test_collection.py
index 3eb398b9f..97c46d8c2 100644
--- a/testing/test_collection.py
+++ b/testing/test_collection.py
@@ -1206,3 +1206,30 @@ def test_collect_pkg_init_and_file_in_args(testdir):
"*2 passed in*",
]
)
+
+
+@pytest.mark.skipif(
+ not hasattr(py.path.local, "mksymlinkto"),
+ reason="symlink not available on this platform",
+)
+@pytest.mark.parametrize("use_pkg", (True, False))
+def test_collect_sub_with_symlinks(use_pkg, testdir):
+ sub = testdir.mkdir("sub")
+ if use_pkg:
+ sub.ensure("__init__.py")
+ sub.ensure("test_file.py").write("def test_file(): pass")
+
+ # Create a broken symlink.
+ sub.join("test_broken.py").mksymlinkto("test_doesnotexist.py")
+
+ # Symlink that gets collected.
+ sub.join("test_symlink.py").mksymlinkto("test_file.py")
+
+ result = testdir.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*",
+ ]
+ )