test_ok2/testing/test_cacheprovider.py

1311 lines
44 KiB
Python

import os
import shutil
from pathlib import Path
from typing import Generator
from typing import List
import pytest
from _pytest.config import ExitCode
from _pytest.monkeypatch import MonkeyPatch
from _pytest.pytester import Pytester
from _pytest.tmpdir import TempPathFactory
pytest_plugins = ("pytester",)
class TestNewAPI:
def test_config_cache_mkdir(self, pytester: Pytester) -> None:
pytester.makeini("[pytest]")
config = pytester.parseconfigure()
assert config.cache is not None
with pytest.raises(ValueError):
config.cache.mkdir("key/name")
p = config.cache.mkdir("name")
assert p.is_dir()
def test_config_cache_dataerror(self, pytester: Pytester) -> None:
pytester.makeini("[pytest]")
config = pytester.parseconfigure()
assert config.cache is not None
cache = config.cache
pytest.raises(TypeError, lambda: cache.set("key/name", cache))
config.cache.set("key/name", 0)
config.cache._getvaluepath("key/name").write_bytes(b"123invalid")
val = config.cache.get("key/name", -2)
assert val == -2
@pytest.mark.filterwarnings("ignore:could not create cache path")
def test_cache_writefail_cachfile_silent(self, pytester: Pytester) -> None:
pytester.makeini("[pytest]")
pytester.path.joinpath(".pytest_cache").write_text("gone wrong")
config = pytester.parseconfigure()
cache = config.cache
assert cache is not None
cache.set("test/broken", [])
@pytest.fixture
def unwritable_cache_dir(self, pytester: Pytester) -> Generator[Path, None, None]:
cache_dir = pytester.path.joinpath(".pytest_cache")
cache_dir.mkdir()
mode = cache_dir.stat().st_mode
cache_dir.chmod(0)
if os.access(cache_dir, os.W_OK):
pytest.skip("Failed to make cache dir unwritable")
yield cache_dir
cache_dir.chmod(mode)
@pytest.mark.filterwarnings(
"ignore:could not create cache path:pytest.PytestWarning"
)
def test_cache_writefail_permissions(
self, unwritable_cache_dir: Path, pytester: Pytester
) -> None:
pytester.makeini("[pytest]")
config = pytester.parseconfigure()
cache = config.cache
assert cache is not None
cache.set("test/broken", [])
@pytest.mark.filterwarnings("default")
def test_cache_failure_warns(
self,
pytester: Pytester,
monkeypatch: MonkeyPatch,
unwritable_cache_dir: Path,
) -> None:
monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1")
pytester.makepyfile("def test_error(): raise Exception")
result = pytester.runpytest()
assert result.ret == 1
# warnings from nodeids, lastfailed, and stepwise
result.stdout.fnmatch_lines(
[
# Validate location/stacklevel of warning from cacheprovider.
"*= warnings summary =*",
"*/cacheprovider.py:*",
" */cacheprovider.py:*: PytestCacheWarning: could not create cache path "
f"{unwritable_cache_dir}/v/cache/nodeids: *",
' config.cache.set("cache/nodeids", sorted(self.cached_nodeids))',
"*1 failed, 3 warnings in*",
]
)
def test_config_cache(self, pytester: Pytester) -> None:
pytester.makeconftest(
"""
def pytest_configure(config):
# see that we get cache information early on
assert hasattr(config, "cache")
"""
)
pytester.makepyfile(
"""
def test_session(pytestconfig):
assert hasattr(pytestconfig, "cache")
"""
)
result = pytester.runpytest()
assert result.ret == 0
result.stdout.fnmatch_lines(["*1 passed*"])
def test_cachefuncarg(self, pytester: Pytester) -> None:
pytester.makepyfile(
"""
import pytest
def test_cachefuncarg(cache):
val = cache.get("some/thing", None)
assert val is None
cache.set("some/thing", [1])
pytest.raises(TypeError, lambda: cache.get("some/thing"))
val = cache.get("some/thing", [])
assert val == [1]
"""
)
result = pytester.runpytest()
assert result.ret == 0
result.stdout.fnmatch_lines(["*1 passed*"])
def test_custom_rel_cache_dir(self, pytester: Pytester) -> None:
rel_cache_dir = os.path.join("custom_cache_dir", "subdir")
pytester.makeini(
"""
[pytest]
cache_dir = {cache_dir}
""".format(
cache_dir=rel_cache_dir
)
)
pytester.makepyfile(test_errored="def test_error():\n assert False")
pytester.runpytest()
assert pytester.path.joinpath(rel_cache_dir).is_dir()
def test_custom_abs_cache_dir(
self, pytester: Pytester, tmp_path_factory: TempPathFactory
) -> None:
tmp = tmp_path_factory.mktemp("tmp")
abs_cache_dir = tmp / "custom_cache_dir"
pytester.makeini(
"""
[pytest]
cache_dir = {cache_dir}
""".format(
cache_dir=abs_cache_dir
)
)
pytester.makepyfile(test_errored="def test_error():\n assert False")
pytester.runpytest()
assert abs_cache_dir.is_dir()
def test_custom_cache_dir_with_env_var(
self, pytester: Pytester, monkeypatch: MonkeyPatch
) -> None:
monkeypatch.setenv("env_var", "custom_cache_dir")
pytester.makeini(
"""
[pytest]
cache_dir = {cache_dir}
""".format(
cache_dir="$env_var"
)
)
pytester.makepyfile(test_errored="def test_error():\n assert False")
pytester.runpytest()
assert pytester.path.joinpath("custom_cache_dir").is_dir()
@pytest.mark.parametrize("env", ((), ("TOX_ENV_DIR", "/tox_env_dir")))
def test_cache_reportheader(env, pytester: Pytester, monkeypatch: MonkeyPatch) -> None:
pytester.makepyfile("""def test_foo(): pass""")
if env:
monkeypatch.setenv(*env)
expected = os.path.join(env[1], ".pytest_cache")
else:
monkeypatch.delenv("TOX_ENV_DIR", raising=False)
expected = ".pytest_cache"
result = pytester.runpytest("-v")
result.stdout.fnmatch_lines(["cachedir: %s" % expected])
def test_cache_reportheader_external_abspath(
pytester: Pytester, tmp_path_factory: TempPathFactory
) -> None:
external_cache = tmp_path_factory.mktemp(
"test_cache_reportheader_external_abspath_abs"
)
pytester.makepyfile("def test_hello(): pass")
pytester.makeini(
"""
[pytest]
cache_dir = {abscache}
""".format(
abscache=external_cache
)
)
result = pytester.runpytest("-v")
result.stdout.fnmatch_lines([f"cachedir: {external_cache}"])
def test_cache_show(pytester: Pytester) -> None:
result = pytester.runpytest("--cache-show")
assert result.ret == 0
result.stdout.fnmatch_lines(["*cache is empty*"])
pytester.makeconftest(
"""
def pytest_configure(config):
config.cache.set("my/name", [1,2,3])
config.cache.set("my/hello", "world")
config.cache.set("other/some", {1:2})
dp = config.cache.mkdir("mydb")
dp.joinpath("hello").touch()
dp.joinpath("world").touch()
"""
)
result = pytester.runpytest()
assert result.ret == 5 # no tests executed
result = pytester.runpytest("--cache-show")
result.stdout.fnmatch_lines(
[
"*cachedir:*",
"*- cache values for '[*]' -*",
"cache/nodeids contains:",
"my/name contains:",
" [1, 2, 3]",
"other/some contains:",
" {*'1': 2}",
"*- cache directories for '[*]' -*",
"*mydb/hello*length 0*",
"*mydb/world*length 0*",
]
)
assert result.ret == 0
result = pytester.runpytest("--cache-show", "*/hello")
result.stdout.fnmatch_lines(
[
"*cachedir:*",
"*- cache values for '[*]/hello' -*",
"my/hello contains:",
" *'world'",
"*- cache directories for '[*]/hello' -*",
"d/mydb/hello*length 0*",
]
)
stdout = result.stdout.str()
assert "other/some" not in stdout
assert "d/mydb/world" not in stdout
assert result.ret == 0
class TestLastFailed:
def test_lastfailed_usecase(
self, pytester: Pytester, monkeypatch: MonkeyPatch
) -> None:
monkeypatch.setattr("sys.dont_write_bytecode", True)
p = pytester.makepyfile(
"""
def test_1(): assert 0
def test_2(): assert 0
def test_3(): assert 1
"""
)
result = pytester.runpytest(str(p))
result.stdout.fnmatch_lines(["*2 failed*"])
p = pytester.makepyfile(
"""
def test_1(): assert 1
def test_2(): assert 1
def test_3(): assert 0
"""
)
result = pytester.runpytest(str(p), "--lf")
result.stdout.fnmatch_lines(
[
"collected 3 items / 1 deselected / 2 selected",
"run-last-failure: rerun previous 2 failures",
"*= 2 passed, 1 deselected in *",
]
)
result = pytester.runpytest(str(p), "--lf")
result.stdout.fnmatch_lines(
[
"collected 3 items",
"run-last-failure: no previously failed tests, not deselecting items.",
"*1 failed*2 passed*",
]
)
pytester.path.joinpath(".pytest_cache", ".git").mkdir(parents=True)
result = pytester.runpytest(str(p), "--lf", "--cache-clear")
result.stdout.fnmatch_lines(["*1 failed*2 passed*"])
assert pytester.path.joinpath(".pytest_cache", "README.md").is_file()
assert pytester.path.joinpath(".pytest_cache", ".git").is_dir()
# Run this again to make sure clear-cache is robust
if os.path.isdir(".pytest_cache"):
shutil.rmtree(".pytest_cache")
result = pytester.runpytest("--lf", "--cache-clear")
result.stdout.fnmatch_lines(["*1 failed*2 passed*"])
def test_failedfirst_order(self, pytester: Pytester) -> None:
pytester.makepyfile(
test_a="def test_always_passes(): pass",
test_b="def test_always_fails(): assert 0",
)
result = pytester.runpytest()
# Test order will be collection order; alphabetical
result.stdout.fnmatch_lines(["test_a.py*", "test_b.py*"])
result = pytester.runpytest("--ff")
# Test order will be failing tests first
result.stdout.fnmatch_lines(
[
"collected 2 items",
"run-last-failure: rerun previous 1 failure first",
"test_b.py*",
"test_a.py*",
]
)
def test_lastfailed_failedfirst_order(self, pytester: Pytester) -> None:
pytester.makepyfile(
test_a="def test_always_passes(): assert 1",
test_b="def test_always_fails(): assert 0",
)
result = pytester.runpytest()
# Test order will be collection order; alphabetical
result.stdout.fnmatch_lines(["test_a.py*", "test_b.py*"])
result = pytester.runpytest("--lf", "--ff")
# Test order will be failing tests first
result.stdout.fnmatch_lines(["test_b.py*"])
result.stdout.no_fnmatch_line("*test_a.py*")
def test_lastfailed_difference_invocations(
self, pytester: Pytester, monkeypatch: MonkeyPatch
) -> None:
monkeypatch.setattr("sys.dont_write_bytecode", True)
pytester.makepyfile(
test_a="""
def test_a1(): assert 0
def test_a2(): assert 1
""",
test_b="def test_b1(): assert 0",
)
p = pytester.path.joinpath("test_a.py")
p2 = pytester.path.joinpath("test_b.py")
result = pytester.runpytest()
result.stdout.fnmatch_lines(["*2 failed*"])
result = pytester.runpytest("--lf", p2)
result.stdout.fnmatch_lines(["*1 failed*"])
pytester.makepyfile(test_b="def test_b1(): assert 1")
result = pytester.runpytest("--lf", p2)
result.stdout.fnmatch_lines(["*1 passed*"])
result = pytester.runpytest("--lf", p)
result.stdout.fnmatch_lines(
[
"collected 2 items / 1 deselected / 1 selected",
"run-last-failure: rerun previous 1 failure",
"*= 1 failed, 1 deselected in *",
]
)
def test_lastfailed_usecase_splice(
self, pytester: Pytester, monkeypatch: MonkeyPatch
) -> None:
monkeypatch.setattr("sys.dont_write_bytecode", True)
pytester.makepyfile(
"def test_1(): assert 0", test_something="def test_2(): assert 0"
)
p2 = pytester.path.joinpath("test_something.py")
result = pytester.runpytest()
result.stdout.fnmatch_lines(["*2 failed*"])
result = pytester.runpytest("--lf", p2)
result.stdout.fnmatch_lines(["*1 failed*"])
result = pytester.runpytest("--lf")
result.stdout.fnmatch_lines(["*2 failed*"])
def test_lastfailed_xpass(self, pytester: Pytester) -> None:
pytester.inline_runsource(
"""
import pytest
@pytest.mark.xfail
def test_hello():
assert 1
"""
)
config = pytester.parseconfigure()
assert config.cache is not None
lastfailed = config.cache.get("cache/lastfailed", -1)
assert lastfailed == -1
def test_non_serializable_parametrize(self, pytester: Pytester) -> None:
"""Test that failed parametrized tests with unmarshable parameters
don't break pytest-cache.
"""
pytester.makepyfile(
r"""
import pytest
@pytest.mark.parametrize('val', [
b'\xac\x10\x02G',
])
def test_fail(val):
assert False
"""
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(["*1 failed in*"])
@pytest.mark.parametrize("parent", ("session", "package"))
def test_terminal_report_lastfailed(self, pytester: Pytester, parent: str) -> None:
if parent == "package":
pytester.makepyfile(
__init__="",
)
test_a = pytester.makepyfile(
test_a="""
def test_a1(): pass
def test_a2(): pass
"""
)
test_b = pytester.makepyfile(
test_b="""
def test_b1(): assert 0
def test_b2(): assert 0
"""
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(["collected 4 items", "*2 failed, 2 passed in*"])
result = pytester.runpytest("--lf")
result.stdout.fnmatch_lines(
[
"collected 2 items",
"run-last-failure: rerun previous 2 failures (skipped 1 file)",
"*2 failed in*",
]
)
result = pytester.runpytest(test_a, "--lf")
result.stdout.fnmatch_lines(
[
"collected 2 items",
"run-last-failure: 2 known failures not in selected tests",
"*2 passed in*",
]
)
result = pytester.runpytest(test_b, "--lf")
result.stdout.fnmatch_lines(
[
"collected 2 items",
"run-last-failure: rerun previous 2 failures",
"*2 failed in*",
]
)
result = pytester.runpytest("test_b.py::test_b1", "--lf")
result.stdout.fnmatch_lines(
[
"collected 1 item",
"run-last-failure: rerun previous 1 failure",
"*1 failed in*",
]
)
def test_terminal_report_failedfirst(self, pytester: Pytester) -> None:
pytester.makepyfile(
test_a="""
def test_a1(): assert 0
def test_a2(): pass
"""
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(["collected 2 items", "*1 failed, 1 passed in*"])
result = pytester.runpytest("--ff")
result.stdout.fnmatch_lines(
[
"collected 2 items",
"run-last-failure: rerun previous 1 failure first",
"*1 failed, 1 passed in*",
]
)
def test_lastfailed_collectfailure(
self, pytester: Pytester, monkeypatch: MonkeyPatch
) -> None:
pytester.makepyfile(
test_maybe="""
import os
env = os.environ
if '1' == env['FAILIMPORT']:
raise ImportError('fail')
def test_hello():
assert '0' == env['FAILTEST']
"""
)
def rlf(fail_import, fail_run):
monkeypatch.setenv("FAILIMPORT", str(fail_import))
monkeypatch.setenv("FAILTEST", str(fail_run))
pytester.runpytest("-q")
config = pytester.parseconfigure()
assert config.cache is not None
lastfailed = config.cache.get("cache/lastfailed", -1)
return lastfailed
lastfailed = rlf(fail_import=0, fail_run=0)
assert lastfailed == -1
lastfailed = rlf(fail_import=1, fail_run=0)
assert list(lastfailed) == ["test_maybe.py"]
lastfailed = rlf(fail_import=0, fail_run=1)
assert list(lastfailed) == ["test_maybe.py::test_hello"]
def test_lastfailed_failure_subset(
self, pytester: Pytester, monkeypatch: MonkeyPatch
) -> None:
pytester.makepyfile(
test_maybe="""
import os
env = os.environ
if '1' == env['FAILIMPORT']:
raise ImportError('fail')
def test_hello():
assert '0' == env['FAILTEST']
"""
)
pytester.makepyfile(
test_maybe2="""
import os
env = os.environ
if '1' == env['FAILIMPORT']:
raise ImportError('fail')
def test_hello():
assert '0' == env['FAILTEST']
def test_pass():
pass
"""
)
def rlf(fail_import, fail_run, args=()):
monkeypatch.setenv("FAILIMPORT", str(fail_import))
monkeypatch.setenv("FAILTEST", str(fail_run))
result = pytester.runpytest("-q", "--lf", *args)
config = pytester.parseconfigure()
assert config.cache is not None
lastfailed = config.cache.get("cache/lastfailed", -1)
return result, lastfailed
result, lastfailed = rlf(fail_import=0, fail_run=0)
assert lastfailed == -1
result.stdout.fnmatch_lines(["*3 passed*"])
result, lastfailed = rlf(fail_import=1, fail_run=0)
assert sorted(list(lastfailed)) == ["test_maybe.py", "test_maybe2.py"]
result, lastfailed = rlf(fail_import=0, fail_run=0, args=("test_maybe2.py",))
assert list(lastfailed) == ["test_maybe.py"]
# edge case of test selection - even if we remember failures
# from other tests we still need to run all tests if no test
# matches the failures
result, lastfailed = rlf(fail_import=0, fail_run=0, args=("test_maybe2.py",))
assert list(lastfailed) == ["test_maybe.py"]
result.stdout.fnmatch_lines(["*2 passed*"])
def test_lastfailed_creates_cache_when_needed(self, pytester: Pytester) -> None:
# Issue #1342
pytester.makepyfile(test_empty="")
pytester.runpytest("-q", "--lf")
assert not os.path.exists(".pytest_cache/v/cache/lastfailed")
pytester.makepyfile(test_successful="def test_success():\n assert True")
pytester.runpytest("-q", "--lf")
assert not os.path.exists(".pytest_cache/v/cache/lastfailed")
pytester.makepyfile(test_errored="def test_error():\n assert False")
pytester.runpytest("-q", "--lf")
assert os.path.exists(".pytest_cache/v/cache/lastfailed")
def test_xfail_not_considered_failure(self, pytester: Pytester) -> None:
pytester.makepyfile(
"""
import pytest
@pytest.mark.xfail
def test(): assert 0
"""
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(["*1 xfailed*"])
assert self.get_cached_last_failed(pytester) == []
def test_xfail_strict_considered_failure(self, pytester: Pytester) -> None:
pytester.makepyfile(
"""
import pytest
@pytest.mark.xfail(strict=True)
def test(): pass
"""
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(["*1 failed*"])
assert self.get_cached_last_failed(pytester) == [
"test_xfail_strict_considered_failure.py::test"
]
@pytest.mark.parametrize("mark", ["mark.xfail", "mark.skip"])
def test_failed_changed_to_xfail_or_skip(
self, pytester: Pytester, mark: str
) -> None:
pytester.makepyfile(
"""
import pytest
def test(): assert 0
"""
)
result = pytester.runpytest()
assert self.get_cached_last_failed(pytester) == [
"test_failed_changed_to_xfail_or_skip.py::test"
]
assert result.ret == 1
pytester.makepyfile(
"""
import pytest
@pytest.{mark}
def test(): assert 0
""".format(
mark=mark
)
)
result = pytester.runpytest()
assert result.ret == 0
assert self.get_cached_last_failed(pytester) == []
assert result.ret == 0
@pytest.mark.parametrize("quiet", [True, False])
@pytest.mark.parametrize("opt", ["--ff", "--lf"])
def test_lf_and_ff_prints_no_needless_message(
self, quiet: bool, opt: str, pytester: Pytester
) -> None:
# Issue 3853
pytester.makepyfile("def test(): assert 0")
args = [opt]
if quiet:
args.append("-q")
result = pytester.runpytest(*args)
result.stdout.no_fnmatch_line("*run all*")
result = pytester.runpytest(*args)
if quiet:
result.stdout.no_fnmatch_line("*run all*")
else:
assert "rerun previous" in result.stdout.str()
def get_cached_last_failed(self, pytester: Pytester) -> List[str]:
config = pytester.parseconfigure()
assert config.cache is not None
return sorted(config.cache.get("cache/lastfailed", {}))
def test_cache_cumulative(self, pytester: Pytester) -> None:
"""Test workflow where user fixes errors gradually file by file using --lf."""
# 1. initial run
test_bar = pytester.makepyfile(
test_bar="""
def test_bar_1(): pass
def test_bar_2(): assert 0
"""
)
test_foo = pytester.makepyfile(
test_foo="""
def test_foo_3(): pass
def test_foo_4(): assert 0
"""
)
pytester.runpytest()
assert self.get_cached_last_failed(pytester) == [
"test_bar.py::test_bar_2",
"test_foo.py::test_foo_4",
]
# 2. fix test_bar_2, run only test_bar.py
pytester.makepyfile(
test_bar="""
def test_bar_1(): pass
def test_bar_2(): pass
"""
)
result = pytester.runpytest(test_bar)
result.stdout.fnmatch_lines(["*2 passed*"])
# ensure cache does not forget that test_foo_4 failed once before
assert self.get_cached_last_failed(pytester) == ["test_foo.py::test_foo_4"]
result = pytester.runpytest("--last-failed")
result.stdout.fnmatch_lines(
[
"collected 1 item",
"run-last-failure: rerun previous 1 failure (skipped 1 file)",
"*= 1 failed in *",
]
)
assert self.get_cached_last_failed(pytester) == ["test_foo.py::test_foo_4"]
# 3. fix test_foo_4, run only test_foo.py
test_foo = pytester.makepyfile(
test_foo="""
def test_foo_3(): pass
def test_foo_4(): pass
"""
)
result = pytester.runpytest(test_foo, "--last-failed")
result.stdout.fnmatch_lines(
[
"collected 2 items / 1 deselected / 1 selected",
"run-last-failure: rerun previous 1 failure",
"*= 1 passed, 1 deselected in *",
]
)
assert self.get_cached_last_failed(pytester) == []
result = pytester.runpytest("--last-failed")
result.stdout.fnmatch_lines(["*4 passed*"])
assert self.get_cached_last_failed(pytester) == []
def test_lastfailed_no_failures_behavior_all_passed(
self, pytester: Pytester
) -> None:
pytester.makepyfile(
"""
def test_1(): pass
def test_2(): pass
"""
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(["*2 passed*"])
result = pytester.runpytest("--lf")
result.stdout.fnmatch_lines(["*2 passed*"])
result = pytester.runpytest("--lf", "--lfnf", "all")
result.stdout.fnmatch_lines(["*2 passed*"])
# Ensure the list passed to pytest_deselected is a copy,
# and not a reference which is cleared right after.
pytester.makeconftest(
"""
deselected = []
def pytest_deselected(items):
global deselected
deselected = items
def pytest_sessionfinish():
print("\\ndeselected={}".format(len(deselected)))
"""
)
result = pytester.runpytest("--lf", "--lfnf", "none")
result.stdout.fnmatch_lines(
[
"collected 2 items / 2 deselected / 0 selected",
"run-last-failure: no previously failed tests, deselecting all items.",
"deselected=2",
"* 2 deselected in *",
]
)
assert result.ret == ExitCode.NO_TESTS_COLLECTED
def test_lastfailed_no_failures_behavior_empty_cache(
self, pytester: Pytester
) -> None:
pytester.makepyfile(
"""
def test_1(): pass
def test_2(): assert 0
"""
)
result = pytester.runpytest("--lf", "--cache-clear")
result.stdout.fnmatch_lines(["*1 failed*1 passed*"])
result = pytester.runpytest("--lf", "--cache-clear", "--lfnf", "all")
result.stdout.fnmatch_lines(["*1 failed*1 passed*"])
result = pytester.runpytest("--lf", "--cache-clear", "--lfnf", "none")
result.stdout.fnmatch_lines(["*2 desel*"])
def test_lastfailed_skip_collection(self, pytester: Pytester) -> None:
"""
Test --lf behavior regarding skipping collection of files that are not marked as
failed in the cache (#5172).
"""
pytester.makepyfile(
**{
"pkg1/test_1.py": """
import pytest
@pytest.mark.parametrize('i', range(3))
def test_1(i): pass
""",
"pkg2/test_2.py": """
import pytest
@pytest.mark.parametrize('i', range(5))
def test_1(i):
assert i not in (1, 3)
""",
}
)
# first run: collects 8 items (test_1: 3, test_2: 5)
result = pytester.runpytest()
result.stdout.fnmatch_lines(["collected 8 items", "*2 failed*6 passed*"])
# second run: collects only 5 items from test_2, because all tests from test_1 have passed
result = pytester.runpytest("--lf")
result.stdout.fnmatch_lines(
[
"collected 2 items",
"run-last-failure: rerun previous 2 failures (skipped 1 file)",
"*= 2 failed in *",
]
)
# add another file and check if message is correct when skipping more than 1 file
pytester.makepyfile(
**{
"pkg1/test_3.py": """
def test_3(): pass
"""
}
)
result = pytester.runpytest("--lf")
result.stdout.fnmatch_lines(
[
"collected 2 items",
"run-last-failure: rerun previous 2 failures (skipped 2 files)",
"*= 2 failed in *",
]
)
def test_lastfailed_skip_collection_with_nesting(self, pytester: Pytester) -> None:
"""Check that file skipping works even when the file with failures is
nested at a different level of the collection tree."""
pytester.makepyfile(
**{
"test_1.py": """
def test_1(): pass
""",
"pkg/__init__.py": "",
"pkg/test_2.py": """
def test_2(): assert False
""",
}
)
# first run
result = pytester.runpytest()
result.stdout.fnmatch_lines(["collected 2 items", "*1 failed*1 passed*"])
# second run - test_1.py is skipped.
result = pytester.runpytest("--lf")
result.stdout.fnmatch_lines(
[
"collected 1 item",
"run-last-failure: rerun previous 1 failure (skipped 1 file)",
"*= 1 failed in *",
]
)
def test_lastfailed_with_known_failures_not_being_selected(
self, pytester: Pytester
) -> None:
pytester.makepyfile(
**{
"pkg1/test_1.py": """def test_1(): assert 0""",
"pkg1/test_2.py": """def test_2(): pass""",
}
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(["collected 2 items", "* 1 failed, 1 passed in *"])
Path("pkg1/test_1.py").unlink()
result = pytester.runpytest("--lf")
result.stdout.fnmatch_lines(
[
"collected 1 item",
"run-last-failure: 1 known failures not in selected tests",
"* 1 passed in *",
]
)
# Recreate file with known failure.
pytester.makepyfile(**{"pkg1/test_1.py": """def test_1(): assert 0"""})
result = pytester.runpytest("--lf")
result.stdout.fnmatch_lines(
[
"collected 1 item",
"run-last-failure: rerun previous 1 failure (skipped 1 file)",
"* 1 failed in *",
]
)
# Remove/rename test: collects the file again.
pytester.makepyfile(**{"pkg1/test_1.py": """def test_renamed(): assert 0"""})
result = pytester.runpytest("--lf", "-rf")
result.stdout.fnmatch_lines(
[
"collected 2 items",
"run-last-failure: 1 known failures not in selected tests",
"pkg1/test_1.py F *",
"pkg1/test_2.py . *",
"FAILED pkg1/test_1.py::test_renamed - assert 0",
"* 1 failed, 1 passed in *",
]
)
result = pytester.runpytest("--lf", "--co")
result.stdout.fnmatch_lines(
[
"collected 1 item",
"run-last-failure: rerun previous 1 failure (skipped 1 file)",
"",
"<Module pkg1/test_1.py>",
" <Function test_renamed>",
]
)
def test_lastfailed_args_with_deselected(self, pytester: Pytester) -> None:
"""Test regression with --lf running into NoMatch error.
This was caused by it not collecting (non-failed) nodes given as
arguments.
"""
pytester.makepyfile(
**{
"pkg1/test_1.py": """
def test_pass(): pass
def test_fail(): assert 0
""",
}
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(["collected 2 items", "* 1 failed, 1 passed in *"])
assert result.ret == 1
result = pytester.runpytest("pkg1/test_1.py::test_pass", "--lf", "--co")
assert result.ret == 0
result.stdout.fnmatch_lines(
[
"*collected 1 item",
"run-last-failure: 1 known failures not in selected tests",
"",
"<Module pkg1/test_1.py>",
" <Function test_pass>",
],
consecutive=True,
)
result = pytester.runpytest(
"pkg1/test_1.py::test_pass", "pkg1/test_1.py::test_fail", "--lf", "--co"
)
assert result.ret == 0
result.stdout.fnmatch_lines(
[
"collected 2 items / 1 deselected / 1 selected",
"run-last-failure: rerun previous 1 failure",
"",
"<Module pkg1/test_1.py>",
" <Function test_fail>",
"*= 1/2 tests collected (1 deselected) in *",
],
)
def test_lastfailed_with_class_items(self, pytester: Pytester) -> None:
"""Test regression with --lf deselecting whole classes."""
pytester.makepyfile(
**{
"pkg1/test_1.py": """
class TestFoo:
def test_pass(self): pass
def test_fail(self): assert 0
def test_other(): assert 0
""",
}
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(["collected 3 items", "* 2 failed, 1 passed in *"])
assert result.ret == 1
result = pytester.runpytest("--lf", "--co")
assert result.ret == 0
result.stdout.fnmatch_lines(
[
"collected 3 items / 1 deselected / 2 selected",
"run-last-failure: rerun previous 2 failures",
"",
"<Module pkg1/test_1.py>",
" <Class TestFoo>",
" <Function test_fail>",
" <Function test_other>",
"",
"*= 2/3 tests collected (1 deselected) in *",
],
consecutive=True,
)
def test_lastfailed_with_all_filtered(self, pytester: Pytester) -> None:
pytester.makepyfile(
**{
"pkg1/test_1.py": """
def test_fail(): assert 0
def test_pass(): pass
""",
}
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(["collected 2 items", "* 1 failed, 1 passed in *"])
assert result.ret == 1
# Remove known failure.
pytester.makepyfile(
**{
"pkg1/test_1.py": """
def test_pass(): pass
""",
}
)
result = pytester.runpytest("--lf", "--co")
result.stdout.fnmatch_lines(
[
"collected 1 item",
"run-last-failure: 1 known failures not in selected tests",
"",
"<Module pkg1/test_1.py>",
" <Function test_pass>",
"",
"*= 1 test collected in*",
],
consecutive=True,
)
assert result.ret == 0
def test_packages(self, pytester: Pytester) -> None:
"""Regression test for #7758.
The particular issue here was that Package nodes were included in the
filtering, being themselves Modules for the __init__.py, even if they
had failed Modules in them.
The tests includes a test in an __init__.py file just to make sure the
fix doesn't somehow regress that, it is not critical for the issue.
"""
pytester.makepyfile(
**{
"__init__.py": "",
"a/__init__.py": "def test_a_init(): assert False",
"a/test_one.py": "def test_1(): assert False",
"b/__init__.py": "",
"b/test_two.py": "def test_2(): assert False",
},
)
pytester.makeini(
"""
[pytest]
python_files = *.py
"""
)
result = pytester.runpytest()
result.assert_outcomes(failed=3)
result = pytester.runpytest("--lf")
result.assert_outcomes(failed=3)
def test_non_python_file_skipped(
self,
pytester: Pytester,
dummy_yaml_custom_test: None,
) -> None:
pytester.makepyfile(
**{
"test_bad.py": """def test_bad(): assert False""",
},
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(["collected 2 items", "* 1 failed, 1 passed in *"])
result = pytester.runpytest("--lf")
result.stdout.fnmatch_lines(
[
"collected 1 item",
"run-last-failure: rerun previous 1 failure (skipped 1 file)",
"* 1 failed in *",
]
)
class TestNewFirst:
def test_newfirst_usecase(self, pytester: Pytester) -> None:
pytester.makepyfile(
**{
"test_1/test_1.py": """
def test_1(): assert 1
""",
"test_2/test_2.py": """
def test_1(): assert 1
""",
}
)
p1 = pytester.path.joinpath("test_1/test_1.py")
os.utime(p1, ns=(p1.stat().st_atime_ns, int(1e9)))
result = pytester.runpytest("-v")
result.stdout.fnmatch_lines(
["*test_1/test_1.py::test_1 PASSED*", "*test_2/test_2.py::test_1 PASSED*"]
)
result = pytester.runpytest("-v", "--nf")
result.stdout.fnmatch_lines(
["*test_2/test_2.py::test_1 PASSED*", "*test_1/test_1.py::test_1 PASSED*"]
)
p1.write_text("def test_1(): assert 1\n" "def test_2(): assert 1\n")
os.utime(p1, ns=(p1.stat().st_atime_ns, int(1e9)))
result = pytester.runpytest("--nf", "--collect-only", "-q")
result.stdout.fnmatch_lines(
[
"test_1/test_1.py::test_2",
"test_2/test_2.py::test_1",
"test_1/test_1.py::test_1",
]
)
# Newest first with (plugin) pytest_collection_modifyitems hook.
pytester.makepyfile(
myplugin="""
def pytest_collection_modifyitems(items):
items[:] = sorted(items, key=lambda item: item.nodeid)
print("new_items:", [x.nodeid for x in items])
"""
)
pytester.syspathinsert()
result = pytester.runpytest("--nf", "-p", "myplugin", "--collect-only", "-q")
result.stdout.fnmatch_lines(
[
"new_items: *test_1.py*test_1.py*test_2.py*",
"test_1/test_1.py::test_2",
"test_2/test_2.py::test_1",
"test_1/test_1.py::test_1",
]
)
def test_newfirst_parametrize(self, pytester: Pytester) -> None:
pytester.makepyfile(
**{
"test_1/test_1.py": """
import pytest
@pytest.mark.parametrize('num', [1, 2])
def test_1(num): assert num
""",
"test_2/test_2.py": """
import pytest
@pytest.mark.parametrize('num', [1, 2])
def test_1(num): assert num
""",
}
)
p1 = pytester.path.joinpath("test_1/test_1.py")
os.utime(p1, ns=(p1.stat().st_atime_ns, int(1e9)))
result = pytester.runpytest("-v")
result.stdout.fnmatch_lines(
[
"*test_1/test_1.py::test_1[1*",
"*test_1/test_1.py::test_1[2*",
"*test_2/test_2.py::test_1[1*",
"*test_2/test_2.py::test_1[2*",
]
)
result = pytester.runpytest("-v", "--nf")
result.stdout.fnmatch_lines(
[
"*test_2/test_2.py::test_1[1*",
"*test_2/test_2.py::test_1[2*",
"*test_1/test_1.py::test_1[1*",
"*test_1/test_1.py::test_1[2*",
]
)
p1.write_text(
"import pytest\n"
"@pytest.mark.parametrize('num', [1, 2, 3])\n"
"def test_1(num): assert num\n"
)
os.utime(p1, ns=(p1.stat().st_atime_ns, int(1e9)))
# Running only a subset does not forget about existing ones.
result = pytester.runpytest("-v", "--nf", "test_2/test_2.py")
result.stdout.fnmatch_lines(
["*test_2/test_2.py::test_1[1*", "*test_2/test_2.py::test_1[2*"]
)
result = pytester.runpytest("-v", "--nf")
result.stdout.fnmatch_lines(
[
"*test_1/test_1.py::test_1[3*",
"*test_2/test_2.py::test_1[1*",
"*test_2/test_2.py::test_1[2*",
"*test_1/test_1.py::test_1[1*",
"*test_1/test_1.py::test_1[2*",
]
)
class TestReadme:
def check_readme(self, pytester: Pytester) -> bool:
config = pytester.parseconfigure()
assert config.cache is not None
readme = config.cache._cachedir.joinpath("README.md")
return readme.is_file()
def test_readme_passed(self, pytester: Pytester) -> None:
pytester.makepyfile("def test_always_passes(): pass")
pytester.runpytest()
assert self.check_readme(pytester) is True
def test_readme_failed(self, pytester: Pytester) -> None:
pytester.makepyfile("def test_always_fails(): assert 0")
pytester.runpytest()
assert self.check_readme(pytester) is True
def test_gitignore(pytester: Pytester) -> None:
"""Ensure we automatically create .gitignore file in the pytest_cache directory (#3286)."""
from _pytest.cacheprovider import Cache
config = pytester.parseconfig()
cache = Cache.for_config(config, _ispytest=True)
cache.set("foo", "bar")
msg = "# Created by pytest automatically.\n*\n"
gitignore_path = cache._cachedir.joinpath(".gitignore")
assert gitignore_path.read_text(encoding="UTF-8") == msg
# Does not overwrite existing/custom one.
gitignore_path.write_text("custom")
cache.set("something", "else")
assert gitignore_path.read_text(encoding="UTF-8") == "custom"
def test_preserve_keys_order(pytester: Pytester) -> None:
"""Ensure keys order is preserved when saving dicts (#9205)."""
from _pytest.cacheprovider import Cache
config = pytester.parseconfig()
cache = Cache.for_config(config, _ispytest=True)
cache.set("foo", {"z": 1, "b": 2, "a": 3, "d": 10})
read_back = cache.get("foo", None)
assert list(read_back.items()) == [("z", 1), ("b", 2), ("a", 3), ("d", 10)]
def test_does_not_create_boilerplate_in_existing_dirs(pytester: Pytester) -> None:
from _pytest.cacheprovider import Cache
pytester.makeini(
"""
[pytest]
cache_dir = .
"""
)
config = pytester.parseconfig()
cache = Cache.for_config(config, _ispytest=True)
cache.set("foo", "bar")
assert os.path.isdir("v") # cache contents
assert not os.path.exists(".gitignore")
assert not os.path.exists("README.md")
def test_cachedir_tag(pytester: Pytester) -> None:
"""Ensure we automatically create CACHEDIR.TAG file in the pytest_cache directory (#4278)."""
from _pytest.cacheprovider import Cache
from _pytest.cacheprovider import CACHEDIR_TAG_CONTENT
config = pytester.parseconfig()
cache = Cache.for_config(config, _ispytest=True)
cache.set("foo", "bar")
cachedir_tag_path = cache._cachedir.joinpath("CACHEDIR.TAG")
assert cachedir_tag_path.read_bytes() == CACHEDIR_TAG_CONTENT
def test_clioption_with_cacheshow_and_help(pytester: Pytester) -> None:
result = pytester.runpytest("--cache-show", "--help")
assert result.ret == 0