Skip to content

Commit 292ce54

Browse files
committed
Add option for sparse ordering behavior
1 parent 6be3bfe commit 292ce54

File tree

6 files changed

+318
-102
lines changed

6 files changed

+318
-102
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
## Unreleased
44

5+
### Added
6+
- added configuration option for sparse sorting, e.g. the possibility to
7+
fill gaps between ordinals with unordered tests (see also
8+
[this issue](https://github.com/ftobia/pytest-ordering/issues/14) in
9+
`pytest-ordering`)
10+
511
## [Version 0.8.1](https://pypi.org/project/pytest-order/0.8.1/) (2020-11-02)
612

713
### Added

docs/source/index.rst

Lines changed: 49 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ Relationship with pytest-ordering
1717
``pytest-order`` is a fork of
1818
`pytest-ordering <https://github.com/ftobia/pytest-ordering>`__, which is
1919
not maintained anymore. The idea and most of the code has been created by
20-
Frank Tobia, the author of that plugin.
20+
Frank Tobia, the author of that plugin, and contributors to the project.
2121

2222
However, ``pytest-order`` is not compatible with ``pytest-ordering`` due to the
2323
changed marker name (``order`` instead of ``run``) and the removal of all
24-
other special markers for consistence (see
24+
other special markers for consistence (as has been discussed in
2525
`this issue <https://github.com/ftobia/pytest-ordering/issues/38>`__). This
2626
also avoids clashes between the plugins if they are both installed.
2727

@@ -279,9 +279,9 @@ question is ordered behind all other tests.
279279

280280
Configuration
281281
=============
282-
Currently there are two command line option that change the behavior of the
283-
plugin. As for any option, you can add the options to your ``pytest.ini`` if
284-
you want to have them always applied.
282+
There are a few command line options that change the behavior of the
283+
plugin. As with any pytest option, you can add the options to your
284+
``pytest.ini`` if you want to have them applied to all tests automatically.
285285

286286
``--indulgent-ordering``
287287
------------------------
@@ -320,40 +320,30 @@ separate test functions, these test functions are handled separately from the
320320
test classes. If a module has no test classes, the effect is the same as
321321
if using ``--order-scope=module``.
322322

323-
Miscellaneous
324-
=============
325-
326-
Usage with pytest-xdist
327-
-----------------------
328-
The ``pytest-xdist`` plugin schedules tests unordered, and the order
329-
configured by ``pytest-order`` will normally not be preserved. But
330-
if we use the ``--dist=loadfile`` option, provided by ``xdist``, all tests
331-
from one file will be run in the same thread. So, to make the two plugins work
332-
together, we have to put each group of dependent tests in one file, and call
333-
pytest with ``--dist=loadfile`` (this is taken from
334-
`this issue <https://github.com/ftobia/pytest-ordering/issues/36>`__).
335-
336-
Sparse ordinal behavior
337-
-----------------------
338-
Sparse ordering (e.g. missing some ordinals in your markers) behaves the
339-
same as if the the ordinals are consecutive. For example, these tests:
323+
``--sparse-ordering``
324+
---------------------
325+
Ordering tests by ordinals where some numbers are missing by default behaves
326+
the same as if the the ordinals are consecutive. For example, these tests:
340327

341328
.. code:: python
342329
343330
import pytest
344331
345-
@pytest.mark.order(4)
332+
@pytest.mark.order(3)
346333
def test_two():
347334
assert True
348335
349336
def test_three():
350337
assert True
351338
352-
@pytest.mark.order(2)
353-
def test_two():
339+
def test_four():
340+
assert True
341+
342+
@pytest.mark.order(1)
343+
def test_one():
354344
assert True
355345
356-
have the same output as:
346+
are executed in the same order as:
357347

358348
.. code:: python
359349
@@ -366,17 +356,42 @@ have the same output as:
366356
def test_three():
367357
assert True
368358
359+
def test_four():
360+
assert True
361+
369362
@pytest.mark.order(0)
370-
def test_two():
363+
def test_one():
371364
assert True
372365
373-
namely, the tests are run in the order ``test_one``, ``test_two``,
374-
``test_three``, regardless of the gaps between numbers, and the starting
375-
number being not 0. It would be possible to change the ordering behavior to
376-
fill the gaps between the numbers with tests without a marker, as long as
377-
any are available. However, this leads to some not very intuitive behavior,
378-
so it is currently not implemented. If there will be demand for this kind
379-
of behavior, it can be added in a later version.
366+
namely, they are run in the order ``test_one``, ``test_two``, ``test_three``
367+
and ``test_four``. The gaps between numbers, and the fact that the starting
368+
number is not 0, are ignored. This is consistent with the current behavior of
369+
``pytest-ordering``.
370+
371+
If you use the ``--sparse-ordering`` option, the behavior will change: now
372+
all missing numbers (starting with 0) are filled with unordered tests, as
373+
long as unordered tests are left. So the shown example will now order as
374+
``test_three`` (filled in for the missing number 0), ``test_one``,
375+
``test_four`` (filled in for the missing number 2), and ``test_two``. This
376+
will also work for tests with negative order numbers (or the respective names).
377+
The missing ordinals are filled with unordered tests first from the start,
378+
then from the end if there are negative numbers, and the rest will be in
379+
between (e.g. between positive and negative numbers), as it is without this
380+
option.
381+
382+
383+
Miscellaneous
384+
=============
385+
386+
Usage with pytest-xdist
387+
-----------------------
388+
The ``pytest-xdist`` plugin schedules tests unordered, and the order
389+
configured by ``pytest-order`` will normally not be preserved. But
390+
if we use the ``--dist=loadfile`` option, provided by ``xdist``, all tests
391+
from one file will be run in the same thread. So, to make the two plugins work
392+
together, we have to put each group of dependent tests in one file, and call
393+
pytest with ``--dist=loadfile`` (this is taken from
394+
`this issue <https://github.com/ftobia/pytest-ordering/issues/36>`__).
380395

381396
.. toctree::
382397
:maxdepth: 2

pytest_order/__init__.py

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def pytest_configure(config):
4444
)
4545
config.addinivalue_line("markers", config_line)
4646

47-
if config.getoption("indulgent-ordering"):
47+
if config.getoption("indulgent_ordering"):
4848
# We need to dynamically add this `tryfirst` decorator to the plugin:
4949
# only when the CLI option is present should the decorator be added.
5050
# Thus, we manually run the decorator on the class function and
@@ -65,14 +65,18 @@ def pytest_addoption(parser):
6565
"""Set up CLI option for pytest"""
6666
group = parser.getgroup("ordering")
6767
group.addoption("--indulgent-ordering", action="store_true",
68-
dest="indulgent-ordering",
68+
dest="indulgent_ordering",
6969
help="Request that the sort order provided by "
7070
"pytest-order be applied before other sorting, "
7171
"allowing the other sorting to have priority")
7272
group.addoption("--order-scope", action="store",
73-
dest="order-scope",
73+
dest="order_scope",
7474
help="Defines the scope used for ordering. Possible values"
7575
"are 'session' (default), 'module', and 'class'")
76+
group.addoption("--sparse-ordering", action="store_true",
77+
dest="sparse_ordering",
78+
help="If there are gaps between ordinals they are filled "
79+
"with unordered tests.")
7680

7781

7882
class OrderingPlugin(object):
@@ -126,14 +130,6 @@ def mark_binning(item, keys, start, end, before, after, unordered):
126130
return False
127131

128132

129-
def insert(items, sort):
130-
if isinstance(items, tuple):
131-
list_items = items[1]
132-
else:
133-
list_items = items
134-
sort += list_items
135-
136-
137133
def insert_before(name, items, sort):
138134
regex_name = re.escape(name) + r"(:?\.\w+)?$"
139135
for pos, item in enumerate(sort):
@@ -160,7 +156,7 @@ def insert_after(name, items, sort):
160156
return False
161157

162158

163-
def do_modify_items(items):
159+
def do_modify_items(items, sparse_ordering):
164160
before_item = {}
165161
after_item = {}
166162
start_item = {}
@@ -174,13 +170,8 @@ def do_modify_items(items):
174170
start_item = sorted(start_item.items())
175171
end_item = sorted(end_item.items())
176172

177-
sorted_list = []
178-
179-
for entries in start_item:
180-
insert(entries, sorted_list)
181-
insert(unordered_list, sorted_list)
182-
for entries in end_item:
183-
insert(entries, sorted_list)
173+
sorted_list = sort_numbered_items(start_item, end_item, unordered_list,
174+
sparse_ordering)
184175

185176
still_left = 0
186177
length = len(before_item) + len(after_item)
@@ -216,24 +207,51 @@ def do_modify_items(items):
216207
return sorted_list
217208

218209

210+
def sort_numbered_items(start_item, end_item, unordered_list, sparse_ordering):
211+
sorted_list = []
212+
index = 0
213+
for entries in start_item:
214+
if sparse_ordering:
215+
while entries[0] > index and unordered_list:
216+
sorted_list.append(unordered_list.pop(0))
217+
index += 1
218+
sorted_list += entries[1]
219+
index += len(entries[1])
220+
mid_index = len(sorted_list)
221+
index = -1
222+
for entries in reversed(end_item):
223+
if sparse_ordering:
224+
while entries[0] < index and unordered_list:
225+
sorted_list.insert(mid_index, unordered_list.pop())
226+
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)
232+
return sorted_list
233+
234+
219235
def modify_items(session, config, items):
220-
scope = config.getoption("order-scope")
236+
sparse_ordering = config.getoption("sparse_ordering")
237+
scope = config.getoption("order_scope")
221238
if scope not in ("session", "module", "class"):
222239
if scope is not None:
223240
warn("Unknown order scope '{}', ignoring it. "
224241
"Valid scopes are 'session', 'module' and 'class'."
225242
.format(scope))
226243
scope = "session"
227244
if scope == "session":
228-
sorted_list = do_modify_items(items)
245+
sorted_list = do_modify_items(items, sparse_ordering)
229246
elif scope == "module":
230247
module_items = OrderedDict()
231248
for item in items:
232249
module_path = item.nodeid[:item.nodeid.index("::")]
233250
module_items.setdefault(module_path, []).append(item)
234251
sorted_list = []
235252
for module_item_list in module_items.values():
236-
sorted_list.extend(do_modify_items(module_item_list))
253+
sorted_list.extend(do_modify_items(
254+
module_item_list, sparse_ordering))
237255
else: # class scope
238256
class_items = OrderedDict()
239257
for item in items:
@@ -244,6 +262,7 @@ def modify_items(session, config, items):
244262
class_items.setdefault(class_path, []).append(item)
245263
sorted_list = []
246264
for class_item_list in class_items.values():
247-
sorted_list.extend(do_modify_items(class_item_list))
265+
sorted_list.extend(do_modify_items(
266+
class_item_list, sparse_ordering))
248267

249268
items[:] = sorted_list

tests/conftest.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
@pytest.fixture
77
def item_names_for(testdir):
88
def _item_names_for(tests_content):
9-
# some strange code to extract sorted items
109
items = testdir.getitems(tests_content)
1110
hook = testdir.config.hook
1211
hook.pytest_collection_modifyitems(session=items[0].session,

0 commit comments

Comments
 (0)