Skip to content

Commit ac28e5a

Browse files
committed
Add the "--dashboard" option to create a dashboard
1 parent dc20567 commit ac28e5a

File tree

6 files changed

+413
-34
lines changed

6 files changed

+413
-34
lines changed

examples/raw_parameter_script.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@
6969
sb.demo_mode = False
7070
sb.time_limit = None
7171
sb.demo_sleep = 1
72+
sb.dashboard = False
73+
sb._dash_initialized = False
7274
sb.message_duration = 2
7375
sb.block_images = False
7476
sb.remote_debug = False

seleniumbase/core/log_helper.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,8 @@ def log_screenshot(test_logpath, driver, screenshot=None, get=False):
2727
print("WARNING: Unable to get screenshot for failure logs!")
2828

2929

30-
def log_test_failure_data(test, test_logpath, driver, browser, url=None):
31-
basic_info_name = settings.BASIC_INFO_NAME
32-
basic_file_path = "%s/%s" % (test_logpath, basic_info_name)
33-
log_file = codecs.open(basic_file_path, "w+", "utf-8")
34-
if url:
35-
last_page = url
36-
else:
37-
last_page = get_last_page(driver)
30+
def get_master_time():
31+
""" Returns (timestamp, the_date, the_time) """
3832
timestamp = str(int(time.time())) + " (Unix Timestamp)"
3933
now = datetime.datetime.now()
4034
utc_offset = -time.timezone / 3600.0
@@ -63,6 +57,18 @@ def log_test_failure_data(test, test_logpath, driver, browser, url=None):
6357
the_time = now.strftime("%I:%M:%S %p ") + time_zone
6458
if the_time.startswith("0"):
6559
the_time = the_time[1:]
60+
return timestamp, the_date, the_time
61+
62+
63+
def log_test_failure_data(test, test_logpath, driver, browser, url=None):
64+
basic_info_name = settings.BASIC_INFO_NAME
65+
basic_file_path = "%s/%s" % (test_logpath, basic_info_name)
66+
log_file = codecs.open(basic_file_path, "w+", "utf-8")
67+
if url:
68+
last_page = url
69+
else:
70+
last_page = get_last_page(driver)
71+
timestamp, the_date, the_time = get_master_time()
6672
test_id = get_test_id(test)
6773
data_to_save = []
6874
data_to_save.append("%s" % test_id)

seleniumbase/fixtures/base_case.py

Lines changed: 244 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3261,6 +3261,9 @@ def set_time_limit(self, time_limit):
32613261

32623262
def skip(self, reason=""):
32633263
""" Mark the test as Skipped. """
3264+
if self.dashboard:
3265+
test_id = self.__get_test_id_2()
3266+
sb_config._results[test_id] = "Skipped"
32643267
self.skipTest(reason)
32653268

32663269
############
@@ -4036,11 +4039,18 @@ def __create_highchart(
40364039
},
40374040
plotOptions: {
40384041
pie: {
4042+
size: "95%",
40394043
allowPointSelect: true,
4044+
animation: false,
40404045
cursor: 'pointer',
40414046
dataLabels: {
4042-
enabled: false,
4043-
format: '{point.name}: {point.y:.1f}%'
4047+
// enabled: false,
4048+
// format: '{point.name}: {point.y:.0f}',
4049+
formatter: function() {
4050+
if (this.y > 0) {
4051+
return this.point.name + ': ' + this.point.y
4052+
}
4053+
}
40444054
},
40454055
states: {
40464056
hover: {
@@ -4069,7 +4079,10 @@ def __create_highchart(
40694079
plotOptions: {
40704080
series: {
40714081
showInLegend: true,
4072-
animation: true,
4082+
animation: false,
4083+
dataLabels: {
4084+
enabled: true
4085+
},
40734086
shadow: false,
40744087
lineWidth: 3,
40754088
fillOpacity: 0.5,
@@ -4158,13 +4171,15 @@ def add_data_point(self, label, value, color=None, chart_name=None):
41584171
if self._chart_first_series[chart_name]:
41594172
self._chart_label[chart_name].append(label)
41604173

4161-
def save_chart(self, chart_name=None, filename=None):
4174+
def save_chart(self, chart_name=None, filename=None, folder=None):
41624175
""" Saves a SeleniumBase-generated chart to a file for later use.
41634176
@Params
41644177
chart_name - If creating multiple charts at the same time,
41654178
use this to select the one you wish to use.
41664179
filename - The name of the HTML file that you wish to
41674180
save the chart to. (filename must end in ".html")
4181+
folder - The name of the folder where you wish to
4182+
save the HTML file. (Default: "./saved_charts/")
41684183
"""
41694184
if not chart_name:
41704185
chart_name = "default"
@@ -4199,7 +4214,10 @@ def save_chart(self, chart_name=None, filename=None):
41994214
axis += "'%s'," % label
42004215
axis += "], crosshair: false},"
42014216
the_html = the_html.replace("xAxis: { },", axis)
4202-
saved_charts_folder = constants.Charts.SAVED_FOLDER
4217+
if not folder:
4218+
saved_charts_folder = constants.Charts.SAVED_FOLDER
4219+
else:
4220+
saved_charts_folder = folder
42034221
if saved_charts_folder.endswith("/"):
42044222
saved_charts_folder = saved_charts_folder[:-1]
42054223
if not os.path.exists(saved_charts_folder):
@@ -6268,6 +6286,8 @@ def setUp(self, masterqa_mode=False):
62686286
self.guest_mode = sb_config.guest_mode
62696287
self.devtools = sb_config.devtools
62706288
self.remote_debug = sb_config.remote_debug
6289+
self.dashboard = sb_config.dashboard
6290+
self._dash_initialized = sb_config._dashboard_initialized
62716291
self.swiftshader = sb_config.swiftshader
62726292
self.user_data_dir = sb_config.user_data_dir
62736293
self.extension_zip = sb_config.extension_zip
@@ -6385,6 +6405,16 @@ def setUp(self, masterqa_mode=False):
63856405
"AppleWebKit/537.36 (KHTML, like Gecko) "
63866406
"Chrome/76.0.3809.132 Mobile Safari/537.36")
63876407

6408+
# Dashboard pre-processing:
6409+
if self.dashboard:
6410+
sb_config._sbase_detected = True
6411+
sb_config._only_unittest = False
6412+
if not self._dash_initialized:
6413+
sb_config._dashboard_initialized = True
6414+
sb_config._sbase_detected = True
6415+
self._dash_initialized = True
6416+
self.__process_dashboard(False, init=True)
6417+
63886418
has_url = False
63896419
if self._reuse_session:
63906420
if not hasattr(sb_config, 'shared_driver'):
@@ -6598,13 +6628,219 @@ def __get_test_id(self):
65986628
test_id = self._sb_test_identifier
65996629
return test_id
66006630

6631+
def __get_test_id_2(self):
6632+
""" The id for SeleniumBase Dashboard entries. """
6633+
test_id = "%s.%s.%s" % (self.__class__.__module__.split('.')[-1],
6634+
self.__class__.__name__,
6635+
self._testMethodName)
6636+
if self._sb_test_identifier and len(str(self._sb_test_identifier)) > 6:
6637+
test_id = self._sb_test_identifier
6638+
if test_id.count('.') > 1:
6639+
test_id = '.'.join(test_id.split('.')[1:])
6640+
return test_id
6641+
6642+
def __get_display_id(self):
6643+
test_id = "%s.py::%s::%s" % (
6644+
self.__class__.__module__.replace('.', '/'),
6645+
self.__class__.__name__,
6646+
self._testMethodName)
6647+
if self._sb_test_identifier and len(str(self._sb_test_identifier)) > 6:
6648+
test_id = self._sb_test_identifier
6649+
if hasattr(self, "_using_sb_fixture_class"):
6650+
if test_id.count('.') >= 2:
6651+
parts = test_id.split('.')
6652+
full = parts[-3] + '.py::' + parts[-2] + '::' + parts[-1]
6653+
test_id = full
6654+
elif hasattr(self, "_using_sb_fixture_no_class"):
6655+
if test_id.count('.') >= 1:
6656+
parts = test_id.split('.')
6657+
full = parts[-2] + '.py::' + parts[-1]
6658+
test_id = full
6659+
return test_id
6660+
66016661
def __create_log_path_as_needed(self, test_logpath):
66026662
if not os.path.exists(test_logpath):
66036663
try:
66046664
os.makedirs(test_logpath)
66056665
except Exception:
66066666
pass # Only reachable during multi-threaded runs
66076667

6668+
def __process_dashboard(self, has_exception, init=False):
6669+
''' SeleniumBase Dashboard Processing '''
6670+
if len(sb_config._extra_dash_entries) > 1:
6671+
# First take care of existing entries from non-SeleniumBase tests
6672+
for test_id in sb_config._extra_dash_entries:
6673+
if test_id in sb_config._results.keys():
6674+
if sb_config._results[test_id] == "Skipped":
6675+
sb_config.item_count_skipped += 1
6676+
sb_config.item_count_untested -= 1
6677+
elif sb_config._results[test_id] == "Failed":
6678+
sb_config.item_count_failed += 1
6679+
sb_config.item_count_untested -= 1
6680+
elif sb_config._results[test_id] == "Passed":
6681+
sb_config.item_count_passed += 1
6682+
sb_config.item_count_untested -= 1
6683+
else: # Mark "Skipped" if unknown
6684+
sb_config.item_count_skipped += 1
6685+
sb_config.item_count_untested -= 1
6686+
sb_config._extra_dash_entries = [] # Reset the list to empty
6687+
# Process new entries
6688+
test_id = self.__get_test_id_2()
6689+
dud = "seleniumbase/plugins/pytest_plugin.py::BaseClass::base_method"
6690+
if not init:
6691+
duration_ms = int(time.time() * 1000) - sb_config.start_time_ms
6692+
duration = float(duration_ms) / 1000.0
6693+
sb_config._duration[test_id] = duration
6694+
if test_id not in sb_config._display_id.keys():
6695+
sb_config._display_id[test_id] = self.__get_display_id()
6696+
if sb_config._display_id[test_id] == dud:
6697+
return
6698+
if hasattr(self, "_using_sb_fixture") and (
6699+
test_id not in sb_config._results.keys()):
6700+
cwd = os.getcwd()
6701+
if '\\' in cwd:
6702+
cwd = cwd.split('\\')[-1]
6703+
else:
6704+
cwd = cwd.split('/')[-1]
6705+
if test_id.count('.') > 1:
6706+
alt_test_id = '.'.join(test_id.split('.')[1:])
6707+
if alt_test_id in sb_config._results.keys():
6708+
sb_config._results.pop(alt_test_id)
6709+
if test_id in sb_config._results.keys() and (
6710+
sb_config._results[test_id] == "Skipped"):
6711+
sb_config.item_count_skipped += 1
6712+
sb_config.item_count_untested -= 1
6713+
sb_config._results[test_id] = "Skipped"
6714+
elif has_exception:
6715+
sb_config._results[test_id] = "Failed"
6716+
sb_config.item_count_failed += 1
6717+
sb_config.item_count_untested -= 1
6718+
else:
6719+
sb_config._results[test_id] = "Passed"
6720+
sb_config.item_count_passed += 1
6721+
sb_config.item_count_untested -= 1
6722+
num_passed = sb_config.item_count_passed
6723+
num_failed = sb_config.item_count_failed
6724+
num_skipped = sb_config.item_count_skipped
6725+
num_untested = sb_config.item_count_untested
6726+
self.create_pie_chart(title=constants.Dashboard.TITLE)
6727+
self.add_data_point("Passed", num_passed, color="#84d474")
6728+
self.add_data_point("Untested", num_untested, color="#eaeaea")
6729+
self.add_data_point("Skipped", num_skipped, color="#efd8b4")
6730+
self.add_data_point("Failed", num_failed, color="#f17476")
6731+
style = (
6732+
'<link rel="stylesheet" '
6733+
'href="%s">' % constants.Dashboard.STYLE_CSS)
6734+
auto_refresh_html = ''
6735+
if num_untested > 0:
6736+
# Refresh every X seconds when waiting for more test results
6737+
auto_refresh_html = constants.Dashboard.META_REFRESH_HTML
6738+
head = (
6739+
'<head><meta charset="utf-8" />'
6740+
'<meta property="og:image" '
6741+
'content="https://seleniumbase.io/img/dash_pie.png">'
6742+
'<link rel="shortcut icon" '
6743+
'href="https://seleniumbase.io/img/dash_pie_2.png">'
6744+
'%s'
6745+
'<title>Dashboard</title>'
6746+
'%s</head>' % (auto_refresh_html, style))
6747+
table_html = (
6748+
'<div></div>'
6749+
'<table border="1px solid #e6e6e6;" width="100%;" padding: 5px;'
6750+
' font-size="12px;" text-align="left;" id="results-table">'
6751+
'<thead id="results-table-head"><tr>'
6752+
'<th col="result">Result</th><th col="name">Test</th>'
6753+
'<th col="duration">Duration</th><th col="links">Links</th>'
6754+
'</tr></thead>')
6755+
the_failed = []
6756+
the_skipped = []
6757+
the_passed = []
6758+
the_untested = []
6759+
for key in sb_config._results.keys():
6760+
t_res = sb_config._results[key]
6761+
t_dur = sb_config._duration[key]
6762+
t_d_id = sb_config._display_id[key]
6763+
res_low = t_res.lower()
6764+
if sb_config._results[key] == "Failed":
6765+
the_failed.append([res_low, t_res, t_d_id, t_dur])
6766+
if sb_config._results[key] == "Skipped":
6767+
the_skipped.append([res_low, t_res, t_d_id, t_dur])
6768+
if sb_config._results[key] == "Passed":
6769+
the_passed.append([res_low, t_res, t_d_id, t_dur])
6770+
if sb_config._results[key] == "Untested":
6771+
the_untested.append([res_low, t_res, t_d_id, t_dur])
6772+
for row in the_failed:
6773+
row = (
6774+
'<tbody class="%s results-table-row"><tr>'
6775+
'<td class="col-result">%s</td><td>%s</td><td>%s</td>'
6776+
'<td><a href="latest_logs/">latest_logs/</a></td>'
6777+
'</tr></tbody>' % (row[0], row[1], row[2], row[3]))
6778+
table_html += row
6779+
for row in the_skipped:
6780+
row = (
6781+
'<tbody class="%s results-table-row"><tr>'
6782+
'<td class="col-result">%s</td><td>%s</td><td>%s</td>'
6783+
'<td></td></tr></tbody>' % (row[0], row[1], row[2], row[3]))
6784+
table_html += row
6785+
for row in the_passed:
6786+
row = (
6787+
'<tbody class="%s results-table-row"><tr>'
6788+
'<td class="col-result">%s</td><td>%s</td><td>%s</td>'
6789+
'<td></td></tr></tbody>' % (row[0], row[1], row[2], row[3]))
6790+
table_html += row
6791+
for row in the_untested:
6792+
row = (
6793+
'<tbody class="%s results-table-row"><tr>'
6794+
'<td class="col-result">%s</td><td>%s</td><td>%s</td>'
6795+
'<td></td></tr></tbody>' % (row[0], row[1], row[2], row[3]))
6796+
table_html += row
6797+
table_html += "</table>"
6798+
add_more = "<br /><b>Last updated:</b> "
6799+
timestamp, the_date, the_time = log_helper.get_master_time()
6800+
last_updated = "%s at %s" % (the_date, the_time)
6801+
add_more = add_more + "%s" % last_updated
6802+
status = "<p></p><div><b>Status:</b> Awaiting results..."
6803+
status += " (Refresh the page for updates)"
6804+
if num_untested == 0:
6805+
status = "<p></p><div><b>Status:</b> Test Run Complete:"
6806+
if num_failed == 0:
6807+
if num_passed > 0:
6808+
if num_skipped == 0:
6809+
status += " <b>Success!</b> (All tests passed)"
6810+
else:
6811+
status += " <b>Success!</b> (No failing tests)"
6812+
else:
6813+
status += " All tests were skipped!"
6814+
else:
6815+
latest_logs_dir = "latest_logs/"
6816+
log_msg = "See latest logs for details"
6817+
if num_failed == 1:
6818+
status += (
6819+
' <b>1 test failed!</b> (<a href="%s">%s</a>)'
6820+
'' % (latest_logs_dir, log_msg))
6821+
else:
6822+
status += (
6823+
' <b>%s tests failed!</b> (<a href="%s">%s</a>)'
6824+
'' % (num_failed, latest_logs_dir, log_msg))
6825+
status += "</div><p></p>"
6826+
add_more = add_more + status
6827+
gen_by = (
6828+
'<p><div>Generated by: <b><a href="https://seleniumbase.io/">'
6829+
'SeleniumBase</a></b></div></p><p></p>')
6830+
add_more = add_more + gen_by
6831+
# Have dashboard auto-refresh on updates when using an http server
6832+
refresh_line = (
6833+
'<script type="text/javascript" src="%s">'
6834+
'</script>' % constants.Dashboard.LIVE_JS)
6835+
add_more = add_more + refresh_line
6836+
the_html = head + self.extract_chart() + table_html + add_more
6837+
abs_path = os.path.abspath('.')
6838+
file_path = os.path.join(abs_path, "dashboard.html")
6839+
out_file = codecs.open(file_path, "w+", encoding="utf-8")
6840+
out_file.writelines(the_html)
6841+
out_file.close()
6842+
time.sleep(0.05) # Add time for dashboard server to process updates
6843+
66086844
def has_exception(self):
66096845
""" (This method should ONLY be used in custom tearDown() methods.)
66106846
This method returns True if the test failed or raised an exception.
@@ -6691,6 +6927,7 @@ def tearDown(self):
66916927
# Save a screenshot if logging is on when an exception occurs
66926928
if has_exception:
66936929
self.__add_pytest_html_extra()
6930+
sb_config._has_exception = True
66946931
if self.with_testing_base and not has_exception and (
66956932
self.save_screenshot_after_test):
66966933
test_logpath = self.log_path + "/" + test_id
@@ -6742,6 +6979,8 @@ def tearDown(self):
67426979
log_helper.log_page_source(
67436980
test_logpath, self.driver,
67446981
self.__last_page_source)
6982+
if self.dashboard:
6983+
self.__process_dashboard(has_exception)
67456984
# (Pytest) Finally close all open browser windows
67466985
self.__quit_all_drivers()
67476986
if self.headless:

seleniumbase/fixtures/constants.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ class Charts:
2727
SAVED_FOLDER = "saved_charts"
2828

2929

30+
class Dashboard:
31+
TITLE = "SeleniumBase Test Results Dashboard"
32+
STYLE_CSS = 'https://seleniumbase.io/cdn/css/pytest_style.css'
33+
META_REFRESH_HTML = '<meta http-equiv="refresh" content="10">'
34+
# LIVE_JS = 'https://livejs.com/live.js#html'
35+
LIVE_JS = 'https://seleniumbase.io/cdn/js/live.js#html'
36+
37+
3038
class SavedCookies:
3139
STORAGE_FOLDER = "saved_cookies"
3240

0 commit comments

Comments
 (0)