Skip to content

Commit f62683c

Browse files
authored
Merge pull request #11 from jbasko/dev
Seed
2 parents 619309c + 712a02b commit f62683c

File tree

9 files changed

+144
-40
lines changed

9 files changed

+144
-40
lines changed

README.rst

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ pytest-random-order
55
:target: https://travis-ci.org/jbasko/pytest-random-order
66

77
pytest-random-order is a plugin for `pytest <http://pytest.org>`_ that randomises the order in which
8-
tests are run to reveal unwanted coupling between tests. The plugin allows user to control the level
8+
tests are run to reveal unwanted coupling between tests. It allows user to control the level
99
of randomness they want to introduce and to disable reordering on subsets of tests.
10+
Tests can be rerun in a specific order by passing a seed value reported in a previous test run.
1011

1112

1213
Quick Start
@@ -36,6 +37,13 @@ To disable reordering of tests in a module or class, use pytest marker notation:
3637

3738
pytestmark = pytest.mark.random_order(disabled=True)
3839

40+
To rerun tests in a particular order:
41+
42+
::
43+
44+
$ pytest -v --random-order-seed=<value-reported-in-previous-run>
45+
46+
3947
Design
4048
------
4149

@@ -117,6 +125,28 @@ No matter what will be the bucket type for the test run, ``test_number_one`` wil
117125
before ``test_number_two``.
118126

119127

128+
Rerun Tests in the Same Order (Same Seed)
129+
+++++++++++++++++++++++++++++++++++++++++
130+
131+
If you discover a failing test because you reordered tests, you will probably want to be able to rerun the tests
132+
in the same failing order. To allow reproducing test order, the plugin reports the seed value it used with pseudo random number
133+
generator:
134+
135+
::
136+
137+
============================= test session starts ==============================
138+
..
139+
Using --random-order-bucket=module
140+
Using --random-order-seed=24775
141+
...
142+
143+
You can now the ``--random-order-seed=...`` bit as an argument to the next run to produce the same order:
144+
145+
::
146+
147+
$ pytest -v --random-order-seed=24775
148+
149+
120150
Disable the Plugin
121151
++++++++++++++++++
122152

pytest_random_order/plugin.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import random
12
import sys
23
import traceback
34

@@ -14,18 +15,34 @@ def pytest_addoption(parser):
1415
choices=('global', 'package', 'module', 'class'),
1516
help='Limit reordering of test items across units of code',
1617
)
18+
group.addoption(
19+
'--random-order-seed',
20+
action='store',
21+
dest='random_order_seed',
22+
default=None,
23+
help='Seed for the test order randomiser to produce a random order that can be reproduced using this seed',
24+
)
1725

1826

1927
def pytest_configure(config):
2028
config.addinivalue_line("markers", "random_order(disabled=True): disable reordering of tests within a module or class")
2129

30+
if config.getoption('random_order_seed'):
31+
seed = str(config.getoption('random_order_seed'))
32+
else:
33+
seed = str(random.randint(1, 1000000))
34+
config.random_order_seed = seed
35+
2236

2337
def pytest_report_header(config):
24-
out = None
38+
out = ''
2539

2640
if config.getoption('random_order_bucket'):
2741
bucket = config.getoption('random_order_bucket')
28-
out = "Using --random-order-bucket={0}".format(bucket)
42+
out += "Using --random-order-bucket={}\n".format(bucket)
43+
44+
if hasattr(config, 'random_order_seed'):
45+
out += 'Using --random-order-seed={}\n'.format(getattr(config, 'random_order_seed'))
2946

3047
return out
3148

@@ -36,8 +53,9 @@ def pytest_collection_modifyitems(session, config, items):
3653
item_ids = _get_set_of_item_ids(items)
3754

3855
try:
56+
seed = getattr(config, 'random_order_seed', None)
3957
bucket_type = config.getoption('random_order_bucket')
40-
_shuffle_items(items, bucket_key=_random_order_item_keys[bucket_type], disable=_disable)
58+
_shuffle_items(items, bucket_key=_random_order_item_keys[bucket_type], disable=_disable, seed=seed)
4159

4260
except Exception as e:
4361
# See the finally block -- we only fail if we have lost user's tests.

pytest_random_order/shuffler.py

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
ItemKey.__new__.__defaults__ = (None, None)
2020

2121

22-
def _shuffle_items(items, bucket_key=None, disable=None, _shuffle_buckets=True):
22+
def _shuffle_items(items, bucket_key=None, disable=None, seed=None):
2323
"""
2424
Shuffles a list of `items` in place.
2525
@@ -31,26 +31,15 @@ def _shuffle_items(items, bucket_key=None, disable=None, _shuffle_buckets=True):
3131
Bucket defines the boundaries across which items will not
3232
be shuffled.
3333
34-
If `disable` is function and returns True for ALL items
35-
in a bucket, items in this bucket will remain in their original order.
36-
37-
`_shuffle_buckets` is for testing only. Setting it to False may not produce
38-
the outcome you'd expect in all scenarios because if two non-contiguous sections of items belong
39-
to the same bucket, the items in these sections will be reshuffled as if they all belonged
40-
to the first section.
41-
Example:
42-
[A1, A2, B1, B2, A3, A4]
43-
44-
where letter denotes bucket key,
45-
with _shuffle_buckets=False may be reshuffled to:
46-
[B2, B1, A3, A1, A4, A2]
47-
48-
or as well to:
49-
[A3, A2, A4, A1, B1, B2]
50-
51-
because all A's belong to the same bucket and will be grouped together.
34+
`disable` is a function that takes an item and returns a falsey value
35+
if this item is ok to be shuffled. It returns a truthy value otherwise and
36+
the truthy value is used as part of the item's key when determining the bucket
37+
it belongs to.
5238
"""
5339

40+
if seed is not None:
41+
random.seed(seed)
42+
5443
# If `bucket_key` is falsey, shuffle is global.
5544
if not bucket_key and not disable:
5645
random.shuffle(items)

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

tests/conftest.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,19 @@ def get_test_calls():
3131
Returns a function to get runtest calls out from testdir.pytestrun result object.
3232
"""
3333
return _get_test_calls
34+
35+
36+
@pytest.fixture
37+
def twenty_tests():
38+
code = []
39+
for i in range(20):
40+
code.append('def test_a{}(): assert True\n'.format(str(i).zfill(2)))
41+
return ''.join(code)
42+
43+
44+
@pytest.fixture
45+
def twenty_cls_tests():
46+
code = []
47+
for i in range(20):
48+
code.append('\tdef test_b{}(self): self.assertTrue\n'.format(str(i).zfill(2)))
49+
return ''.join(code)

tests/test_actual_test_runs.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# -*- coding: utf-8 -*-
22
import collections
3+
import re
34

45
import py
56
import pytest
@@ -148,3 +149,52 @@ def test_it_works_with_actual_tests(tmp_tree_of_tests, get_test_calls, bucket):
148149
sequences.add(seq)
149150

150151
assert 1 < len(sequences) <= 5
152+
153+
154+
def test_random_order_seed_is_respected(testdir, twenty_tests, get_test_calls):
155+
testdir.makepyfile(twenty_tests)
156+
call_sequences = {
157+
'1': None,
158+
'2': None,
159+
'3': None,
160+
}
161+
for seed in call_sequences.keys():
162+
result = testdir.runpytest('--random-order-seed={}'.format(seed))
163+
164+
result.stdout.fnmatch_lines([
165+
'*Using --random-order-seed={}*'.format(seed),
166+
])
167+
168+
result.assert_outcomes(passed=20)
169+
call_sequences[seed] = get_test_calls(result)
170+
171+
for seed in call_sequences.keys():
172+
result = testdir.runpytest('--random-order-seed={}'.format(seed))
173+
result.assert_outcomes(passed=20)
174+
assert call_sequences[seed] == get_test_calls(result)
175+
176+
assert call_sequences['1'] != call_sequences['2'] != call_sequences['3']
177+
178+
179+
def test_generated_seed_is_reported_and_run_can_be_reproduced(testdir, twenty_tests, get_test_calls):
180+
testdir.makepyfile(twenty_tests)
181+
result = testdir.runpytest('-v')
182+
result.assert_outcomes(passed=20)
183+
result.stdout.fnmatch_lines([
184+
'*Using --random-order-seed=*'
185+
])
186+
calls = get_test_calls(result)
187+
188+
# find the seed in output
189+
seed = None
190+
for line in result.outlines:
191+
g = re.match('^Using --random-order-seed=(.+)$', line)
192+
if g:
193+
seed = g.group(1)
194+
break
195+
assert seed
196+
197+
result2 = testdir.runpytest('-v', '--random-order-seed={}'.format(seed))
198+
result2.assert_outcomes(passed=20)
199+
calls2 = get_test_calls(result2)
200+
assert calls == calls2

tests/test_cli.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ def test_help_message(testdir):
55
result.stdout.fnmatch_lines([
66
'random-order:',
77
'*--random-order-bucket={global,package,module,class}*',
8+
'*--random-order-seed=*',
89
])
910

1011

tests/test_markers.py

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,6 @@
11
import pytest
22

33

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.fixture
13-
def twenty_cls_tests():
14-
code = []
15-
for i in range(20):
16-
code.append('\tdef test_b{}(self): self.assertTrue\n'.format(str(i).zfill(2)))
17-
return ''.join(code)
18-
19-
204
@pytest.mark.parametrize('disabled', [True, False])
215
def test_marker_disables_random_order_in_module(testdir, twenty_tests, get_test_calls, disabled):
226
testdir.makepyfile(

tests/test_shuffle.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,19 @@ def test_shuffle_respects_two_distinct_disabled_groups_in_one_bucket():
130130
return
131131

132132
assert False
133+
134+
135+
def test_shuffle_respects_seed():
136+
sorted_items = list(range(30))
137+
138+
for seed in range(20):
139+
# Reset
140+
items1 = list(range(30))
141+
_shuffle_items(items1, seed=seed)
142+
143+
assert items1 != sorted_items
144+
145+
items2 = list(range(30))
146+
_shuffle_items(items2, seed=seed)
147+
148+
assert items2 == items1

0 commit comments

Comments
 (0)