Skip to content

Commit 0173fa1

Browse files
authored
Merge pull request #2 from jbasko/dev
Docs & plugin failure handling
2 parents f13e3bb + 2a19778 commit 0173fa1

File tree

7 files changed

+165
-29
lines changed

7 files changed

+165
-29
lines changed

README.rst

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,45 @@
11
pytest-random-order
22
===================================
33

4-
**pytest** plugin to randomise the order of tests within module, package, or globally.
4+
This is a **pytest** plugin to randomise the order in which tests are run with a little bit of control
5+
over how much randomness one allows.
56

6-
This plugin allows you to control the level at which the order of tests is randomised
7-
through ``--random-order-mode`` command line option.
7+
It is a good idea to randomise the order in which your tests run
8+
because a test running early as part of a larger suite of tests may have left
9+
the system under test in a particularly fortunate state for a subsequent test to pass.
10+
11+
How Random
12+
__________
13+
14+
**pytest-random-order** groups tests in buckets, shuffles them within buckets and then shuffles the buckets.
15+
16+
User can choose among four types of buckets to use:
17+
18+
* ``class``
19+
* ``module`` -- **this is the default setting**
20+
* ``package``
21+
* ``global`` -- all tests fall in the same bucket, full randomness, tests probably take longer to run
22+
23+
If you have three buckets of tests ``A``, ``B``, and ``C`` with three tests ``1`` and ``2``, and ``3`` in each of them,
24+
then here are just two of many potential orderings that non-global randomisation can produce:
25+
26+
::
27+
28+
C2 C1 C3 A3 A1 A2 B3 B2 B1
29+
30+
A2 A1 A3 C1 C2 C3 B2 B1 B3
31+
32+
As you can see, all C tests are executed "next" to each other and so are tests in buckets A and B.
33+
Tests from any bucket X are guaranteed to not be interspersed with tests from another bucket Y.
34+
For example, if you choose bucket type ``module`` then bucket X contains all tests that are in this module.
35+
36+
Note that modules (and hence tests inside those modules) that belong to package ``x.y`` do not belong
37+
to package ``x.y.z``, so they will fall in different buckets when randomising with ``package`` bucket type.
838

939
By default, your tests will be randomised at ``module`` level which means that
1040
tests within a single module X will be executed in no particular order, but tests from
1141
other modules will not be mixed in between tests of module X.
1242

13-
Similarly, you can randomise the order of tests at ``package`` and ``global`` levels.
14-
1543
----
1644

1745
Installation
@@ -25,21 +53,38 @@ Installation
2553
Usage
2654
-----
2755

56+
The plugin is enabled by default. To randomise the order of tests within modules, just run pytest as always:
57+
2858
::
2959

3060
$ pytest -v
3161

32-
$ pytest -v --random-order-mode=global
62+
It is best to start with smallest bucket type (``class`` or ``module`` depending on whether you have class-based tests),
63+
and switch to a larger bucket type when you are sure your tests handle that.
64+
65+
If your tests rely on fixtures that are module or session-scoped, more randomised order of tests will mean slower tests.
66+
You probably don't want to randomise at ``global`` or ``package`` level while you are developing and need a quick confirmation
67+
that nothing big is broken.
68+
69+
::
70+
71+
$ pytest -v --random-order-bucket=class
3372

34-
$ pytest -v --random-order-mode=package
73+
$ pytest -v --random-order-bucket=module
3574

36-
$ pytest -v --random-order-mode=module
75+
$ pytest -v --random-order-bucket=package
3776

38-
$ pytest -v --random-order-mode=class
77+
$ pytest -v --random-order-bucket=global
78+
79+
If the plugin misbehaves or you just want to assure yourself that it is not the plugin making your tests fail or
80+
pass undeservedly, you can disable it:
81+
82+
::
83+
84+
$ pytest -p no:random-order -v
3985

4086

4187
License
4288
-------
4389

4490
Distributed under the terms of the MIT license, "pytest-random-order" is free and open source software
45-

pytest_random_order.py

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
# -*- coding: utf-8 -*-
22

33
import random
4+
import sys
5+
import traceback
46

57

68
def pytest_addoption(parser):
79
group = parser.getgroup('random-order')
810
group.addoption(
9-
'--random-order-mode',
11+
'--random-order-bucket',
1012
action='store',
11-
dest='random_order_mode',
13+
dest='random_order_bucket',
1214
default='module',
1315
choices=('global', 'package', 'module', 'class'),
1416
help='Limit reordering of test items across units of code',
@@ -18,9 +20,9 @@ def pytest_addoption(parser):
1820
def pytest_report_header(config):
1921
out = None
2022

21-
if config.getoption('random_order_mode'):
22-
mode = config.getoption('random_order_mode')
23-
out = "Using --random-order-mode={0}".format(mode)
23+
if config.getoption('random_order_bucket'):
24+
bucket = config.getoption('random_order_bucket')
25+
out = "Using --random-order-bucket={0}".format(bucket)
2426

2527
return out
2628

@@ -75,6 +77,34 @@ def _shuffle_items(items, key=None, preserve_bucket_order=False):
7577
return
7678

7779

80+
def _get_set_of_item_ids(items):
81+
s = {}
82+
try:
83+
s = set(item.nodeid for item in items)
84+
finally:
85+
return s
86+
87+
7888
def pytest_collection_modifyitems(session, config, items):
79-
shuffle_mode = config.getoption('random_order_mode')
80-
_shuffle_items(items, key=_random_order_item_keys[shuffle_mode])
89+
failure = None
90+
item_ids = _get_set_of_item_ids(items)
91+
92+
try:
93+
shuffle_mode = config.getoption('random_order_bucket')
94+
_shuffle_items(items, key=_random_order_item_keys[shuffle_mode])
95+
96+
except Exception as e:
97+
# If the number of items is still the same, we assume that we haven't messed up too hard
98+
# and we can just return the list of items as it is.
99+
_, _, exc_tb = sys.exc_info()
100+
failure = 'pytest-random-order plugin has failed with {!r}:\n{}'.format(
101+
e, ''.join(traceback.format_tb(exc_tb, 10))
102+
)
103+
config.warn(0, failure, None)
104+
105+
finally:
106+
# Fail only if we have lost user's tests
107+
if item_ids != _get_set_of_item_ids(items):
108+
if not failure:
109+
failure = 'pytest-random-order plugin has failed miserably'
110+
raise RuntimeError(failure)

requirements.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
pytest
2+
tox
3+
coverage
4+
py

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.2.5',
16+
version='0.3.0',
1717
author='Jazeps Basko',
1818
author_email='[email protected]',
1919
maintainer='Jazeps Basko',

tests/test_actual_test_runs.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ def test_ex2(self):
104104
return testdir
105105

106106

107-
def check_call_sequence(seq, shuffle_mode='module'):
107+
def check_call_sequence(seq, bucket='module'):
108108
all_values = collections.defaultdict(list)
109109
num_switches = collections.defaultdict(int)
110110

@@ -131,39 +131,39 @@ def inspect_attr(this_call, prev_call, attr_name):
131131
# These are just sanity tests, the actual shuffling is tested in test_shuffle,
132132
# assertions here are very relaxed.
133133

134-
if shuffle_mode == 'global':
134+
if bucket == 'global':
135135
if num_module_switches <= num_modules:
136136
pytest.fail('Too few module switches for global shuffling')
137137
if num_package_switches <= num_packages:
138138
pytest.fail('Too few package switches for global shuffling')
139139

140-
elif shuffle_mode == 'package':
140+
elif bucket == 'package':
141141
assert num_package_switches == num_packages
142142
if num_module_switches <= num_modules:
143143
pytest.fail('Too few module switches for package-limited shuffling')
144144

145-
elif shuffle_mode == 'module':
145+
elif bucket == 'module':
146146
assert num_module_switches == num_modules
147147

148-
elif shuffle_mode == 'class':
148+
elif bucket == 'class':
149149
# Each class can contribute to 1 or 2 switches.
150150
assert num_class_switches <= num_classes * 2
151151

152-
# Class shuffle is a subset of module shuffle.
152+
# Class bucket is a special case of module bucket.
153153
# We have two classes in one module and these could be reshuffled so
154154
# the module could appear in sequence of buckets two times.
155155
assert num_modules <= num_module_switches <= num_modules + 1
156156

157157

158-
@pytest.mark.parametrize('mode', ['class', 'module', 'package', 'global'])
159-
def test_it_works_with_actual_tests(tmp_tree_of_tests, mode):
158+
@pytest.mark.parametrize('bucket', ['class', 'module', 'package', 'global'])
159+
def test_it_works_with_actual_tests(tmp_tree_of_tests, bucket):
160160
sequences = set()
161161

162162
for x in range(5):
163-
result = tmp_tree_of_tests.runpytest('--random-order-mode={}'.format(mode), '--verbose')
163+
result = tmp_tree_of_tests.runpytest('--random-order-bucket={}'.format(bucket), '--verbose')
164164
result.assert_outcomes(passed=14, failed=3)
165165
seq = get_runtest_call_sequence(result)
166-
check_call_sequence(seq, shuffle_mode=mode)
166+
check_call_sequence(seq, bucket=bucket)
167167
assert len(seq) == 17
168168
sequences.add(seq)
169169

tests/test_cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ def test_help_message(testdir):
44
)
55
result.stdout.fnmatch_lines([
66
'random-order:',
7-
'*--random-order-mode={global,package,module,class}*',
7+
'*--random-order-bucket={global,package,module,class}*',
88
])

tests/test_plugin_failure.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import pytest
2+
3+
4+
def acceptably_failing_shuffle_items(items, **kwargs):
5+
# Does not mess up items collection
6+
raise ValueError('shuffling failed')
7+
8+
9+
def critically_failing_shuffle_items(items, **kwargs):
10+
# Messes up items collection, an item is effectively lost
11+
items[1] = items[0]
12+
raise ValueError('shuffling failed')
13+
14+
15+
def critically_not_failing_shuffle_items(items, **kwargs):
16+
# This shuffler doesn't raise an exception but it does lose test cases
17+
items[1] = items[0]
18+
19+
20+
@pytest.fixture
21+
def simple_testdir(testdir):
22+
testdir.makepyfile("""
23+
def test_a1():
24+
assert True
25+
26+
def test_a2():
27+
assert True
28+
""")
29+
return testdir
30+
31+
32+
def test_faulty_shuffle_that_preserves_items_does_not_fail_test_run(monkeypatch, simple_testdir):
33+
monkeypatch.setattr('pytest_random_order._shuffle_items', acceptably_failing_shuffle_items)
34+
35+
result = simple_testdir.runpytest()
36+
result.assert_outcomes(passed=2)
37+
result.stdout.fnmatch_lines("""
38+
*W0 None pytest-random-order plugin has failed with ValueError*
39+
""")
40+
41+
42+
def test_faulty_shuffle_that_loses_items_fails_test_run(monkeypatch, simple_testdir):
43+
monkeypatch.setattr('pytest_random_order._shuffle_items', critically_failing_shuffle_items)
44+
result = simple_testdir.runpytest()
45+
result.assert_outcomes(passed=0, failed=0, skipped=0)
46+
result.stdout.fnmatch_lines("""
47+
*INTERNALERROR> RuntimeError: pytest-random-order plugin has failed with ValueError*
48+
""")
49+
50+
51+
def test_seemingly_ok_shuffle_that_loses_items_fails_test_run(monkeypatch, simple_testdir):
52+
monkeypatch.setattr('pytest_random_order._shuffle_items', critically_not_failing_shuffle_items)
53+
result = simple_testdir.runpytest()
54+
result.assert_outcomes(passed=0, failed=0, skipped=0)
55+
result.stdout.fnmatch_lines("""
56+
*INTERNALERROR> RuntimeError: pytest-random-order plugin has failed miserably*
57+
""")

0 commit comments

Comments
 (0)