openCC/testing/acceptance_test.py

1480 lines
49 KiB
Python

# mypy: allow-untyped-defs
import dataclasses
import importlib.metadata
import os
from pathlib import Path
import subprocess
import sys
import types
from _pytest.config import ExitCode
from _pytest.pathlib import symlink_or_skip
from _pytest.pytester import Pytester
import pytest
def prepend_pythonpath(*dirs) -> str:
cur = os.getenv("PYTHONPATH")
if cur:
dirs += (cur,)
return os.pathsep.join(str(p) for p in dirs)
class TestGeneralUsage:
def test_config_error(self, pytester: Pytester) -> None:
pytester.copy_example("conftest_usageerror/conftest.py")
result = pytester.runpytest(pytester.path)
assert result.ret == ExitCode.USAGE_ERROR
result.stderr.fnmatch_lines(["*ERROR: hello"])
result.stdout.fnmatch_lines(["*pytest_unconfigure_called"])
def test_root_conftest_syntax_error(self, pytester: Pytester) -> None:
pytester.makepyfile(conftest="raise SyntaxError\n")
result = pytester.runpytest()
result.stderr.fnmatch_lines(["*raise SyntaxError*"])
assert result.ret != 0
def test_early_hook_error_issue38_1(self, pytester: Pytester) -> None:
pytester.makeconftest(
"""
def pytest_sessionstart():
0 / 0
"""
)
result = pytester.runpytest(pytester.path)
assert result.ret != 0
# tracestyle is native by default for hook failures
result.stdout.fnmatch_lines(
["*INTERNALERROR*File*conftest.py*line 2*", "*0 / 0*"]
)
result = pytester.runpytest(pytester.path, "--fulltrace")
assert result.ret != 0
# tracestyle is native by default for hook failures
result.stdout.fnmatch_lines(
["*INTERNALERROR*def pytest_sessionstart():*", "*INTERNALERROR*0 / 0*"]
)
def test_early_hook_configure_error_issue38(self, pytester: Pytester) -> None:
pytester.makeconftest(
"""
def pytest_configure():
0 / 0
"""
)
result = pytester.runpytest(pytester.path)
assert result.ret != 0
# here we get it on stderr
result.stderr.fnmatch_lines(
["*INTERNALERROR*File*conftest.py*line 2*", "*0 / 0*"]
)
def test_file_not_found(self, pytester: Pytester) -> None:
result = pytester.runpytest("asd")
assert result.ret != 0
result.stderr.fnmatch_lines(["ERROR: file or directory not found: asd"])
def test_file_not_found_unconfigure_issue143(self, pytester: Pytester) -> None:
pytester.makeconftest(
"""
def pytest_configure():
print("---configure")
def pytest_unconfigure():
print("---unconfigure")
"""
)
result = pytester.runpytest("-s", "asd")
assert result.ret == ExitCode.USAGE_ERROR
result.stderr.fnmatch_lines(["ERROR: file or directory not found: asd"])
result.stdout.fnmatch_lines(["*---configure", "*---unconfigure"])
def test_config_preparse_plugin_option(self, pytester: Pytester) -> None:
pytester.makepyfile(
pytest_xyz="""
def pytest_addoption(parser):
parser.addoption("--xyz", dest="xyz", action="store")
"""
)
pytester.makepyfile(
test_one="""
def test_option(pytestconfig):
assert pytestconfig.option.xyz == "123"
"""
)
result = pytester.runpytest("-p", "pytest_xyz", "--xyz=123", syspathinsert=True)
assert result.ret == 0
result.stdout.fnmatch_lines(["*1 passed*"])
@pytest.mark.parametrize("load_cov_early", [True, False])
def test_early_load_setuptools_name(
self, pytester: Pytester, monkeypatch, load_cov_early
) -> None:
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD")
pytester.makepyfile(mytestplugin1_module="")
pytester.makepyfile(mytestplugin2_module="")
pytester.makepyfile(mycov_module="")
pytester.syspathinsert()
loaded = []
@dataclasses.dataclass
class DummyEntryPoint:
name: str
module: str
group: str = "pytest11"
def load(self):
__import__(self.module)
loaded.append(self.name)
return sys.modules[self.module]
entry_points = [
DummyEntryPoint("myplugin1", "mytestplugin1_module"),
DummyEntryPoint("myplugin2", "mytestplugin2_module"),
DummyEntryPoint("mycov", "mycov_module"),
]
@dataclasses.dataclass
class DummyDist:
entry_points: object
files: object = ()
def my_dists():
return (DummyDist(entry_points),)
monkeypatch.setattr(importlib.metadata, "distributions", my_dists)
params = ("-p", "mycov") if load_cov_early else ()
pytester.runpytest_inprocess(*params)
if load_cov_early:
assert loaded == ["mycov", "myplugin1", "myplugin2"]
else:
assert loaded == ["myplugin1", "myplugin2", "mycov"]
@pytest.mark.parametrize("import_mode", ["prepend", "append", "importlib"])
def test_assertion_rewrite(self, pytester: Pytester, import_mode) -> None:
p = pytester.makepyfile(
"""
def test_this():
x = 0
assert x
"""
)
result = pytester.runpytest(p, f"--import-mode={import_mode}")
result.stdout.fnmatch_lines(["> assert x", "E assert 0"])
assert result.ret == 1
def test_nested_import_error(self, pytester: Pytester) -> None:
p = pytester.makepyfile(
"""
import import_fails
def test_this():
assert import_fails.a == 1
"""
)
pytester.makepyfile(import_fails="import does_not_work")
result = pytester.runpytest(p)
result.stdout.fnmatch_lines(
[
"ImportError while importing test module*",
"*No module named *does_not_work*",
]
)
assert result.ret == 2
def test_not_collectable_arguments(self, pytester: Pytester) -> None:
p1 = pytester.makepyfile("")
p2 = pytester.makefile(".pyc", "123")
result = pytester.runpytest(p1, p2)
assert result.ret == ExitCode.USAGE_ERROR
result.stderr.fnmatch_lines(
[
f"ERROR: not found: {p2}",
"(no match in any of *)",
"",
]
)
@pytest.mark.filterwarnings("default")
def test_better_reporting_on_conftest_load_failure(
self, pytester: Pytester
) -> None:
"""Show a user-friendly traceback on conftest import failures (#486, #3332)"""
pytester.makepyfile("")
conftest = pytester.makeconftest(
"""
def foo():
import qwerty
foo()
"""
)
result = pytester.runpytest("--help")
result.stdout.fnmatch_lines(
"""
*--version*
*warning*conftest.py*
"""
)
result = pytester.runpytest()
assert result.stdout.lines == []
assert result.stderr.lines == [
f"ImportError while loading conftest '{conftest}'.",
"conftest.py:3: in <module>",
" foo()",
"conftest.py:2: in foo",
" import qwerty",
"E ModuleNotFoundError: No module named 'qwerty'",
]
def test_early_skip(self, pytester: Pytester) -> None:
pytester.mkdir("xyz")
pytester.makeconftest(
"""
import pytest
def pytest_collect_file():
pytest.skip("early")
"""
)
result = pytester.runpytest()
assert result.ret == ExitCode.NO_TESTS_COLLECTED
result.stdout.fnmatch_lines(["*1 skip*"])
def test_issue88_initial_file_multinodes(self, pytester: Pytester) -> None:
pytester.copy_example("issue88_initial_file_multinodes")
p = pytester.makepyfile("def test_hello(): pass")
result = pytester.runpytest(p, "--collect-only")
result.stdout.fnmatch_lines(["*MyFile*test_issue88*", "*Module*test_issue88*"])
def test_issue93_initialnode_importing_capturing(self, pytester: Pytester) -> None:
pytester.makeconftest(
"""
import sys
print("should not be seen")
sys.stderr.write("stder42\\n")
"""
)
result = pytester.runpytest()
assert result.ret == ExitCode.NO_TESTS_COLLECTED
result.stdout.no_fnmatch_line("*should not be seen*")
assert "stderr42" not in result.stderr.str()
def test_conftest_printing_shows_if_error(self, pytester: Pytester) -> None:
pytester.makeconftest(
"""
print("should be seen")
assert 0
"""
)
result = pytester.runpytest()
assert result.ret != 0
assert "should be seen" in result.stdout.str()
def test_issue109_sibling_conftests_not_loaded(self, pytester: Pytester) -> None:
sub1 = pytester.mkdir("sub1")
sub2 = pytester.mkdir("sub2")
sub1.joinpath("conftest.py").write_text("assert 0", encoding="utf-8")
result = pytester.runpytest(sub2)
assert result.ret == ExitCode.NO_TESTS_COLLECTED
sub2.joinpath("__init__.py").touch()
p = sub2.joinpath("test_hello.py")
p.touch()
result = pytester.runpytest(p)
assert result.ret == ExitCode.NO_TESTS_COLLECTED
result = pytester.runpytest(sub1)
assert result.ret == ExitCode.USAGE_ERROR
def test_directory_skipped(self, pytester: Pytester) -> None:
pytester.makeconftest(
"""
import pytest
def pytest_ignore_collect():
pytest.skip("intentional")
"""
)
pytester.makepyfile("def test_hello(): pass")
result = pytester.runpytest()
assert result.ret == ExitCode.NO_TESTS_COLLECTED
result.stdout.fnmatch_lines(["*1 skipped*"])
def test_multiple_items_per_collector_byid(self, pytester: Pytester) -> None:
c = pytester.makeconftest(
"""
import pytest
class MyItem(pytest.Item):
def runtest(self):
pass
class MyCollector(pytest.File):
def collect(self):
return [MyItem.from_parent(name="xyz", parent=self)]
def pytest_collect_file(file_path, parent):
if file_path.name.startswith("conftest"):
return MyCollector.from_parent(path=file_path, parent=parent)
"""
)
result = pytester.runpytest(c.name + "::" + "xyz")
assert result.ret == 0
result.stdout.fnmatch_lines(["*1 pass*"])
def test_skip_on_generated_funcarg_id(self, pytester: Pytester) -> None:
pytester.makeconftest(
"""
import pytest
def pytest_generate_tests(metafunc):
metafunc.parametrize('x', [3], ids=['hello-123'])
def pytest_runtest_setup(item):
print(item.keywords)
if 'hello-123' in item.keywords:
pytest.skip("hello")
assert 0
"""
)
p = pytester.makepyfile("""def test_func(x): pass""")
res = pytester.runpytest(p)
assert res.ret == 0
res.stdout.fnmatch_lines(["*1 skipped*"])
def test_direct_addressing_selects(self, pytester: Pytester) -> None:
p = pytester.makepyfile(
"""
def pytest_generate_tests(metafunc):
metafunc.parametrize('i', [1, 2], ids=["1", "2"])
def test_func(i):
pass
"""
)
res = pytester.runpytest(p.name + "::" + "test_func[1]")
assert res.ret == 0
res.stdout.fnmatch_lines(["*1 passed*"])
def test_direct_addressing_selects_duplicates(self, pytester: Pytester) -> None:
p = pytester.makepyfile(
"""
import pytest
@pytest.mark.parametrize("a", [1, 2, 10, 11, 2, 1, 12, 11])
def test_func(a):
pass
"""
)
result = pytester.runpytest(p)
result.assert_outcomes(failed=0, passed=8)
def test_direct_addressing_selects_duplicates_1(self, pytester: Pytester) -> None:
p = pytester.makepyfile(
"""
import pytest
@pytest.mark.parametrize("a", [1, 2, 10, 11, 2, 1, 12, 1_1,2_1])
def test_func(a):
pass
"""
)
result = pytester.runpytest(p)
result.assert_outcomes(failed=0, passed=9)
def test_direct_addressing_selects_duplicates_2(self, pytester: Pytester) -> None:
p = pytester.makepyfile(
"""
import pytest
@pytest.mark.parametrize("a", ["a","b","c","a","a1"])
def test_func(a):
pass
"""
)
result = pytester.runpytest(p)
result.assert_outcomes(failed=0, passed=5)
def test_direct_addressing_notfound(self, pytester: Pytester) -> None:
p = pytester.makepyfile(
"""
def test_func():
pass
"""
)
res = pytester.runpytest(p.name + "::" + "test_notfound")
assert res.ret
res.stderr.fnmatch_lines(["*ERROR*not found*"])
def test_docstring_on_hookspec(self) -> None:
from _pytest import hookspec
for name, value in vars(hookspec).items():
if name.startswith("pytest_"):
assert value.__doc__, "no docstring for %s" % name
def test_initialization_error_issue49(self, pytester: Pytester) -> None:
pytester.makeconftest(
"""
def pytest_configure():
x
"""
)
result = pytester.runpytest()
assert result.ret == 3 # internal error
result.stderr.fnmatch_lines(["INTERNAL*pytest_configure*", "INTERNAL*x*"])
assert "sessionstarttime" not in result.stderr.str()
@pytest.mark.parametrize("lookfor", ["test_fun.py::test_a"])
def test_issue134_report_error_when_collecting_member(
self, pytester: Pytester, lookfor
) -> None:
pytester.makepyfile(
test_fun="""
def test_a():
pass
def"""
)
result = pytester.runpytest(lookfor)
result.stdout.fnmatch_lines(["*SyntaxError*"])
if "::" in lookfor:
result.stderr.fnmatch_lines(["*ERROR*"])
assert result.ret == 4 # usage error only if item not found
def test_report_all_failed_collections_initargs(self, pytester: Pytester) -> None:
pytester.makeconftest(
"""
from _pytest.config import ExitCode
def pytest_sessionfinish(exitstatus):
assert exitstatus == ExitCode.USAGE_ERROR
print("pytest_sessionfinish_called")
"""
)
pytester.makepyfile(test_a="def", test_b="def")
result = pytester.runpytest("test_a.py::a", "test_b.py::b")
result.stderr.fnmatch_lines(["*ERROR*test_a.py::a*", "*ERROR*test_b.py::b*"])
result.stdout.fnmatch_lines(["pytest_sessionfinish_called"])
assert result.ret == ExitCode.USAGE_ERROR
def test_namespace_import_doesnt_confuse_import_hook(
self, pytester: Pytester
) -> None:
"""Ref #383.
Python 3.3's namespace package messed with our import hooks.
Importing a module that didn't exist, even if the ImportError was
gracefully handled, would make our test crash.
"""
pytester.mkdir("not_a_package")
p = pytester.makepyfile(
"""
try:
from not_a_package import doesnt_exist
except ImportError:
# We handle the import error gracefully here
pass
def test_whatever():
pass
"""
)
res = pytester.runpytest(p.name)
assert res.ret == 0
def test_unknown_option(self, pytester: Pytester) -> None:
result = pytester.runpytest("--qwlkej")
result.stderr.fnmatch_lines(
"""
*unrecognized*
"""
)
def test_getsourcelines_error_issue553(
self, pytester: Pytester, monkeypatch
) -> None:
monkeypatch.setattr("inspect.getsourcelines", None)
p = pytester.makepyfile(
"""
def raise_error(obj):
raise OSError('source code not available')
import inspect
inspect.getsourcelines = raise_error
def test_foo(invalid_fixture):
pass
"""
)
res = pytester.runpytest(p)
res.stdout.fnmatch_lines(
["*source code not available*", "E*fixture 'invalid_fixture' not found"]
)
def test_plugins_given_as_strings(
self, pytester: Pytester, monkeypatch, _sys_snapshot
) -> None:
"""Test that str values passed to main() as `plugins` arg are
interpreted as module names to be imported and registered (#855)."""
with pytest.raises(ImportError) as excinfo:
pytest.main([str(pytester.path)], plugins=["invalid.module"])
assert "invalid" in str(excinfo.value)
p = pytester.path.joinpath("test_test_plugins_given_as_strings.py")
p.write_text("def test_foo(): pass", encoding="utf-8")
mod = types.ModuleType("myplugin")
monkeypatch.setitem(sys.modules, "myplugin", mod)
assert pytest.main(args=[str(pytester.path)], plugins=["myplugin"]) == 0
def test_parametrized_with_bytes_regex(self, pytester: Pytester) -> None:
p = pytester.makepyfile(
"""
import re
import pytest
@pytest.mark.parametrize('r', [re.compile(b'foo')])
def test_stuff(r):
pass
"""
)
res = pytester.runpytest(p)
res.stdout.fnmatch_lines(["*1 passed*"])
def test_parametrized_with_null_bytes(self, pytester: Pytester) -> None:
"""Test parametrization with values that contain null bytes and unicode characters (#2644, #2957)"""
p = pytester.makepyfile(
"""\
import pytest
@pytest.mark.parametrize("data", [b"\\x00", "\\x00", 'ação'])
def test_foo(data):
assert data
"""
)
res = pytester.runpytest(p)
res.assert_outcomes(passed=3)
# Warning ignore because of:
# https://github.com/python/cpython/issues/85308
# Can be removed once Python<3.12 support is dropped.
@pytest.mark.filterwarnings("ignore:'encoding' argument not specified")
def test_command_line_args_from_file(
self, pytester: Pytester, tmp_path: Path
) -> None:
pytester.makepyfile(
test_file="""
import pytest
class TestClass:
@pytest.mark.parametrize("a", ["x","y"])
def test_func(self, a):
pass
"""
)
tests = [
"test_file.py::TestClass::test_func[x]",
"test_file.py::TestClass::test_func[y]",
"-q",
]
args_file = pytester.maketxtfile(tests="\n".join(tests))
result = pytester.runpytest(f"@{args_file}")
result.assert_outcomes(failed=0, passed=2)
class TestInvocationVariants:
def test_earlyinit(self, pytester: Pytester) -> None:
p = pytester.makepyfile(
"""
import pytest
assert hasattr(pytest, 'mark')
"""
)
result = pytester.runpython(p)
assert result.ret == 0
def test_pydoc(self, pytester: Pytester) -> None:
result = pytester.runpython_c("import pytest;help(pytest)")
assert result.ret == 0
s = result.stdout.str()
assert "MarkGenerator" in s
def test_import_star_pytest(self, pytester: Pytester) -> None:
p = pytester.makepyfile(
"""
from pytest import *
#Item
#File
main
skip
xfail
"""
)
result = pytester.runpython(p)
assert result.ret == 0
def test_double_pytestcmdline(self, pytester: Pytester) -> None:
p = pytester.makepyfile(
run="""
import pytest
pytest.main()
pytest.main()
"""
)
pytester.makepyfile(
"""
def test_hello():
pass
"""
)
result = pytester.runpython(p)
result.stdout.fnmatch_lines(["*1 passed*", "*1 passed*"])
def test_python_minus_m_invocation_ok(self, pytester: Pytester) -> None:
p1 = pytester.makepyfile("def test_hello(): pass")
res = pytester.run(sys.executable, "-m", "pytest", str(p1))
assert res.ret == 0
def test_python_minus_m_invocation_fail(self, pytester: Pytester) -> None:
p1 = pytester.makepyfile("def test_fail(): 0/0")
res = pytester.run(sys.executable, "-m", "pytest", str(p1))
assert res.ret == 1
def test_python_pytest_package(self, pytester: Pytester) -> None:
p1 = pytester.makepyfile("def test_pass(): pass")
res = pytester.run(sys.executable, "-m", "pytest", str(p1))
assert res.ret == 0
res.stdout.fnmatch_lines(["*1 passed*"])
def test_invoke_with_invalid_type(self) -> None:
with pytest.raises(
TypeError, match="expected to be a list of strings, got: '-h'"
):
pytest.main("-h") # type: ignore[arg-type]
def test_invoke_with_path(self, pytester: Pytester, capsys) -> None:
retcode = pytest.main([str(pytester.path)])
assert retcode == ExitCode.NO_TESTS_COLLECTED
out, err = capsys.readouterr()
def test_invoke_plugin_api(self, capsys) -> None:
class MyPlugin:
def pytest_addoption(self, parser):
parser.addoption("--myopt")
pytest.main(["-h"], plugins=[MyPlugin()])
out, err = capsys.readouterr()
assert "--myopt" in out
def test_pyargs_importerror(self, pytester: Pytester, monkeypatch) -> None:
monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", False)
path = pytester.mkpydir("tpkg")
path.joinpath("test_hello.py").write_text("raise ImportError", encoding="utf-8")
result = pytester.runpytest("--pyargs", "tpkg.test_hello", syspathinsert=True)
assert result.ret != 0
result.stdout.fnmatch_lines(["collected*0*items*/*1*error"])
def test_pyargs_only_imported_once(self, pytester: Pytester) -> None:
pkg = pytester.mkpydir("foo")
pkg.joinpath("test_foo.py").write_text(
"print('hello from test_foo')\ndef test(): pass", encoding="utf-8"
)
pkg.joinpath("conftest.py").write_text(
"def pytest_configure(config): print('configuring')", encoding="utf-8"
)
result = pytester.runpytest(
"--pyargs", "foo.test_foo", "-s", syspathinsert=True
)
# should only import once
assert result.outlines.count("hello from test_foo") == 1
# should only configure once
assert result.outlines.count("configuring") == 1
def test_pyargs_filename_looks_like_module(self, pytester: Pytester) -> None:
pytester.path.joinpath("conftest.py").touch()
pytester.path.joinpath("t.py").write_text("def test(): pass", encoding="utf-8")
result = pytester.runpytest("--pyargs", "t.py")
assert result.ret == ExitCode.OK
def test_cmdline_python_package(self, pytester: Pytester, monkeypatch) -> None:
import warnings
monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", False)
path = pytester.mkpydir("tpkg")
path.joinpath("test_hello.py").write_text(
"def test_hello(): pass", encoding="utf-8"
)
path.joinpath("test_world.py").write_text(
"def test_world(): pass", encoding="utf-8"
)
result = pytester.runpytest("--pyargs", "tpkg")
assert result.ret == 0
result.stdout.fnmatch_lines(["*2 passed*"])
result = pytester.runpytest("--pyargs", "tpkg.test_hello", syspathinsert=True)
assert result.ret == 0
result.stdout.fnmatch_lines(["*1 passed*"])
empty_package = pytester.mkpydir("empty_package")
monkeypatch.setenv("PYTHONPATH", str(empty_package), prepend=os.pathsep)
# the path which is not a package raises a warning on pypy;
# no idea why only pypy and not normal python warn about it here
with warnings.catch_warnings():
warnings.simplefilter("ignore", ImportWarning)
result = pytester.runpytest("--pyargs", ".")
assert result.ret == 0
result.stdout.fnmatch_lines(["*2 passed*"])
monkeypatch.setenv("PYTHONPATH", str(pytester), prepend=os.pathsep)
result = pytester.runpytest("--pyargs", "tpkg.test_missing", syspathinsert=True)
assert result.ret != 0
result.stderr.fnmatch_lines(["*not*found*test_missing*"])
def test_cmdline_python_namespace_package(
self, pytester: Pytester, monkeypatch
) -> None:
"""Test --pyargs option with namespace packages (#1567).
Ref: https://packaging.python.org/guides/packaging-namespace-packages/
"""
monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False)
search_path = []
for dirname in "hello", "world":
d = pytester.mkdir(dirname)
search_path.append(d)
ns = d.joinpath("ns_pkg")
ns.mkdir()
ns.joinpath("__init__.py").write_text(
"__import__('pkg_resources').declare_namespace(__name__)",
encoding="utf-8",
)
lib = ns.joinpath(dirname)
lib.mkdir()
lib.joinpath("__init__.py").touch()
lib.joinpath(f"test_{dirname}.py").write_text(
f"def test_{dirname}(): pass\ndef test_other():pass",
encoding="utf-8",
)
# The structure of the test directory is now:
# .
# ├── hello
# │ └── ns_pkg
# │ ├── __init__.py
# │ └── hello
# │ ├── __init__.py
# │ └── test_hello.py
# └── world
# └── ns_pkg
# ├── __init__.py
# └── world
# ├── __init__.py
# └── test_world.py
# NOTE: the different/reversed ordering is intentional here.
monkeypatch.setenv("PYTHONPATH", prepend_pythonpath(*search_path))
for p in search_path:
monkeypatch.syspath_prepend(p)
# mixed module and filenames:
monkeypatch.chdir("world")
# pgk_resources.declare_namespace has been deprecated in favor of implicit namespace packages.
# pgk_resources has been deprecated entirely.
# While we could change the test to use implicit namespace packages, seems better
# to still ensure the old declaration via declare_namespace still works.
ignore_w = (
r"-Wignore:Deprecated call to `pkg_resources.declare_namespace",
r"-Wignore:pkg_resources is deprecated",
)
result = pytester.runpytest(
"--pyargs", "-v", "ns_pkg.hello", "ns_pkg/world", *ignore_w
)
assert result.ret == 0
result.stdout.fnmatch_lines(
[
"test_hello.py::test_hello*PASSED*",
"test_hello.py::test_other*PASSED*",
"ns_pkg/world/test_world.py::test_world*PASSED*",
"ns_pkg/world/test_world.py::test_other*PASSED*",
"*4 passed in*",
]
)
# specify tests within a module
pytester.chdir()
result = pytester.runpytest(
"--pyargs", "-v", "ns_pkg.world.test_world::test_other"
)
assert result.ret == 0
result.stdout.fnmatch_lines(
["*test_world.py::test_other*PASSED*", "*1 passed*"]
)
def test_invoke_test_and_doctestmodules(self, pytester: Pytester) -> None:
p = pytester.makepyfile(
"""
def test():
pass
"""
)
result = pytester.runpytest(str(p) + "::test", "--doctest-modules")
result.stdout.fnmatch_lines(["*1 passed*"])
def test_cmdline_python_package_symlink(
self, pytester: Pytester, monkeypatch
) -> None:
"""
--pyargs with packages with path containing symlink can have conftest.py in
their package (#2985)
"""
monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False)
dirname = "lib"
d = pytester.mkdir(dirname)
foo = d.joinpath("foo")
foo.mkdir()
foo.joinpath("__init__.py").touch()
lib = foo.joinpath("bar")
lib.mkdir()
lib.joinpath("__init__.py").touch()
lib.joinpath("test_bar.py").write_text(
"def test_bar(): pass\ndef test_other(a_fixture):pass", encoding="utf-8"
)
lib.joinpath("conftest.py").write_text(
"import pytest\n@pytest.fixture\ndef a_fixture():pass", encoding="utf-8"
)
d_local = pytester.mkdir("symlink_root")
symlink_location = d_local / "lib"
symlink_or_skip(d, symlink_location, target_is_directory=True)
# The structure of the test directory is now:
# .
# ├── symlink_root
# │ └── lib -> ../lib
# └── lib
# └── foo
# ├── __init__.py
# └── bar
# ├── __init__.py
# ├── conftest.py
# └── test_bar.py
# NOTE: the different/reversed ordering is intentional here.
search_path = ["lib", os.path.join("symlink_root", "lib")]
monkeypatch.setenv("PYTHONPATH", prepend_pythonpath(*search_path))
for p in search_path:
monkeypatch.syspath_prepend(p)
# module picked up in symlink-ed directory:
# It picks up symlink_root/lib/foo/bar (symlink) via sys.path.
result = pytester.runpytest("--pyargs", "-v", "foo.bar")
pytester.chdir()
assert result.ret == 0
result.stdout.fnmatch_lines(
[
"symlink_root/lib/foo/bar/test_bar.py::test_bar PASSED*",
"symlink_root/lib/foo/bar/test_bar.py::test_other PASSED*",
"*2 passed*",
]
)
def test_cmdline_python_package_not_exists(self, pytester: Pytester) -> None:
result = pytester.runpytest("--pyargs", "tpkgwhatv")
assert result.ret
result.stderr.fnmatch_lines(["ERROR*module*or*package*not*found*"])
@pytest.mark.xfail(reason="decide: feature or bug")
def test_noclass_discovery_if_not_testcase(self, pytester: Pytester) -> None:
testpath = pytester.makepyfile(
"""
import unittest
class TestHello(object):
def test_hello(self):
assert self.attr
class RealTest(unittest.TestCase, TestHello):
attr = 42
"""
)
reprec = pytester.inline_run(testpath)
reprec.assertoutcome(passed=1)
def test_doctest_id(self, pytester: Pytester) -> None:
pytester.makefile(
".txt",
"""
>>> x=3
>>> x
4
""",
)
testid = "test_doctest_id.txt::test_doctest_id.txt"
expected_lines = [
"*= FAILURES =*",
"*_ ?doctest? test_doctest_id.txt _*",
"FAILED test_doctest_id.txt::test_doctest_id.txt",
"*= 1 failed in*",
]
result = pytester.runpytest(testid, "-rf", "--tb=short")
result.stdout.fnmatch_lines(expected_lines)
# Ensure that re-running it will still handle it as
# doctest.DocTestFailure, which was not the case before when
# re-importing doctest, but not creating a new RUNNER_CLASS.
result = pytester.runpytest(testid, "-rf", "--tb=short")
result.stdout.fnmatch_lines(expected_lines)
def test_core_backward_compatibility(self) -> None:
"""Test backward compatibility for get_plugin_manager function. See #787."""
import _pytest.config
assert (
type(_pytest.config.get_plugin_manager())
is _pytest.config.PytestPluginManager
)
def test_has_plugin(self, request) -> None:
"""Test hasplugin function of the plugin manager (#932)."""
assert request.config.pluginmanager.hasplugin("python")
class TestDurations:
source = """
from _pytest import timing
def test_something():
pass
def test_2():
timing.sleep(0.010)
def test_1():
timing.sleep(0.002)
def test_3():
timing.sleep(0.020)
"""
def test_calls(self, pytester: Pytester, mock_timing) -> None:
pytester.makepyfile(self.source)
result = pytester.runpytest_inprocess("--durations=10")
assert result.ret == 0
result.stdout.fnmatch_lines_random(
["*durations*", "*call*test_3*", "*call*test_2*"]
)
result.stdout.fnmatch_lines(
["(8 durations < 0.005s hidden. Use -vv to show these durations.)"]
)
def test_calls_show_2(self, pytester: Pytester, mock_timing) -> None:
pytester.makepyfile(self.source)
result = pytester.runpytest_inprocess("--durations=2")
assert result.ret == 0
lines = result.stdout.get_lines_after("*slowest*durations*")
assert "4 passed" in lines[2]
def test_calls_showall(self, pytester: Pytester, mock_timing) -> None:
pytester.makepyfile(self.source)
result = pytester.runpytest_inprocess("--durations=0")
assert result.ret == 0
tested = "3"
for x in tested:
for y in ("call",): # 'setup', 'call', 'teardown':
for line in result.stdout.lines:
if ("test_%s" % x) in line and y in line:
break
else:
raise AssertionError(f"not found {x} {y}")
def test_calls_showall_verbose(self, pytester: Pytester, mock_timing) -> None:
pytester.makepyfile(self.source)
result = pytester.runpytest_inprocess("--durations=0", "-vv")
assert result.ret == 0
for x in "123":
for y in ("call",): # 'setup', 'call', 'teardown':
for line in result.stdout.lines:
if ("test_%s" % x) in line and y in line:
break
else:
raise AssertionError(f"not found {x} {y}")
def test_with_deselected(self, pytester: Pytester, mock_timing) -> None:
pytester.makepyfile(self.source)
result = pytester.runpytest_inprocess("--durations=2", "-k test_3")
assert result.ret == 0
result.stdout.fnmatch_lines(["*durations*", "*call*test_3*"])
def test_with_failing_collection(self, pytester: Pytester, mock_timing) -> None:
pytester.makepyfile(self.source)
pytester.makepyfile(test_collecterror="""xyz""")
result = pytester.runpytest_inprocess("--durations=2", "-k test_1")
assert result.ret == 2
result.stdout.fnmatch_lines(["*Interrupted: 1 error during collection*"])
# Collection errors abort test execution, therefore no duration is
# output
result.stdout.no_fnmatch_line("*duration*")
def test_with_not(self, pytester: Pytester, mock_timing) -> None:
pytester.makepyfile(self.source)
result = pytester.runpytest_inprocess("-k not 1")
assert result.ret == 0
class TestDurationsWithFixture:
source = """
import pytest
from _pytest import timing
@pytest.fixture
def setup_fixt():
timing.sleep(2)
def test_1(setup_fixt):
timing.sleep(5)
"""
def test_setup_function(self, pytester: Pytester, mock_timing) -> None:
pytester.makepyfile(self.source)
result = pytester.runpytest_inprocess("--durations=10")
assert result.ret == 0
result.stdout.fnmatch_lines_random(
"""
*durations*
5.00s call *test_1*
2.00s setup *test_1*
"""
)
def test_zipimport_hook(pytester: Pytester) -> None:
"""Test package loader is being used correctly (see #1837)."""
zipapp = pytest.importorskip("zipapp")
pytester.path.joinpath("app").mkdir()
pytester.makepyfile(
**{
"app/foo.py": """
import pytest
def main():
pytest.main(['--pyargs', 'foo'])
"""
}
)
target = pytester.path.joinpath("foo.zip")
zipapp.create_archive(
str(pytester.path.joinpath("app")), str(target), main="foo:main"
)
result = pytester.runpython(target)
assert result.ret == 0
result.stderr.fnmatch_lines(["*not found*foo*"])
result.stdout.no_fnmatch_line("*INTERNALERROR>*")
def test_import_plugin_unicode_name(pytester: Pytester) -> None:
pytester.makepyfile(myplugin="")
pytester.makepyfile("def test(): pass")
pytester.makeconftest("pytest_plugins = ['myplugin']")
r = pytester.runpytest()
assert r.ret == 0
def test_pytest_plugins_as_module(pytester: Pytester) -> None:
"""Do not raise an error if pytest_plugins attribute is a module (#3899)"""
pytester.makepyfile(
**{
"__init__.py": "",
"pytest_plugins.py": "",
"conftest.py": "from . import pytest_plugins",
"test_foo.py": "def test(): pass",
}
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(["* 1 passed in *"])
def test_deferred_hook_checking(pytester: Pytester) -> None:
"""Check hooks as late as possible (#1821)."""
pytester.syspathinsert()
pytester.makepyfile(
**{
"plugin.py": """
class Hooks(object):
def pytest_my_hook(self, config):
pass
def pytest_configure(config):
config.pluginmanager.add_hookspecs(Hooks)
""",
"conftest.py": """
pytest_plugins = ['plugin']
def pytest_my_hook(config):
return 40
""",
"test_foo.py": """
def test(request):
assert request.config.hook.pytest_my_hook(config=request.config) == [40]
""",
}
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(["* 1 passed *"])
def test_fixture_values_leak(pytester: Pytester) -> None:
"""Ensure that fixture objects are properly destroyed by the garbage collector at the end of their expected
life-times (#2981).
"""
pytester.makepyfile(
"""
import dataclasses
import gc
import pytest
import weakref
@dataclasses.dataclass
class SomeObj:
name: str
fix_of_test1_ref = None
session_ref = None
@pytest.fixture(scope='session')
def session_fix():
global session_ref
obj = SomeObj(name='session-fixture')
session_ref = weakref.ref(obj)
return obj
@pytest.fixture
def fix(session_fix):
global fix_of_test1_ref
obj = SomeObj(name='local-fixture')
fix_of_test1_ref = weakref.ref(obj)
return obj
def test1(fix):
assert fix_of_test1_ref() is fix
def test2():
gc.collect()
# fixture "fix" created during test1 must have been destroyed by now
assert fix_of_test1_ref() is None
"""
)
# Running on subprocess does not activate the HookRecorder
# which holds itself a reference to objects in case of the
# pytest_assert_reprcompare hook
result = pytester.runpytest_subprocess()
result.stdout.fnmatch_lines(["* 2 passed *"])
def test_fixture_order_respects_scope(pytester: Pytester) -> None:
"""Ensure that fixtures are created according to scope order (#2405)."""
pytester.makepyfile(
"""
import pytest
data = {}
@pytest.fixture(scope='module')
def clean_data():
data.clear()
@pytest.fixture(autouse=True)
def add_data():
data.update(value=True)
@pytest.mark.usefixtures('clean_data')
def test_value():
assert data.get('value')
"""
)
result = pytester.runpytest()
assert result.ret == 0
def test_frame_leak_on_failing_test(pytester: Pytester) -> None:
"""Pytest would leak garbage referencing the frames of tests that failed
that could never be reclaimed (#2798).
Unfortunately it was not possible to remove the actual circles because most of them
are made of traceback objects which cannot be weakly referenced. Those objects at least
can be eventually claimed by the garbage collector.
"""
pytester.makepyfile(
"""
import gc
import weakref
class Obj:
pass
ref = None
def test1():
obj = Obj()
global ref
ref = weakref.ref(obj)
assert 0
def test2():
gc.collect()
assert ref() is None
"""
)
result = pytester.runpytest_subprocess()
result.stdout.fnmatch_lines(["*1 failed, 1 passed in*"])
def test_fixture_mock_integration(pytester: Pytester) -> None:
"""Test that decorators applied to fixture are left working (#3774)"""
p = pytester.copy_example("acceptance/fixture_mock_integration.py")
result = pytester.runpytest(p)
result.stdout.fnmatch_lines(["*1 passed*"])
def test_usage_error_code(pytester: Pytester) -> None:
result = pytester.runpytest("-unknown-option-")
assert result.ret == ExitCode.USAGE_ERROR
def test_warn_on_async_function(pytester: Pytester) -> None:
# In the below we .close() the coroutine only to avoid
# "RuntimeWarning: coroutine 'test_2' was never awaited"
# which messes with other tests.
pytester.makepyfile(
test_async="""
async def test_1():
pass
async def test_2():
pass
def test_3():
coro = test_2()
coro.close()
return coro
"""
)
result = pytester.runpytest("-Wdefault")
result.stdout.fnmatch_lines(
[
"test_async.py::test_1",
"test_async.py::test_2",
"test_async.py::test_3",
"*async def functions are not natively supported*",
"*3 skipped, 3 warnings in*",
]
)
# ensure our warning message appears only once
assert (
result.stdout.str().count("async def functions are not natively supported") == 1
)
def test_warn_on_async_gen_function(pytester: Pytester) -> None:
pytester.makepyfile(
test_async="""
async def test_1():
yield
async def test_2():
yield
def test_3():
return test_2()
"""
)
result = pytester.runpytest("-Wdefault")
result.stdout.fnmatch_lines(
[
"test_async.py::test_1",
"test_async.py::test_2",
"test_async.py::test_3",
"*async def functions are not natively supported*",
"*3 skipped, 3 warnings in*",
]
)
# ensure our warning message appears only once
assert (
result.stdout.str().count("async def functions are not natively supported") == 1
)
def test_pdb_can_be_rewritten(pytester: Pytester) -> None:
pytester.makepyfile(
**{
"conftest.py": """
import pytest
pytest.register_assert_rewrite("pdb")
""",
"__init__.py": "",
"pdb.py": """
def check():
assert 1 == 2
""",
"test_pdb.py": """
def test():
import pdb
assert pdb.check()
""",
}
)
# Disable debugging plugin itself to avoid:
# > INTERNALERROR> AttributeError: module 'pdb' has no attribute 'set_trace'
result = pytester.runpytest_subprocess("-p", "no:debugging", "-vv")
result.stdout.fnmatch_lines(
[
" def check():",
"> assert 1 == 2",
"E assert 1 == 2",
"",
"pdb.py:2: AssertionError",
"*= 1 failed in *",
]
)
assert result.ret == 1
def test_tee_stdio_captures_and_live_prints(pytester: Pytester) -> None:
testpath = pytester.makepyfile(
"""
import sys
def test_simple():
print ("@this is stdout@")
print ("@this is stderr@", file=sys.stderr)
"""
)
result = pytester.runpytest_subprocess(
testpath,
"--capture=tee-sys",
"--junitxml=output.xml",
"-o",
"junit_logging=all",
)
# ensure stdout/stderr were 'live printed'
result.stdout.fnmatch_lines(["*@this is stdout@*"])
result.stderr.fnmatch_lines(["*@this is stderr@*"])
# now ensure the output is in the junitxml
fullXml = pytester.path.joinpath("output.xml").read_text(encoding="utf-8")
assert "@this is stdout@\n" in fullXml
assert "@this is stderr@\n" in fullXml
@pytest.mark.skipif(
sys.platform == "win32",
reason="Windows raises `OSError: [Errno 22] Invalid argument` instead",
)
def test_no_brokenpipeerror_message(pytester: Pytester) -> None:
"""Ensure that the broken pipe error message is suppressed.
In some Python versions, it reaches sys.unraisablehook, in others
a BrokenPipeError exception is propagated, but either way it prints
to stderr on shutdown, so checking nothing is printed is enough.
"""
popen = pytester.popen((*pytester._getpytestargs(), "--help"))
popen.stdout.close()
ret = popen.wait()
assert popen.stderr.read() == b""
assert ret == 1
# Cleanup.
popen.stderr.close()
def test_function_return_non_none_warning(pytester: Pytester) -> None:
pytester.makepyfile(
"""
def test_stuff():
return "something"
"""
)
res = pytester.runpytest()
res.stdout.fnmatch_lines(["*Did you mean to use `assert` instead of `return`?*"])
def test_doctest_and_normal_imports_with_importlib(pytester: Pytester) -> None:
"""
Regression test for #10811: previously import_path with ImportMode.importlib would
not return a module if already in sys.modules, resulting in modules being imported
multiple times, which causes problems with modules that have import side effects.
"""
# Uses the exact reproducer form #10811, given it is very minimal
# and illustrates the problem well.
pytester.makepyfile(
**{
"pmxbot/commands.py": "from . import logging",
"pmxbot/logging.py": "",
"tests/__init__.py": "",
"tests/test_commands.py": """
import importlib
from pmxbot import logging
class TestCommands:
def test_boo(self):
assert importlib.import_module('pmxbot.logging') is logging
""",
}
)
pytester.makeini(
"""
[pytest]
addopts=
--doctest-modules
--import-mode importlib
"""
)
result = pytester.runpytest_subprocess()
result.stdout.fnmatch_lines("*1 passed*")
@pytest.mark.skip(reason="Test is not isolated")
def test_issue_9765(pytester: Pytester) -> None:
"""Reproducer for issue #9765 on Windows
https://github.com/pytest-dev/pytest/issues/9765
"""
pytester.makepyprojecttoml(
"""
[tool.pytest.ini_options]
addopts = "-p my_package.plugin.my_plugin"
"""
)
pytester.makepyfile(
**{
"setup.py": (
"""
from setuptools import setup
if __name__ == '__main__':
setup(name='my_package', packages=['my_package', 'my_package.plugin'])
"""
),
"my_package/__init__.py": "",
"my_package/conftest.py": "",
"my_package/test_foo.py": "def test(): pass",
"my_package/plugin/__init__.py": "",
"my_package/plugin/my_plugin.py": (
"""
import pytest
def pytest_configure(config):
class SimplePlugin:
@pytest.fixture(params=[1, 2, 3])
def my_fixture(self, request):
yield request.param
config.pluginmanager.register(SimplePlugin())
"""
),
}
)
subprocess.run([sys.executable, "setup.py", "develop"], check=True)
try:
# We are using subprocess.run rather than pytester.run on purpose.
# pytester.run is adding the current directory to PYTHONPATH which avoids
# the bug. We also use pytest rather than python -m pytest for the same
# PYTHONPATH reason.
subprocess.run(
["pytest", "my_package"], capture_output=True, check=True, text=True
)
except subprocess.CalledProcessError as exc:
raise AssertionError(
f"pytest command failed:\n{exc.stdout=!s}\n{exc.stderr=!s}"
) from exc