-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Description
When doing some changes to a test report generation plugin, I was generating tests of various kinds from a script and ran it through pytest. I noticed that the runtime was non-linear. When the number of failing tests were close to 0, a couple of thousand tests were fine (<5 sec). If I on the other hand had about 10% failing tests, the runtime would be near half a minute, and after some experimenting I could see the runtime increased exponentially. Given that we have quite a few failing tests at times (and several thousand tests), this was quite noticeable.
I also ruled out that this was a function of the number of failing tests, as keeping the total the same, but doubling the percentage only shows a linear graph:
I removed the content of the conftest.py, so I think the below should be a pretty good way of recreating it.
-
a detailed description of the bug or problem you are having
-
output of
pip list
from the virtual environment you are using -
pytest and operating system versions
-
minimal example if possible
-
Pytest 8.3.4
-
Ubuntu 24.04.2 LTS (on WSL2 Windows 11)
pip list
attrs 25.1.0
certifi 2025.1.31
charset-normalizer 3.4.1
grpcio 1.70.0
grpcio-tools 1.70.0
idna 3.10
iniconfig 2.0.0
isodate 0.7.2
line-profiler 4.2.0
lxml 5.3.1
markdown 3.7
nodeenv 1.9.1
nodejs-wheel-binaries 22.14.0
packaging 24.2
platformdirs 4.3.6
pluggy 1.5.0
protobuf 5.29.3
pyright 1.1.394
pytest 8.3.4
pytz 2025.1
requests 2.32.3
requests-file 2.1.0
requests-toolbelt 1.0.0
ruff 0.9.7
setuptools 75.8.0
typing-extensions 4.12.2
urllib3 2.3.0
zeep 4.3.1
Minimal example
run.sh
Run like FAILING=10 TOTAL=1600 ./run.sh
(replace TOTAL with 200,400,600,800,1000,1200, ...)
#!/bin/bash
./generate_tests.py --failing ${FAILING:-10} --total ${TOTAL:-200} --filename generated_test.py
pytest ./generated_tests.py
generate_tests.py
#!/usr/bin/env python3
import argparse
import random
import re
def generate_test_case(name: str, should_fail: bool, marks: list[str]) -> str:
decorators = "".join([f"@pytest.mark.{mark}\n" for mark in marks])
if should_fail:
return f"{decorators}def test_{name}():\n pytest.fail(\"bogus reason\")\n"
else:
return f"{decorators}def test_{name}():\n pass\n"
def parse_mark_args(mark_arg: str, total: int) -> dict:
mark_distribution = {}
if not mark_arg:
return mark_distribution
pattern = re.compile(r"(\w+):(\d+)")
for match in mark_arg.split(','):
m = pattern.match(match.strip())
if m:
mark = m.group(1)
percentage = int(m.group(2))
count = round(total * (percentage / 100))
mark_distribution[mark] = count
return mark_distribution
def assign_marks(total: int, mark_distribution: dict) -> list[list[str]]:
mark_lists = [[] for _ in range(total)]
for mark, count in mark_distribution.items():
indices = random.sample(range(total), count)
for idx in indices:
mark_lists[idx].append(mark)
return mark_lists
def main():
parser = argparse.ArgumentParser(description="Generate bogus pytest test cases.")
parser.add_argument("--failing", type=int, default=30, help="Percentage of tests that should fail (default 30%)")
parser.add_argument("--total", type=int, required=True, help="Total number of test cases")
parser.add_argument("--filename", type=str, required=True, help="Output filename for test cases")
parser.add_argument("--mark", type=str, help="Comma-separated list of marks and percentages, e.g. foo:20,bar:10")
args = parser.parse_args()
total = args.total
failing_percentage = args.failing
failing_count = round(total * (failing_percentage / 100))
passing_count = total - failing_count
test_cases = [True] * failing_count + [False] * passing_count
random.shuffle(test_cases)
mark_distribution = parse_mark_args(args.mark, total)
mark_lists = assign_marks(total, mark_distribution)
print("Generated test suite with bogus test cases")
with open(args.filename, "w") as f:
f.write("\nimport pytest\n\n")
for i, should_fail in enumerate(test_cases):
marks = mark_lists[i]
test_code = generate_test_case(f"case_{i+1}", should_fail, marks)
f.write(test_code + "\n")
if __name__ == "__main__":
main()