diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b33ba167d..522d63aa2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -135,9 +135,17 @@ time or change existing behaviors in order to make them less surprising/more use never fail because tuples are always truthy and are usually a mistake (see `#1562`_). Thanks `@kvas-it`_, for the PR. +* Experimentally introduce new ``"invocation"`` fixture scope. At invocation scope a + fixture function is cached in the same way as the fixture or test function that requests it. + You can now use the builtin ``monkeypatch`` fixture from ``session``-scoped fixtures + where previously you would get an error that you can not use a ``function``-scoped fixture from a + ``session``-scoped one.* + Thanks `@nicoddemus`_ for the PR. + * Allow passing a custom debugger class (e.g. ``--pdbcls=IPython.core.debugger:Pdb``). Thanks to `@anntzer`_ for the PR. + * * diff --git a/_pytest/capture.py b/_pytest/capture.py index 48c14fbc8..f97cf6256 100644 --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -161,7 +161,7 @@ def capsys(request): captured output available via ``capsys.readouterr()`` method calls which return a ``(out, err)`` tuple. """ - if "capfd" in request._funcargs: + if "capfd" in request.fixturenames: raise request.raiseerror(error_capsysfderror) request.node._capfuncarg = c = CaptureFixture(SysCapture, request) return c @@ -172,7 +172,7 @@ def capfd(request): captured output available via ``capfd.readouterr()`` method calls which return a ``(out, err)`` tuple. """ - if "capsys" in request._funcargs: + if "capsys" in request.fixturenames: request.raiseerror(error_capsysfderror) if not hasattr(os, 'dup'): pytest.skip("capfd funcarg needs os.dup") diff --git a/_pytest/fixtures.py b/_pytest/fixtures.py index ab437459b..a42ddf627 100644 --- a/_pytest/fixtures.py +++ b/_pytest/fixtures.py @@ -260,8 +260,6 @@ class FuncFixtureInfo: self.name2fixturedefs = name2fixturedefs - - class FixtureRequest(FuncargnamesCompatAttr): """ A request for a fixture from a test or fixture function. @@ -276,34 +274,51 @@ class FixtureRequest(FuncargnamesCompatAttr): self.fixturename = None #: Scope string, one of "function", "class", "module", "session" self.scope = "function" - self._funcargs = {} - self._fixturedefs = {} + # rename both attributes below because their key has changed; better an attribute error + # than subtle key misses; also backward incompatibility + self._fixture_values = {} # (argname, scope) -> fixture value + self._fixture_defs = {} # (argname, scope) -> FixtureDef fixtureinfo = pyfuncitem._fixtureinfo self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy() self._arg2index = {} - self.fixturenames = fixtureinfo.names_closure self._fixturemanager = pyfuncitem.session._fixturemanager + @property + def fixturenames(self): + # backward incompatible note: now a readonly property + return list(self._pyfuncitem._fixtureinfo.names_closure) + @property def node(self): """ underlying collection node (depends on current request scope)""" return self._getscopeitem(self.scope) - def _getnextfixturedef(self, argname): - fixturedefs = self._arg2fixturedefs.get(argname, None) + def _getnextfixturedef(self, argname, scope): + def trygetfixturedefs(argname): + fixturedefs = self._arg2fixturedefs.get(argname, None) + if fixturedefs is None: + fixturedefs = self._arg2fixturedefs.get(argname + ':' + scope, None) + return fixturedefs + + fixturedefs = trygetfixturedefs(argname) if fixturedefs is None: # we arrive here because of a a dynamic call to # getfixturevalue(argname) usage which was naturally # not known at parsing/collection time - fixturedefs = self._fixturemanager.getfixturedefs( - argname, self._pyfuncitem.parent.nodeid) - self._arg2fixturedefs[argname] = fixturedefs + parentid = self._pyfuncitem.parent.nodeid + fixturedefs = self._fixturemanager.getfixturedefs(argname, parentid) + if fixturedefs: + self._arg2fixturedefs[argname] = fixturedefs + fixturedefs_by_argname = self._fixturemanager.getfixturedefs_multiple_scopes(argname, parentid) + if fixturedefs_by_argname: + self._arg2fixturedefs.update(fixturedefs_by_argname) + fixturedefs = trygetfixturedefs(argname) # fixturedefs list is immutable so we maintain a decreasing index - index = self._arg2index.get(argname, 0) - 1 + index = self._arg2index.get((argname, scope), 0) - 1 if fixturedefs is None or (-index > len(fixturedefs)): raise FixtureLookupError(argname, self) - self._arg2index[argname] = index + self._arg2index[(argname, scope)] = index return fixturedefs[index] @property @@ -442,10 +457,10 @@ class FixtureRequest(FuncargnamesCompatAttr): def _get_active_fixturedef(self, argname): try: - return self._fixturedefs[argname] + return self._fixture_defs[(argname, self.scope)] except KeyError: try: - fixturedef = self._getnextfixturedef(argname) + fixturedef = self._getnextfixturedef(argname, self.scope) except FixtureLookupError: if argname == "request": class PseudoFixtureDef: @@ -456,8 +471,8 @@ class FixtureRequest(FuncargnamesCompatAttr): # remove indent to prevent the python3 exception # from leaking into the call result = self._getfixturevalue(fixturedef) - self._funcargs[argname] = result - self._fixturedefs[argname] = fixturedef + self._fixture_values[(argname, self.scope)] = result + self._fixture_defs[(argname, self.scope)] = fixturedef return fixturedef def _get_fixturestack(self): @@ -578,11 +593,10 @@ class SubRequest(FixtureRequest): self._fixturedef = fixturedef self.addfinalizer = fixturedef.addfinalizer self._pyfuncitem = request._pyfuncitem - self._funcargs = request._funcargs - self._fixturedefs = request._fixturedefs + self._fixture_values = request._fixture_values + self._fixture_defs = request._fixture_defs self._arg2fixturedefs = request._arg2fixturedefs self._arg2index = request._arg2index - self.fixturenames = request.fixturenames self._fixturemanager = request._fixturemanager def __repr__(self): @@ -622,7 +636,7 @@ class FixtureLookupError(LookupError): fspath, lineno = getfslineno(function) try: lines, _ = inspect.getsourcelines(get_real_func(function)) - except (IOError, IndexError): + except (IOError, IndexError, TypeError): error_msg = "file %s, line %s: source code not available" addline(error_msg % (fspath, lineno+1)) else: @@ -636,9 +650,9 @@ class FixtureLookupError(LookupError): if msg is None: fm = self.request._fixturemanager available = [] - for name, fixturedef in fm._arg2fixturedefs.items(): - parentid = self.request._pyfuncitem.parent.nodeid - faclist = list(fm._matchfactories(fixturedef, parentid)) + parentid = self.request._pyfuncitem.parent.nodeid + for name, fixturedefs in fm._arg2fixturedefs.items(): + faclist = list(fm._matchfactories(fixturedefs, parentid)) if faclist: available.append(name) msg = "fixture %r not found" % (self.argname,) @@ -749,7 +763,7 @@ class FixtureDef: assert not hasattr(self, "cached_result") ihook = self._fixturemanager.session.ihook - ihook.pytest_fixture_setup(fixturedef=self, request=request) + return ihook.pytest_fixture_setup(fixturedef=self, request=request) def __repr__(self): return ("" % @@ -984,10 +998,12 @@ class FixtureManager: parentid = parentnode.nodeid fixturenames_closure = self._getautousenames(parentid) + def merge(otherlist): for arg in otherlist: if arg not in fixturenames_closure: fixturenames_closure.append(arg) + merge(fixturenames) arg2fixturedefs = {} lastlen = -1 @@ -1000,6 +1016,11 @@ class FixtureManager: if fixturedefs: arg2fixturedefs[argname] = fixturedefs merge(fixturedefs[-1].argnames) + fixturedefs_by_argname = self.getfixturedefs_multiple_scopes(argname, parentid) + if fixturedefs_by_argname: + arg2fixturedefs.update(fixturedefs_by_argname) + for fixturedefs in fixturedefs_by_argname.values(): + merge(fixturedefs[-1].argnames) return fixturenames_closure, arg2fixturedefs def pytest_generate_tests(self, metafunc): @@ -1018,7 +1039,7 @@ class FixtureManager: indirect=True, scope=fixturedef.scope, ids=fixturedef.ids) else: - continue # will raise FixtureLookupError at setup time + continue # will raise FixtureLookupError at setup time def pytest_collection_modifyitems(self, items): # separate parametrized setups @@ -1057,25 +1078,43 @@ class FixtureManager: msg = 'fixtures cannot have "pytest_funcarg__" prefix ' \ 'and be decorated with @pytest.fixture:\n%s' % name assert not name.startswith(self._argprefix), msg - fixturedef = FixtureDef(self, nodeid, name, obj, - marker.scope, marker.params, - unittest=unittest, ids=marker.ids) - faclist = self._arg2fixturedefs.setdefault(name, []) - if fixturedef.has_location: - faclist.append(fixturedef) + + def new_fixture_def(name, scope): + """Create and registers a new FixtureDef with given name and scope.""" + fixture_def = FixtureDef(self, nodeid, name, obj, + scope, marker.params, + unittest=unittest, ids=marker.ids) + + faclist = self._arg2fixturedefs.setdefault(name, []) + if fixture_def.has_location: + faclist.append(fixture_def) + else: + # fixturedefs with no location are at the front + # so this inserts the current fixturedef after the + # existing fixturedefs from external plugins but + # before the fixturedefs provided in conftests. + i = len([f for f in faclist if not f.has_location]) + faclist.insert(i, fixture_def) + if marker.autouse: + autousenames.append(name) + + if marker.scope == 'invocation': + for new_scope in scopes: + new_fixture_def(name + ':{0}'.format(new_scope), new_scope) else: - # fixturedefs with no location are at the front - # so this inserts the current fixturedef after the - # existing fixturedefs from external plugins but - # before the fixturedefs provided in conftests. - i = len([f for f in faclist if not f.has_location]) - faclist.insert(i, fixturedef) - if marker.autouse: - autousenames.append(name) + new_fixture_def(name, marker.scope) + if autousenames: self._nodeid_and_autousenames.append((nodeid or '', autousenames)) def getfixturedefs(self, argname, nodeid): + """ + Gets a list of fixtures which are applicable to the given node id. + + :param str argname: name of the fixture to search for + :param str nodeid: full node id of the requesting test. + :return: list[FixtureDef] + """ try: fixturedefs = self._arg2fixturedefs[argname] except KeyError: @@ -1087,3 +1126,24 @@ class FixtureManager: for fixturedef in fixturedefs: if nodeid.startswith(fixturedef.baseid): yield fixturedef + + def getfixturedefs_multiple_scopes(self, argname, nodeid): + """ + Gets multiple scoped fixtures which are applicable to the given nodeid. Multiple scoped + fixtures are created by "invocation" scoped fixtures and have argnames in + the form: ":" (for example "tmpdir:session"). + + :return: dict of "argname" -> [FixtureDef]. + + Arguments similar to ``getfixturedefs``. + """ + prefix = argname + ':' + fixturedefs_by_argname = dict((k, v) for k, v in self._arg2fixturedefs.items() + if k.startswith(prefix)) + if fixturedefs_by_argname: + result = {} + for argname, fixturedefs in fixturedefs_by_argname.items(): + result[argname] = tuple(self._matchfactories(fixturedefs, nodeid)) + return result + else: + return None diff --git a/_pytest/monkeypatch.py b/_pytest/monkeypatch.py index d1de4a679..258de385f 100644 --- a/_pytest/monkeypatch.py +++ b/_pytest/monkeypatch.py @@ -10,7 +10,7 @@ import pytest RE_IMPORT_ERROR_NAME = re.compile("^No module named (.*)$") -@pytest.fixture +@pytest.fixture(scope='invocation') def monkeypatch(request): """The returned ``monkeypatch`` fixture provides these helper methods to modify objects, dictionaries or os.environ:: @@ -25,9 +25,11 @@ def monkeypatch(request): monkeypatch.chdir(path) All modifications will be undone after the requesting - test function has finished. The ``raising`` + test function or fixture has finished. The ``raising`` parameter determines if a KeyError or AttributeError will be raised if the set/deletion operation has no target. + + This fixture is ``invocation``-scoped. """ mpatch = MonkeyPatch() request.addfinalizer(mpatch.undo) @@ -97,7 +99,8 @@ notset = Notset() class MonkeyPatch: - """ Object keeping a record of setattr/item/env/syspath changes. """ + """ Object returned by the ``monkeypatch`` fixture keeping a record of setattr/item/env/syspath changes. + """ def __init__(self): self._setattr = [] diff --git a/_pytest/python.py b/_pytest/python.py index fb374381d..37151d907 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1506,4 +1506,3 @@ class Function(FunctionMixin, pytest.Item, fixtures.FuncargnamesCompatAttr): super(Function, self).setup() fixtures.fillfixtures(self) - diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index 2077f2db0..daf4f278c 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -603,7 +603,7 @@ first execute with one instance and then finalizers are called before the next fixture instance is created. Among other things, this eases testing of applications which create and use global state. -The following example uses two parametrized funcargs, one of which is +The following example uses two parametrized fixture, one of which is scoped on a per-module basis, and all the functions perform ``print`` calls to show the setup/teardown flow:: @@ -863,6 +863,14 @@ All test methods in this TestClass will use the transaction fixture while other test classes or functions in the module will not use it unless they also add a ``transact`` reference. +invocation-scoped fixtures +-------------------------- + +pytest 3.0 introduced a new advanced scope for fixtures: ``"invocation"``. Fixtures marked with +this scope can be requested from any other scope, providing a version of the fixture for that scope. + +See more in :ref:`invocation_scoped_fixture`. + Shifting (visibility of) fixture functions ---------------------------------------------------- diff --git a/doc/en/invocation-fixture.rst b/doc/en/invocation-fixture.rst new file mode 100644 index 000000000..80f00adf7 --- /dev/null +++ b/doc/en/invocation-fixture.rst @@ -0,0 +1,63 @@ +.. _invocation_scoped_fixture: + +Invocation-scoped fixtures +========================== + +.. versionadded:: 3.0 + +.. note:: + This feature is experimental, so if decided that it brings too much problems + or considered too complicated it might be removed in pytest ``3.1``. + +Fixtures can be defined with ``invocation`` scope, meaning that the fixture +can be requested by fixtures from any scope, but when they do they assume +the same scope as the fixture requesting it. + +An ``invocation``-scoped fixture can be requested from different scopes +in the same test session, in which case each scope will have its own copy. + +Example +------- + +Consider a fixture which manages external process execution: +this fixture provides auxiliary methods for tests and fixtures to start external +processes while making sure the +processes terminate at the appropriate time. Because it makes sense +to start a webserver for the entire session and also to execute a numerical +simulation for a single test function, the ``process_manager`` +fixture can be declared as ``invocation``, so each scope gets its own +value and can manage processes which will live for the duration of the scope. + +.. code-block:: python + + import pytest + + @pytest.fixture(scope='invocation') + def process_manager(): + """ + Return a ProcessManager instance which can be used to start + long-lived processes and ensures they are terminated at the + appropriate scope. + """ + m = ProcessManager() + yield m + m.shutdown_all() + + + @pytest.fixture(scope='session') + def server(process_manager): + process_manager.start(sys.executable, 'server.py') + + + @pytest.fixture(scope='function') + def start_simulation(process_manager): + import functools + return functools.partial(process_manager.start, + sys.executable, 'simulator.py') + + +In the above code, simulators started using ``start_simulation`` will be +terminated when the test function exits, while the server will be kept +active for the entire simulation run, being terminated when the test session +finishes. + diff --git a/doc/en/monkeypatch.rst b/doc/en/monkeypatch.rst index 4155a3a34..cc6b94958 100644 --- a/doc/en/monkeypatch.rst +++ b/doc/en/monkeypatch.rst @@ -6,7 +6,7 @@ Monkeypatching/mocking modules and environments Sometimes tests need to invoke functionality which depends on global settings or which invokes code which cannot be easily -tested such as network access. The ``monkeypatch`` function argument +tested such as network access. The ``monkeypatch`` fixture helps you to safely set/delete an attribute, dictionary item or environment variable or to modify ``sys.path`` for importing. See the `monkeypatch blog post`_ for some introduction material @@ -14,6 +14,9 @@ and a discussion of its motivation. .. _`monkeypatch blog post`: http://tetamap.wordpress.com/2009/03/03/monkeypatching-in-unit-tests-done-right/ +As of pytest-3.0, the ``monkeypatch`` fixture is :ref:`invocation-scoped ` +meaning it can be requested from fixtures of any scope. + Simple example: monkeypatching functions --------------------------------------------------- @@ -53,27 +56,31 @@ This autouse fixture will be executed for each test function and it will delete the method ``request.session.Session.request`` so that any attempts within tests to create http requests will fail. -example: setting an attribute on some class ------------------------------------------------------- +example: setting an environment variable for the test session +------------------------------------------------------------- -If you need to patch out ``os.getcwd()`` to return an artificial -value:: +If you would like for an environment variable to be +configured for the entire test session, you can add this to your +top-level ``conftest.py`` file: - def test_some_interaction(monkeypatch): - monkeypatch.setattr("os.getcwd", lambda: "/") +.. code-block:: python -which is equivalent to the long form:: + # content of conftest.py + @pytest.fixture(scope='session', autouse=True) + def enable_debugging(monkeypatch): + monkeypatch.setenv("DEBUGGING_VERBOSITY", "4") - def test_some_interaction(monkeypatch): - import os - monkeypatch.setattr(os, "getcwd", lambda: "/") - +This auto-use fixture will set the ``DEBUGGING_VERBOSITY`` environment variable for +the entire test session. + +Note that the ability to use a ``monkeypatch`` fixture from a ``session``-scoped +fixture was added in pytest-3.0. -Method reference of the monkeypatch function argument ------------------------------------------------------ +Method reference of the monkeypatch fixture +------------------------------------------- -.. autoclass:: monkeypatch +.. autoclass:: MonkeyPatch :members: setattr, replace, delattr, setitem, delitem, setenv, delenv, syspath_prepend, chdir, undo ``monkeypatch.setattr/delattr/delitem/delenv()`` all diff --git a/testing/python/invocation_scope.py b/testing/python/invocation_scope.py new file mode 100644 index 000000000..81801067d --- /dev/null +++ b/testing/python/invocation_scope.py @@ -0,0 +1,214 @@ + +def test_invocation_request(testdir): + """ + Simple test case with session and module scopes requesting an + invocation-scoped fixture. + """ + testdir.makeconftest(""" + import pytest + + @pytest.fixture(scope='invocation') + def my_name(request): + if request.scope == 'function': + return request.function.__name__ + elif request.scope == 'module': + return request.module.__name__ + elif request.scope == 'session': + return '' + + @pytest.fixture(scope='session') + def session_name(my_name): + return my_name + + @pytest.fixture(scope='module') + def module_name(my_name): + return my_name + """) + testdir.makepyfile(test_module_foo=""" + def test_foo(my_name, module_name, session_name): + assert my_name == 'test_foo' + assert module_name == 'test_module_foo' + assert session_name == '' + """) + testdir.makepyfile(test_module_bar=""" + def test_bar(my_name, module_name, session_name): + assert my_name == 'test_bar' + assert module_name == 'test_module_bar' + assert session_name == '' + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines(['*2 passed*']) + + +def test_override_invocation_scoped(testdir): + """Test that it's possible to override invocation-scoped fixtures.""" + testdir.makeconftest(""" + import pytest + + @pytest.fixture(scope='invocation') + def magic_value(request): + if request.scope == 'function': + return 1 + elif request.scope == 'module': + return 100 + + @pytest.fixture(scope='module') + def module_magic_value(magic_value): + return magic_value * 2 + """) + testdir.makepyfile(test_module_override=""" + import pytest + + @pytest.fixture(scope='module') + def magic_value(): + return 42 + + def test_override(magic_value, module_magic_value): + assert magic_value == 42 + assert module_magic_value == 42 * 2 + """) + testdir.makepyfile(test_normal=""" + def test_normal(magic_value, module_magic_value): + assert magic_value == 1 + assert module_magic_value == 200 + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines(['*2 passed*']) + + +class TestAcceptance: + """ + Complete acceptance test for a invocation-scoped fixture. + """ + + def test_acceptance(self, testdir): + """ + Tests a "stack" fixture which provides a separate list to each scope which uses it. + + Some notes: + + - For each scope, define 2 fixtures of the same scope which use the "stack" fixture, + to ensure they get the same "stack" instance for that scope. + - Creates multiple test files, which tests on each modifying and checking fixtures to + ensure things are working properly. + """ + testdir.makeconftest(""" + import pytest + + @pytest.fixture(scope='invocation') + def stack(): + return [] + + @pytest.fixture(scope='session') + def session1_fix(stack): + stack.append('session1_fix') + return stack + + @pytest.fixture(scope='session') + def session2_fix(stack): + stack.append('session2_fix') + return stack + + @pytest.fixture(scope='module') + def module1_fix(stack): + stack.append('module1_fix') + return stack + + @pytest.fixture(scope='module') + def module2_fix(stack): + stack.append('module2_fix') + return stack + + @pytest.fixture(scope='class') + def class1_fix(stack): + stack.append('class1_fix') + return stack + + @pytest.fixture(scope='class') + def class2_fix(stack): + stack.append('class2_fix') + return stack + """) + testdir.makepyfile(test_0=""" + import pytest + + @pytest.fixture + def func_stack(stack): + return stack + + def test_scoped_instances(session1_fix, session2_fix, module1_fix, module2_fix, + class1_fix, class2_fix, stack, func_stack): + assert session1_fix is session2_fix + assert module1_fix is module2_fix + assert class1_fix is class2_fix + assert stack is func_stack + + assert session1_fix is not module2_fix + assert module2_fix is not class1_fix + assert class1_fix is not stack + """) + testdir.makepyfile(test_1=""" + def test_func_1(request, session1_fix, session2_fix, module1_fix, module2_fix, stack): + assert stack == [] + + assert session1_fix == ['session1_fix', 'session2_fix'] + session1_fix.append('test_1::test_func_1') + + assert module1_fix == ['module1_fix', 'module2_fix'] + module1_fix.append('test_1::test_func_1') + + + class Test: + + def test_1(self, request, session1_fix, module1_fix, class1_fix, class2_fix, stack): + assert stack == [] + + assert session1_fix == ['session1_fix', 'session2_fix', 'test_1::test_func_1'] + session1_fix.append('test_1::Test::test_1') + + assert module1_fix == ['module1_fix', 'module2_fix', 'test_1::test_func_1'] + module1_fix.append('test_1::test_func_1') + + assert class1_fix == ['class1_fix', 'class2_fix'] + class1_fix.append('test_1::Test::test_1') + + def test_2(self, request, class1_fix, class2_fix): + assert class1_fix == ['class1_fix', 'class2_fix', 'test_1::Test::test_1'] + class1_fix.append('Test.test_2') + + + def test_func_2(request, session1_fix, session2_fix, module1_fix, class1_fix, class2_fix, stack): + assert stack == [] + assert session1_fix == ['session1_fix', 'session2_fix', 'test_1::test_func_1', + 'test_1::Test::test_1'] + session1_fix.append('test_1::test_func_2') + + assert module1_fix == ['module1_fix', 'module2_fix', 'test_1::test_func_1', 'test_1::test_func_1'] + + assert class1_fix == ['class1_fix', 'class2_fix'] + """) + testdir.makepyfile(test_2=""" + import pytest + + @pytest.fixture(scope='session') + def another_session_stack(stack): + stack.append('other_session_stack') + return stack + + def test_func_2(request, another_session_stack, module1_fix, stack): + assert stack == [] + assert another_session_stack == [ + 'session1_fix', + 'session2_fix', + 'test_1::test_func_1', + 'test_1::Test::test_1', + 'test_1::test_func_2', + 'other_session_stack', + ] + assert module1_fix == ['module1_fix'] + """) + result = testdir.runpytest() + assert result.ret == 0 + result.stdout.fnmatch_lines('* 6 passed in *') + + diff --git a/testing/test_capture.py b/testing/test_capture.py index 2e69cfc85..c197c85e7 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -421,6 +421,25 @@ class TestCaptureFixture: "*capsys*capfd*same*time*", "*2 error*"]) + def test_capturing_getfixturevalue(self, testdir): + """Test that asking for "capfd" and "capsys" using request.getfixturevalue + in the same test is an error. + """ + testdir.makepyfile(""" + def test_one(capsys, request): + request.getfixturevalue("capfd") + def test_two(capfd, request): + request.getfixturevalue("capsys") + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "*test_one*", + "*capsys*capfd*same*time*", + "*test_two*", + "*capsys*capfd*same*time*", + "*2 failed in*", + ]) + @pytest.mark.parametrize("method", ["sys", "fd"]) def test_capture_is_represented_on_failure_issue128(self, testdir, method): p = testdir.makepyfile(""" diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index 7599be47f..0c152a8b6 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -328,4 +328,36 @@ def test_issue1338_name_resolving(): try: monkeypatch.delattr('requests.sessions.Session.request') finally: - monkeypatch.undo() \ No newline at end of file + monkeypatch.undo() + + +def test_invocation_scoped_monkeypatch(testdir): + testdir.makeconftest(""" + import pytest + import sys + + @pytest.fixture(scope='module') + def stamp_sys(monkeypatch): + monkeypatch.setattr(sys, 'module_stamped', True, raising=False) + """) + testdir.makepyfile(test_inv_mokeypatch_1=""" + import sys + + def test_stamp_1(monkeypatch, stamp_sys): + assert sys.module_stamped + monkeypatch.setattr(sys, 'function_stamped', True, raising=False) + assert sys.function_stamped + + def test_stamp_2(monkeypatch): + assert sys.module_stamped + assert not hasattr(sys, 'function_stamped') + """) + testdir.makepyfile(test_inv_mokeypatch_2=""" + import sys + + def test_no_stamps(): + assert not hasattr(sys, 'module_stamped') + assert not hasattr(sys, 'function_stamped') + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines(['*3 passed*'])