Skip to content

Commit 8909c78

Browse files
committed
Initial commit
0 parents  commit 8909c78

File tree

9 files changed

+397
-0
lines changed

9 files changed

+397
-0
lines changed

.gitignore

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Byte-compiled / optimized / DLL files
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
6+
# C extensions
7+
*.so
8+
9+
# Distribution / packaging
10+
.Python
11+
env/
12+
build/
13+
develop-eggs/
14+
dist/
15+
downloads/
16+
eggs/
17+
.eggs/
18+
lib/
19+
lib64/
20+
parts/
21+
sdist/
22+
var/
23+
*.egg-info/
24+
.installed.cfg
25+
*.egg
26+
27+
# PyInstaller
28+
# Usually these files are written by a python script from a template
29+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
30+
*.manifest
31+
*.spec
32+
33+
# Installer logs
34+
pip-log.txt
35+
pip-delete-this-directory.txt
36+
37+
# Unit test / coverage reports
38+
htmlcov/
39+
.tox/
40+
.coverage
41+
.coverage.*
42+
.cache
43+
nosetests.xml
44+
coverage.xml
45+
*,cover
46+
.hypothesis/
47+
48+
# Translations
49+
*.mo
50+
*.pot
51+
52+
# Django stuff:
53+
*.log
54+
local_settings.py
55+
56+
# Flask instance folder
57+
instance/
58+
59+
# Sphinx documentation
60+
docs/_build/
61+
62+
# PyBuilder
63+
target/
64+
65+
# IPython Notebook
66+
.ipynb_checkpoints
67+
68+
# pyenv
69+
.python-version
70+

.travis.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Config file for automatic testing at travis-ci.org
2+
3+
sudo: false
4+
language: python
5+
python:
6+
- "2.7"
7+
8+
install:
9+
- pip install tox
10+
- "TOX_ENV=${TRAVIS_PYTHON_VERSION/[0-9].[0-9]/py${TRAVIS_PYTHON_VERSION/.}}"
11+
script: tox -e $TOX_ENV
12+
13+
before_cache:
14+
- rm -rf $HOME/.cache/pip/log
15+
cache:
16+
directories:
17+
- $HOME/.cache/pip

LICENSE

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
2+
The MIT License (MIT)
3+
4+
Copyright (c) 2016 Jazeps Basko
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in
14+
all copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
THE SOFTWARE.

MANIFEST.in

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
include LICENSE
2+
include README.rst
3+
4+
recursive-exclude * __pycache__
5+
recursive-exclude * *.py[co]

pytest_random_order.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# -*- coding: utf-8 -*-
2+
3+
import random
4+
5+
import pytest
6+
7+
8+
def pytest_addoption(parser):
9+
group = parser.getgroup('random-order')
10+
# group.addoption(
11+
# '--random-order-seed',
12+
# action='store',
13+
# type=int,
14+
# dest='random_order_seed',
15+
# default=None,
16+
# help='Seed value to reproduce a particular order',
17+
# )
18+
group.addoption(
19+
'--random-order-mode',
20+
action='store',
21+
dest='random_order_mode',
22+
default='module',
23+
choices=('global', 'package', 'module'),
24+
help='Limit reordering of test items across units of code',
25+
)
26+
27+
28+
def pytest_report_header(config):
29+
out = None
30+
31+
if config.getoption('random_order_mode'):
32+
mode = config.getoption('random_order_mode')
33+
out = "Using --random-order-mode={0}".format(mode)
34+
35+
return out
36+
37+
38+
_random_order_item_keys = {
39+
'global': lambda x: None,
40+
'package': lambda x: x.module.__package__,
41+
'module': lambda x: x.module.__name__,
42+
}
43+
44+
45+
def pytest_collection_modifyitems(session, config, items):
46+
sections = []
47+
48+
# One of: global, package, module
49+
shuffle_mode = config.getoption('random_order_mode')
50+
51+
item_key = _random_order_item_keys[shuffle_mode]
52+
53+
# Mark the beginning and ending of each key's test items in the items collection
54+
# so we know the boundaries of reshuffle.
55+
for i, item in enumerate(items):
56+
key = item_key(item)
57+
if not sections:
58+
assert i == 0
59+
sections.append([key, i, None])
60+
elif sections[-1][0] != key:
61+
sections[-1][2] = i
62+
sections.append([key, i, None])
63+
64+
if sections:
65+
sections[-1][2] = len(items)
66+
67+
for key, i, j in sections:
68+
key_items = items[i:j]
69+
random.shuffle(key_items)
70+
items[i:j] = key_items

setup.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
4+
import os
5+
import codecs
6+
from setuptools import setup
7+
8+
9+
def read(fname):
10+
file_path = os.path.join(os.path.dirname(__file__), fname)
11+
return codecs.open(file_path, encoding='utf-8').read()
12+
13+
14+
setup(
15+
name='pytest-random-order',
16+
version='0.1.0',
17+
author='Jazeps Basko',
18+
author_email='[email protected]',
19+
maintainer='Jazeps Basko',
20+
maintainer_email='[email protected]',
21+
license='MIT',
22+
url='https://github.com/jbasko/pytest-random-order',
23+
description='Randomise the order in which tests are run',
24+
long_description=read('README.rst'),
25+
py_modules=['pytest_random_order'],
26+
install_requires=['pytest>=2.9.2'],
27+
classifiers=[
28+
'Development Status :: 4 - Beta',
29+
'Framework :: Pytest',
30+
'Intended Audience :: Developers',
31+
'Topic :: Software Development :: Testing',
32+
'Programming Language :: Python',
33+
'Programming Language :: Python :: 2',
34+
'Programming Language :: Python :: 2.7',
35+
'License :: OSI Approved :: MIT License',
36+
],
37+
entry_points={
38+
'pytest11': [
39+
'random-order = pytest_random_order',
40+
],
41+
},
42+
)

tests/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pytest_plugins = 'pytester'

tests/test_random_order.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# -*- coding: utf-8 -*-
2+
import py
3+
import pytest
4+
5+
6+
def test_help_message(testdir):
7+
result = testdir.runpytest(
8+
'--help',
9+
)
10+
result.stdout.fnmatch_lines([
11+
'random-order:',
12+
# '*--random-order-seed=RANDOM_ORDER_SEED*',
13+
'*--random-order-mode={global,package,module}*',
14+
])
15+
16+
17+
def get_runtest_call_sequence(result, key=None):
18+
"""
19+
Returns a tuple of names of test methods that were run
20+
in the order they were run.
21+
"""
22+
calls = []
23+
24+
for c in result.reprec.getcalls('pytest_runtest_call'):
25+
calls.append((c.item.module.__package__, c.item.module.__name__, c.item.name))
26+
return tuple(calls)
27+
28+
29+
@pytest.fixture
30+
def tmp_tree_of_tests(testdir):
31+
"""
32+
Creates a directory structure:
33+
tmpdir/
34+
shallow_tests/
35+
test_a.py
36+
deep_tests/
37+
test_b.py
38+
test_c.py
39+
40+
If module name doesn't start with "test_", it isn't picked up by runpytest.
41+
"""
42+
43+
sup = testdir.mkpydir('shallow_tests')
44+
45+
sup.join('test_a.py').write(py.code.Source("""
46+
def test_a1():
47+
assert False
48+
def test_a2():
49+
assert True
50+
def test_a3():
51+
assert True
52+
"""))
53+
54+
sup.join('test_ax.py').write(py.code.Source("""
55+
def test_ax1():
56+
assert True
57+
def test_ax2():
58+
assert True
59+
def test_ax3():
60+
assert True
61+
"""))
62+
63+
sub = testdir.mkpydir('shallow_tests/deep_tests')
64+
65+
sub.join('test_b.py').write(py.code.Source("""
66+
def test_b1():
67+
assert True
68+
def test_b2():
69+
assert False
70+
def test_b3():
71+
assert True
72+
"""))
73+
74+
sub.join('test_c.py').write(py.code.Source("""
75+
def test_c1():
76+
assert True
77+
"""))
78+
79+
sub.join('test_d.py').write(py.code.Source("""
80+
def test_d1():
81+
assert True
82+
def test_d2():
83+
assert True
84+
"""))
85+
86+
return testdir
87+
88+
89+
def check_call_sequence(seq, shuffle_mode='module'):
90+
packages_seen = []
91+
modules_seen = []
92+
93+
num_package_switches = 0
94+
num_module_switches = 0
95+
96+
for package, module, item in seq:
97+
if not packages_seen:
98+
num_package_switches += 1
99+
packages_seen.append(package)
100+
elif packages_seen[-1] == package:
101+
# Same package as the previous item
102+
pass
103+
else:
104+
num_package_switches += 1
105+
if package not in packages_seen:
106+
packages_seen.append(package)
107+
108+
if not modules_seen:
109+
num_module_switches += 1
110+
modules_seen.append(module)
111+
elif modules_seen[-1] == module:
112+
# Same module as the previous item
113+
pass
114+
else:
115+
num_module_switches += 1
116+
if module not in modules_seen:
117+
modules_seen.append(module)
118+
119+
if shuffle_mode == 'global':
120+
if len(packages_seen) >= num_package_switches:
121+
pytest.fail(
122+
'Too few package switches ({}) for '
123+
'random-shuffle-mode=global. Packages seen: {}'.format(
124+
num_package_switches,
125+
packages_seen,
126+
)
127+
)
128+
129+
if shuffle_mode == 'package':
130+
if len(packages_seen) != num_package_switches:
131+
pytest.fail('There were more package switches than number of packages')
132+
133+
if len(modules_seen) >= num_module_switches:
134+
pytest.fail(
135+
'Suspiciously few module switches ({}) for '
136+
'random-shuffle-mode=package. Modules seen: {}'.format(
137+
num_module_switches,
138+
modules_seen,
139+
)
140+
)
141+
142+
elif shuffle_mode == 'module' and len(modules_seen) != num_module_switches:
143+
pytest.fail('There were more module switches than number of modules')
144+
145+
146+
@pytest.mark.parametrize('mode', ['module', 'package', 'global'])
147+
def test_it_works(tmp_tree_of_tests, mode):
148+
sequences = set()
149+
150+
for x in xrange(5):
151+
result = tmp_tree_of_tests.runpytest('--random-order-mode={}'.format(mode), '--verbose')
152+
result.assert_outcomes(passed=10, failed=2)
153+
seq = get_runtest_call_sequence(result)
154+
check_call_sequence(seq, shuffle_mode=mode)
155+
assert len(seq) == 12
156+
sequences.add(seq)
157+
158+
assert 1 < len(sequences) <= 5

0 commit comments

Comments
 (0)