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.
Files changed (26) hide show
  1. robo_automation_test_kit/__init__.py +8 -0
  2. robo_automation_test_kit/hookspec.py +36 -0
  3. robo_automation_test_kit/plugin.py +867 -0
  4. robo_automation_test_kit/templates/email_report/email_template.html +0 -0
  5. robo_automation_test_kit/templates/html_report/components/category-chart.html +148 -0
  6. robo_automation_test_kit/templates/html_report/components/center-chart.html +97 -0
  7. robo_automation_test_kit/templates/html_report/components/phase-chart.html +148 -0
  8. robo_automation_test_kit/templates/html_report/components/results-table.html +240 -0
  9. robo_automation_test_kit/templates/html_report/components/status-center-chart.html +148 -0
  10. robo_automation_test_kit/templates/html_report/components/summary-chart.html +94 -0
  11. robo_automation_test_kit/templates/html_report/html_template.html +62 -0
  12. robo_automation_test_kit/templates/html_report/scripts/css/material-icons.css +20 -0
  13. robo_automation_test_kit/templates/html_report/scripts/css/report.css +714 -0
  14. robo_automation_test_kit/templates/html_report/scripts/css/robo-fonts.css +24 -0
  15. robo_automation_test_kit/templates/html_report/scripts/js/chart.js +14 -0
  16. robo_automation_test_kit/templates/html_report/scripts/js/report.js +319 -0
  17. robo_automation_test_kit/utils/RoboHelper.py +420 -0
  18. robo_automation_test_kit/utils/__init__.py +19 -0
  19. robo_automation_test_kit/utils/reports/EmailReportUtils.py +0 -0
  20. robo_automation_test_kit/utils/reports/HtmlReportUtils.py +154 -0
  21. robo_automation_test_kit/utils/reports/__init__.py +3 -0
  22. robo_automation_test_kit-1.0.0.dist-info/METADATA +132 -0
  23. robo_automation_test_kit-1.0.0.dist-info/RECORD +26 -0
  24. robo_automation_test_kit-1.0.0.dist-info/WHEEL +4 -0
  25. robo_automation_test_kit-1.0.0.dist-info/entry_points.txt +3 -0
  26. 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
+ ]
@@ -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)
@@ -0,0 +1,3 @@
1
+ """
2
+ Report utilities for HTML and Email report generation
3
+ """