Skip to content

Commit 24e18ab

Browse files
authored
Merge pull request #875 from seleniumbase/combine-dashboard-with-parallel-tests
Allow parallel tests with "-n=NUM" when using "--dashboard" mode, and more
2 parents 83faa23 + b8107a7 commit 24e18ab

File tree

14 files changed

+172
-45
lines changed

14 files changed

+172
-45
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ report.xml
9999

100100
# Dashboard
101101
dashboard.html
102+
dashboard.json
103+
dash_pie.json
104+
dashboard.lock
102105

103106
# Allure Reports / Results
104107
allure_report
@@ -134,6 +137,7 @@ docs/*/*/*/*.md
134137
# Other
135138
selenium-server-standalone.jar
136139
proxy.zip
140+
proxy.lock
137141
verbose_hub_server.dat
138142
verbose_node_server.dat
139143
ip_of_grid_hub.dat

docs/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ docutils==0.17
77
Jinja2==2.11.3
88
readme-renderer==29.0
99
pymdown-extensions==8.1.1
10-
importlib-metadata==3.10.0;python_version>="3.6"
10+
importlib-metadata==3.10.1;python_version>="3.6"
1111
lunr==0.5.8
1212
mkdocs==1.1.2
1313
mkdocs-material==7.1.1

examples/raw_parameter_script.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
sb.use_auto_ext = False
5454
sb.no_sandbox = False
5555
sb.disable_gpu = False
56+
sb._multithreaded = False
5657
sb._reuse_session = False
5758
sb._crumbs = False
5859
sb.visual_baseline = False

help_docs/syntax_formats.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
--------
1111

12-
<b>SeleniumBase</b> supports 17 different syntax formats for structuring tests. (<i>The first 6 are the most common.</i>)
12+
<b>SeleniumBase</b> supports 17 different syntax formats (<i>design patterns</i>) for structuring tests. (<i>The first 6 are the most common.</i>)
1313

1414
<h3><img src="https://seleniumbase.io/img/green_logo.png" title="SeleniumBase" width="32" /> 1. <code>BaseCase</code> direct inheritance</h3>
1515

requirements.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ msedge-selenium-tools==3.141.3
2828
more-itertools==5.0.0;python_version<"3.5"
2929
more-itertools==8.7.0;python_version>="3.5"
3030
cssselect==1.1.0
31+
filelock==3.0.12
32+
fasteners==0.16
3133
pluggy==0.13.1
3234
py==1.8.1;python_version<"3.5"
3335
py==1.10.0;python_version>="3.5"
@@ -91,7 +93,7 @@ pdfminer.six==20201018;python_version>="3.5"
9193
coverage==5.5
9294
pytest-cov==2.11.1
9395
flake8==3.7.9;python_version<"3.5"
94-
flake8==3.9.0;python_version>="3.5"
96+
flake8==3.9.1;python_version>="3.5"
9597
pyflakes==2.1.1;python_version<"3.5"
9698
pyflakes==2.3.1;python_version>="3.5"
9799
pycodestyle==2.5.0;python_version<"3.5"

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__ = "1.60.0"
2+
__version__ = "1.61.0"

seleniumbase/console_scripts/sb_mkdir.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,9 @@ def main():
253253
data.append("report.html")
254254
data.append("report.xml")
255255
data.append("dashboard.html")
256+
data.append("dashboard.json")
257+
data.append("dash_pie.json")
258+
data.append("dashboard.lock")
256259
data.append("allure_report")
257260
data.append("allure-report")
258261
data.append("allure_results")
@@ -265,6 +268,7 @@ def main():
265268
data.append("visual_baseline")
266269
data.append("selenium-server-standalone.jar")
267270
data.append("proxy.zip")
271+
data.append("proxy.lock")
268272
data.append("verbose_hub_server.dat")
269273
data.append("verbose_node_server.dat")
270274
data.append("ip_of_grid_hub.dat")

seleniumbase/core/browser_launcher.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import os
33
import re
44
import sys
5-
import time
65
import urllib3
76
import warnings
87
from selenium import webdriver
@@ -29,6 +28,7 @@
2928
DISABLE_CSP_ZIP_PATH = "%s/%s" % (EXTENSIONS_DIR, "disable_csp.zip")
3029
PROXY_ZIP_PATH = proxy_helper.PROXY_ZIP_PATH
3130
PROXY_ZIP_PATH_2 = proxy_helper.PROXY_ZIP_PATH_2
31+
PROXY_ZIP_LOCK = proxy_helper.PROXY_ZIP_LOCK
3232
PLATFORM = sys.platform
3333
IS_WINDOWS = False
3434
LOCAL_CHROMEDRIVER = None
@@ -93,21 +93,18 @@ def _add_chrome_proxy_extension(
9393
""" Implementation of https://stackoverflow.com/a/35293284 for
9494
https://stackoverflow.com/questions/12848327/
9595
(Run Selenium on a proxy server that requires authentication.) """
96-
import random
9796
arg_join = " ".join(sys.argv)
98-
if not ("-n" in sys.argv or "-n=" in arg_join or arg_join == "-c"):
97+
if not ("-n" in sys.argv or " -n=" in arg_join or arg_join == "-c"):
9998
# Single-threaded
10099
proxy_helper.create_proxy_zip(proxy_string, proxy_user, proxy_pass)
101100
else:
102101
# Pytest multi-threaded test
103-
import threading
104-
lock = threading.Lock()
105-
with lock:
106-
time.sleep(random.uniform(0.02, 0.15))
102+
import fasteners
103+
proxy_zip_lock = fasteners.InterProcessLock(PROXY_ZIP_LOCK)
104+
with proxy_zip_lock:
107105
if not os.path.exists(PROXY_ZIP_PATH):
108106
proxy_helper.create_proxy_zip(
109107
proxy_string, proxy_user, proxy_pass)
110-
time.sleep(random.uniform(0.1, 0.2))
111108
proxy_zip = PROXY_ZIP_PATH
112109
if not os.path.exists(PROXY_ZIP_PATH):
113110
# Handle "Permission denied" on the default proxy.zip path

seleniumbase/core/proxy_helper.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
PROXY_ZIP_PATH = "%s/%s" % (DRIVER_DIR, "proxy.zip")
88
DOWNLOADS_DIR = constants.Files.DOWNLOADS_FOLDER
99
PROXY_ZIP_PATH_2 = "%s/%s" % (DOWNLOADS_DIR, "proxy.zip")
10+
PROXY_ZIP_LOCK = "%s/%s" % (DOWNLOADS_DIR, "proxy.lock")
1011

1112

1213
def create_proxy_zip(proxy_string, proxy_user, proxy_pass):
@@ -90,5 +91,7 @@ def remove_proxy_zip_if_present():
9091
os.remove(PROXY_ZIP_PATH)
9192
elif os.path.exists(PROXY_ZIP_PATH_2):
9293
os.remove(PROXY_ZIP_PATH_2)
94+
if os.path.exists(PROXY_ZIP_LOCK):
95+
os.remove(PROXY_ZIP_LOCK)
9396
except Exception:
9497
pass

seleniumbase/fixtures/base_case.py

Lines changed: 79 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ def __init__(self, *args, **kwargs):
8080
self.__device_width = None
8181
self.__device_height = None
8282
self.__device_pixel_ratio = None
83+
self.__driver_browser_map = {}
8384
# Requires self._* instead of self.__* for external class use
8485
self._language = "English"
8586
self._presentation_slides = {}
@@ -2134,8 +2135,10 @@ def get_new_driver(self, browser=None, headless=None, locale_code=None,
21342135
device_height=d_height,
21352136
device_pixel_ratio=d_p_r)
21362137
self._drivers_list.append(new_driver)
2138+
self.__driver_browser_map[new_driver] = browser_name
21372139
if switch_to:
21382140
self.driver = new_driver
2141+
self.browser = browser_name
21392142
if self.headless:
21402143
# Make sure the invisible browser window is big enough
21412144
width = settings.HEADLESS_START_WIDTH
@@ -2209,11 +2212,15 @@ def switch_to_driver(self, driver):
22092212
""" Sets self.driver to the specified driver.
22102213
You may need this if using self.get_new_driver() in your code. """
22112214
self.driver = driver
2215+
if self.driver in self.__driver_browser_map:
2216+
self.browser = self.__driver_browser_map[self.driver]
22122217

22132218
def switch_to_default_driver(self):
22142219
""" Sets self.driver to the default/original driver. """
22152220
self.__check_scope()
22162221
self.driver = self._default_driver
2222+
if self.driver in self.__driver_browser_map:
2223+
self.browser = self.__driver_browser_map[self.driver]
22172224

22182225
def save_screenshot(self, name, folder=None):
22192226
""" The screenshot will be in PNG format. """
@@ -6497,7 +6504,7 @@ def check_window(self, name="default", level=0, baseline=False):
64976504
self.check_window(name="github_page", level=2)
64986505
self.check_window(name="wikipedia_page", level=3)
64996506
"""
6500-
self.__check_scope()
6507+
self.wait_for_ready_state_complete()
65016508
if level == "0":
65026509
level = 0
65036510
if level == "1":
@@ -6510,11 +6517,10 @@ def check_window(self, name="default", level=0, baseline=False):
65106517
raise Exception('Parameter "level" must be set to 0, 1, 2, or 3!')
65116518

65126519
if self.demo_mode:
6513-
raise Exception(
6514-
"WARNING: Using Demo Mode will break layout tests "
6515-
"that use the check_window() method due to custom "
6516-
"HTML edits being made on the page!\n"
6517-
"Please rerun without using Demo Mode!")
6520+
message = (
6521+
"WARNING: Using check_window() from Demo Mode may lead "
6522+
"to unexpected results caused by Demo Mode HTML changes.")
6523+
logging.info(message)
65186524

65196525
module = self.__class__.__module__
65206526
if '.' in module and len(module.split('.')[-1]) > 1:
@@ -7263,15 +7269,20 @@ def setUp(self, masterqa_mode=False):
72637269
self.guest_mode = sb_config.guest_mode
72647270
self.devtools = sb_config.devtools
72657271
self.remote_debug = sb_config.remote_debug
7272+
self._multithreaded = sb_config._multithreaded
7273+
self._reuse_session = sb_config.reuse_session
7274+
self._crumbs = sb_config.crumbs
72667275
self.dashboard = sb_config.dashboard
72677276
self._dash_initialized = sb_config._dashboard_initialized
7277+
if self.dashboard and self._multithreaded:
7278+
import fasteners
7279+
self.dash_lock = fasteners.InterProcessLock(
7280+
constants.Dashboard.LOCKFILE)
72687281
self.swiftshader = sb_config.swiftshader
72697282
self.user_data_dir = sb_config.user_data_dir
72707283
self.extension_zip = sb_config.extension_zip
72717284
self.extension_dir = sb_config.extension_dir
72727285
self.maximize_option = sb_config.maximize_option
7273-
self._reuse_session = sb_config.reuse_session
7274-
self._crumbs = sb_config.crumbs
72757286
self.save_screenshot_after_test = sb_config.save_screenshot
72767287
self.visual_baseline = sb_config.visual_baseline
72777288
self.timeout_multiplier = sb_config.timeout_multiplier
@@ -7385,13 +7396,21 @@ def setUp(self, masterqa_mode=False):
73857396

73867397
# Dashboard pre-processing:
73877398
if self.dashboard:
7388-
sb_config._sbase_detected = True
7389-
sb_config._only_unittest = False
7390-
if not self._dash_initialized:
7391-
sb_config._dashboard_initialized = True
7399+
if self._multithreaded:
7400+
with self.dash_lock:
7401+
sb_config._sbase_detected = True
7402+
sb_config._only_unittest = False
7403+
if not self._dash_initialized:
7404+
sb_config._dashboard_initialized = True
7405+
self._dash_initialized = True
7406+
self.__process_dashboard(False, init=True)
7407+
else:
73927408
sb_config._sbase_detected = True
7393-
self._dash_initialized = True
7394-
self.__process_dashboard(False, init=True)
7409+
sb_config._only_unittest = False
7410+
if not self._dash_initialized:
7411+
sb_config._dashboard_initialized = True
7412+
self._dash_initialized = True
7413+
self.__process_dashboard(False, init=True)
73957414

73967415
has_url = False
73977416
if self._reuse_session:
@@ -7690,6 +7709,23 @@ def __create_log_path_as_needed(self, test_logpath):
76907709

76917710
def __process_dashboard(self, has_exception, init=False):
76927711
''' SeleniumBase Dashboard Processing '''
7712+
existing_res = sb_config._results # Used by multithreaded tests
7713+
if self._multithreaded:
7714+
abs_path = os.path.abspath('.')
7715+
dash_json_loc = constants.Dashboard.DASH_JSON
7716+
dash_jsonpath = os.path.join(abs_path, dash_json_loc)
7717+
if not init and os.path.exists(dash_jsonpath):
7718+
with open(dash_jsonpath, 'r') as f:
7719+
dash_json = f.read().strip()
7720+
dash_data, d_id, dash_runtimes, d_stats = json.loads(dash_json)
7721+
num_passed, num_failed, num_skipped, num_untested = d_stats
7722+
sb_config._results = dash_data
7723+
sb_config._display_id = d_id
7724+
sb_config._duration = dash_runtimes
7725+
sb_config.item_count_passed = num_passed
7726+
sb_config.item_count_failed = num_failed
7727+
sb_config.item_count_skipped = num_skipped
7728+
sb_config.item_count_untested = num_untested
76937729
if len(sb_config._extra_dash_entries) > 0:
76947730
# First take care of existing entries from non-SeleniumBase tests
76957731
for test_id in sb_config._extra_dash_entries:
@@ -7735,6 +7771,11 @@ def __process_dashboard(self, has_exception, init=False):
77357771
sb_config.item_count_skipped += 1
77367772
sb_config.item_count_untested -= 1
77377773
sb_config._results[test_id] = "Skipped"
7774+
elif self._multithreaded and test_id in existing_res.keys() and (
7775+
existing_res[test_id] == "Skipped"):
7776+
sb_config.item_count_skipped += 1
7777+
sb_config.item_count_untested -= 1
7778+
sb_config._results[test_id] = "Skipped"
77387779
elif has_exception:
77397780
# pytest-rerunfailures may cause duplicate results
77407781
if test_id not in sb_config._results.keys() or (
@@ -7772,6 +7813,14 @@ def __process_dashboard(self, has_exception, init=False):
77727813
if sb_config._using_html_report:
77737814
# Add the pie chart to the pytest html report
77747815
sb_config._saved_dashboard_pie = self.extract_chart()
7816+
if self._multithreaded:
7817+
abs_path = os.path.abspath('.')
7818+
dash_pie = json.dumps(sb_config._saved_dashboard_pie)
7819+
dash_pie_loc = constants.Dashboard.DASH_PIE
7820+
pie_path = os.path.join(abs_path, dash_pie_loc)
7821+
pie_file = codecs.open(pie_path, "w+", encoding="utf-8")
7822+
pie_file.writelines(dash_pie)
7823+
pie_file.close()
77757824
head = (
77767825
'<head><meta charset="utf-8" />'
77777826
'<meta property="og:image" '
@@ -7881,6 +7930,17 @@ def __process_dashboard(self, has_exception, init=False):
78817930
out_file.writelines(the_html)
78827931
out_file.close()
78837932
time.sleep(0.05) # Add time for dashboard server to process updates
7933+
if self._multithreaded:
7934+
d_stats = (num_passed, num_failed, num_skipped, num_untested)
7935+
_results = sb_config._results
7936+
_display_id = sb_config._display_id
7937+
_duration = sb_config._duration
7938+
dash_json = json.dumps((_results, _display_id, _duration, d_stats))
7939+
dash_json_loc = constants.Dashboard.DASH_JSON
7940+
dash_jsonpath = os.path.join(abs_path, dash_json_loc)
7941+
dash_json_file = codecs.open(dash_jsonpath, "w+", encoding="utf-8")
7942+
dash_json_file.writelines(dash_json)
7943+
dash_json_file.close()
78847944

78857945
def has_exception(self):
78867946
""" (This method should ONLY be used in custom tearDown() methods.)
@@ -8025,7 +8085,11 @@ def tearDown(self):
80258085
test_logpath, self.driver,
80268086
self.__last_page_source)
80278087
if self.dashboard:
8028-
self.__process_dashboard(has_exception)
8088+
if self._multithreaded:
8089+
with self.dash_lock:
8090+
self.__process_dashboard(has_exception)
8091+
else:
8092+
self.__process_dashboard(has_exception)
80298093
# (Pytest) Finally close all open browser windows
80308094
self.__quit_all_drivers()
80318095
if self.headless:

0 commit comments

Comments
 (0)