Skip to content

Commit e8eb0c6

Browse files
authored
Merge pull request #1545 from seleniumbase/performance-testing
Performance Testing decorators
2 parents db0ac9b + fbc1190 commit e8eb0c6

File tree

9 files changed

+190
-14
lines changed

9 files changed

+190
-14
lines changed

examples/example_logs/ReadMe.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ pytest test_suite.py --dashboard --html=report.html
7272

7373
--------
7474

75-
If viewing ``pytest-html`` reports in [Jenkins](https://www.jenkins.io/), you may need to [configure Jenkins settings](https://stackoverflow.com/a/46197356) for the HTML to render correctly. This is due to [Jenkins CSP changes](https://www.jenkins.io/doc/book/system-administration/security/configuring-content-security-policy/). That setting can be changed from ``Manage Jenkins`` > ``Script Console`` by running:
75+
If viewing ``pytest-html`` reports in [Jenkins](https://www.jenkins.io/), you may need to [configure Jenkins settings](https://stackoverflow.com/a/46197356) for the HTML to render correctly. This is due to [Jenkins CSP changes](https://www.jenkins.io/doc/book/security/configuring-content-security-policy/). That setting can be changed from ``Manage Jenkins`` > ``Script Console`` by running:
7676

7777
```
7878
System.setProperty("hudson.model.DirectoryBrowserSupport.CSP", "")

examples/performance_test.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""
2+
Performance test example.
3+
4+
Uses decorators.print_runtime(), which prints the runtime duration
5+
of a method or "with"-block after the method (or block) completes.
6+
Also raises an exception when exceeding the time "limit" if set.
7+
8+
Arguments ->
9+
description # Optional - Shows description in print output.
10+
limit # Optional - Fail if the duration is above the limit.
11+
12+
Method / Function example usage ->
13+
from seleniumbase import decorators
14+
15+
@decorators.print_runtime("My Method")
16+
def my_method():
17+
# code ...
18+
# code ...
19+
20+
"with"-block example usage ->
21+
from seleniumbase import decorators
22+
23+
with decorators.print_runtime("My Code Block"):
24+
# code ...
25+
# code ...
26+
"""
27+
from seleniumbase import BaseCase
28+
from seleniumbase import decorators
29+
30+
31+
class PerformanceClass(BaseCase):
32+
@decorators.print_runtime("Open Swag Labs and Log In")
33+
def login_to_swag_labs(self):
34+
with decorators.print_runtime("Open Swag Labs"):
35+
self.open("https://www.saucedemo.com")
36+
self.type("#user-name", "standard_user")
37+
self.type("#password", "secret_sauce\n")
38+
39+
def test_performance_of_swag_labs(self):
40+
self.login_to_swag_labs()
41+
self.assert_element("div.inventory_list")
42+
self.assert_exact_text("PRODUCTS", "span.title")
43+
with decorators.print_runtime("Add backpack and see cart"):
44+
self.click('button[name*="backpack"]')
45+
self.click("#shopping_cart_container a")
46+
self.assert_text("Backpack", "div.cart_item")
47+
with decorators.print_runtime("Remove backpack from cart"):
48+
self.click('button:contains("Remove")') # HTML innerText
49+
self.assert_text_not_visible("Backpack", "div.cart_item")
50+
with decorators.print_runtime("Log out from Swag Labs", 3):
51+
self.js_click("a#logout_sidebar_link")
52+
self.assert_element("div#login_button_container")

examples/time_limit_test.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,27 @@
1-
""" This test fails on purpose to demonstrate the time-limit feature
2-
for tests that run longer than the time limit specified (seconds).
3-
The time-limit clock starts after the browser has fully launched,
4-
which is after pytest starts it's own internal clock for tests.
5-
Usage: (inside tests) => self.set_time_limit(SECONDS)
6-
Usage: (command-line) => --time-limit=SECONDS """
7-
81
import pytest
92
from seleniumbase import BaseCase
3+
from seleniumbase import decorators
104

115

126
class TimeLimitTests(BaseCase):
137
@pytest.mark.expected_failure
14-
def test_time_limit_feature(self):
15-
self.set_time_limit(5) # Fail test if time exceeds 5 seconds
16-
self.open("https://xkcd.com/1658/")
8+
def test_runtime_limit_decorator(self):
9+
"""This test fails on purpose to show the runtime_limit() decorator
10+
for code blocks that run longer than the time limit specified."""
11+
print("\n(This test should fail)")
12+
self.open("https://xkcd.com/1190")
13+
with decorators.runtime_limit(0.5):
14+
self.sleep(0.75)
15+
16+
@pytest.mark.expected_failure
17+
def test_set_time_limit_method(self):
18+
"""This test fails on purpose to show the set_time_limit() method
19+
for tests that run longer than the time limit specified (seconds).
20+
The time-limit clock starts after the browser has fully launched,
21+
which is after pytest starts it's own internal clock for tests.
22+
Usage: (inside tests) => self.set_time_limit(SECONDS)
23+
Usage: (command-line) => --time-limit=SECONDS"""
24+
self.set_time_limit(2.5) # Fail test if time exceeds 2.5 seconds
1725
print("\n(This test should fail)")
18-
self.sleep(7)
26+
self.open("https://xkcd.com/1658")
27+
self.sleep(3)

mkdocs_build/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ keyring==23.9.3
1212
pkginfo==1.8.3
1313
Jinja2==3.1.2
1414
click==8.1.3
15-
zipp==3.8.1
15+
zipp==3.9.0
1616
ghp-import==2.1.0
1717
readme-renderer==37.2
1818
pymdown-extensions==9.6

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ urllib3==1.26.12
4444
requests==2.27.1;python_version<"3.6"
4545
requests==2.27.1;python_version>="3.6" and python_version<"3.7"
4646
requests==2.28.1;python_version>="3.7"
47+
requests-toolbelt==0.10.0
4748
nose==1.3.7
4849
sniffio==1.3.0;python_version>="3.7"
4950
h11==0.14.0;python_version>="3.7"

seleniumbase/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
# seleniumbase package
2-
__version__ = "4.5.5"
2+
__version__ = "4.5.6"

seleniumbase/common/ReadMe.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
#### Use these Python decorators with your test methods as needed:
66

7+
* @print_runtime(description=None, limit=None)
8+
9+
* @runtime_limit(limit, description=None)
10+
711
* @retry_on_exception(tries=6, delay=1, backoff=2, max_delay=32)
812

913
* @rate_limited(max_per_second)

seleniumbase/common/decorators.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,118 @@
33
import sys
44
import time
55
import warnings
6+
from contextlib import contextmanager
67
from functools import wraps
78

89

10+
@contextmanager
11+
def print_runtime(description=None, limit=None):
12+
"""Print the runtime duration of a method or "with"-block after completion.
13+
If limit, fail if the runtime duration exceeds the limit after completion.
14+
15+
Method / Function example usage ->
16+
from seleniumbase import decorators
17+
18+
@decorators.print_runtime("My Method")
19+
def my_method():
20+
# code ...
21+
# code ...
22+
23+
"with"-block example usage ->
24+
from seleniumbase import decorators
25+
26+
with decorators.print_runtime("My Code Block"):
27+
# code ...
28+
# code ...
29+
"""
30+
if not description:
31+
description = "Code Block"
32+
description = str(description)
33+
if limit:
34+
limit = float("%.2f" % limit)
35+
if limit < 0.01:
36+
limit = 0.01 # Minimum runtime limit
37+
exception = None
38+
start_time = time.time()
39+
try:
40+
yield
41+
except Exception as e:
42+
exception = e
43+
raise
44+
finally:
45+
end_time = time.time()
46+
run_time = end_time - start_time
47+
# Print times with a statistically significant number of decimal places
48+
if run_time < 0.0001:
49+
print(" {%s} ran for %.7f seconds." % (description, run_time))
50+
elif run_time < 0.001:
51+
print(" {%s} ran for %.6f seconds." % (description, run_time))
52+
elif run_time < 0.01:
53+
print(" {%s} ran for %.5f seconds." % (description, run_time))
54+
elif run_time < 0.1:
55+
print(" {%s} ran for %.4f seconds." % (description, run_time))
56+
elif run_time < 1:
57+
print(" {%s} ran for %.3f seconds." % (description, run_time))
58+
else:
59+
print(" {%s} ran for %.2f seconds." % (description, run_time))
60+
if limit and limit > 0 and run_time > limit:
61+
message = (
62+
"\n {%s} duration of %.2fs exceeded the time limit of %.2fs!"
63+
% (description, run_time, limit)
64+
)
65+
if exception:
66+
message = exception.msg + "\nAND " + message
67+
raise Exception(message)
68+
69+
70+
@contextmanager
71+
def runtime_limit(limit, description=None):
72+
"""
73+
Fail if the runtime duration of a method or "with"-block exceeds the limit.
74+
(The failure won't occur until after the method or "with"-block completes.)
75+
76+
Method / Function example usage ->
77+
from seleniumbase import decorators
78+
79+
@decorators.runtime_limit(4.5)
80+
def my_method():
81+
# code ...
82+
# code ...
83+
84+
"with"-block example usage ->
85+
from seleniumbase import decorators
86+
87+
with decorators.runtime_limit(32):
88+
# code ...
89+
# code ...
90+
"""
91+
limit = float("%.2f" % limit)
92+
if limit < 0.01:
93+
limit = 0.01 # Minimum runtime limit
94+
if not description:
95+
description = "Code Block"
96+
description = str(description)
97+
exception = None
98+
start_time = time.time()
99+
try:
100+
yield
101+
except Exception as e:
102+
exception = e
103+
raise
104+
finally:
105+
end_time = time.time()
106+
run_time = end_time - start_time
107+
# Fail if the runtime of the code block exceeds the limit
108+
if limit and limit > 0 and run_time > limit:
109+
message = (
110+
"\n {%s} duration of %.2fs exceeded the time limit of %.2fs!"
111+
% (description, run_time, limit)
112+
)
113+
if exception:
114+
message = exception.msg + "\nAND " + message
115+
raise Exception(message)
116+
117+
9118
def retry_on_exception(tries=6, delay=1, backoff=2, max_delay=32):
10119
"""
11120
Decorator for implementing exponential backoff for retrying on failures.

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@
169169
'urllib3==1.26.12', # Stay in sync with "requests"
170170
'requests==2.27.1;python_version<"3.7"',
171171
'requests==2.28.1;python_version>="3.7"',
172+
'requests-toolbelt==0.10.0',
172173
"nose==1.3.7",
173174
'sniffio==1.3.0;python_version>="3.7"',
174175
'h11==0.14.0;python_version>="3.7"',

0 commit comments

Comments
 (0)