robotframework-failuresummary 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ from .listener import ROBOT_LISTENER_API_VERSION, start_suite, start_test, output_file, log_file, close
2
+
3
+ __all__ = [
4
+ "ROBOT_LISTENER_API_VERSION",
5
+ "start_suite",
6
+ "start_test",
7
+ "output_file",
8
+ "log_file",
9
+ "close"
10
+ ]
@@ -0,0 +1,34 @@
1
+ function openLogTab(url) {
2
+ var tab = window.open('', 'rf_log_viewer');
3
+ if (tab) {
4
+ var parts = url.split('#');
5
+ var base = parts[0];
6
+ var hash = parts[1] ? parts[1] : '';
7
+ var separator = base.indexOf('?') !== -1 ? '&' : '?';
8
+
9
+ // Cache-buster forces log.html to evaluate fresh parameters even on repetitive clicks
10
+ tab.location.href = base + separator + 'cb=' + new Date().getTime() + (hash ? '#' + hash : '');
11
+
12
+ if (hash) {
13
+ var checks = 0;
14
+ var interval = setInterval(function() {
15
+ checks++;
16
+ if (tab.util && tab.util.expandElementWithId) {
17
+ clearInterval(interval);
18
+ try {
19
+ // Direct call to RF internal framework to force full ancestral layout tree expansion
20
+ tab.util.expandElementWithId(hash);
21
+ var element = tab.document.getElementById(hash);
22
+ if (element) {
23
+ element.scrollIntoView({ block: "center", behavior: "smooth" });
24
+ }
25
+ } catch(e) {
26
+ console.log("Deep expand injection waiting...", e);
27
+ }
28
+ }
29
+ if (checks > 50) clearInterval(interval);
30
+ }, 100);
31
+ }
32
+ tab.focus();
33
+ }
34
+ }
@@ -0,0 +1,250 @@
1
+ import atexit
2
+ import logging
3
+ import os
4
+ from robot.api import ExecutionResult
5
+ import html as html_escaper
6
+ import sys
7
+ from urllib3.util.retry import Retry
8
+ import glob
9
+
10
+ Retry.DEFAULT = Retry(0)
11
+ ROBOT_LISTENER_API_VERSION = 3
12
+
13
+ _output_dir = None
14
+ _is_pipeline = os.environ.get('RF_SUMMARY_PIPELINE_MODE', 'false').lower() == 'true'
15
+ _close_called = False
16
+
17
+ _SCREENSHOT_PATTERNS = [
18
+ 'selenium-screenshot-*.png', 'screenshot-*.png', '*-screenshot-*.png', 'selenium-*.png',
19
+ 'browser-screenshot-*.png', 'playwright-screenshot-*.png', 'playwright-*.png',
20
+ 'robotframework-browser-screenshot-*.png', 'browser-*.png', '*.webm', 'trace-*.zip',
21
+ ]
22
+
23
+ _SELENIUM_LOGGERS = ['SeleniumLibrary', 'selenium.webdriver.remote.remote_connection', 'urllib3.connectionpool']
24
+ _BROWSER_LOGGERS = ['Browser', 'Browser.utils', 'Browser.playwright', 'grpc', 'asyncio']
25
+
26
+
27
+ def _resolve_output_dir_from_argv():
28
+ global _output_dir
29
+ if _output_dir:
30
+ return _output_dir
31
+ args = sys.argv
32
+ for i, arg in enumerate(args):
33
+ if arg in ('-d', '--outputdir') and i + 1 < len(args):
34
+ resolved = os.path.abspath(args[i + 1])
35
+ _output_dir = resolved
36
+ return resolved
37
+ return None
38
+
39
+
40
+ def find_deepest_failures(item):
41
+ failures = []
42
+ if hasattr(item, 'body') and item.body:
43
+ failing_children = [k for k in item.body if hasattr(k, 'status') and k.status == 'FAIL']
44
+ if not failing_children:
45
+ if hasattr(item, 'status') and item.status == 'FAIL':
46
+ failures.append(item)
47
+ else:
48
+ for child in failing_children:
49
+ failures.extend(find_deepest_failures(child))
50
+ elif hasattr(item, 'status') and item.status == 'FAIL':
51
+ failures.append(item)
52
+ return failures
53
+
54
+
55
+ def extract_fail_details(failure):
56
+ fail_msg = None
57
+ exception_lines = []
58
+ raw_messages = []
59
+ if hasattr(failure, 'messages'):
60
+ raw_messages.extend(list(failure.messages))
61
+ if hasattr(failure, 'body'):
62
+ for item in failure.body:
63
+ if hasattr(item, 'message') and hasattr(item, 'level'):
64
+ raw_messages.append(item)
65
+
66
+ for msg in raw_messages:
67
+ level = getattr(msg, 'level', '') or ''
68
+ text = getattr(msg, 'message', '') or ''
69
+ if level == 'FAIL' and not fail_msg:
70
+ fail_msg = text.strip()
71
+ for raw_line in text.splitlines():
72
+ line = raw_line.strip()
73
+ if not line or line == 'None':
74
+ continue
75
+ if any(x in line for x in ('Error:', 'Exception:', 'Fault:', 'Warning:')):
76
+ if line not in exception_lines:
77
+ exception_lines.append(line)
78
+ return fail_msg, exception_lines
79
+
80
+
81
+ def _generate_summary():
82
+ global _close_called, _output_dir
83
+ if _close_called:
84
+ return
85
+ _close_called = True
86
+
87
+ print("##[section]Generating failure summary...")
88
+ _resolve_output_dir_from_argv()
89
+ output_dir = _output_dir if _output_dir else '.'
90
+ output_xml = os.path.join(output_dir, 'output.xml')
91
+ summary_path = os.path.join(output_dir, 'failure_summary.html')
92
+
93
+ if not os.path.exists(output_xml):
94
+ print("##[warning]Could not find output.xml at: " + output_xml)
95
+ return
96
+
97
+ try:
98
+ result = ExecutionResult(output_xml)
99
+ except Exception as e:
100
+ print("##[error]Could not read output.xml. Error: " + str(e))
101
+ return
102
+
103
+ final_report_data = []
104
+
105
+ def collect_failures_from_suite(suite):
106
+ for test in suite.tests:
107
+ if test.status == 'FAIL':
108
+ deepest_failure_objects = []
109
+ if test.setup and test.setup.status == 'FAIL':
110
+ deepest_failure_objects.extend(find_deepest_failures(test.setup))
111
+ for item in test.body:
112
+ if hasattr(item, 'status') and item.status == 'FAIL':
113
+ deepest_failure_objects.extend(find_deepest_failures(item))
114
+ if test.teardown and test.teardown.status == 'FAIL':
115
+ deepest_failure_objects.extend(find_deepest_failures(test.teardown))
116
+
117
+ unique_failures = {failure.id: failure for failure in deepest_failure_objects}.values()
118
+
119
+ for failure in unique_failures:
120
+ path = []
121
+ current = failure
122
+ while current and hasattr(current, 'parent') and current.id != test.id:
123
+ name_or_type = getattr(current, 'name', None) or getattr(current, 'type', None)
124
+ if name_or_type:
125
+ path.insert(0, str(name_or_type))
126
+ current = current.parent
127
+
128
+ fail_msg, exception_lines = extract_fail_details(failure)
129
+ final_report_data.append({
130
+ 'test_name': test.name,
131
+ 'test_id': test.id,
132
+ 'failure_path': ' > '.join(path),
133
+ 'failure_id': failure.id,
134
+ 'fail_msg': fail_msg,
135
+ 'exception_lines': exception_lines
136
+ })
137
+ for child_suite in suite.suites:
138
+ collect_failures_from_suite(child_suite)
139
+
140
+ collect_failures_from_suite(result.suite)
141
+
142
+ if not final_report_data:
143
+ print("##[section]No failures found - all tests passed!")
144
+ if os.path.exists(summary_path):
145
+ try: os.remove(summary_path)
146
+ except Exception: pass
147
+ return
148
+
149
+ # Modern package asset extraction via importlib
150
+ try:
151
+ from importlib.resources import files
152
+ html_template = files("RobotFailureSummary").joinpath("templates", "summary.html").read_text(encoding="utf-8")
153
+ javascript_code = files("RobotFailureSummary").joinpath("js", "expander.js").read_text(encoding="utf-8")
154
+ except Exception:
155
+ current_dir = os.path.dirname(os.path.abspath(__file__))
156
+ with open(os.path.join(current_dir, 'templates', 'summary.html'), 'r', encoding='utf-8') as tmpl:
157
+ html_template = tmpl.read()
158
+ with open(os.path.join(current_dir, 'js', 'expander.js'), 'r', encoding='utf-8') as js_file:
159
+ javascript_code = js_file.read()
160
+
161
+ cards_html = ""
162
+ for f in final_report_data:
163
+ path_display = " → ".join([f'<span>{html_escaper.escape(p)}</span>' for p in f['failure_path'].split(' > ') if p])
164
+ link = f"log.html?expand={f['failure_id']}#{f['failure_id']}"
165
+ fail_block = f'<div class="detail-block"><div class="detail-label">⛔ Fail Message:</div><div class="fail-text">{html_escaper.escape(f["fail_msg"])}</div></div>' if f.get('fail_msg') else ""
166
+
167
+ exception_block = ""
168
+ if f.get('exception_lines'):
169
+ exception_rows = "".join(f'<div class="exception-text">{html_escaper.escape(line)}</div>' for line in f['exception_lines'])
170
+ exception_block = f'<div class="detail-block"><div class="detail-label">🔴 Exception / Error:</div>{exception_rows}</div>'
171
+
172
+ cards_html += f"""
173
+ <div class="failure-card">
174
+ <span class="test-name">{html_escaper.escape(f['test_name'])}</span>
175
+ <div class="path">{path_display}</div>
176
+ {fail_block}
177
+ {exception_block}
178
+ <a class="jump-btn" onclick="openLogTab('{link}')">Jump to Failing Keyword ↗</a>
179
+ </div>
180
+ """
181
+
182
+ final_html = html_template.replace("{{JAVASCRIPT}}", javascript_code).replace("{{CARDS}}", cards_html)
183
+
184
+ try:
185
+ with open(summary_path, "w", encoding="utf-8") as f:
186
+ f.write(final_html)
187
+ print("Failure Summary generated successfully: " + summary_path)
188
+ except Exception as e:
189
+ print("##[error]Failed to write failure_summary.html: " + str(e))
190
+
191
+
192
+ atexit.register(_generate_summary)
193
+
194
+ def _mute_noisy_loggers():
195
+ for name in _SELENIUM_LOGGERS + _BROWSER_LOGGERS:
196
+ logging.getLogger(name).setLevel(logging.INFO)
197
+
198
+ def start_suite(data, result):
199
+ global _output_dir
200
+ from robot.running.context import EXECUTION_CONTEXTS
201
+ if EXECUTION_CONTEXTS.current:
202
+ EXECUTION_CONTEXTS.current.output.set_log_level('TRACE')
203
+ if hasattr(result, 'suite') and hasattr(result.suite, 'source'):
204
+ _output_dir = os.path.dirname(result.suite.source)
205
+ if not _output_dir:
206
+ _output_dir = os.environ.get('ROBOT_OUTPUT_DIR', None)
207
+ if not _is_pipeline:
208
+ cleanup_old_files()
209
+ _mute_noisy_loggers()
210
+
211
+ def cleanup_old_files():
212
+ global _output_dir
213
+ output_dir = _output_dir if _output_dir else '.'
214
+ if not os.path.exists(output_dir): return
215
+ try:
216
+ for pattern in _SCREENSHOT_PATTERNS:
217
+ for file_path in glob.glob(os.path.join(output_dir, pattern)):
218
+ try: os.remove(file_path)
219
+ except Exception: pass
220
+ for sub in ('browser', 'screenshots', 'playwright-report'):
221
+ sub_path = os.path.join(output_dir, sub)
222
+ if os.path.isdir(sub_path):
223
+ for pattern in _SCREENSHOT_PATTERNS:
224
+ for file_path in glob.glob(os.path.join(sub_path, pattern)):
225
+ try: os.remove(file_path)
226
+ except Exception: pass
227
+ summary_path = os.path.join(output_dir, 'failure_summary.html')
228
+ if os.path.exists(summary_path): os.remove(summary_path)
229
+ except Exception: pass
230
+
231
+ def start_test(data, result):
232
+ from robot.running.context import EXECUTION_CONTEXTS
233
+ if EXECUTION_CONTEXTS.current:
234
+ EXECUTION_CONTEXTS.current.output.set_log_level('TRACE')
235
+
236
+ def output_file(path):
237
+ global _output_dir
238
+ _output_dir = os.path.dirname(path)
239
+
240
+ def log_file(path):
241
+ try:
242
+ with open(path, 'r', encoding='utf-8') as f:
243
+ content = f.read()
244
+ content = content.replace('"minLevel":"INFO"', '"minLevel":"TRACE"').replace("'minLevel':'INFO'", "'minLevel':'TRACE'")
245
+ with open(path, 'w', encoding='utf-8') as f:
246
+ f.write(content)
247
+ except Exception: pass
248
+
249
+ def close():
250
+ _generate_summary()
@@ -0,0 +1,30 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Failure Summary</title>
6
+ <style>
7
+ body { font-family: Arial, sans-serif; padding: 20px; background: #1a1a2e; color: #eee; }
8
+ h2 { color: #e94560; }
9
+ .failure-card { background: #16213e; border-left: 4px solid #e94560; padding: 12px 16px; margin: 10px 0; border-radius: 4px; }
10
+ .test-name { font-weight: bold; color: #0f3460; background: #e94560; padding: 2px 8px; border-radius: 3px; font-size: 12px; }
11
+ .path { margin: 8px 0; color: #aaa; font-size: 13px; word-break: break-all; }
12
+ .path span { color: #fff; }
13
+ a.jump-btn { display: inline-block; margin-top: 6px; background: #e94560; color: white; padding: 4px 12px; border-radius: 4px; text-decoration: none; font-size: 13px; cursor: pointer; }
14
+ a.jump-btn:hover { background: #c73652; }
15
+ .detail-block { margin-top: 10px; padding: 8px 12px; background: #0f1b33; border-radius: 4px; font-size: 13px; border: 1px solid #2a2a4a; }
16
+ .detail-label { color: #e94560; font-weight: bold; font-size: 11px; letter-spacing: 0.8px; text-transform: uppercase; margin-bottom: 4px; }
17
+ .fail-text { color: #ffcdd2; font-family: monospace; white-space: pre-wrap; word-break: break-all; }
18
+ .exception-text { color: #ff6b6b; font-family: monospace; white-space: pre-wrap; word-break: break-all; margin: 2px 0; }
19
+ </style>
20
+ </head>
21
+ <body>
22
+ <h2>⚠ Failure Summary — Click to Jump to Root Cause</h2>
23
+
24
+ {{CARDS}}
25
+
26
+ <script>
27
+ {{JAVASCRIPT}}
28
+ </script>
29
+ </body>
30
+ </html>
@@ -0,0 +1,34 @@
1
+ function openLogTab(url) {
2
+ var tab = window.open('', 'rf_log_viewer');
3
+ if (tab) {
4
+ var parts = url.split('#');
5
+ var base = parts[0];
6
+ var hash = parts[1] ? parts[1] : '';
7
+ var separator = base.indexOf('?') !== -1 ? '&' : '?';
8
+
9
+ // Cache-buster forces log.html to evaluate fresh parameters even on repetitive clicks
10
+ tab.location.href = base + separator + 'cb=' + new Date().getTime() + (hash ? '#' + hash : '');
11
+
12
+ if (hash) {
13
+ var checks = 0;
14
+ var interval = setInterval(function() {
15
+ checks++;
16
+ if (tab.util && tab.util.expandElementWithId) {
17
+ clearInterval(interval);
18
+ try {
19
+ // Direct call to RF internal framework to force full ancestral layout tree expansion
20
+ tab.util.expandElementWithId(hash);
21
+ var element = tab.document.getElementById(hash);
22
+ if (element) {
23
+ element.scrollIntoView({ block: "center", behavior: "smooth" });
24
+ }
25
+ } catch(e) {
26
+ console.log("Deep expand injection waiting...", e);
27
+ }
28
+ }
29
+ if (checks > 50) clearInterval(interval);
30
+ }, 100);
31
+ }
32
+ tab.focus();
33
+ }
34
+ }
@@ -0,0 +1,30 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Failure Summary</title>
6
+ <style>
7
+ body { font-family: Arial, sans-serif; padding: 20px; background: #1a1a2e; color: #eee; }
8
+ h2 { color: #e94560; }
9
+ .failure-card { background: #16213e; border-left: 4px solid #e94560; padding: 12px 16px; margin: 10px 0; border-radius: 4px; }
10
+ .test-name { font-weight: bold; color: #0f3460; background: #e94560; padding: 2px 8px; border-radius: 3px; font-size: 12px; }
11
+ .path { margin: 8px 0; color: #aaa; font-size: 13px; word-break: break-all; }
12
+ .path span { color: #fff; }
13
+ a.jump-btn { display: inline-block; margin-top: 6px; background: #e94560; color: white; padding: 4px 12px; border-radius: 4px; text-decoration: none; font-size: 13px; cursor: pointer; }
14
+ a.jump-btn:hover { background: #c73652; }
15
+ .detail-block { margin-top: 10px; padding: 8px 12px; background: #0f1b33; border-radius: 4px; font-size: 13px; border: 1px solid #2a2a4a; }
16
+ .detail-label { color: #e94560; font-weight: bold; font-size: 11px; letter-spacing: 0.8px; text-transform: uppercase; margin-bottom: 4px; }
17
+ .fail-text { color: #ffcdd2; font-family: monospace; white-space: pre-wrap; word-break: break-all; }
18
+ .exception-text { color: #ff6b6b; font-family: monospace; white-space: pre-wrap; word-break: break-all; margin: 2px 0; }
19
+ </style>
20
+ </head>
21
+ <body>
22
+ <h2>⚠ Failure Summary — Click to Jump to Root Cause</h2>
23
+
24
+ {{CARDS}}
25
+
26
+ <script>
27
+ {{JAVASCRIPT}}
28
+ </script>
29
+ </body>
30
+ </html>
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: robotframework-failuresummary
3
+ Version: 1.0.0
4
+ Summary: A standard Robot Framework listener that mutes background engine noise and generates an interactive, deep-linking failure summary report.
5
+ Project-URL: Homepage, https://github.com/Srinivasan2802/robotframework-failuresummary
6
+ Author-email: Srinivasan A <sriniagt.cse@gmail.com>
7
+ License: MIT
8
+ Classifier: Framework :: Robot Framework
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Software Development :: Testing
13
+ Requires-Python: >=3.9
14
+ Requires-Dist: robotframework>=5.0
15
+ Requires-Dist: urllib3
@@ -0,0 +1,9 @@
1
+ RobotFailureSummary/__init__.py,sha256=ojyMGL_hoyXzyqOdJ9eoH8Wy2tTftzC_keX3WuGpGzc,245
2
+ RobotFailureSummary/listener.py,sha256=rJqaK5P0SJ3K8Xqh8NL06nQragnrVM2WvbBqDsAXjBc,10263
3
+ RobotFailureSummary/js/expander.js,sha256=HzjMMIfHS8dSrby2VhDYW5eXeJmhmXwFwFqx_uNIMM8,1440
4
+ RobotFailureSummary/templates/summary.html,sha256=0xPlByOF9Ps_SPekVg--_PjeN6lzMPFWzvNO8f-ENE8,1619
5
+ robotframework_failuresummary-1.0.0.data/data/RobotFailureSummary/js/expander.js,sha256=HzjMMIfHS8dSrby2VhDYW5eXeJmhmXwFwFqx_uNIMM8,1440
6
+ robotframework_failuresummary-1.0.0.data/data/RobotFailureSummary/templates/summary.html,sha256=0xPlByOF9Ps_SPekVg--_PjeN6lzMPFWzvNO8f-ENE8,1619
7
+ robotframework_failuresummary-1.0.0.dist-info/METADATA,sha256=zjUVEjFGQKnrZgs1Qytbtwzqj1h2y5HGOOOx7ncyxws,693
8
+ robotframework_failuresummary-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
+ robotframework_failuresummary-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any