Skip to content

Commit 887c83e

Browse files
committed
Add the ability to run parallel tests when using the Dashboard
1 parent e1087d7 commit 887c83e

File tree

3 files changed

+127
-20
lines changed

3 files changed

+127
-20
lines changed

seleniumbase/fixtures/base_case.py

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7269,15 +7269,20 @@ def setUp(self, masterqa_mode=False):
72697269
self.guest_mode = sb_config.guest_mode
72707270
self.devtools = sb_config.devtools
72717271
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
72727275
self.dashboard = sb_config.dashboard
72737276
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)
72747281
self.swiftshader = sb_config.swiftshader
72757282
self.user_data_dir = sb_config.user_data_dir
72767283
self.extension_zip = sb_config.extension_zip
72777284
self.extension_dir = sb_config.extension_dir
72787285
self.maximize_option = sb_config.maximize_option
7279-
self._reuse_session = sb_config.reuse_session
7280-
self._crumbs = sb_config.crumbs
72817286
self.save_screenshot_after_test = sb_config.save_screenshot
72827287
self.visual_baseline = sb_config.visual_baseline
72837288
self.timeout_multiplier = sb_config.timeout_multiplier
@@ -7391,13 +7396,21 @@ def setUp(self, masterqa_mode=False):
73917396

73927397
# Dashboard pre-processing:
73937398
if self.dashboard:
7394-
sb_config._sbase_detected = True
7395-
sb_config._only_unittest = False
7396-
if not self._dash_initialized:
7397-
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:
73987408
sb_config._sbase_detected = True
7399-
self._dash_initialized = True
7400-
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)
74017414

74027415
has_url = False
74037416
if self._reuse_session:
@@ -7696,6 +7709,23 @@ def __create_log_path_as_needed(self, test_logpath):
76967709

76977710
def __process_dashboard(self, has_exception, init=False):
76987711
''' 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
76997729
if len(sb_config._extra_dash_entries) > 0:
77007730
# First take care of existing entries from non-SeleniumBase tests
77017731
for test_id in sb_config._extra_dash_entries:
@@ -7741,6 +7771,11 @@ def __process_dashboard(self, has_exception, init=False):
77417771
sb_config.item_count_skipped += 1
77427772
sb_config.item_count_untested -= 1
77437773
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"
77447779
elif has_exception:
77457780
# pytest-rerunfailures may cause duplicate results
77467781
if test_id not in sb_config._results.keys() or (
@@ -7778,6 +7813,14 @@ def __process_dashboard(self, has_exception, init=False):
77787813
if sb_config._using_html_report:
77797814
# Add the pie chart to the pytest html report
77807815
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()
77817824
head = (
77827825
'<head><meta charset="utf-8" />'
77837826
'<meta property="og:image" '
@@ -7887,6 +7930,17 @@ def __process_dashboard(self, has_exception, init=False):
78877930
out_file.writelines(the_html)
78887931
out_file.close()
78897932
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()
78907944

78917945
def has_exception(self):
78927946
""" (This method should ONLY be used in custom tearDown() methods.)
@@ -8031,7 +8085,11 @@ def tearDown(self):
80318085
test_logpath, self.driver,
80328086
self.__last_page_source)
80338087
if self.dashboard:
8034-
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)
80358093
# (Pytest) Finally close all open browser windows
80368094
self.__quit_all_drivers()
80378095
if self.headless:

seleniumbase/fixtures/constants.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,13 @@ class Charts:
2929

3030
class Dashboard:
3131
TITLE = "SeleniumBase Test Results Dashboard"
32-
STYLE_CSS = 'https://seleniumbase.io/cdn/css/pytest_style.css'
32+
STYLE_CSS = "https://seleniumbase.io/cdn/css/pytest_style.css"
3333
META_REFRESH_HTML = '<meta http-equiv="refresh" content="12">'
3434
# LIVE_JS = 'https://livejs.com/live.js#html'
35-
LIVE_JS = 'https://seleniumbase.io/cdn/js/live.js#html'
35+
LIVE_JS = "https://seleniumbase.io/cdn/js/live.js#html"
36+
LOCKFILE = Files.DOWNLOADS_FOLDER + "/dashboard.lock"
37+
DASH_JSON = Files.DOWNLOADS_FOLDER + "/dashboard.json"
38+
DASH_PIE = Files.DOWNLOADS_FOLDER + "/dash_pie.json"
3639

3740

3841
class SavedCookies:

seleniumbase/plugins/pytest_plugin.py

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -582,13 +582,18 @@ def pytest_addoption(parser):
582582
"\n It's not thread-safe for WebDriver processes! "
583583
"\n Use --time-limit=s from SeleniumBase instead!\n")
584584

585-
# The SeleniumBase Dashboard does not yet support multi-threadeded tests.
586-
if "--dashboard" in sys_argv:
587-
arg_join = " ".join(sys_argv)
588-
if ("-n" in sys_argv) or ("-n=" in arg_join):
589-
raise Exception(
590-
"\n\n Multi-threading is not yet supported using --dashboard"
591-
"\n (You can speed up tests using --reuse-session / --rs)\n")
585+
# Dashboard Mode does not support tests using forked subprocesses.
586+
if "--forked" in sys_argv and "--dashboard" in sys_argv:
587+
raise Exception(
588+
'\n\n Dashboard Mode does NOT support forked subprocesses!'
589+
'\n (*** DO NOT combine "--forked" with "--dashboard"! ***)\n')
590+
591+
# Reuse-Session Mode does not support tests using forked subprocesses.
592+
if "--forked" in sys_argv and (
593+
"--rs" in sys_argv or "--reuse-session" in sys_argv):
594+
raise Exception(
595+
'\n\n Reuse-Session Mode does NOT support forked subprocesses!'
596+
'\n (DO NOT combine "--forked" with "--rs"/"--reuse-session"!)\n')
592597

593598
# As a shortcut, you can use "--edge" instead of "--browser=edge", etc,
594599
# but you can only specify one default browser for tests. (Default: chrome)
@@ -874,6 +879,16 @@ def pytest_sessionfinish(session):
874879

875880

876881
def pytest_terminal_summary(terminalreporter):
882+
latest_logs_dir = os.getcwd() + "/latest_logs/"
883+
if sb_config._multithreaded:
884+
if os.path.exists(latest_logs_dir) and os.listdir(latest_logs_dir):
885+
sb_config._has_exception = True
886+
if sb_config.dashboard:
887+
abs_path = os.path.abspath('.')
888+
dash_lock = constants.Dashboard.LOCKFILE
889+
dash_lock_path = os.path.join(abs_path, dash_lock)
890+
if os.path.exists(dash_lock_path):
891+
sb_config._only_unittest = False
877892
if sb_config._has_exception and (
878893
sb_config.dashboard and not sb_config._only_unittest):
879894
# Print link a second time because the first one may be off-screen
@@ -882,7 +897,6 @@ def pytest_terminal_summary(terminalreporter):
882897
"-", "Dashboard: %s" % dashboard_file)
883898
if sb_config._has_exception or sb_config.save_screenshot:
884899
# Log files are generated during test failures and Screenshot Mode
885-
latest_logs_dir = os.getcwd() + "/latest_logs/"
886900
terminalreporter.write_sep(
887901
"-", "LogPath: %s" % latest_logs_dir)
888902

@@ -903,10 +917,19 @@ def pytest_unconfigure():
903917
if hasattr(sb_config, 'log_path'):
904918
log_helper.archive_logs_if_set(
905919
sb_config.log_path, sb_config.archive_logs)
906-
907920
# Dashboard post-processing: Disable time-based refresh and stamp complete
921+
if sb_config._multithreaded and sb_config.dashboard:
922+
abs_path = os.path.abspath('.')
923+
dash_lock = constants.Dashboard.LOCKFILE
924+
dash_lock_path = os.path.join(abs_path, dash_lock)
925+
if os.path.exists(dash_lock_path):
926+
sb_config._only_unittest = False
908927
if hasattr(sb_config, 'dashboard') and (
909928
sb_config.dashboard and not sb_config._only_unittest):
929+
if sb_config._multithreaded:
930+
import fasteners
931+
dash_lock = fasteners.InterProcessLock(
932+
constants.Dashboard.LOCKFILE)
910933
stamp = ""
911934
if sb_config._dash_is_html_report:
912935
# (If the Dashboard URL is the same as the HTML Report URL:)
@@ -924,19 +947,35 @@ def pytest_unconfigure():
924947
swap_with_3 = '<td class="col-result">Unreported</td>'
925948
find_it_4 = 'href="https://seleniumbase.io/img/dash_pie.png"'
926949
swap_with_4 = 'href="https://seleniumbase.io/img/dash_pie_2.png"'
950+
find_it_5 = 'content="https://seleniumbase.io/img/dash_pie.png"'
951+
swap_with_5 = 'content="https://seleniumbase.io/img/dash_pie_2.png"'
927952
try:
953+
if sb_config._multithreaded:
954+
dash_lock.acquire()
928955
abs_path = os.path.abspath('.')
929956
dashboard_path = os.path.join(abs_path, "dashboard.html")
930957
# Part 1: Finalizing the dashboard / integrating html report
931958
if os.path.exists(dashboard_path):
932959
the_html_d = None
933960
with open(dashboard_path, 'r', encoding='utf-8') as f:
934961
the_html_d = f.read()
962+
if sb_config._multithreaded and "-c" in sys.argv:
963+
# Threads have "-c" in sys.argv, except for the last
964+
raise Exception('Break out of "try" block.')
965+
if sb_config._multithreaded:
966+
dash_pie_loc = constants.Dashboard.DASH_PIE
967+
pie_path = os.path.join(abs_path, dash_pie_loc)
968+
if os.path.exists(pie_path):
969+
import json
970+
with open(pie_path, 'r') as f:
971+
dash_pie = f.read().strip()
972+
sb_config._saved_dashboard_pie = json.loads(dash_pie)
935973
# If the test run doesn't complete by itself, stop refresh
936974
the_html_d = the_html_d.replace(find_it, swap_with)
937975
the_html_d = the_html_d.replace(find_it_2, swap_with_2)
938976
the_html_d = the_html_d.replace(find_it_3, swap_with_3)
939977
the_html_d = the_html_d.replace(find_it_4, swap_with_4)
978+
the_html_d = the_html_d.replace(find_it_5, swap_with_5)
940979
the_html_d += stamp
941980
if sb_config._dash_is_html_report and (
942981
sb_config._saved_dashboard_pie):
@@ -985,8 +1024,13 @@ def pytest_unconfigure():
9851024
the_html_r += sb_config._dash_final_summary
9861025
with open(html_report_path, "w", encoding='utf-8') as f:
9871026
f.write(the_html_r) # Finalize the HTML report
1027+
except KeyboardInterrupt:
1028+
pass
9881029
except Exception:
9891030
pass
1031+
finally:
1032+
if sb_config._multithreaded:
1033+
dash_lock.release()
9901034

9911035

9921036
@pytest.fixture()
@@ -1037,6 +1081,8 @@ def pytest_runtest_makereport(item, call):
10371081
pytest_html = item.config.pluginmanager.getplugin('html')
10381082
outcome = yield
10391083
report = outcome.get_result()
1084+
if sb_config._multithreaded:
1085+
sb_config._using_html_report = True # For Dashboard use
10401086
if pytest_html and report.when == 'call' and (
10411087
hasattr(sb_config, 'dashboard')):
10421088
if sb_config.dashboard and not sb_config._sbase_detected:

0 commit comments

Comments
 (0)