Skip to content

Commit ce58843

Browse files
authored
v0.8.0 --failed-first works (#30)
Closes #28 - stops breaking `--failed-first` flag.
1 parent 641fe30 commit ce58843

File tree

8 files changed

+205
-43
lines changed

8 files changed

+205
-43
lines changed

README.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,13 @@ You can now use the ``--random-order-seed=...`` bit as an argument to the next r
156156
$ pytest -v --random-order-seed=24775
157157

158158

159+
Run Last Failed Tests First
160+
+++++++++++++++++++++++++++
161+
162+
Since v0.8.0 pytest cache plugin's ``--failed-first`` flag is supported -- tests that failed in the last run
163+
will be run before tests that passed irrespective of shuffling bucket type.
164+
165+
159166
Disable Randomisation or the Plugin
160167
+++++++++++++++++++++++++++++++++++
161168

pytest_random_order/bucket_types.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
try:
2+
from collections import OrderedDict
3+
except ImportError:
4+
from ordereddict import OrderedDict
5+
import functools
6+
7+
bucket_type_keys = OrderedDict()
8+
9+
10+
def bucket_type_key(bucket_type):
11+
"""
12+
Registers a function that calculates test item key for the specified bucket type.
13+
"""
14+
15+
def decorator(f):
16+
17+
@functools.wraps(f)
18+
def wrapped(item, session):
19+
key = f(item)
20+
21+
if session is not None:
22+
for handler in session.pytest_random_order_bucket_type_key_handlers:
23+
key = handler(item, key)
24+
25+
return key
26+
27+
bucket_type_keys[bucket_type] = wrapped
28+
return wrapped
29+
30+
return decorator
31+
32+
33+
@bucket_type_key('global')
34+
def get_global_key(item):
35+
return None
36+
37+
38+
@bucket_type_key('package')
39+
def get_package_key(item):
40+
return item.module.__package__
41+
42+
43+
@bucket_type_key('module')
44+
def get_module_key(item):
45+
return item.module.__name__
46+
47+
48+
@bucket_type_key('class')
49+
def get_class_key(item):
50+
if item.cls:
51+
return item.module.__name__, item.cls.__name__
52+
else:
53+
return item.module.__name__
54+
55+
56+
@bucket_type_key('parent')
57+
def get_parent_key(item):
58+
return item.parent
59+
60+
61+
@bucket_type_key('grandparent')
62+
def get_grandparent_key(item):
63+
return item.parent.parent
64+
65+
66+
@bucket_type_key('none')
67+
def get_none_key(item):
68+
raise RuntimeError('When shuffling is disabled (bucket_type=none), item key should not be calculated')
69+
70+
71+
bucket_types = bucket_type_keys.keys()

pytest_random_order/cache.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""
2+
This module is called "cache" because it builds on the "cache" plugin:
3+
4+
https://docs.pytest.org/en/latest/cache.html
5+
6+
"""
7+
8+
FAILED_FIRST_LAST_FAILED_BUCKET_KEY = '<failed_first_last_failed>'
9+
10+
11+
def process_failed_first_last_failed(session, config, items):
12+
if not config.getoption('failedfirst'):
13+
return
14+
15+
last_failed_raw = config.cache.get('cache/lastfailed', None)
16+
if not last_failed_raw:
17+
return
18+
19+
# Get the names of last failed tests
20+
last_failed = []
21+
for key in last_failed_raw.keys():
22+
parts = key.split('::')
23+
if len(parts) == 3:
24+
last_failed.append(tuple(parts))
25+
elif len(parts) == 2:
26+
last_failed.append((parts[0], None, parts[1]))
27+
else:
28+
raise NotImplementedError()
29+
30+
def assign_last_failed_to_same_bucket(item, key):
31+
if item.nodeid in last_failed_raw:
32+
return FAILED_FIRST_LAST_FAILED_BUCKET_KEY
33+
else:
34+
return key
35+
36+
session.pytest_random_order_bucket_type_key_handlers.append(assign_last_failed_to_same_bucket)

pytest_random_order/plugin.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import sys
33
import traceback
44

5+
from pytest_random_order.bucket_types import bucket_type_keys, bucket_types
6+
from pytest_random_order.cache import process_failed_first_last_failed
57
from pytest_random_order.shuffler import _get_set_of_item_ids, _shuffle_items, _disable
68

79

@@ -12,7 +14,7 @@ def pytest_addoption(parser):
1214
action='store',
1315
dest='random_order_bucket',
1416
default='module',
15-
choices=('global', 'package', 'module', 'class', 'parent', 'grandparent', 'none'),
17+
choices=bucket_types,
1618
help='Limit reordering of test items across units of code',
1719
)
1820
group.addoption(
@@ -25,7 +27,10 @@ def pytest_addoption(parser):
2527

2628

2729
def pytest_configure(config):
28-
config.addinivalue_line("markers", "random_order(disabled=True): disable reordering of tests within a module or class")
30+
config.addinivalue_line(
31+
'markers',
32+
'random_order(disabled=True): disable reordering of tests within a module or class'
33+
)
2934

3035

3136
def pytest_report_header(config):
@@ -43,13 +48,22 @@ def pytest_report_header(config):
4348
def pytest_collection_modifyitems(session, config, items):
4449
failure = None
4550

51+
session.pytest_random_order_bucket_type_key_handlers = []
52+
process_failed_first_last_failed(session, config, items)
53+
4654
item_ids = _get_set_of_item_ids(items)
4755

4856
try:
4957
seed = str(config.getoption('random_order_seed'))
5058
bucket_type = config.getoption('random_order_bucket')
5159
if bucket_type != 'none':
52-
_shuffle_items(items, bucket_key=_random_order_item_keys[bucket_type], disable=_disable, seed=seed)
60+
_shuffle_items(
61+
items,
62+
bucket_key=bucket_type_keys[bucket_type],
63+
disable=_disable,
64+
seed=seed,
65+
session=session,
66+
)
5367

5468
except Exception as e:
5569
# See the finally block -- we only fail if we have lost user's tests.
@@ -65,13 +79,3 @@ def pytest_collection_modifyitems(session, config, items):
6579
if not failure:
6680
failure = 'pytest-random-order plugin has failed miserably'
6781
raise RuntimeError(failure)
68-
69-
70-
_random_order_item_keys = {
71-
'global': lambda x: None,
72-
'package': lambda x: x.module.__package__,
73-
'module': lambda x: x.module.__name__,
74-
'class': lambda x: (x.module.__name__, x.cls.__name__) if x.cls else x.module.__name__,
75-
'parent': lambda x: x.parent,
76-
'grandparent': lambda x: x.parent.parent,
77-
}

pytest_random_order/shuffler.py

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# -*- coding: utf-8 -*-
22

33
from collections import namedtuple
4+
5+
from pytest_random_order.cache import FAILED_FIRST_LAST_FAILED_BUCKET_KEY
6+
47
try:
58
from collections import OrderedDict
69
except ImportError:
@@ -23,7 +26,7 @@
2326
ItemKey.__new__.__defaults__ = (None, None)
2427

2528

26-
def _shuffle_items(items, bucket_key=None, disable=None, seed=None):
29+
def _shuffle_items(items, bucket_key=None, disable=None, seed=None, session=None):
2730
"""
2831
Shuffles a list of `items` in place.
2932
@@ -52,11 +55,11 @@ def _shuffle_items(items, bucket_key=None, disable=None, seed=None):
5255
def get_full_bucket_key(item):
5356
assert bucket_key or disable
5457
if bucket_key and disable:
55-
return ItemKey(bucket=bucket_key(item), disabled=disable(item))
58+
return ItemKey(bucket=bucket_key(item, session), disabled=disable(item, session))
5659
elif disable:
57-
return ItemKey(disabled=disable(item))
60+
return ItemKey(disabled=disable(item, session))
5861
else:
59-
return ItemKey(bucket=bucket_key(item))
62+
return ItemKey(bucket=bucket_key(item, session))
6063

6164
# For a sequence of items A1, A2, B1, B2, C1, C2,
6265
# where key(A1) == key(A2) == key(C1) == key(C2),
@@ -69,15 +72,29 @@ def get_full_bucket_key(item):
6972
buckets[full_bucket_key].append(item)
7073

7174
# Shuffle inside a bucket
72-
for bucket in buckets.keys():
73-
if not bucket.disabled:
74-
random.shuffle(buckets[bucket])
7575

76-
# Shuffle buckets
7776
bucket_keys = list(buckets.keys())
78-
random.shuffle(bucket_keys)
7977

80-
items[:] = [item for bk in bucket_keys for item in buckets[bk]]
78+
for full_bucket_key in buckets.keys():
79+
if full_bucket_key.bucket == FAILED_FIRST_LAST_FAILED_BUCKET_KEY:
80+
# Do not shuffle the last failed bucket
81+
continue
82+
83+
if not full_bucket_key.disabled:
84+
random.shuffle(buckets[full_bucket_key])
85+
86+
# Shuffle buckets
87+
88+
# Only the first bucket can be FAILED_FIRST_LAST_FAILED_BUCKET_KEY
89+
if bucket_keys and bucket_keys[0].bucket == FAILED_FIRST_LAST_FAILED_BUCKET_KEY:
90+
new_bucket_keys = list(buckets.keys())[1:]
91+
random.shuffle(new_bucket_keys)
92+
new_bucket_keys.insert(0, bucket_keys[0])
93+
else:
94+
new_bucket_keys = list(buckets.keys())
95+
random.shuffle(new_bucket_keys)
96+
97+
items[:] = [item for bk in new_bucket_keys for item in buckets[bk]]
8198
return
8299

83100

@@ -89,7 +106,7 @@ def _get_set_of_item_ids(items):
89106
return s
90107

91108

92-
def _disable(item):
109+
def _disable(item, session):
93110
marker = item.get_marker('random_order')
94111
if marker:
95112
is_disabled = marker.kwargs.get('disabled', False)

setup.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
#!/usr/bin/env python
22
# -*- coding: utf-8 -*-
33

4-
import os
54
import codecs
5+
import os
6+
67
from setuptools import setup
78

89

@@ -13,7 +14,7 @@ def read(fname):
1314

1415
setup(
1516
name='pytest-random-order',
16-
version='0.7.0',
17+
version='0.8.0',
1718
author='Jazeps Basko',
1819
author_email='jazeps.basko@gmail.com',
1920
maintainer='Jazeps Basko',
@@ -22,7 +23,12 @@ def read(fname):
2223
url='https://github.com/jbasko/pytest-random-order',
2324
description='Randomise the order in which pytest tests are run with some control over the randomness',
2425
long_description=read('README.rst'),
25-
py_modules=['pytest_random_order.plugin', 'pytest_random_order.shuffler'],
26+
py_modules=[
27+
'pytest_random_order.bucket_types',
28+
'pytest_random_order.cache',
29+
'pytest_random_order.plugin',
30+
'pytest_random_order.shuffler',
31+
],
2632
install_requires=['pytest>=2.9.2'],
2733
classifiers=[
2834
'Development Status :: 5 - Production/Stable',

tests/test_actual_test_runs.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,24 @@ def test_generated_seed_is_reported_and_run_can_be_reproduced(testdir, twenty_te
206206
result2.assert_outcomes(passed=20)
207207
calls2 = get_test_calls(result2)
208208
assert calls == calls2
209+
210+
211+
@pytest.mark.parametrize('bucket', [
212+
'global',
213+
'package',
214+
'module',
215+
'class',
216+
'parent',
217+
'grandparent',
218+
'none',
219+
])
220+
def test_failed_first(tmp_tree_of_tests, get_test_calls, bucket):
221+
result1 = tmp_tree_of_tests.runpytest('--random-order-bucket={0}'.format(bucket), '--verbose')
222+
result1.assert_outcomes(passed=14, failed=3)
223+
224+
result2 = tmp_tree_of_tests.runpytest('--random-order-bucket={0}'.format(bucket), '--failed-first', '--verbose')
225+
result2.assert_outcomes(passed=14, failed=3)
226+
227+
calls2 = get_test_calls(result2)
228+
first_three_tests = set(c.name for c in calls2[:3])
229+
assert set(['test_a1', 'test_b2', 'test_ee2']) == first_three_tests

tests/test_shuffle.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,30 @@
44
from pytest_random_order.shuffler import _shuffle_items
55

66

7-
def identity_key(x):
8-
return x
7+
def identity_key(item, session):
8+
return item
99

1010

11-
def modulus_2_key(x):
12-
return x % 2
11+
def modulus_2_key(item, session):
12+
return item % 2
1313

1414

15-
def lt_10_key(x):
16-
return x < 10
15+
def lt_10_key(item, session):
16+
return item < 10
1717

1818

19-
def disable_if_gt_1000(x):
19+
def disable_if_gt_1000(item, session):
2020
# if disable returns a truthy value, it must also be usable as a key.
21-
if x > 1000:
22-
return x // 1000
21+
if item > 1000:
22+
return item // 1000
2323
else:
2424
return False
2525

2626

2727
@pytest.mark.parametrize('key', [
2828
None,
29-
lambda x: None,
30-
lambda x: x % 2,
29+
lambda item, session: None,
30+
lambda item, session: item % 2,
3131
])
3232
def test_shuffles_empty_list_in_place(key):
3333
items = []
@@ -39,8 +39,8 @@ def test_shuffles_empty_list_in_place(key):
3939

4040
@pytest.mark.parametrize('key', [
4141
None,
42-
lambda x: None,
43-
lambda x: x % 2,
42+
lambda item, session: None,
43+
lambda item, session: item % 2,
4444
])
4545
def test_shuffles_one_item_list_in_place(key):
4646
items = [42]
@@ -67,10 +67,10 @@ def test_two_bucket_reshuffle():
6767
_shuffle_items(items, bucket_key=lt_10_key)
6868
assert items != items_copy
6969
for i, item in enumerate(items):
70-
if lt_10_key(i):
71-
assert lt_10_key(item) == lt_10_key(items[0]), items
70+
if lt_10_key(i, None):
71+
assert lt_10_key(item, None) == lt_10_key(items[0], None), items
7272
else:
73-
assert lt_10_key(item) == lt_10_key(items[10]), items
73+
assert lt_10_key(item, None) == lt_10_key(items[10], None), items
7474

7575

7676
def test_eight_bucket_reshuffle():

0 commit comments

Comments
 (0)