Skip to content

Commit 2e58c2c

Browse files
committed
Add the --time-limit=s feature to fail long-running tests
1 parent 456464b commit 2e58c2c

File tree

9 files changed

+104
-8
lines changed

9 files changed

+104
-8
lines changed

examples/raw_parameter_script.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
sb.with_s3_logging = False
5757
sb.js_checking_on = False
5858
sb.is_pytest = False
59+
sb.time_limit = None
5960
sb.slow_mode = False
6061
sb.demo_mode = False
6162
sb.demo_sleep = 1

examples/timeout_test.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
""" This test fails on purpose to demonstrate the timeout feature
2-
for tests that run longer than the time limit specified. """
1+
""" This test fails on purpose to demonstrate the time-limit feature
2+
for tests that run longer than the time limit specified in seconds.
3+
Usage: (inside tests) -> self.set_time_limit(SECONDS) """
34

45
import pytest
5-
import time
66
from seleniumbase import BaseCase
77

88

99
class MyTestClass(BaseCase):
1010

1111
@pytest.mark.expected_failure
12-
@pytest.mark.timeout(6) # The test will fail if it runs longer than this
13-
def test_timeout_failure(self):
12+
def test_time_limit_feature(self):
13+
self.set_time_limit(6) # Test fails if run-time exceeds limit
1414
self.open("https://xkcd.com/1658/")
15-
time.sleep(7)
15+
self.sleep(7)

help_docs/method_summary.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,8 @@ self.js_update_text(selector, new_value, by=By.CSS_SELECTOR, timeout=None)
262262

263263
self.jquery_update_text(selector, new_value, by=By.CSS_SELECTOR, timeout=None)
264264

265+
self.set_time_limit(time_limit)
266+
265267
########
266268

267269
self.add_css_link(css_link)

seleniumbase/fixtures/base_case.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def test_anything(self):
5555
from seleniumbase.fixtures import js_utils
5656
from seleniumbase.fixtures import page_actions
5757
from seleniumbase.fixtures import page_utils
58+
from seleniumbase.fixtures import shared_utils
5859
from seleniumbase.fixtures import xpath_to_css
5960
logging.getLogger("requests").setLevel(logging.ERROR)
6061
logging.getLogger("urllib3").setLevel(logging.ERROR)
@@ -1662,7 +1663,17 @@ def wait_for_angularjs(self, timeout=None, **kwargs):
16621663
js_utils.wait_for_angularjs(self.driver, timeout, **kwargs)
16631664

16641665
def sleep(self, seconds):
1665-
time.sleep(seconds)
1666+
if not sb_config.time_limit:
1667+
time.sleep(seconds)
1668+
else:
1669+
start_ms = time.time() * 1000.0
1670+
stop_ms = start_ms + (seconds * 1000.0)
1671+
for x in range(int(seconds * 5)):
1672+
shared_utils.check_if_time_limit_exceeded()
1673+
now_ms = time.time() * 1000.0
1674+
if now_ms >= stop_ms:
1675+
break
1676+
time.sleep(0.2)
16661677

16671678
def activate_jquery(self):
16681679
""" If "jQuery is not defined", use this method to activate it for use.
@@ -2456,6 +2467,22 @@ def jquery_update_text(self, selector, new_value, by=By.CSS_SELECTOR,
24562467
element.send_keys('\n')
24572468
self.__demo_mode_pause_if_active()
24582469

2470+
def set_time_limit(self, time_limit):
2471+
if time_limit:
2472+
try:
2473+
sb_config.time_limit = float(time_limit)
2474+
except Exception:
2475+
sb_config.time_limit = None
2476+
else:
2477+
sb_config.time_limit = None
2478+
if sb_config.time_limit and sb_config.time_limit > 0:
2479+
sb_config.time_limit_ms = int(sb_config.time_limit * 1000.0)
2480+
self.time_limit = sb_config.time_limit
2481+
else:
2482+
self.time_limit = None
2483+
sb_config.time_limit = None
2484+
sb_config.time_limit_ms = None
2485+
24592486
############
24602487

24612488
def add_css_link(self, css_link):
@@ -3238,6 +3265,7 @@ def wait_for_link_text_present(self, link_text, timeout=None):
32383265
start_ms = time.time() * 1000.0
32393266
stop_ms = start_ms + (timeout * 1000.0)
32403267
for x in range(int(timeout * 5)):
3268+
shared_utils.check_if_time_limit_exceeded()
32413269
try:
32423270
if not self.is_link_text_present(link_text):
32433271
raise Exception(
@@ -3258,6 +3286,7 @@ def wait_for_partial_link_text_present(self, link_text, timeout=None):
32583286
start_ms = time.time() * 1000.0
32593287
stop_ms = start_ms + (timeout * 1000.0)
32603288
for x in range(int(timeout * 5)):
3289+
shared_utils.check_if_time_limit_exceeded()
32613290
try:
32623291
if not self.is_partial_link_text_present(link_text):
32633292
raise Exception(
@@ -4146,6 +4175,7 @@ def setUp(self, masterqa_mode=False):
41464175
self.demo_mode = sb_config.demo_mode
41474176
self.demo_sleep = sb_config.demo_sleep
41484177
self.highlights = sb_config.highlights
4178+
self.time_limit = sb_config.time_limit
41494179
self.environment = sb_config.environment
41504180
self.env = self.environment # Add a shortened version
41514181
self.with_selenium = sb_config.with_selenium # Should be True
@@ -4243,15 +4273,27 @@ def setUp(self, masterqa_mode=False):
42434273
# pyvirtualdisplay might not be necessary anymore because
42444274
# Chrome and Firefox now have built-in headless displays
42454275
pass
4276+
else:
4277+
# (Nosetests / Not Pytest)
4278+
pass # Setup performed in plugins
42464279

42474280
# Verify that SeleniumBase is installed successfully
42484281
if not hasattr(self, "browser"):
42494282
raise Exception("""SeleniumBase plugins DID NOT load!\n\n"""
42504283
"""*** Please REINSTALL SeleniumBase using: >\n"""
42514284
""" >>> "pip install -r requirements.txt"\n"""
42524285
""" >>> "python setup.py install" """)
4286+
4287+
# Configure the test time limit (if used)
4288+
self.set_time_limit(self.time_limit)
4289+
4290+
# Set the start time for the test (in ms)
4291+
sb_config.start_time_ms = int(time.time() * 1000.0)
4292+
4293+
# Parse the settings file
42534294
if self.settings_file:
42544295
settings_parser.set_settings(self.settings_file)
4296+
42554297
# Mobile Emulator device metrics: CSS Width, CSS Height, & Pixel-Ratio
42564298
if self.device_metrics:
42574299
metrics_string = self.device_metrics

seleniumbase/fixtures/js_utils.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from seleniumbase.config import settings
1111
from seleniumbase.core import style_sheet
1212
from seleniumbase.fixtures import constants
13+
from seleniumbase.fixtures import shared_utils
1314

1415

1516
def wait_for_ready_state_complete(driver, timeout=settings.EXTREME_TIMEOUT):
@@ -19,10 +20,10 @@ def wait_for_ready_state_complete(driver, timeout=settings.EXTREME_TIMEOUT):
1920
fully loaded (although AJAX and other loads might still be happening).
2021
This method will wait until document.readyState == "complete".
2122
"""
22-
2323
start_ms = time.time() * 1000.0
2424
stop_ms = start_ms + (timeout * 1000.0)
2525
for x in range(int(timeout * 10)):
26+
shared_utils.check_if_time_limit_exceeded()
2627
try:
2728
ready_state = driver.execute_script("return document.readyState")
2829
except WebDriverException:

seleniumbase/fixtures/page_actions.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from seleniumbase.config import settings
3737
from seleniumbase.core import log_helper
3838
from seleniumbase.fixtures import page_utils
39+
from seleniumbase.fixtures import shared_utils
3940
ENI_Exception = selenium_exceptions.ElementNotInteractableException
4041

4142

@@ -215,6 +216,7 @@ def wait_for_element_present(driver, selector, by=By.CSS_SELECTOR,
215216
start_ms = time.time() * 1000.0
216217
stop_ms = start_ms + (timeout * 1000.0)
217218
for x in range(int(timeout * 10)):
219+
shared_utils.check_if_time_limit_exceeded()
218220
try:
219221
element = driver.find_element(by=by, value=selector)
220222
return element
@@ -249,6 +251,7 @@ def wait_for_element_visible(driver, selector, by=By.CSS_SELECTOR,
249251
start_ms = time.time() * 1000.0
250252
stop_ms = start_ms + (timeout * 1000.0)
251253
for x in range(int(timeout * 10)):
254+
shared_utils.check_if_time_limit_exceeded()
252255
try:
253256
element = driver.find_element(by=by, value=selector)
254257
if element.is_displayed():
@@ -294,6 +297,7 @@ def wait_for_text_visible(driver, text, selector, by=By.CSS_SELECTOR,
294297
start_ms = time.time() * 1000.0
295298
stop_ms = start_ms + (timeout * 1000.0)
296299
for x in range(int(timeout * 10)):
300+
shared_utils.check_if_time_limit_exceeded()
297301
try:
298302
element = driver.find_element(by=by, value=selector)
299303
if element.is_displayed() and text in element.text:
@@ -336,6 +340,7 @@ def wait_for_exact_text_visible(driver, text, selector, by=By.CSS_SELECTOR,
336340
start_ms = time.time() * 1000.0
337341
stop_ms = start_ms + (timeout * 1000.0)
338342
for x in range(int(timeout * 10)):
343+
shared_utils.check_if_time_limit_exceeded()
339344
try:
340345
element = driver.find_element(by=by, value=selector)
341346
if element.is_displayed() and text.strip() == element.text.strip():
@@ -372,6 +377,7 @@ def wait_for_element_absent(driver, selector, by=By.CSS_SELECTOR,
372377
start_ms = time.time() * 1000.0
373378
stop_ms = start_ms + (timeout * 1000.0)
374379
for x in range(int(timeout * 10)):
380+
shared_utils.check_if_time_limit_exceeded()
375381
try:
376382
driver.find_element(by=by, value=selector)
377383
now_ms = time.time() * 1000.0
@@ -402,6 +408,7 @@ def wait_for_element_not_visible(driver, selector, by=By.CSS_SELECTOR,
402408
start_ms = time.time() * 1000.0
403409
stop_ms = start_ms + (timeout * 1000.0)
404410
for x in range(int(timeout * 10)):
411+
shared_utils.check_if_time_limit_exceeded()
405412
try:
406413
element = driver.find_element(by=by, value=selector)
407414
if element.is_displayed():
@@ -439,6 +446,7 @@ def wait_for_text_not_visible(driver, text, selector, by=By.CSS_SELECTOR,
439446
start_ms = time.time() * 1000.0
440447
stop_ms = start_ms + (timeout * 1000.0)
441448
for x in range(int(timeout * 10)):
449+
shared_utils.check_if_time_limit_exceeded()
442450
if not is_text_visible(driver, text, selector, by=by):
443451
return True
444452
now_ms = time.time() * 1000.0
@@ -603,6 +611,7 @@ def wait_for_and_switch_to_alert(driver, timeout=settings.LARGE_TIMEOUT):
603611
start_ms = time.time() * 1000.0
604612
stop_ms = start_ms + (timeout * 1000.0)
605613
for x in range(int(timeout * 10)):
614+
shared_utils.check_if_time_limit_exceeded()
606615
try:
607616
alert = driver.switch_to.alert
608617
# Raises exception if no alert present
@@ -628,6 +637,7 @@ def switch_to_frame(driver, frame, timeout=settings.SMALL_TIMEOUT):
628637
start_ms = time.time() * 1000.0
629638
stop_ms = start_ms + (timeout * 1000.0)
630639
for x in range(int(timeout * 10)):
640+
shared_utils.check_if_time_limit_exceeded()
631641
try:
632642
driver.switch_to.frame(frame)
633643
return True
@@ -665,6 +675,7 @@ def switch_to_window(driver, window, timeout=settings.SMALL_TIMEOUT):
665675
stop_ms = start_ms + (timeout * 1000.0)
666676
if isinstance(window, int):
667677
for x in range(int(timeout * 10)):
678+
shared_utils.check_if_time_limit_exceeded()
668679
try:
669680
window_handle = driver.window_handles[window]
670681
driver.switch_to.window(window_handle)
@@ -678,6 +689,7 @@ def switch_to_window(driver, window, timeout=settings.SMALL_TIMEOUT):
678689
else:
679690
window_handle = window
680691
for x in range(int(timeout * 10)):
692+
shared_utils.check_if_time_limit_exceeded()
681693
try:
682694
driver.switch_to.window(window_handle)
683695
return True

seleniumbase/fixtures/shared_utils.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""
2+
This module contains shared utility methods.
3+
"""
4+
import time
5+
from seleniumbase import config as sb_config
6+
7+
8+
def check_if_time_limit_exceeded():
9+
if sb_config.time_limit:
10+
time_limit = sb_config.time_limit
11+
now_ms = int(time.time() * 1000)
12+
if now_ms > sb_config.start_time_ms + sb_config.time_limit_ms:
13+
display_time_limit = time_limit
14+
plural = "s"
15+
if float(int(time_limit)) == float(time_limit):
16+
display_time_limit = int(time_limit)
17+
if display_time_limit == 1:
18+
plural = ""
19+
raise Exception(
20+
"This test has exceeded the time limit of %s second%s!"
21+
"" % (display_time_limit, plural))

seleniumbase/plugins/pytest_plugin.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def pytest_addoption(parser):
3030
--headed (The option to run tests with a GUI on Linux OS.)
3131
--start-page=URL (The starting URL for the web browser when tests begin.)
3232
--archive-logs (Archive old log files instead of deleting them.)
33+
--time-limit (The option to set a time limit per test before failing it.)
3334
--slow (The option to slow down the automation.)
3435
--demo (The option to visually see test actions as they occur.)
3536
--demo-sleep=SECONDS (The option to wait longer after Demo Mode actions.)
@@ -247,6 +248,12 @@ def pytest_addoption(parser):
247248
default=True,
248249
help="""This is used by the BaseCase class to tell apart
249250
pytest runs from nosetest runs. (Automatic)""")
251+
parser.addoption('--time_limit', '--time-limit', '--timelimit',
252+
action='store',
253+
dest='time_limit',
254+
default=None,
255+
help="""Use this to set a time limit per test, in seconds.
256+
If a test runs beyond the limit, it fails.""")
250257
parser.addoption('--slow_mode', '--slow-mode', '--slow',
251258
action="store_true",
252259
dest='slow_mode',
@@ -387,6 +394,7 @@ def pytest_configure(config):
387394
sb_config.database_env = config.getoption('database_env')
388395
sb_config.log_path = 'latest_logs/' # (No longer editable!)
389396
sb_config.archive_logs = config.getoption('archive_logs')
397+
sb_config.time_limit = config.getoption('time_limit')
390398
sb_config.slow_mode = config.getoption('slow_mode')
391399
sb_config.demo_mode = config.getoption('demo_mode')
392400
sb_config.demo_sleep = config.getoption('demo_sleep')

seleniumbase/plugins/selenium_plugin.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class SeleniumBrowser(Plugin):
2424
--headless (The option to run tests headlessly. The default on Linux OS.)
2525
--headed (The option to run tests with a GUI on Linux OS.)
2626
--start-page=URL (The starting URL for the web browser when tests begin.)
27+
--time-limit (The option to set a time limit per test before failing it.)
2728
--slow (The option to slow down the automation.)
2829
--demo (The option to visually see test actions as they occur.)
2930
--demo-sleep=SECONDS (The option to wait longer after Demo Mode actions.)
@@ -167,6 +168,13 @@ def options(self, parser, env):
167168
help="""Designates the starting URL for the web browser
168169
when each test begins.
169170
Default: None.""")
171+
parser.add_option(
172+
'--time_limit', '--time-limit', '--timelimit',
173+
action='store',
174+
dest='time_limit',
175+
default=None,
176+
help="""Use this to set a time limit per test, in seconds.
177+
If a test runs beyond the limit, it fails.""")
170178
parser.add_option(
171179
'--slow_mode', '--slow-mode', '--slow',
172180
action="store_true",
@@ -300,6 +308,7 @@ def beforeTest(self, test):
300308
test.test.user_agent = self.options.user_agent
301309
test.test.mobile_emulator = self.options.mobile_emulator
302310
test.test.device_metrics = self.options.device_metrics
311+
test.test.time_limit = self.options.time_limit
303312
test.test.slow_mode = self.options.slow_mode
304313
test.test.demo_mode = self.options.demo_mode
305314
test.test.demo_sleep = self.options.demo_sleep

0 commit comments

Comments
 (0)