robo-automation-test-kit 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.
- robo_automation_test_kit/__init__.py +8 -0
- robo_automation_test_kit/hookspec.py +36 -0
- robo_automation_test_kit/plugin.py +867 -0
- robo_automation_test_kit/templates/email_report/email_template.html +0 -0
- robo_automation_test_kit/templates/html_report/components/category-chart.html +148 -0
- robo_automation_test_kit/templates/html_report/components/center-chart.html +97 -0
- robo_automation_test_kit/templates/html_report/components/phase-chart.html +148 -0
- robo_automation_test_kit/templates/html_report/components/results-table.html +240 -0
- robo_automation_test_kit/templates/html_report/components/status-center-chart.html +148 -0
- robo_automation_test_kit/templates/html_report/components/summary-chart.html +94 -0
- robo_automation_test_kit/templates/html_report/html_template.html +62 -0
- robo_automation_test_kit/templates/html_report/scripts/css/material-icons.css +20 -0
- robo_automation_test_kit/templates/html_report/scripts/css/report.css +714 -0
- robo_automation_test_kit/templates/html_report/scripts/css/robo-fonts.css +24 -0
- robo_automation_test_kit/templates/html_report/scripts/js/chart.js +14 -0
- robo_automation_test_kit/templates/html_report/scripts/js/report.js +319 -0
- robo_automation_test_kit/utils/RoboHelper.py +420 -0
- robo_automation_test_kit/utils/__init__.py +19 -0
- robo_automation_test_kit/utils/reports/EmailReportUtils.py +0 -0
- robo_automation_test_kit/utils/reports/HtmlReportUtils.py +154 -0
- robo_automation_test_kit/utils/reports/__init__.py +3 -0
- robo_automation_test_kit-1.0.0.dist-info/METADATA +132 -0
- robo_automation_test_kit-1.0.0.dist-info/RECORD +26 -0
- robo_automation_test_kit-1.0.0.dist-info/WHEEL +4 -0
- robo_automation_test_kit-1.0.0.dist-info/entry_points.txt +3 -0
- robo_automation_test_kit-1.0.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
# Report generation utilities and helpers
|
|
2
|
+
from .reports.HtmlReportUtils import get_html_template
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from jinja2 import Environment, FileSystemLoader
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
import zipfile
|
|
7
|
+
import os
|
|
8
|
+
import logging
|
|
9
|
+
import shutil
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
logger.propagate = True
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def profile_name_from_driver(driver) -> str:
|
|
17
|
+
|
|
18
|
+
# Log profile name from driver user-data-dir argument
|
|
19
|
+
profile_name: str = ""
|
|
20
|
+
for arg in driver.options.arguments:
|
|
21
|
+
if arg.startswith("--user-data-dir="):
|
|
22
|
+
profile_dir = arg.split("=", 1)[1]
|
|
23
|
+
profile_name = os.path.basename(profile_dir)
|
|
24
|
+
break
|
|
25
|
+
|
|
26
|
+
return profile_name
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def load_test_data(path: Path):
|
|
30
|
+
"""Load test data rows from CSV or Excel file using pandas.
|
|
31
|
+
|
|
32
|
+
Supports multiple file formats and encodings:
|
|
33
|
+
- CSV files with utf-8-sig, latin-1, or utf-8 encoding
|
|
34
|
+
- Excel workbooks (.xlsx)
|
|
35
|
+
|
|
36
|
+
Returns a list of dict rows suitable for pytest parametrization.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
# Print to stderr to ensure visibility in xdist mode
|
|
40
|
+
import sys
|
|
41
|
+
|
|
42
|
+
# Validate file exists
|
|
43
|
+
if not os.path.exists(path):
|
|
44
|
+
logger.error(f"Data file not found: {path}")
|
|
45
|
+
return []
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
import pandas as pd
|
|
49
|
+
except ImportError:
|
|
50
|
+
logger.error("pandas not installed; cannot load data file")
|
|
51
|
+
return []
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
if zipfile.is_zipfile(path):
|
|
55
|
+
df = pd.read_excel(
|
|
56
|
+
path, engine="openpyxl", dtype=str, keep_default_na=False
|
|
57
|
+
)
|
|
58
|
+
else:
|
|
59
|
+
df = None
|
|
60
|
+
for enc in ("utf-8-sig", "latin-1", "utf-8"):
|
|
61
|
+
try:
|
|
62
|
+
df = pd.read_csv(
|
|
63
|
+
path, encoding=enc, dtype=str, keep_default_na=False
|
|
64
|
+
)
|
|
65
|
+
break
|
|
66
|
+
except UnicodeDecodeError:
|
|
67
|
+
df = None
|
|
68
|
+
if df is None:
|
|
69
|
+
logger.error(
|
|
70
|
+
f"Could not load CSV file {path} with any supported encoding"
|
|
71
|
+
)
|
|
72
|
+
return []
|
|
73
|
+
df = df.fillna("")
|
|
74
|
+
|
|
75
|
+
return df.to_dict(orient="records")
|
|
76
|
+
except Exception as exc:
|
|
77
|
+
logger.error(f"Error loading data file {path}: {exc}", exc_info=True)
|
|
78
|
+
return []
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_env(key: str, default: str = "") -> str:
|
|
82
|
+
value = os.getenv(key, default).strip()
|
|
83
|
+
return value if value else default
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def extract_test_case_name_from_docstring(item, report):
|
|
87
|
+
"""Extract test case name from function docstring or nodeid."""
|
|
88
|
+
docstring = str(item.function.__doc__)
|
|
89
|
+
if docstring:
|
|
90
|
+
return docstring.strip()
|
|
91
|
+
else:
|
|
92
|
+
return report.nodeid
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def print_results_summary(all_results):
|
|
96
|
+
|
|
97
|
+
header = "{:<10} {:<30} {:<10} {:<10} {:<20} {:<20} {:<10} {:<10} {:<20}".format(
|
|
98
|
+
"Status",
|
|
99
|
+
"Title",
|
|
100
|
+
"Phase",
|
|
101
|
+
"Request Category",
|
|
102
|
+
"Request Sub Category",
|
|
103
|
+
"Center",
|
|
104
|
+
"Duration",
|
|
105
|
+
"Error Log",
|
|
106
|
+
"Test Name",
|
|
107
|
+
)
|
|
108
|
+
sep = "-" * 150
|
|
109
|
+
print("\nTest Results Summary:")
|
|
110
|
+
print(header)
|
|
111
|
+
print(sep)
|
|
112
|
+
if not all_results:
|
|
113
|
+
print(sep)
|
|
114
|
+
return
|
|
115
|
+
for result in all_results:
|
|
116
|
+
# If duration is a float or int, format as HH:MM:SS
|
|
117
|
+
duration_val = result.get("duration", "")
|
|
118
|
+
if isinstance(duration_val, (float, int)):
|
|
119
|
+
hours = int(duration_val // 3600)
|
|
120
|
+
minutes = int((duration_val % 3600) // 60)
|
|
121
|
+
seconds = int(duration_val % 60)
|
|
122
|
+
duration_str = f"{hours:02}:{minutes:02}:{seconds:02}"
|
|
123
|
+
else:
|
|
124
|
+
duration_str = str(duration_val)
|
|
125
|
+
row = "{:<10} {:<30} {:<10} {:<10} {:<20} {:<20} {:<10} {:<10} {:<20}".format(
|
|
126
|
+
result.get("test_status", ""),
|
|
127
|
+
result.get("title", ""),
|
|
128
|
+
result.get("Phase", ""),
|
|
129
|
+
result.get("Request Category", ""),
|
|
130
|
+
result.get("Request Sub Category", ""),
|
|
131
|
+
result.get("Center", ""),
|
|
132
|
+
duration_str,
|
|
133
|
+
result.get("error_log", ""),
|
|
134
|
+
result.get("test_name", ""),
|
|
135
|
+
)
|
|
136
|
+
print(row)
|
|
137
|
+
print(sep)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# Flatten if results is a list of lists or dicts
|
|
141
|
+
def flatten_results(res, cfg):
|
|
142
|
+
"""Flatten and aggregate test results from workers."""
|
|
143
|
+
if cfg is None:
|
|
144
|
+
return
|
|
145
|
+
if isinstance(res, dict):
|
|
146
|
+
cfg._test_results_from_workers.append(res)
|
|
147
|
+
elif isinstance(res, list):
|
|
148
|
+
for x in res:
|
|
149
|
+
flatten_results(x, cfg)
|
|
150
|
+
else:
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def build_test_data(item):
|
|
155
|
+
"""
|
|
156
|
+
Build test result data dictionary from test execution information.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
item: pytest Item object containing test metadata
|
|
160
|
+
custom_attribute_data: Optional dict with custom attributes from robo_custom_attribute_data hook
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Dictionary containing test result data:
|
|
164
|
+
- test_status: PASSED, FAILED, or SKIPPED
|
|
165
|
+
- test_id: Test name/nodeid
|
|
166
|
+
- error_log: Exception message if test failed
|
|
167
|
+
- duration: Total execution time in seconds (sum of all phases)
|
|
168
|
+
- Any additional fields from custom_attributes dict
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
# Get stored call phase exception info
|
|
172
|
+
call_excinfo = getattr(item, "_call_excinfo", None)
|
|
173
|
+
|
|
174
|
+
# Determine test status and error log from call phase
|
|
175
|
+
if call_excinfo is None:
|
|
176
|
+
status = "PASSED"
|
|
177
|
+
error_log = ""
|
|
178
|
+
else:
|
|
179
|
+
# Safely extract error message
|
|
180
|
+
try:
|
|
181
|
+
error_repr = call_excinfo.getrepr()
|
|
182
|
+
error_log = (
|
|
183
|
+
error_repr.reprcrash.message
|
|
184
|
+
if error_repr.reprcrash
|
|
185
|
+
else str(call_excinfo.value)
|
|
186
|
+
)
|
|
187
|
+
except (AttributeError, Exception):
|
|
188
|
+
error_log = (
|
|
189
|
+
str(call_excinfo.value) if call_excinfo.value else "Unknown error"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Determine status based on exception type
|
|
193
|
+
if call_excinfo.typename == "Skipped":
|
|
194
|
+
status = "SKIPPED"
|
|
195
|
+
else:
|
|
196
|
+
status = "FAILED"
|
|
197
|
+
# Calculate total duration (setup + call + teardown)
|
|
198
|
+
total_duration = sum(item._phase_durations.values())
|
|
199
|
+
|
|
200
|
+
test_id = getattr(item, "name", item.nodeid)
|
|
201
|
+
|
|
202
|
+
data_row = {
|
|
203
|
+
# "test_case_name": test_case_name,
|
|
204
|
+
"test_status": status,
|
|
205
|
+
"test_id": test_id,
|
|
206
|
+
"error_log": error_log,
|
|
207
|
+
"duration": total_duration,
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return data_row
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# ============================================================================
|
|
214
|
+
# Report Generation Functions (merged from report_generator.py)
|
|
215
|
+
# ============================================================================
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def aggregate_test_results(config):
|
|
219
|
+
"""
|
|
220
|
+
Aggregate test results from master process and xdist workers.
|
|
221
|
+
|
|
222
|
+
Collects results from config.test_results_summary (master process)
|
|
223
|
+
and config._test_results_from_workers (aggregated worker results).
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
config: Pytest config object
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
List of aggregated test result dictionaries
|
|
230
|
+
"""
|
|
231
|
+
report_rows = []
|
|
232
|
+
|
|
233
|
+
# Include results from config.test_results_summary (initialized in pytest_configure)
|
|
234
|
+
if hasattr(config, "test_results_summary") and config.test_results_summary:
|
|
235
|
+
master_results = [r for r in config.test_results_summary if isinstance(r, dict)]
|
|
236
|
+
report_rows.extend(master_results)
|
|
237
|
+
|
|
238
|
+
# Include results from xdist workers (aggregated via pytest_testnodedown)
|
|
239
|
+
if (
|
|
240
|
+
hasattr(config, "_test_results_from_workers")
|
|
241
|
+
and config._test_results_from_workers
|
|
242
|
+
):
|
|
243
|
+
for entry in config._test_results_from_workers:
|
|
244
|
+
if isinstance(entry, dict):
|
|
245
|
+
report_rows.append(entry)
|
|
246
|
+
elif isinstance(entry, list):
|
|
247
|
+
worker_results = [r for r in entry if isinstance(r, dict)]
|
|
248
|
+
report_rows.extend(worker_results)
|
|
249
|
+
|
|
250
|
+
return report_rows
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def create_report_summary(report_rows, start_time=None):
|
|
254
|
+
"""
|
|
255
|
+
Create summary object for HTML report template.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
report_rows: List of test result dictionaries
|
|
259
|
+
start_time: Datetime object for test session start
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Dictionary containing summary statistics for the report
|
|
263
|
+
"""
|
|
264
|
+
# Calculate test duration
|
|
265
|
+
if start_time:
|
|
266
|
+
end_time = datetime.now()
|
|
267
|
+
duration = end_time - start_time
|
|
268
|
+
duration_str = str(duration).split(".")[0] # Remove microseconds
|
|
269
|
+
else:
|
|
270
|
+
duration_str = ""
|
|
271
|
+
|
|
272
|
+
# Calculate summary statistics
|
|
273
|
+
total = len(report_rows)
|
|
274
|
+
passed = sum(1 for r in report_rows if r.get("test_status") == "PASSED")
|
|
275
|
+
failed = sum(1 for r in report_rows if r.get("test_status") in ["ERROR", "FAILED"])
|
|
276
|
+
skipped = sum(1 for r in report_rows if r.get("test_status") == "SKIPPED")
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
"env_name": os.getenv("APP_ENV", "").upper(),
|
|
280
|
+
"project_name": os.getenv("PROJECT_NAME", ""),
|
|
281
|
+
"test_framework": os.getenv("TEST_FRAMEWORK", "Robo Automation Framework"),
|
|
282
|
+
"total": total,
|
|
283
|
+
"duration": duration_str,
|
|
284
|
+
"passed": passed,
|
|
285
|
+
"failed": failed,
|
|
286
|
+
"skipped": skipped,
|
|
287
|
+
"rerun": 0, # Not tracked in current implementation
|
|
288
|
+
"generated_date": datetime.now().strftime("%m-%d-%Y"),
|
|
289
|
+
"generated_time": datetime.now().strftime("%I:%M:%S %p"),
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def format_duration(seconds):
|
|
294
|
+
"""
|
|
295
|
+
Convert duration in seconds to HH:MM:SS format string.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
seconds: Duration in seconds (float or int)
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
Formatted duration string as HH:MM:SS
|
|
302
|
+
"""
|
|
303
|
+
if isinstance(seconds, (float, int)):
|
|
304
|
+
hours = int(seconds // 3600)
|
|
305
|
+
minutes = int((seconds % 3600) // 60)
|
|
306
|
+
secs = int(seconds % 60)
|
|
307
|
+
return f"{hours:02}:{minutes:02}:{secs:02}"
|
|
308
|
+
return str(seconds)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def get_report_path():
|
|
312
|
+
"""
|
|
313
|
+
Determine and create the report path from environment configuration.
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
Path object for the HTML report file location
|
|
317
|
+
"""
|
|
318
|
+
report_path = get_env("REPORT_PATH", "reports")
|
|
319
|
+
report_dir = Path(report_path)
|
|
320
|
+
report_dir.mkdir(parents=True, exist_ok=True)
|
|
321
|
+
return report_dir / "test_report.html"
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def generate_report(report_rows, report_summary, start_time):
|
|
325
|
+
"""
|
|
326
|
+
Generate and save HTML report with test results.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
report_rows: List of test result dictionaries
|
|
330
|
+
report_summary: Summary dictionary for the report
|
|
331
|
+
start_time: Datetime object for test session start
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
Path to the generated HTML report
|
|
335
|
+
"""
|
|
336
|
+
|
|
337
|
+
# Prepare template data with raw numeric durations
|
|
338
|
+
report_title = get_env("REPORT_TITLE", "Test Execution Report")
|
|
339
|
+
template_data = {
|
|
340
|
+
"report_title": report_title,
|
|
341
|
+
"summary": report_summary,
|
|
342
|
+
"report_rows": report_rows,
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
# Load CSS and JS files for embedding
|
|
346
|
+
try:
|
|
347
|
+
scripts_dir = Path(__file__).parent.parent / "templates" / "html_report" / "scripts"
|
|
348
|
+
|
|
349
|
+
# Read CSS files
|
|
350
|
+
css_path = scripts_dir / "css" / "report.css"
|
|
351
|
+
css_content = css_path.read_text(encoding="utf-8") if css_path.exists() else ""
|
|
352
|
+
|
|
353
|
+
material_icons_path = scripts_dir / "css" / "material-icons.css"
|
|
354
|
+
material_icons_content = (
|
|
355
|
+
material_icons_path.read_text(encoding="utf-8")
|
|
356
|
+
if material_icons_path.exists()
|
|
357
|
+
else ""
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
robo_fonts_path = scripts_dir / "css" / "robo-fonts.css"
|
|
361
|
+
robo_fonts_content = (
|
|
362
|
+
robo_fonts_path.read_text(encoding="utf-8")
|
|
363
|
+
if robo_fonts_path.exists()
|
|
364
|
+
else ""
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
# Read Chart.js library
|
|
368
|
+
chart_js_path = scripts_dir / "js" / "chart.js"
|
|
369
|
+
chart_js_content = (
|
|
370
|
+
chart_js_path.read_text(encoding="utf-8")
|
|
371
|
+
if chart_js_path.exists()
|
|
372
|
+
else ""
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
# Read merged JS file
|
|
376
|
+
report_js_path = scripts_dir / "js" / "report.js"
|
|
377
|
+
|
|
378
|
+
report_js_content = (
|
|
379
|
+
report_js_path.read_text(encoding="utf-8")
|
|
380
|
+
if report_js_path.exists()
|
|
381
|
+
else ""
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
# Add to template data
|
|
385
|
+
template_data["embedded_css"] = css_content
|
|
386
|
+
template_data["embedded_material_icons"] = material_icons_content
|
|
387
|
+
template_data["embedded_robo_fonts"] = robo_fonts_content
|
|
388
|
+
template_data["embedded_chart_js"] = chart_js_content
|
|
389
|
+
template_data["embedded_report_js"] = report_js_content
|
|
390
|
+
except Exception as e:
|
|
391
|
+
# If reading fails, use empty strings
|
|
392
|
+
template_data["embedded_css"] = ""
|
|
393
|
+
template_data["embedded_material_icons"] = ""
|
|
394
|
+
template_data["embedded_robo_fonts"] = ""
|
|
395
|
+
template_data["embedded_chart_js"] = ""
|
|
396
|
+
template_data["embedded_report_js"] = ""
|
|
397
|
+
|
|
398
|
+
# Load template using get_html_template() which checks source first
|
|
399
|
+
template = get_html_template()
|
|
400
|
+
|
|
401
|
+
# Register custom Jinja2 filter for duration formatting
|
|
402
|
+
template.globals['format_duration'] = format_duration
|
|
403
|
+
|
|
404
|
+
# Render and save report
|
|
405
|
+
try:
|
|
406
|
+
html_content = template.render(**template_data)
|
|
407
|
+
|
|
408
|
+
report_path = get_report_path()
|
|
409
|
+
|
|
410
|
+
with open(report_path, "w", encoding="utf-8") as f:
|
|
411
|
+
f.write(html_content)
|
|
412
|
+
|
|
413
|
+
print(f"\nHTML report generated: {report_path.absolute()}", flush=True)
|
|
414
|
+
return str(report_path.absolute())
|
|
415
|
+
except Exception as e:
|
|
416
|
+
print(f"\nError generating HTML report: {e}", flush=True)
|
|
417
|
+
import traceback
|
|
418
|
+
|
|
419
|
+
traceback.print_exc()
|
|
420
|
+
return None
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for robo-reporter
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .RoboHelper import (
|
|
6
|
+
load_test_data,
|
|
7
|
+
get_env,
|
|
8
|
+
extract_test_case_name_from_docstring,
|
|
9
|
+
print_results_summary,
|
|
10
|
+
flatten_results,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
'load_test_data',
|
|
15
|
+
'get_env',
|
|
16
|
+
'extract_test_case_name_from_docstring',
|
|
17
|
+
'print_results_summary',
|
|
18
|
+
'flatten_results',
|
|
19
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
def get_report_data(start_time):
|
|
2
|
+
"""
|
|
3
|
+
Build the report_data dictionary for report generation, using config and environment variables.
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
project_root = os.path.abspath(
|
|
9
|
+
os.path.join(os.path.dirname(__file__), "..", "..", "..")
|
|
10
|
+
)
|
|
11
|
+
report_dir = os.path.join(project_root, "reports")
|
|
12
|
+
return {
|
|
13
|
+
"project_name": os.getenv("PROJECT_NAME", ""),
|
|
14
|
+
"env_name": os.getenv("APP_ENV", ""),
|
|
15
|
+
"test_framework": os.getenv("TEST_FRAMEWORK", "Robo Automation Framework"),
|
|
16
|
+
"start_time": start_time,
|
|
17
|
+
"end_time": datetime.now(),
|
|
18
|
+
"report_dir": report_dir,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_html_template():
|
|
26
|
+
"""
|
|
27
|
+
Returns the Jinja2 template object for the HTML report.
|
|
28
|
+
Checks for source template in project working directory first, then falls back to package template.
|
|
29
|
+
"""
|
|
30
|
+
import os
|
|
31
|
+
from jinja2 import Environment, FileSystemLoader
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
# Check for source template in current working directory only
|
|
35
|
+
source_template_dir = Path.cwd() / "templates" / "html_report"
|
|
36
|
+
source_template_file = source_template_dir / "html_template.html"
|
|
37
|
+
|
|
38
|
+
if source_template_file.exists():
|
|
39
|
+
# print(f"Loading source template from: {source_template_dir}", flush=True)
|
|
40
|
+
env = Environment(loader=FileSystemLoader(str(source_template_dir)))
|
|
41
|
+
return env.get_template("html_template.html")
|
|
42
|
+
|
|
43
|
+
# Fall back to package template inside robo_automation_test_kit directory
|
|
44
|
+
package_root = os.path.abspath(
|
|
45
|
+
os.path.join(os.path.dirname(__file__), "..", "..")
|
|
46
|
+
)
|
|
47
|
+
package_template_dir = os.path.join(package_root, "templates", "html_report")
|
|
48
|
+
env = Environment(loader=FileSystemLoader(package_template_dir))
|
|
49
|
+
return env.get_template("html_template.html")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_report_summary(all_results, report_data):
|
|
54
|
+
"""
|
|
55
|
+
Create the summary object for the report, including environment, project, test framework, total, and duration.
|
|
56
|
+
"""
|
|
57
|
+
from datetime import datetime
|
|
58
|
+
start_time = report_data.get("start_time", None)
|
|
59
|
+
end_time = report_data.get("end_time", None)
|
|
60
|
+
if start_time is not None and end_time is not None:
|
|
61
|
+
total_seconds = int((end_time - start_time).total_seconds())
|
|
62
|
+
hours = total_seconds // 3600
|
|
63
|
+
minutes = (total_seconds % 3600) // 60
|
|
64
|
+
seconds = total_seconds % 60
|
|
65
|
+
duration_str = f"{hours:02}:{minutes:02}:{seconds:02}"
|
|
66
|
+
else:
|
|
67
|
+
duration_str = "-"
|
|
68
|
+
|
|
69
|
+
# Count test statuses for summary chart, treating 'ERROR' as 'FAILED'
|
|
70
|
+
status_counts = {"PASSED": 0, "FAILED": 0, "SKIPPED": 0, "RERUN": 0}
|
|
71
|
+
for result in all_results:
|
|
72
|
+
status = str(result.get("test_status", "")).upper()
|
|
73
|
+
if status == "ERROR":
|
|
74
|
+
status = "FAILED"
|
|
75
|
+
if status in status_counts:
|
|
76
|
+
status_counts[status] += 1
|
|
77
|
+
return {
|
|
78
|
+
"env_name": report_data.get("env_name", ""),
|
|
79
|
+
"project_name": report_data.get("project_name", ""),
|
|
80
|
+
"test_framework": report_data.get("test_framework", ""),
|
|
81
|
+
"total": len(all_results),
|
|
82
|
+
"duration": duration_str,
|
|
83
|
+
"passed": status_counts["PASSED"],
|
|
84
|
+
"failed": status_counts["FAILED"],
|
|
85
|
+
"skipped": status_counts["SKIPPED"],
|
|
86
|
+
"rerun": status_counts["RERUN"],
|
|
87
|
+
"generated_date": datetime.now().strftime('%m-%d-%Y'),
|
|
88
|
+
"generated_time": datetime.now().strftime('%I:%M:%S %p'),
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def generate_and_save_html_report(all_results, start_time):
|
|
93
|
+
"""
|
|
94
|
+
Generate and save the HTML report in the report directory with a timestamped filename.
|
|
95
|
+
Returns the path to the generated report.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
report_data = get_report_data(start_time)
|
|
99
|
+
|
|
100
|
+
report_title = report_data.get("project_name", "NA")
|
|
101
|
+
report_dir = report_data.get("report_dir", None)
|
|
102
|
+
|
|
103
|
+
if report_dir is None:
|
|
104
|
+
# Use the project root (three levels up from this file)
|
|
105
|
+
project_root = os.path.abspath(
|
|
106
|
+
os.path.join(os.path.dirname(__file__), "..", "..", "..")
|
|
107
|
+
)
|
|
108
|
+
report_dir = os.path.join(project_root, "reports")
|
|
109
|
+
os.makedirs(report_dir, exist_ok=True)
|
|
110
|
+
|
|
111
|
+
# Sanitize report_title for filename (remove/replace problematic characters)
|
|
112
|
+
import re
|
|
113
|
+
from datetime import datetime
|
|
114
|
+
|
|
115
|
+
safe_title = re.sub(r"[^a-zA-Z0-9_-]", "_", report_title)
|
|
116
|
+
now_str = datetime.now().strftime("_%Y%m%d_%H%M%S")
|
|
117
|
+
html_report_path = os.path.join(report_dir, f"{safe_title}{now_str}.html")
|
|
118
|
+
generate_html_report(all_results, html_report_path, report_data)
|
|
119
|
+
return html_report_path
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def generate_html_report(all_results, output_path, report_data=None):
|
|
123
|
+
"""
|
|
124
|
+
Generate an HTML report using the Jinja2 template and all_results.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
# Defensive: allow report_data to be None
|
|
128
|
+
if report_data is None:
|
|
129
|
+
report_data = {}
|
|
130
|
+
report_title = os.getenv("REPORT_TITLE", "Test Report")
|
|
131
|
+
from datetime import datetime
|
|
132
|
+
summary = get_report_summary(all_results, report_data)
|
|
133
|
+
|
|
134
|
+
template = get_html_template()
|
|
135
|
+
|
|
136
|
+
# Create format_duration function and register it with template
|
|
137
|
+
def format_duration_func(seconds):
|
|
138
|
+
if isinstance(seconds, (float, int)):
|
|
139
|
+
hours = int(seconds // 3600)
|
|
140
|
+
minutes = int((seconds % 3600) // 60)
|
|
141
|
+
secs = int(seconds % 60)
|
|
142
|
+
return f"{hours:02}:{minutes:02}:{secs:02}"
|
|
143
|
+
return str(seconds)
|
|
144
|
+
|
|
145
|
+
template.globals['format_duration'] = format_duration_func
|
|
146
|
+
|
|
147
|
+
html_content = template.render(
|
|
148
|
+
report_title=report_title,
|
|
149
|
+
summary=summary,
|
|
150
|
+
all_results=all_results,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
154
|
+
f.write(html_content)
|