diff --git a/pytest_lazyfixture.py b/pytest_lazyfixture.py index e12e9f1..c7f7ea9 100644 --- a/pytest_lazyfixture.py +++ b/pytest_lazyfixture.py @@ -1,9 +1,7 @@ # -*- coding: utf-8 -*- -import os import sys import types from collections import defaultdict -import py import pytest from _pytest.fixtures import scopenum_function @@ -43,6 +41,13 @@ def fill(request): return fill +@pytest.hookimpl(tryfirst=True) +def pytest_fixture_setup(fixturedef, request): + val = getattr(request, 'param', None) + if is_lazy_fixture(val): + request.param = request.getfixturevalue(val.name) + + def pytest_runtest_call(item): if hasattr(item, 'funcargs'): for arg, val in item.funcargs.items(): @@ -50,6 +55,14 @@ def pytest_runtest_call(item): item.funcargs[arg] = item._request.getfixturevalue(val.name) +@pytest.hookimpl(hookwrapper=True) +def pytest_pycollect_makeitem(collector, name, obj): + global current_node + current_node = collector + yield + current_node = None + + @pytest.hookimpl(hookwrapper=True) def pytest_generate_tests(metafunc): yield @@ -66,40 +79,57 @@ def normalize_metafunc_calls(metafunc, valtype): metafunc._calls = newcalls +def parametrize_callspecs(callspecs, metafunc, fname, fparams): + allnewcallspecs = [] + for i, param in enumerate(fparams): + try: + newcallspecs = [call.copy() for call in callspecs] + except TypeError: + # pytest < 3.6.3 + newcallspecs = [call.copy(metafunc) for call in callspecs] + + # TODO: for now it uses only function scope + # TODO: idlist + setmulti_args = ( + {fname: 'params'}, (fname,), (param,), + None, (), scopenum_function, i + ) + try: + for newcallspec in newcallspecs: + newcallspec.setmulti2(*setmulti_args) + except AttributeError: + # pytest < 3.3.0 + for newcallspec in newcallspecs: + newcallspec.setmulti(*setmulti_args) + allnewcallspecs.extend(newcallspecs) + return allnewcallspecs + + def normalize_call(callspec, metafunc, valtype, used_keys=None): fm = metafunc.config.pluginmanager.get_plugin('funcmanage') - config = metafunc.config used_keys = used_keys or set() valtype_keys = set(getattr(callspec, valtype).keys()) - used_keys - newcalls = [] for arg in valtype_keys: val = getattr(callspec, valtype)[arg] if is_lazy_fixture(val): - fname = val.name - nodeid = get_nodeid(metafunc.module, config.rootdir) - fdef = fm.getfixturedefs(fname, nodeid) - if fname not in callspec.params and fdef and fdef[-1].params: - for i, param in enumerate(fdef[0].params): - try: - newcallspec = callspec.copy() - except TypeError: - # pytest < 3.6.3 - newcallspec = callspec.copy(metafunc) - - # TODO: for now it uses only function scope - # TODO: idlist - setmulti_args = ( - {fname: 'params'}, (fname,), (param,), - None, (), scopenum_function, i - ) - try: - newcallspec.setmulti2(*setmulti_args) - except AttributeError: - # pytest < 3.3.0 - newcallspec.setmulti(*setmulti_args) - + try: + _, fixturenames_closure, arg2fixturedefs = fm.getfixtureclosure([val.name], metafunc.definition.parent) + except AttributeError: + # pytest < 3.10.0 + fixturenames_closure, arg2fixturedefs = fm.getfixtureclosure([val.name], current_node) + + extra_fixture_params = [(fname, arg2fixturedefs[fname][-1].params) + for fname in fixturenames_closure if fname not in callspec.params + if arg2fixturedefs.get(fname) and arg2fixturedefs[fname][-1].params] + + if extra_fixture_params: + newcallspecs = [callspec] + for fname, fparams in extra_fixture_params: + newcallspecs = parametrize_callspecs(newcallspecs, metafunc, fname, fparams) + newcalls = [] + for newcallspec in newcallspecs: calls = normalize_call(newcallspec, metafunc, valtype, used_keys | set([arg])) newcalls.extend(calls) return newcalls @@ -151,14 +181,6 @@ def _tree_to_list(trees, leave): return lst -def get_nodeid(module, rootdir): - path = py.path.local(module.__file__) - relpath = path.relto(rootdir) - if os.sep != "/": - relpath = relpath.replace(os.sep, "/") - return relpath - - def lazy_fixture(names): if isinstance(names, string_type): return LazyFixture(names) diff --git a/tests/test_lazyfixture.py b/tests/test_lazyfixture.py index ad67b2a..2b64f25 100644 --- a/tests/test_lazyfixture.py +++ b/tests/test_lazyfixture.py @@ -373,7 +373,7 @@ def zero(request): return request.param @pytest.mark.parametrize(('a', 'b'), [ - pytest.mark.xfail((1, pytest.lazy_fixture('zero')), reason=ZeroDivisionError) + pytest.param(1, pytest.lazy_fixture('zero'), marks=pytest.mark.xfail(reason=ZeroDivisionError)) ]) def test_division(a, b): division(a, b) @@ -411,7 +411,7 @@ def one(): return 1 @pytest.mark.parametrize('a', [ - pytest.mark.skip(pytest.lazy_fixture('one'), reason='skip') + pytest.param(pytest.lazy_fixture('one'), marks=pytest.mark.skip(reason='skip')) ]) def test_skip1(a): assert a == 1 @@ -447,7 +447,7 @@ def test_model_a(self, a): assert a == 1 @pytest.mark.parametrize('a', [ - pytest.mark.skip(pytest.lazy_fixture('one'), reason='skip this') + pytest.param(pytest.lazy_fixture('one'), marks=pytest.mark.skip(reason='skip this')) ]) def test_model_b(self, a): assert a == 1 @@ -580,3 +580,91 @@ def test_sorted_by_dependency(params, expected_paths): ]) def test_sorted_argnames(params, fixturenames, expect_keys): assert list(_sorted_argnames(params, fixturenames)) == expect_keys + + +def test_lazy_fixtures_with_subfixtures(testdir): + testdir.makepyfile(""" + import pytest + + @pytest.fixture(params=["a", "A"]) + def a(request): + return request.param + + @pytest.fixture(params=["b", "B"]) + def b(a, request): + return request.param + a + + @pytest.fixture + def c(a): + return "c" + a + + @pytest.fixture(params=[pytest.lazy_fixture('a'), pytest.lazy_fixture('b'), pytest.lazy_fixture('c')]) + def d(request): + return "d" + request.param + + @pytest.fixture(params=[pytest.lazy_fixture('a'), pytest.lazy_fixture('d'), ""]) + def e(request): + return "e" + request.param + + def test_one(d): + assert d in ("da", "dA", "dba", "dbA", "dBa", "dBA", "dca", "dcA") + + def test_two(e): + assert e in ("ea", "eA", "eda", "edA", "edba", "edbA", "edBa", "edBA", "edca", "edcA", "e") + """) + reprec = testdir.inline_run('-s', '-v') + reprec.assertoutcome(passed=19) + + +def test_lazy_fixtures_in_subfixture(testdir): + testdir.makepyfile(""" + import pytest + + @pytest.fixture + def a(): + return "a" + + @pytest.fixture + def b(): + return "b" + + @pytest.fixture(params=[pytest.lazy_fixture('a'), pytest.lazy_fixture('b')]) + def c(request): + return "c" + request.param + + @pytest.fixture + def d(c): + return "d" + c + + def test_one(d): + assert d in ("dca", "dcb") + """) + reprec = testdir.inline_run('-s', '-v') + reprec.assertoutcome(passed=2) + + +@pytest.mark.parametrize('autouse', [False, True]) +def test_issues23(testdir, autouse): + testdir.makepyfile(""" + import pytest + + @pytest.fixture(params=[0, 1], autouse={}) + def zero(request): + return request.param + + @pytest.fixture(params=[1]) + def one(request, zero): + return zero * request.param + + @pytest.fixture(params=[ + pytest.lazy_fixture('one'), + ]) + def some(request): + return request.param + + def test_func(some): + assert some in [0, 1] + + """.format(autouse)) + reprec = testdir.inline_run('-s', '-v') + reprec.assertoutcome(passed=2)