Skip to content

Commit add20d9

Browse files
committed
Add a "random" flag to randomize the test order (defaults to False).
1 parent f4510ce commit add20d9

File tree

3 files changed

+61
-5
lines changed

3 files changed

+61
-5
lines changed

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,19 +64,21 @@ those who use PyTest, when using PyScript.
6464
5. If a named `pattern` argument is provided, it will be used to match test
6565
modules in the specification for target directories. The default pattern is
6666
"test_*.py".
67-
6. If there is a `conftest.py` file in any of the specified directories
67+
6. If a named `random` boolean argument is provided (default: `False`), then
68+
the order in which modules and tests are run will be randomized.
69+
7. If there is a `conftest.py` file in any of the specified directories
6870
containing a test module, it will be imported for any global `setup` and
6971
`teardown` functions to use for modules found within that directory. These
7072
`setup` and `teardown` functions can be overridden in the individual test
7173
modules.
72-
7. The `result` of awaiting `upytest.run` is a Python dictionary containing
74+
8. The `result` of awaiting `upytest.run` is a Python dictionary containing
7375
lists of tests bucketed under the keys: `"passes"`, `"fails"` and
7476
`"skipped"`. The result also provides information about the Python
7577
interpreter used to run the tests, long with a boolean flag to indicate if
7678
the tests were running in a web worker. These results are JSON serializable
7779
and can be used for further processing and analysis (again, see `main.py`
7880
for an example of this in action.)
79-
8. In your `index.html` make sure you use the `terminal` attribute
81+
9. In your `index.html` make sure you use the `terminal` attribute
8082
when referencing your Python script (as in the `index.html` file in
8183
this repository):
8284
```html

main.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@
3434
"fails": 9,
3535
"skipped": 6,
3636
},
37+
"result_random": {
38+
"passes": 11,
39+
"fails": 9,
40+
"skipped": 6,
41+
},
3742
"result_module": {
3843
"passes": 10,
3944
"fails": 9,
@@ -55,6 +60,9 @@
5560
# Run all tests in the tests directory.
5661
print("\033[1mRunning all tests in directory...\033[0m")
5762
actual_results["result_all"] = await upytest.run("./tests")
63+
# Run all tests in the tests directory in random order.
64+
print("\n\n\033[1mRunning all tests in directory in random order...\033[0m")
65+
actual_results["result_random"] = await upytest.run("./tests", random=True)
5866
# Run all tests in a specific module.
5967
print("\n\n\033[1mRunning all tests in a specific module...\033[0m")
6068
actual_results["result_module"] = await upytest.run(
@@ -90,6 +98,13 @@
9098
test_status
9199
), f"Test {test["test_name"]} does not end with {test_status}"
92100

101+
# Ensure the randomized tests are different from the non-randomized tests.
102+
for test_status in ["passes", "fails", "skipped"]:
103+
assert (
104+
actual_results["result_all"][test_status]
105+
!= actual_results["result_random"][test_status]
106+
), f"Randomized tests are the same as non-randomized tests for {test_status}"
107+
93108
# Ensure the results are JSON serializable.
94109
import json
95110
check = json.dumps(actual_results)
@@ -106,6 +121,15 @@
106121
f" Skipped: {len(actual_results['result_all']['skipped'])}.",
107122
),
108123
),
124+
div(
125+
p(
126+
b("Randomized Tests: "),
127+
f"Passes: {len(actual_results['result_all']['passes'])},"
128+
f" Fails: {len(actual_results['result_all']['fails'])},"
129+
f" Skipped: {len(actual_results['result_all']['skipped'])}.",
130+
f" (Different order to the non-randomized 'All Tests').",
131+
),
132+
),
109133
div(
110134
p(
111135
b("Tests in a Specified Module: "),

upytest.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import io
3131
import inspect
3232
import time
33+
import random
3334
from pathlib import Path
3435
import asyncio
3536
from pyscript import RUNNING_IN_WORKER
@@ -120,6 +121,28 @@ def parse_traceback_from_exception(ex):
120121
return "\n".join(result)
121122

122123

124+
def shuffle(a_list):
125+
"""
126+
Shuffle a list, in place.
127+
128+
This function is needed because MicroPython does not have a random.shuffle
129+
function.
130+
131+
It falls back to random.shuffle if using CPython, otherwise it uses a
132+
simple implementation of the Fisher-Yates in-place shuffle algorithm.
133+
134+
Context:
135+
136+
https://stackoverflow.com/questions/73143243/are-there-any-alternatives-for-the-python-module-randoms-shuffle-function-in
137+
"""
138+
if hasattr(random, "shuffle"):
139+
random.shuffle(a_list)
140+
else:
141+
for i in range(len(a_list) - 1, 0, -1):
142+
j = random.randrange(i+1)
143+
a_list[i], a_list[j] = a_list[j], a_list[i]
144+
145+
123146
class TestCase:
124147
"""
125148
Represents an individual test to run.
@@ -274,7 +297,7 @@ async def print(self, text):
274297
await asyncio.sleep(0)
275298
print(text, end="", flush=True)
276299

277-
async def run(self):
300+
async def run(self, randomize=False):
278301
"""
279302
Run each TestCase instance for this module. If a setup or teardown
280303
exists, these will be evaluated immediately before and after the
@@ -284,6 +307,8 @@ async def run(self):
284307
for each skipped test.
285308
"""
286309
print(f"\n{self.path}: ", end="")
310+
if randomize:
311+
shuffle(self._tests)
287312
for test_case in self.tests:
288313
if self.setup:
289314
if is_awaitable(self.setup):
@@ -466,12 +491,16 @@ async def run(*args, **kwargs):
466491
print("Running in worker: \033[1m", RUNNING_IN_WORKER, "\033[0m")
467492
targets = []
468493
pattern = kwargs.get("pattern", "test_*.py")
494+
randomize = kwargs.get("random", False)
495+
print("Randomize test order: \033[1m", randomize, "\033[0m")
469496
for arg in args:
470497
if isinstance(arg, str):
471498
targets.append(arg)
472499
else:
473500
raise ValueError(f"Unexpected argument: {arg}")
474501
test_modules = discover(targets, pattern)
502+
if randomize:
503+
shuffle(test_modules)
475504
module_count = len(test_modules)
476505
test_count = sum([len(module.tests) for module in test_modules])
477506
print(
@@ -483,7 +512,7 @@ async def run(*args, **kwargs):
483512
passed_tests = []
484513
start = time.time()
485514
for module in test_modules:
486-
await module.run()
515+
await module.run(randomize)
487516
for test in module.tests:
488517
if test.status == FAIL:
489518
failed_tests.append(test)
@@ -542,6 +571,7 @@ async def run(*args, **kwargs):
542571
"platform": sys.platform,
543572
"version": sys.version,
544573
"running_in_worker": RUNNING_IN_WORKER,
574+
"randomize": randomize,
545575
"passes": [test.as_dict for test in passed_tests],
546576
"fails": [test.as_dict for test in failed_tests],
547577
"skipped": [test.as_dict for test in skipped_tests],

0 commit comments

Comments
 (0)