Skip to content

Commit bb31b3c

Browse files
committed
Adds a way to disable shuffling of tests in a module
1 parent cd0ef41 commit bb31b3c

File tree

5 files changed

+154
-16
lines changed

5 files changed

+154
-16
lines changed

README.rst

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,29 @@ pytest-random-order
77
This is a pytest plugin **to randomise the order** in which tests are run **with some control**
88
over how much randomness one allows.
99

10-
It is a good idea to randomise the order in which your tests run
11-
because a test running early as part of a larger suite of tests may have left
10+
Why?
11+
----
12+
13+
It is a good idea to shuffle the order in which your tests run
14+
because a test running early as part of a larger test suite may be leaving
1215
the system under test in a particularly fortunate state for a subsequent test to pass.
1316

14-
How Random
15-
----------
17+
How?
18+
----
1619

1720
**pytest-random-order** groups tests in buckets, shuffles them within buckets and then shuffles the buckets.
1821

1922
You can choose from four types of buckets:
2023

21-
* ``class``
22-
* ``module`` - **this is the default setting**
23-
* ``package``
24-
* ``global`` - all tests fall in the same bucket, full randomness, tests probably take longer to run
24+
``class``
25+
26+
``module``
27+
the default setting
28+
29+
``package``
30+
31+
``global``
32+
all tests fall in the same bucket, full randomness, tests probably take longer to run
2533

2634
If you have three buckets of tests ``A``, ``B``, and ``C`` with three tests ``1`` and ``2``, and ``3`` in each of them,
2735
then here are just two of many potential orderings that non-global randomisation can produce:
@@ -43,6 +51,9 @@ By default, your tests will be randomised at ``module`` level which means that
4351
tests within a single module X will be executed in no particular order, but tests from
4452
other modules will not be mixed in between tests of module X.
4553

54+
The plugin also supports **disabling shuffle on module basis** irrespective of the bucket type
55+
chosen for the test run. See Advanced Options below.
56+
4657
----
4758

4859
Installation
@@ -56,7 +67,8 @@ Installation
5667
Usage
5768
-----
5869

59-
The plugin is enabled by default. To randomise the order of tests within modules, just run pytest as always:
70+
The plugin **is enabled by default**.
71+
To randomise the order of tests within modules, just run pytest as always:
6072

6173
::
6274

@@ -87,6 +99,27 @@ pass undeservedly, you can disable it:
8799
$ pytest -p no:random-order -v
88100

89101

102+
Advanced Options
103+
----------------
104+
105+
Disable Shuffling In a Module
106+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
107+
108+
You can disable shuffling of tests within a single module by placing a pytest marker in the module:
109+
110+
::
111+
112+
pytest.mark.random_order_disabled = True
113+
114+
def test_number_one():
115+
pass
116+
117+
def test_number_two():
118+
pass
119+
120+
No matter what will be the bucket type for the test run, ``test_number_one`` will always run
121+
before ``test_number_two``.
122+
90123
License
91124
-------
92125

pytest_random_order.py

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import sys
55
import traceback
66

7+
import operator
8+
79

810
def pytest_addoption(parser):
911
group = parser.getgroup('random-order')
@@ -35,7 +37,7 @@ def pytest_report_header(config):
3537
}
3638

3739

38-
def _shuffle_items(items, key=None, preserve_bucket_order=False):
40+
def _shuffle_items(items, key=None, disable=None, preserve_bucket_order=False):
3941
"""
4042
Shuffles `items`, a list, in place.
4143
@@ -47,26 +49,46 @@ def _shuffle_items(items, key=None, preserve_bucket_order=False):
4749
Bucket defines the boundaries across which tests will not
4850
be reordered.
4951
52+
If `disable` is function and returns True for ALL items
53+
in a bucket, items in this bucket will remain in their original order.
54+
5055
`preserve_bucket_order` is only customisable for testing purposes.
5156
There is no use case for predefined bucket order, is there?
5257
"""
5358

5459
# If `key` is falsey, shuffle is global.
55-
if not key:
60+
if not key and not disable:
5661
random.shuffle(items)
5762
return
5863

64+
# Use (key(x), disable(x)) as the key because
65+
# when we have a bucket type like package over a disabled module, we must
66+
# not shuffle the disabled module items.
67+
def full_key(x):
68+
if key and disable:
69+
return key(x), disable(x)
70+
elif disable:
71+
return disable(x)
72+
else:
73+
return key(x)
74+
5975
buckets = []
6076
this_key = '__not_initialised__'
6177
for item in items:
6278
prev_key = this_key
63-
this_key = key(item)
79+
this_key = full_key(item)
6480
if this_key != prev_key:
6581
buckets.append([])
6682
buckets[-1].append(item)
6783

68-
# Shuffle within bucket
84+
# Shuffle within bucket unless disable(item) evaluates to True for
85+
# the first item in the bucket.
86+
# This assumes that whoever supplied disable function knows this requirement.
87+
# Fixation of individual items in an otherwise shuffled bucket
88+
# is not supported.
6989
for bucket in buckets:
90+
if callable(disable) and disable(bucket[0]):
91+
continue
7092
random.shuffle(bucket)
7193

7294
# Shuffle buckets
@@ -85,13 +107,28 @@ def _get_set_of_item_ids(items):
85107
return s
86108

87109

110+
_is_random_order_disabled = operator.attrgetter('pytest.mark.random_order_disabled')
111+
112+
113+
def _disable(item):
114+
try:
115+
if _is_random_order_disabled(item.module):
116+
# It is not enough to return just True because in case the shuffling
117+
# is disabled on module, we must preserve the module unchanged
118+
# even when the bucket type for this test run is say package or global.
119+
return item.module.__name__
120+
except AttributeError:
121+
return False
122+
123+
88124
def pytest_collection_modifyitems(session, config, items):
89125
failure = None
126+
90127
item_ids = _get_set_of_item_ids(items)
91128

92129
try:
93-
shuffle_mode = config.getoption('random_order_bucket')
94-
_shuffle_items(items, key=_random_order_item_keys[shuffle_mode])
130+
bucket_type = config.getoption('random_order_bucket')
131+
_shuffle_items(items, key=_random_order_item_keys[bucket_type], disable=_disable)
95132

96133
except Exception as e:
97134
# If the number of items is still the same, we assume that we haven't messed up too hard

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def read(fname):
1313

1414
setup(
1515
name='pytest-random-order',
16-
version='0.3.0',
16+
version='0.3.1',
1717
author='Jazeps Basko',
1818
author_email='[email protected]',
1919
maintainer='Jazeps Basko',

tests/test_markers.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import pytest
2+
3+
4+
@pytest.fixture
5+
def twenty_tests():
6+
code = []
7+
for i in range(20):
8+
code.append('def test_a{}(): assert True\n'.format(str(i).zfill(2)))
9+
return ''.join(code)
10+
11+
12+
@pytest.mark.parametrize('disabled', [True, False])
13+
def test_pytest_mark_random_order_disabled(testdir, twenty_tests, get_test_calls, disabled):
14+
testdir.makepyfile(
15+
'import pytest\n' +
16+
'pytest.mark.random_order_disabled = {}\n'.format(disabled) +
17+
twenty_tests
18+
)
19+
20+
result = testdir.runpytest('--random-order-bucket=module', '-v')
21+
result.assert_outcomes(passed=20)
22+
names = [c.name for c in get_test_calls(testdir.runpytest())]
23+
sorted_names = sorted(list(names))
24+
25+
if disabled:
26+
assert names == sorted_names
27+
else:
28+
assert names != sorted_names

tests/test_shuffle.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ def oddity_key(x):
1212
return x % 2
1313

1414

15+
def disable_if_gt_1000(x):
16+
# if disable returns a truthy value, it must also be usable as a key
17+
return (x > 1000), (x // 1000)
18+
19+
1520
@pytest.mark.parametrize('key', [
1621
None,
1722
lambda x: None,
@@ -95,3 +100,38 @@ def test_shuffles_buckets():
95100
assert items[0] == items[1]
96101
assert items[2] == items[3]
97102
assert items[-1] == items[-2]
103+
104+
105+
def test_shuffle_respects_single_disabled_group_in_each_of_two_buckets():
106+
items = [
107+
11, 13, 9995, 9997, 19, 21, 23, 25, 27, 29, # bucket 1 -- odd numbers
108+
12, 14, 9996, 9998, 20, 22, 24, 26, 28, 30, # bucket 2 -- even numbers
109+
]
110+
items_copy = list(items)
111+
112+
_shuffle_items(items, key=oddity_key, disable=disable_if_gt_1000)
113+
114+
assert items != items_copy
115+
assert items.index(9995) + 1 == items.index(9997)
116+
assert items.index(9996) + 1 == items.index(9998)
117+
118+
119+
def test_shuffle_respects_two_distinct_disabled_groups_in_one_bucket():
120+
# all items are in one oddity bucket, but the two groups
121+
# of large numbers are separate because they are disabled
122+
# from two different units.
123+
# This is simulating two disabled modules within same package.
124+
# The two modules shouldn't be mixed up in one bucket.
125+
items = [
126+
11, 13, 8885, 8887, 8889, 21, 23, 9995, 9997, 9999,
127+
]
128+
items_copy = list(items)
129+
130+
for i in range(5):
131+
_shuffle_items(items, key=oddity_key, disable=disable_if_gt_1000)
132+
if items != items_copy:
133+
assert items[items.index(8885):items.index(8885) + 3] == [8885, 8887, 8889]
134+
assert items[items.index(9995):items.index(9995) + 3] == [9995, 9997, 9999]
135+
return
136+
137+
assert False

0 commit comments

Comments
 (0)