Skip to content

Commit 0cf2cd2

Browse files
committed
Add option group-order-scope for hierarchical ordering
- currently only supports ordinal order markers - see #6
1 parent 4a43b3b commit 0cf2cd2

File tree

5 files changed

+457
-23
lines changed

5 files changed

+457
-23
lines changed

CHANGELOG.md

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

33
## Unreleased
44

5+
### Added
6+
- added `group-order-scope` option to allow hierarchical ordering on module
7+
and class scope.
8+
See [#6](https://github.com/mrbean-bremen/pytest-order/issues/6)
9+
510
## [Version 0.9.4](https://pypi.org/project/pytest-order/0.9.4/) (2021-01-27)
611
Patch release to make packaging easier.
712

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ Features
7878
- ordering of tests relative to each other (via the `before` and `after`
7979
marker attributes)
8080
- session-, module- and class-scope ordering via the ``order-scope`` option
81+
- hierarchical ordering via the ``group-order-scope`` option
8182
- ordering tests with `pytest-dependency` markers if using the
8283
``order-dependencies`` option
8384
- sparse ordering of tests via the ``sparse-ordering`` option

docs/source/index.rst

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,107 @@ Here is what you get using session and module-scoped sorting:
479479
tests/test_module2.py:5: test2 PASSED
480480

481481

482+
``--order-group-scope``
483+
-----------------------
484+
This option is also related to the order scope. It defines the scope inside
485+
which tests may be reordered. Consider you have several test modules which
486+
you want to order, but you don't want to mix the tests of several modules
487+
because the module setup is costly. In this case you can set the group order
488+
scope to "module", meaning that first the tests are ordered inside each
489+
module (the same as with the module order scope), but afterwards the modules
490+
themselves are sorted without changing the order inside each module.
491+
492+
Consider these two test modules:
493+
494+
**tests/test_module1.py**:
495+
496+
.. code::
497+
498+
import pytest
499+
500+
@pytest.mark.order(2)
501+
def test1():
502+
pass
503+
504+
def test2():
505+
pass
506+
507+
**tests/test_module2.py**:
508+
509+
.. code::
510+
511+
import pytest
512+
513+
@pytest.mark.order(1)
514+
def test1():
515+
pass
516+
517+
def test2():
518+
pass
519+
520+
Here is what you get using different scopes:
521+
522+
::
523+
524+
$ pytest tests -vv
525+
============================= test session starts ==============================
526+
...
527+
528+
tests/test_module2.py:9: test1 PASSED
529+
tests/test_module1.py:9: test1 PASSED
530+
tests/test_module1.py:5: test2 PASSED
531+
tests/test_module2.py:5: test2 PASSED
532+
533+
534+
::
535+
536+
$ pytest tests -vv --order-scope=module
537+
============================= test session starts ==============================
538+
...
539+
540+
tests/test_module1.py:9: test1 PASSED
541+
tests/test_module1.py:5: test2 PASSED
542+
tests/test_module2.py:9: test1 PASSED
543+
tests/test_module2.py:5: test2 PASSED
544+
545+
546+
::
547+
548+
$ pytest tests -vv --order-group-scope=module
549+
============================= test session starts ==============================
550+
...
551+
552+
tests/test_module2.py:9: test1 PASSED
553+
tests/test_module2.py:5: test2 PASSED
554+
tests/test_module1.py:9: test1 PASSED
555+
tests/test_module1.py:5: test2 PASSED
556+
557+
The ordering of the module groups is done based on the lowest
558+
non-negative order number present in the module (e.g. the order number of
559+
the first test). If only negative numbers are present, the highest negative
560+
number (e.g. the number of the last test) is used, and these modules will be
561+
ordered at the end. Modules without order numbers will be sorted between
562+
modules with a non-negative order number and modules with a negative order
563+
number, the same way tests are sorted inside a module.
564+
565+
The group order scope defaults to the order scope. In this case the tests are
566+
ordered the same way as without the group order scope. The setting takes effect
567+
only if the scope is less than the order scope, e.g. there are three
568+
possibilities:
569+
570+
- order scope "session", order group scope "module" - this is shown in the
571+
example above: first tests in eac module are ordered, afterwards the modules
572+
- order scope "module", order group scope "class" - first orders tests inside
573+
each class, then the classes inside each module
574+
- order scope "session", order group scope "class" - first orders tests inside
575+
each class, then the classes inside each module, and finally the modules
576+
relatively to each other
577+
578+
.. note::
579+
This option currently does not work with relative markers - respective
580+
support may be added later. It will also not work together with the sparse
581+
ordering option.
582+
482583
``--indulgent-ordering``
483584
------------------------
484585
You may sometimes find that you want to suggest an ordering of tests, while

pytest_order/__init__.py

Lines changed: 129 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88

99
from ._version import __version__ # noqa: F401
1010

11+
# replace by Enum class after Python 2 support is gone
12+
CLASS = 1
13+
MODULE = 2
14+
SESSION = 3
15+
1116
orders_map = {
1217
"first": 0,
1318
"second": 1,
@@ -50,7 +55,7 @@ def pytest_configure(config):
5055
# manually replace it.
5156
# Python 2.7 didn"t allow arbitrary attributes on methods, so we have
5257
# to keep the function as a function and then add it to the class as a
53-
# pseudomethod. Since the class is purely for structuring and `self`
58+
# pseudo method. Since the class is purely for structuring and `self`
5459
# is never referenced, this seems reasonable.
5560
OrderingPlugin.pytest_collection_modifyitems = pytest.hookimpl(
5661
function=modify_items, tryfirst=True)
@@ -71,7 +76,14 @@ def pytest_addoption(parser):
7176
group.addoption("--order-scope", action="store",
7277
dest="order_scope",
7378
help="Defines the scope used for ordering. Possible values"
74-
"are 'session' (default), 'module', and 'class'")
79+
"are 'session' (default), 'module', and 'class'."
80+
"Ordering is only done inside a scope.")
81+
group.addoption("--order-group-scope", action="store",
82+
dest="order_group_scope",
83+
help="Defines the scope used for order groups. Possible "
84+
"values are 'session' (default), 'module', "
85+
"and 'class'. Ordering is first done inside a group, "
86+
"then between groups.")
7587
group.addoption("--sparse-ordering", action="store_true",
7688
dest="sparse_ordering",
7789
help="If there are gaps between ordinals they are filled "
@@ -94,21 +106,40 @@ class OrderingPlugin(object):
94106
class Settings:
95107
sparse_ordering = False
96108
order_dependencies = False
97-
scope = "session"
109+
scope = SESSION
110+
group_scope = SESSION
111+
112+
valid_scopes = {
113+
"class": CLASS,
114+
"module": MODULE,
115+
"session": SESSION
116+
}
98117

99118
@classmethod
100119
def initialize(cls, config):
101120
cls.sparse_ordering = config.getoption("sparse_ordering")
102121
cls.order_dependencies = config.getoption("order_dependencies")
103122
scope = config.getoption("order_scope")
104-
if scope in ("session", "module", "class"):
105-
cls.scope = scope
123+
if scope in cls.valid_scopes:
124+
cls.scope = cls.valid_scopes[scope]
106125
else:
107126
if scope is not None:
108127
warn("Unknown order scope '{}', ignoring it. "
109128
"Valid scopes are 'session', 'module' and 'class'."
110129
.format(scope))
111-
cls.scope = "session"
130+
cls.scope = SESSION
131+
group_scope = config.getoption("order_group_scope")
132+
if group_scope in cls.valid_scopes:
133+
cls.group_scope = cls.valid_scopes[group_scope]
134+
else:
135+
if group_scope is not None:
136+
warn("Unknown order group scope '{}', ignoring it. "
137+
"Valid scopes are 'session', 'module' and 'class'."
138+
.format(group_scope))
139+
cls.group_scope = cls.scope
140+
if cls.group_scope > cls.scope:
141+
warn("Group scope is larger than order scope, ignoring it.")
142+
cls.group_scope = cls.scope
112143

113144

114145
def full_name(item, name=None):
@@ -212,6 +243,64 @@ def insert_after(name, items, sort):
212243
return False
213244

214245

246+
def sorted_groups(groups):
247+
start_groups = []
248+
middle_groups = []
249+
end_groups = []
250+
# TODO: handle relative markers
251+
for group in groups:
252+
if group[0] is None:
253+
middle_groups.append(group[1])
254+
elif group[0] >= 0:
255+
start_groups.append(group)
256+
else:
257+
end_groups.append(group)
258+
259+
start_groups = sorted(start_groups)
260+
end_groups = sorted(end_groups)
261+
groups_sorted = [group[1] for group in start_groups]
262+
groups_sorted.extend(middle_groups)
263+
groups_sorted.extend([group[1] for group in end_groups])
264+
if start_groups:
265+
group_order = start_groups[0][0]
266+
elif end_groups:
267+
group_order = end_groups[-1][0]
268+
else:
269+
group_order = None
270+
return group_order, groups_sorted
271+
272+
273+
def modify_item_groups(items):
274+
if Settings.group_scope < Settings.scope:
275+
sorted_list = []
276+
if Settings.scope == SESSION:
277+
module_items = module_item_groups(items)
278+
module_groups = []
279+
if Settings.group_scope == CLASS:
280+
for module_item in module_items.values():
281+
class_items = class_item_groups(module_item)
282+
class_groups = [do_modify_items(item) for item in
283+
class_items.values()]
284+
module_group = []
285+
group_order, class_groups = sorted_groups(class_groups)
286+
for group in class_groups:
287+
module_group.extend(group)
288+
module_groups.append((group_order, module_group))
289+
else:
290+
module_groups = [do_modify_items(item) for item in
291+
module_items.values()]
292+
for group in sorted_groups(module_groups)[1]:
293+
sorted_list.extend(group)
294+
else: # module scope / class group scope
295+
class_items = class_item_groups(items)
296+
class_groups = [do_modify_items(item) for item in
297+
class_items.values()]
298+
for group in sorted_groups(class_groups)[1]:
299+
sorted_list.extend(group)
300+
return sorted_list
301+
return do_modify_items(items)[1]
302+
303+
215304
def do_modify_items(items):
216305
before_item = {}
217306
after_item = {}
@@ -272,7 +361,14 @@ def do_modify_items(items):
272361
sys.stdout.flush()
273362
print("enqueue them behind the others")
274363

275-
return sorted_list
364+
if start_item:
365+
group_order = start_item[0][0]
366+
elif end_item:
367+
group_order = end_item[-1][0]
368+
else:
369+
group_order = None
370+
371+
return group_order, sorted_list
276372

277373

278374
def sort_numbered_items(start_item, end_item, unordered_list):
@@ -300,26 +396,36 @@ def sort_numbered_items(start_item, end_item, unordered_list):
300396

301397
def modify_items(session, config, items):
302398
Settings.initialize(config)
303-
if Settings.scope == "session":
304-
sorted_list = do_modify_items(items)
305-
elif Settings.scope == "module":
306-
module_items = OrderedDict()
307-
for item in items:
308-
module_path = item.nodeid[:item.nodeid.index("::")]
309-
module_items.setdefault(module_path, []).append(item)
399+
if Settings.scope == SESSION:
400+
sorted_list = modify_item_groups(items)
401+
elif Settings.scope == MODULE:
402+
module_items = module_item_groups(items)
310403
sorted_list = []
311404
for module_item_list in module_items.values():
312-
sorted_list.extend(do_modify_items(module_item_list))
405+
sorted_list.extend(modify_item_groups(module_item_list))
313406
else: # class scope
314-
class_items = OrderedDict()
315-
for item in items:
316-
delim_index = item.nodeid.index("::")
317-
if "::" in item.nodeid[delim_index + 2:]:
318-
delim_index = item.nodeid.index("::", delim_index + 2)
319-
class_path = item.nodeid[:delim_index]
320-
class_items.setdefault(class_path, []).append(item)
407+
class_items = class_item_groups(items)
321408
sorted_list = []
322409
for class_item_list in class_items.values():
323-
sorted_list.extend(do_modify_items(class_item_list))
410+
sorted_list.extend(modify_item_groups(class_item_list))
324411

325412
items[:] = sorted_list
413+
414+
415+
def module_item_groups(items):
416+
module_items = OrderedDict()
417+
for item in items:
418+
module_path = item.nodeid[:item.nodeid.index("::")]
419+
module_items.setdefault(module_path, []).append(item)
420+
return module_items
421+
422+
423+
def class_item_groups(items):
424+
class_items = OrderedDict()
425+
for item in items:
426+
delim_index = item.nodeid.index("::")
427+
if "::" in item.nodeid[delim_index + 2:]:
428+
delim_index = item.nodeid.index("::", delim_index + 2)
429+
class_path = item.nodeid[:delim_index]
430+
class_items.setdefault(class_path, []).append(item)
431+
return class_items

0 commit comments

Comments
 (0)