Skip to content

Commit 170990d

Browse files
committed
Ignore ordering if it would break a dependency
- add configuration option for ordering all dependencies defined by the pytest-dependency plugin - fix typo in sparse ordering implementation, add test
1 parent c9ced10 commit 170990d

File tree

7 files changed

+278
-34
lines changed

7 files changed

+278
-34
lines changed

CHANGELOG.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,16 @@
66
- added configuration option for sparse sorting, e.g. the possibility to
77
fill gaps between ordinals with unordered tests (see also
88
[this issue](https://github.com/ftobia/pytest-ordering/issues/14) in
9-
`pytest-ordering`)
10-
9+
`pytest-ordering`)
10+
- ignore ordering if it would break a dependency defined by the
11+
`pytest-dependency` plugin
12+
- experimental: added configuration option for ordering all dependencies
13+
defined by the `pytest-dependency` plugin
14+
15+
### Infrastructure
16+
- added list of open issues in `pytest-ordering` with respective state
17+
in `pytest-order`
18+
1119
## [Version 0.8.1](https://pypi.org/project/pytest-order/0.8.1/) (2020-11-02)
1220

1321
### Added

docs/source/index.rst

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,8 @@ question is ordered behind all other tests.
275275
The `pytest-dependency <https://pypi.org/project/pytest-dependency/>`__
276276
plugin also manages dependencies between tests (skips tests that depend
277277
on skipped or failed tests), but doesn't do any ordering. You can combine
278-
both plugins if you need both options.
278+
both plugins if you need both options--see :ref:`order-dependencies`
279+
below for more information.
279280

280281
Configuration
281282
=============
@@ -379,6 +380,55 @@ then from the end if there are negative numbers, and the rest will be in
379380
between (e.g. between positive and negative numbers), as it is without this
380381
option.
381382

383+
.. _order-dependencies:
384+
385+
``--order-dependencies``
386+
------------------------
387+
This defines the behavior if the ``pytest-dependency`` plugin is used.
388+
By default, ``dependency`` marks are only considered if they coexist with an
389+
``order`` mark. In this case it is checked if the ordering would break the
390+
dependency, and is ignored if this is the case. Consider the following:
391+
392+
.. code:: python
393+
394+
import pytest
395+
396+
def test_a():
397+
assert True
398+
399+
@pytest.mark.dependency(depends=['test_a'])
400+
@pytest.mark.order("first")
401+
def test_b():
402+
assert True
403+
404+
In this case, the ordering would break the dependency and is therefore
405+
ignored. This behavior is independent of the option. Now consider the
406+
following tests:
407+
408+
.. code:: python
409+
410+
import pytest
411+
412+
@pytest.mark.dependency(depends=['test_b'])
413+
def test_a():
414+
assert True
415+
416+
def test_b():
417+
assert True
418+
419+
By default, ``test_a`` is not run, because it depends on ``test_b``, which
420+
is only run after ``test_b``. If you use ``--order-dependencies``, this will
421+
change--the tests will now be reordered according to the dependency and both
422+
run. Note that a similar feature may be added to ``pytest-dependency`` -
423+
if this is done, this option will not be needed, but for the time being you
424+
can use both plugins together to get this behavior.
425+
Note that ``pytest-order`` does not replace ``pytest-dependency``--it just
426+
adds ordering to the existing functionality if needed.
427+
428+
.. note::
429+
This feature is considered experimental. It will not handle all cases of
430+
defined dependencies. If there is sufficient demand (reflected in issues),
431+
this may be expanded.
382432

383433
Miscellaneous
384434
=============

old_issues.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ Tracks the state of all open issues in pytest-ordering for reference.
3737
- [Unknown mark warning](https://github.com/ftobia/pytest-ordering/issues/57)
3838
obsolete with registered marker :heavy_check_mark:
3939
- [pytest-ordering doesn't honor test dependencies](https://github.com/ftobia/pytest-ordering/issues/58)
40-
unclear what we can do here - maybe handle dependency markers like
41-
before/after markers? :thought_balloon:
40+
- ignore ordering if it would break a dependency
41+
- added configuration option for ordering all dependencies :heavy_check_mark:
4242
- [should pytest-ordering be deprecated in favor of pytest-dependency?](https://github.com/ftobia/pytest-ordering/issues/59)
4343
has been answered (`pytest-dependency` does not support ordering) :heavy_check_mark:
4444
- [py.test ordering doesn't works when methods with order greater than 9 are present](https://github.com/ftobia/pytest-ordering/issues/61)

pytest_order/__init__.py

Lines changed: 59 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ def pytest_addoption(parser):
7777
dest="sparse_ordering",
7878
help="If there are gaps between ordinals they are filled "
7979
"with unordered tests.")
80+
group.addoption("--order-dependencies", action="store_true",
81+
dest="order_dependencies",
82+
help="If set, dependencies added by pytest-dependency will"
83+
"be ordered if needed.")
8084

8185

8286
class OrderingPlugin(object):
@@ -88,14 +92,50 @@ class OrderingPlugin(object):
8892
"""
8993

9094

95+
class Settings:
96+
sparse_ordering = False
97+
order_dependencies = False
98+
scope = "session"
99+
100+
@classmethod
101+
def initialize(cls, config):
102+
cls.sparse_ordering = config.getoption("sparse_ordering")
103+
cls.order_dependencies = config.getoption("order_dependencies")
104+
scope = config.getoption("order_scope")
105+
if scope in ("session", "module", "class"):
106+
cls.scope = scope
107+
else:
108+
if scope is not None:
109+
warn("Unknown order scope '{}', ignoring it. "
110+
"Valid scopes are 'session', 'module' and 'class'."
111+
.format(scope))
112+
cls.scope = "session"
113+
114+
91115
def get_filename(item):
92116
name = item.location[0]
93117
if os.sep in name:
94118
name = item.location[0].rsplit(os.sep, 1)[1]
95119
return name[:-3]
96120

97121

98-
def mark_binning(item, keys, start, end, before, after, unordered):
122+
def mark_binning(item, keys, start, end, before, after, unordered, alias):
123+
if ("dependency" in keys and
124+
(Settings.order_dependencies or "order" in keys)):
125+
# always order dependencies if an order mark is present
126+
# otherwise only if order-dependencies is set
127+
mark = item.get_closest_marker("dependency")
128+
prefix = get_filename(item) + "."
129+
dependent_mark = mark.kwargs.get("depends")
130+
if dependent_mark:
131+
for name in dependent_mark:
132+
if "." not in name:
133+
name = prefix + name
134+
after.setdefault(name, []).append(item)
135+
name_mark = mark.kwargs.get("name")
136+
if name_mark:
137+
alias[prefix + name_mark] = prefix + item.name
138+
99139
if "order" in keys:
100140
mark = item.get_closest_marker("order")
101141
order = mark.args[0] if mark.args else None
@@ -156,22 +196,22 @@ def insert_after(name, items, sort):
156196
return False
157197

158198

159-
def do_modify_items(items, sparse_ordering):
199+
def do_modify_items(items):
160200
before_item = {}
161201
after_item = {}
162202
start_item = {}
163203
end_item = {}
164204
unordered_list = []
205+
alias_names = {}
165206

166207
for item in items:
167208
mark_binning(item, item.keywords.keys(), start_item, end_item,
168-
before_item, after_item, unordered_list)
209+
before_item, after_item, unordered_list, alias_names)
169210

170211
start_item = sorted(start_item.items())
171212
end_item = sorted(end_item.items())
172213

173-
sorted_list = sort_numbered_items(start_item, end_item, unordered_list,
174-
sparse_ordering)
214+
sorted_list = sort_numbered_items(start_item, end_item, unordered_list)
175215

176216
still_left = 0
177217
length = len(before_item) + len(after_item)
@@ -187,7 +227,8 @@ def do_modify_items(items, sparse_ordering):
187227

188228
remove_labels = []
189229
for label, after in after_item.items():
190-
if insert_after(label, after, sorted_list):
230+
name = alias_names[label] if label in alias_names else label
231+
if insert_after(name, after, sorted_list):
191232
remove_labels.append(label)
192233
for label in remove_labels:
193234
del after_item[label]
@@ -207,11 +248,11 @@ def do_modify_items(items, sparse_ordering):
207248
return sorted_list
208249

209250

210-
def sort_numbered_items(start_item, end_item, unordered_list, sparse_ordering):
251+
def sort_numbered_items(start_item, end_item, unordered_list):
211252
sorted_list = []
212253
index = 0
213254
for entries in start_item:
214-
if sparse_ordering:
255+
if Settings.sparse_ordering:
215256
while entries[0] > index and unordered_list:
216257
sorted_list.append(unordered_list.pop(0))
217258
index += 1
@@ -220,38 +261,28 @@ def sort_numbered_items(start_item, end_item, unordered_list, sparse_ordering):
220261
mid_index = len(sorted_list)
221262
index = -1
222263
for entries in reversed(end_item):
223-
if sparse_ordering:
264+
if Settings.sparse_ordering:
224265
while entries[0] < index and unordered_list:
225266
sorted_list.insert(mid_index, unordered_list.pop())
226267
index -= 1
227-
for item in reversed(entries[1]):
228-
sorted_list.insert(mid_index, item)
229-
index += len(entries[1])
230-
for unordered_item in reversed(unordered_list):
231-
sorted_list.insert(mid_index, unordered_item)
268+
sorted_list[mid_index:mid_index] = entries[1]
269+
index -= len(entries[1])
270+
sorted_list[mid_index:mid_index] = unordered_list
232271
return sorted_list
233272

234273

235274
def modify_items(session, config, items):
236-
sparse_ordering = config.getoption("sparse_ordering")
237-
scope = config.getoption("order_scope")
238-
if scope not in ("session", "module", "class"):
239-
if scope is not None:
240-
warn("Unknown order scope '{}', ignoring it. "
241-
"Valid scopes are 'session', 'module' and 'class'."
242-
.format(scope))
243-
scope = "session"
244-
if scope == "session":
245-
sorted_list = do_modify_items(items, sparse_ordering)
246-
elif scope == "module":
275+
Settings.initialize(config)
276+
if Settings.scope == "session":
277+
sorted_list = do_modify_items(items)
278+
elif Settings.scope == "module":
247279
module_items = OrderedDict()
248280
for item in items:
249281
module_path = item.nodeid[:item.nodeid.index("::")]
250282
module_items.setdefault(module_path, []).append(item)
251283
sorted_list = []
252284
for module_item_list in module_items.values():
253-
sorted_list.extend(do_modify_items(
254-
module_item_list, sparse_ordering))
285+
sorted_list.extend(do_modify_items(module_item_list))
255286
else: # class scope
256287
class_items = OrderedDict()
257288
for item in items:
@@ -262,7 +293,6 @@ def modify_items(session, config, items):
262293
class_items.setdefault(class_path, []).append(item)
263294
sorted_list = []
264295
for class_item_list in class_items.values():
265-
sorted_list.extend(do_modify_items(
266-
class_item_list, sparse_ordering))
296+
sorted_list.extend(do_modify_items(class_item_list))
267297

268298
items[:] = sorted_list

tests/test_dependency.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# -*- coding: utf-8 -*-
2+
3+
import pytest
4+
5+
import pytest_order
6+
from utils import write_test, assert_test_order
7+
8+
9+
def test_ignore_order_with_dependency(test_path, capsys):
10+
tests_content = """
11+
import pytest
12+
13+
def test_a():
14+
pass
15+
16+
@pytest.mark.dependency(depends=['test_a'])
17+
@pytest.mark.order("first")
18+
def test_b():
19+
pass
20+
"""
21+
write_test(test_path, tests_content)
22+
pytest.main(["-v", test_path], [pytest_order])
23+
out, err = capsys.readouterr()
24+
assert_test_order(["test_a", "test_b"], out)
25+
26+
27+
def test_order_with_dependency(test_path, capsys):
28+
tests_content = """
29+
import pytest
30+
31+
@pytest.mark.dependency(depends=['test_b'])
32+
@pytest.mark.order("second")
33+
def test_a():
34+
pass
35+
36+
def test_b():
37+
pass
38+
"""
39+
write_test(test_path, tests_content)
40+
pytest.main(["-v", test_path], [pytest_order])
41+
out, err = capsys.readouterr()
42+
assert_test_order(["test_b", "test_a"], out)
43+
44+
45+
def test_dependency_already_ordered(test_path, capsys):
46+
tests_content = """
47+
import pytest
48+
49+
def test_a():
50+
pass
51+
52+
@pytest.mark.dependency(depends=['test_a'])
53+
def test_b():
54+
pass
55+
"""
56+
write_test(test_path, tests_content)
57+
pytest.main(["-v", test_path], [pytest_order])
58+
out, err = capsys.readouterr()
59+
assert_test_order(["test_a", "test_b"], out)
60+
pytest.main(["-v", "--order-dependencies", test_path], [pytest_order])
61+
out, err = capsys.readouterr()
62+
assert_test_order(["test_a", "test_b"], out)
63+
64+
65+
def test_order_dependency(test_path, capsys):
66+
tests_content = """
67+
import pytest
68+
69+
@pytest.mark.dependency(depends=['test_b'])
70+
def test_a():
71+
pass
72+
73+
def test_b():
74+
pass
75+
"""
76+
write_test(test_path, tests_content)
77+
pytest.main(["-v", test_path], [pytest_order])
78+
out, err = capsys.readouterr()
79+
assert_test_order(["test_a", "test_b"], out)
80+
pytest.main(["-v", "--order-dependencies", test_path], [pytest_order])
81+
out, err = capsys.readouterr()
82+
assert_test_order(["test_b", "test_a"], out)
83+
84+
85+
def test_order_multiple_dependencies(test_path, capsys):
86+
tests_content = """
87+
import pytest
88+
89+
@pytest.mark.dependency(depends=["test_b", "test_c"])
90+
def test_a():
91+
pass
92+
93+
def test_b():
94+
pass
95+
96+
def test_c():
97+
pass
98+
"""
99+
write_test(test_path, tests_content)
100+
pytest.main(["-v", test_path], [pytest_order])
101+
out, err = capsys.readouterr()
102+
assert_test_order(["test_a", "test_b", "test_c"], out)
103+
pytest.main(["-v", "--order-dependencies", test_path], [pytest_order])
104+
out, err = capsys.readouterr()
105+
assert_test_order(["test_b", "test_c", "test_a"], out)
106+
107+
108+
def test_order_named_dependency(test_path, capsys):
109+
tests_content = """
110+
import pytest
111+
112+
@pytest.mark.dependency(depends=["my_test"])
113+
def test_a():
114+
pass
115+
116+
@pytest.mark.dependency(name="my_test")
117+
def test_b():
118+
pass
119+
120+
def test_c():
121+
pass
122+
"""
123+
write_test(test_path, tests_content)
124+
pytest.main(["-v", test_path], [pytest_order])
125+
out, err = capsys.readouterr()
126+
assert_test_order(["test_a", "test_b", "test_c"], out)
127+
pytest.main(["-v", "--order-dependencies", test_path], [pytest_order])
128+
out, err = capsys.readouterr()
129+
assert_test_order(["test_b", "test_a", "test_c"], out)

0 commit comments

Comments
 (0)