Skip to content

Commit 29e345d

Browse files
author
Sylvain MARIE
committed
Fixed issue with ordering and setup/teardown for higher-level scope fixtures (session and module scopes) when using union fixtures. Fixes #44.
Added corresponding test
1 parent 2114687 commit 29e345d

File tree

3 files changed

+145
-20
lines changed

3 files changed

+145
-20
lines changed

pytest_cases/common.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -340,18 +340,16 @@ def get_pytest_nodeid(metafunc):
340340
return "unknown"
341341

342342

343-
def get_pytest_scopes():
344-
"""
345-
Returns the list of scopes in order as defined in pytest.
346-
:return:
347-
"""
348-
try:
349-
from _pytest.fixtures import scopes as pt_scopes
350-
except ImportError:
351-
# pytest 2
352-
from _pytest.python import scopes as pt_scopes
353-
return pt_scopes
343+
try:
344+
from _pytest.fixtures import scopes as pt_scopes
345+
except ImportError:
346+
# pytest 2
347+
from _pytest.python import scopes as pt_scopes
348+
349+
350+
def get_pytest_scopenum(scope_str):
351+
return pt_scopes.index(scope_str)
354352

355353

356354
def get_pytest_function_scopenum():
357-
return get_pytest_scopes().index("function")
355+
return pt_scopes.index("function")

pytest_cases/plugin.py

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import pytest
99

1010
from pytest_cases.common import get_pytest_nodeid, get_pytest_function_scopenum, \
11-
is_function_node, get_param_names
11+
is_function_node, get_param_names, get_pytest_scopenum
1212
from pytest_cases.main_fixtures import NOT_USED, is_fixture_union_params, UnionFixtureAlternative, apply_id_style
1313

1414
try: # python 3.3+
@@ -28,6 +28,7 @@
2828

2929

3030
# @hookspec(firstresult=True)
31+
# @pytest.hookimpl(tryfirst=True, hookwrapper=True)
3132
def pytest_collection(session):
3233
# override the fixture manager's method
3334
session._fixturemanager.getfixtureclosure = partial(getfixtureclosure, session._fixturemanager)
@@ -340,13 +341,41 @@ def split_and_build(self,
340341
def has_split(self):
341342
return self.split_fixture_name is not None
342343

344+
def gather_all_required(self, include_children=True, include_parents=True):
345+
"""
346+
Returns a list of all fixtures required by the subtree at this node
347+
:param include_children:
348+
:return:
349+
"""
350+
# first the fixtures required by this node
351+
required = list(self.fixture_defs.keys())
352+
353+
# then the ones required by the parents
354+
if include_parents and self.parent is not None:
355+
required = required + self.parent.gather_all_required(include_children=False)
356+
357+
# then the ones from all the children
358+
if include_children:
359+
for child in self.children.values():
360+
required = required + child.gather_all_required(include_parents=False)
361+
362+
return required
363+
364+
def requires(self, fixturename):
365+
"""
366+
Returns True if the fixture with this name is required by the subtree at this node
367+
:param fixturename:
368+
:return:
369+
"""
370+
return fixturename in self.gather_all_required()
371+
343372
def gather_all_discarded(self):
344373
"""
345374
Returns a list of all fixture names discarded during splits from the parent node down to this node.
346375
Note: this does not include the split done at this node if any, nor all of its subtree.
347376
:return:
348377
"""
349-
discarded = self.split_fixture_discarded_names
378+
discarded = list(self.split_fixture_discarded_names)
350379
if self.parent is not None:
351380
discarded = discarded + self.parent.gather_all_discarded()
352381

@@ -730,12 +759,18 @@ def _cleanup_calls_list(self, calls, nodes, pending):
730759
# For this we use a dirty hack: we add a parameter with they name in the callspec, it seems to be propagated
731760
# in the `request`. TODO is there a better way?
732761
# for fixture in list(fix_closure_tree):
733-
for fixture in n.gather_all_discarded():
734-
if fixture not in c.params and fixture not in c.funcargs:
735-
# explicitly add it as discarded by creating a parameter value for it.
736-
c.params[fixture] = NOT_USED
737-
c.indices[fixture] = 0
738-
c._arg2scopenum[fixture] = function_scope_num
762+
for fixture_name, fixdef in self.metafunc._arg2fixturedefs.items():
763+
if fixture_name not in c.params and fixture_name not in c.funcargs:
764+
if not n.requires(fixture_name):
765+
# explicitly add it as discarded by creating a parameter value for it.
766+
c.params[fixture_name] = NOT_USED
767+
c.indices[fixture_name] = 0
768+
c._arg2scopenum[fixture_name] = get_pytest_scopenum(fixdef[-1].scope)
769+
else:
770+
# explicitly add it as active
771+
c.params[fixture_name] = 'used'
772+
c.indices[fixture_name] = 1
773+
c._arg2scopenum[fixture_name] = get_pytest_scopenum(fixdef[-1].scope)
739774

740775
def _parametrize_calls(self, init_calls, argnames, argvalues, discard_id=False, indirect=False, ids=None,
741776
scope=None, **kwargs):
@@ -905,3 +940,27 @@ def sort_according_to_ref_list(fixturenames, param_names):
905940
for old_i, new_i in zip(cur_indices, target_indices):
906941
sorted_fixturenames[new_i] = fixturenames[old_i]
907942
return sorted_fixturenames
943+
944+
945+
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
946+
def pytest_collection_modifyitems(session, config, items):
947+
"""
948+
An alternative to the `reorder_items` function in fixtures.py
949+
(https://github.com/pytest-dev/pytest/blob/master/src/_pytest/fixtures.py#L209)
950+
951+
We basically set back the previous order once the pytest ordering routine has completed.
952+
953+
TODO we should set back an optimal ordering, but current PR https://github.com/pytest-dev/pytest/pull/3551
954+
will probably not be relevant to handle our "union" fixtures > need to integrate the NOT_USED markers in the method
955+
956+
:param session:
957+
:param config:
958+
:param items:
959+
:return:
960+
"""
961+
962+
# remember initial order
963+
initial_order = copy(items)
964+
yield
965+
# put back the initial order
966+
items[:] = initial_order
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# https://stackoverflow.com/questions/46909275/parametrizing-tests-depending-of-also-parametrized-values-in-pytest
2+
import pytest
3+
4+
from pytest_cases import pytest_parametrize_plus, pytest_fixture_plus, fixture_ref
5+
6+
datasets_contents = {
7+
'datasetA': ['data1_a', 'data2_a', 'data3_a'],
8+
'datasetB': ['data1_b', 'data2_b', 'data3_b']
9+
}
10+
11+
DA = None
12+
13+
@pytest_fixture_plus(scope="module")
14+
def datasetA():
15+
global DA
16+
17+
# setup the database connection
18+
print("setting up dataset A")
19+
assert DA is None
20+
DA = 'DA'
21+
22+
yield DA
23+
24+
# teardown the database connection
25+
print("tearing down dataset A")
26+
assert DA == 'DA'
27+
DA = None
28+
29+
30+
@pytest_fixture_plus(scope="module")
31+
@pytest.mark.parametrize('data_index', range(len(datasets_contents['datasetA'])), ids="idx={}".format)
32+
def data_from_datasetA(datasetA, data_index):
33+
assert datasetA == 'DA'
34+
return datasets_contents['datasetA'][data_index]
35+
36+
37+
DB = None
38+
39+
40+
@pytest_fixture_plus(scope="module")
41+
def datasetB():
42+
global DB
43+
44+
# setup the database connection
45+
print("setting up dataset B")
46+
assert DB is None
47+
DB = 'DB'
48+
49+
yield DB
50+
51+
# teardown the database connection
52+
print("tearing down dataset B")
53+
assert DB == 'DB'
54+
DB = None
55+
56+
57+
@pytest_fixture_plus(scope="module")
58+
@pytest.mark.parametrize('data_index', range(len(datasets_contents['datasetB'])), ids="idx={}".format)
59+
def data_from_datasetB(datasetB, data_index):
60+
assert datasetB == 'DB'
61+
return datasets_contents['datasetB'][data_index]
62+
63+
64+
@pytest_parametrize_plus('data', [fixture_ref('data_from_datasetA'),
65+
fixture_ref('data_from_datasetB')])
66+
def test_databases(data):
67+
# do test
68+
print(data)

0 commit comments

Comments
 (0)