nextog-cli 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.
- nextog/__init__.py +4 -0
- nextog/cli.py +545 -0
- nextog/config/__init__.py +1 -0
- nextog/config/settings.py +132 -0
- nextog/core/__init__.py +1 -0
- nextog/core/engine.py +193 -0
- nextog/core/permissions.py +129 -0
- nextog/core/privacy.py +130 -0
- nextog/core/reporter.py +204 -0
- nextog/core/runner.py +236 -0
- nextog/data/__init__.py +1 -0
- nextog/data/local_db.py +367 -0
- nextog/data/models.py +72 -0
- nextog/data/sync.py +65 -0
- nextog/engines/__init__.py +1 -0
- nextog/engines/api/__init__.py +1 -0
- nextog/engines/api/graphql.py +54 -0
- nextog/engines/api/rest.py +346 -0
- nextog/engines/api/websocket.py +59 -0
- nextog/engines/embedded/__init__.py +1 -0
- nextog/engines/embedded/firmware.py +53 -0
- nextog/engines/embedded/hardware.py +330 -0
- nextog/engines/mobile/__init__.py +1 -0
- nextog/engines/mobile/android.py +333 -0
- nextog/engines/mobile/cross.py +48 -0
- nextog/engines/mobile/ios.py +46 -0
- nextog/engines/system/__init__.py +1 -0
- nextog/engines/system/load.py +121 -0
- nextog/engines/system/performance.py +128 -0
- nextog/engines/system/security.py +170 -0
- nextog/engines/web/__init__.py +1 -0
- nextog/engines/web/accessibility.py +191 -0
- nextog/engines/web/browser.py +387 -0
- nextog/engines/web/elements.py +285 -0
- nextog/engines/web/responsive.py +79 -0
- nextog/live/__init__.py +1 -0
- nextog/live/dashboard.py +30 -0
- nextog/live/panel.py +325 -0
- nextog/reports/__init__.py +1359 -0
- nextog/training/__init__.py +1 -0
- nextog/training/learner.py +269 -0
- nextog/training/patterns.py +102 -0
- nextog/utils/__init__.py +1 -0
- nextog/utils/helpers.py +91 -0
- nextog/utils/logger.py +37 -0
- nextog/utils/validators.py +98 -0
- nextog_cli-1.0.0.dist-info/METADATA +344 -0
- nextog_cli-1.0.0.dist-info/RECORD +51 -0
- nextog_cli-1.0.0.dist-info/WHEEL +5 -0
- nextog_cli-1.0.0.dist-info/entry_points.txt +2 -0
- nextog_cli-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1359 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Industrial Excel Report Generator for nextOG CLI
|
|
3
|
+
Auto-generates professional Excel reports after ANY test run.
|
|
4
|
+
All reports are built automatically after testing completes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import json
|
|
9
|
+
from typing import Dict, List, Optional, Any
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class IndustrialReportGenerator:
|
|
18
|
+
"""
|
|
19
|
+
Generates industrial-grade Excel reports after testing.
|
|
20
|
+
Supports: Web, Mobile, API, Embedded, System tests.
|
|
21
|
+
All data stays local - privacy first.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, db, settings, privacy):
|
|
25
|
+
self.db = db
|
|
26
|
+
self.settings = settings
|
|
27
|
+
self.privacy = privacy
|
|
28
|
+
self.output_dir = Path.home() / ".nextog" / "reports"
|
|
29
|
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
|
|
31
|
+
def generate_after_test(self, test_results: Dict, test_type: str = "auto") -> str:
|
|
32
|
+
"""
|
|
33
|
+
Auto-generate Excel report after any test run.
|
|
34
|
+
Called automatically by all test engines.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
test_results: Results from any test engine
|
|
38
|
+
test_type: Type of test (web, mobile, api, embedded, system, auto)
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Path to generated Excel file
|
|
42
|
+
"""
|
|
43
|
+
try:
|
|
44
|
+
import openpyxl
|
|
45
|
+
from openpyxl.styles import (
|
|
46
|
+
Font, PatternFill, Alignment, Border, Side,
|
|
47
|
+
numbers, NamedStyle
|
|
48
|
+
)
|
|
49
|
+
from openpyxl.chart import BarChart, PieChart, Reference, LineChart
|
|
50
|
+
from openpyxl.chart.label import DataLabelList
|
|
51
|
+
from openpyxl.chart.series import DataPoint
|
|
52
|
+
from openpyxl.utils import get_column_letter
|
|
53
|
+
from openpyxl.formatting.rule import CellIsRule, DataBarRule
|
|
54
|
+
from openpyxl.worksheet.table import Table, TableStyleInfo
|
|
55
|
+
except ImportError:
|
|
56
|
+
console.print("[yellow]⚠ Installing openpyxl for Excel reports...[/yellow]")
|
|
57
|
+
os.system("pip install openpyxl")
|
|
58
|
+
import openpyxl
|
|
59
|
+
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
|
60
|
+
from openpyxl.chart import BarChart, PieChart, Reference
|
|
61
|
+
from openpyxl.chart.label import DataLabelList
|
|
62
|
+
from openpyxl.utils import get_column_letter
|
|
63
|
+
|
|
64
|
+
# Create workbook
|
|
65
|
+
wb = openpyxl.Workbook()
|
|
66
|
+
|
|
67
|
+
# Detect test type if auto
|
|
68
|
+
if test_type == "auto":
|
|
69
|
+
test_type = self._detect_test_type(test_results)
|
|
70
|
+
|
|
71
|
+
# Generate timestamp
|
|
72
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
73
|
+
|
|
74
|
+
# ─── SHEET 1: Executive Summary ───────────────────────────
|
|
75
|
+
ws_summary = wb.active
|
|
76
|
+
ws_summary.title = "Executive Summary"
|
|
77
|
+
self._create_executive_summary(ws_summary, test_results, test_type, timestamp)
|
|
78
|
+
|
|
79
|
+
# ─── SHEET 2: Test Details ────────────────────────────────
|
|
80
|
+
ws_details = wb.create_sheet("Test Details")
|
|
81
|
+
self._create_test_details(ws_details, test_results, test_type)
|
|
82
|
+
|
|
83
|
+
# ─── SHEET 3: Coverage Analysis ───────────────────────────
|
|
84
|
+
ws_coverage = wb.create_sheet("Coverage Analysis")
|
|
85
|
+
self._create_coverage_analysis(ws_coverage, test_results, test_type)
|
|
86
|
+
|
|
87
|
+
# ─── SHEET 4: Element Report ──────────────────────────────
|
|
88
|
+
ws_elements = wb.create_sheet("Elements & Components")
|
|
89
|
+
self._create_element_report(ws_elements, test_results, test_type)
|
|
90
|
+
|
|
91
|
+
# ─── SHEET 5: Defect Log ──────────────────────────────────
|
|
92
|
+
ws_defects = wb.create_sheet("Defect Log")
|
|
93
|
+
self._create_defect_log(ws_defects, test_results, test_type)
|
|
94
|
+
|
|
95
|
+
# ─── SHEET 6: Performance Metrics ─────────────────────────
|
|
96
|
+
ws_perf = wb.create_sheet("Performance Metrics")
|
|
97
|
+
self._create_performance_metrics(ws_perf, test_results, test_type)
|
|
98
|
+
|
|
99
|
+
# ─── SHEET 7: Charts & Graphs ─────────────────────────────
|
|
100
|
+
ws_charts = wb.create_sheet("Charts & Graphs")
|
|
101
|
+
self._create_charts(ws_charts, test_results, test_type)
|
|
102
|
+
|
|
103
|
+
# ─── SHEET 8: Recommendations ─────────────────────────────
|
|
104
|
+
ws_rec = wb.create_sheet("Recommendations")
|
|
105
|
+
self._create_recommendations(ws_rec, test_results, test_type)
|
|
106
|
+
|
|
107
|
+
# ─── SHEET 9: Test Environment ────────────────────────────
|
|
108
|
+
ws_env = wb.create_sheet("Test Environment")
|
|
109
|
+
self._create_test_environment(ws_env, test_type)
|
|
110
|
+
|
|
111
|
+
# ─── SHEET 10: Sign-off Sheet ─────────────────────────────
|
|
112
|
+
ws_sign = wb.create_sheet("Sign-Off")
|
|
113
|
+
self._create_signoff_sheet(ws_sign, test_results, test_type, timestamp)
|
|
114
|
+
|
|
115
|
+
# Auto-fit columns for all sheets
|
|
116
|
+
for ws in wb.worksheets:
|
|
117
|
+
self._auto_fit_columns(ws)
|
|
118
|
+
|
|
119
|
+
# Save workbook
|
|
120
|
+
filename = f"nextOG_{test_type}_Report_{timestamp}.xlsx"
|
|
121
|
+
filepath = self.output_dir / filename
|
|
122
|
+
wb.save(str(filepath))
|
|
123
|
+
|
|
124
|
+
# Record in privacy engine (local only)
|
|
125
|
+
self.privacy.record_activity("report_generated", {
|
|
126
|
+
"file": str(filepath),
|
|
127
|
+
"type": test_type,
|
|
128
|
+
"timestamp": timestamp,
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
# Save to DB
|
|
132
|
+
self.db.save_metadata(f"last_report_{test_type}", str(filepath))
|
|
133
|
+
|
|
134
|
+
return str(filepath)
|
|
135
|
+
|
|
136
|
+
# ═══════════════════════════════════════════════════════════════
|
|
137
|
+
# SHEET 1: Executive Summary
|
|
138
|
+
# ═══════════════════════════════════════════════════════════════
|
|
139
|
+
|
|
140
|
+
def _create_executive_summary(self, ws, results: Dict, test_type: str, timestamp: str):
|
|
141
|
+
"""Create executive summary sheet"""
|
|
142
|
+
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
|
143
|
+
from openpyxl.utils import get_column_letter
|
|
144
|
+
|
|
145
|
+
# Styles
|
|
146
|
+
title_font = Font(name="Calibri", size=20, bold=True, color="FFFFFF")
|
|
147
|
+
title_fill = PatternFill(start_color="1B4F72", end_color="1B4F72", fill_type="solid")
|
|
148
|
+
header_font = Font(name="Calibri", size=12, bold=True, color="FFFFFF")
|
|
149
|
+
header_fill = PatternFill(start_color="2E86C1", end_color="2E86C1", fill_type="solid")
|
|
150
|
+
label_font = Font(name="Calibri", size=11, bold=True)
|
|
151
|
+
value_font = Font(name="Calibri", size=11)
|
|
152
|
+
green_fill = PatternFill(start_color="27AE60", end_color="27AE60", fill_type="solid")
|
|
153
|
+
red_fill = PatternFill(start_color="E74C3C", end_color="E74C3C", fill_type="solid")
|
|
154
|
+
yellow_fill = PatternFill(start_color="F39C12", end_color="F39C12", fill_type="solid")
|
|
155
|
+
white_font = Font(name="Calibri", size=14, bold=True, color="FFFFFF")
|
|
156
|
+
thin_border = Border(
|
|
157
|
+
left=Side(style='thin'), right=Side(style='thin'),
|
|
158
|
+
top=Side(style='thin'), bottom=Side(style='thin')
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Title Row
|
|
162
|
+
ws.merge_cells("A1:H1")
|
|
163
|
+
ws["A1"] = f"🚀 nextOG CLI — {test_type.upper()} Test Report"
|
|
164
|
+
ws["A1"].font = title_font
|
|
165
|
+
ws["A1"].fill = title_fill
|
|
166
|
+
ws["A1"].alignment = Alignment(horizontal="center", vertical="center")
|
|
167
|
+
ws.row_dimensions[1].height = 40
|
|
168
|
+
|
|
169
|
+
# Subtitle
|
|
170
|
+
ws.merge_cells("A2:H2")
|
|
171
|
+
ws["A2"] = f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} | Privacy: LOCAL ONLY | Tool: nextOG CLI v1.0"
|
|
172
|
+
ws["A2"].font = Font(name="Calibri", size=9, italic=True, color="666666")
|
|
173
|
+
ws["A2"].alignment = Alignment(horizontal="center")
|
|
174
|
+
ws.row_dimensions[2].height = 20
|
|
175
|
+
|
|
176
|
+
# ─── Key Metrics Box ─────────────────────────────────────
|
|
177
|
+
ws.merge_cells("A4:B4")
|
|
178
|
+
ws["A4"] = "KEY METRICS"
|
|
179
|
+
ws["A4"].font = header_font
|
|
180
|
+
ws["A4"].fill = header_fill
|
|
181
|
+
ws["B4"].fill = header_fill
|
|
182
|
+
|
|
183
|
+
total = results.get("total", 0)
|
|
184
|
+
passed = results.get("passed", 0)
|
|
185
|
+
failed = results.get("failed", 0)
|
|
186
|
+
skipped = results.get("skipped", 0)
|
|
187
|
+
coverage = results.get("coverage", 0)
|
|
188
|
+
duration = results.get("duration", 0)
|
|
189
|
+
|
|
190
|
+
metrics = [
|
|
191
|
+
("Total Tests", total),
|
|
192
|
+
("Passed ✅", passed),
|
|
193
|
+
("Failed ❌", failed),
|
|
194
|
+
("Skipped ⚠️", skipped),
|
|
195
|
+
("Coverage", f"{coverage}%"),
|
|
196
|
+
("Duration", f"{duration:.1f}s" if duration else "N/A"),
|
|
197
|
+
("Pass Rate", f"{(passed/total*100):.1f}%" if total > 0 else "0%"),
|
|
198
|
+
("Test Type", test_type.upper()),
|
|
199
|
+
]
|
|
200
|
+
|
|
201
|
+
for i, (label, value) in enumerate(metrics, start=5):
|
|
202
|
+
ws[f"A{i}"] = label
|
|
203
|
+
ws[f"A{i}"].font = label_font
|
|
204
|
+
ws[f"A{i}"].border = thin_border
|
|
205
|
+
|
|
206
|
+
ws[f"B{i}"] = value
|
|
207
|
+
ws[f"B{i}"].font = value_font
|
|
208
|
+
ws[f"B{i}"].border = thin_border
|
|
209
|
+
ws[f"B{i}"].alignment = Alignment(horizontal="center")
|
|
210
|
+
|
|
211
|
+
# Color coding
|
|
212
|
+
if "Passed" in label:
|
|
213
|
+
ws[f"B{i}"].fill = PatternFill(start_color="D5F5E3", end_color="D5F5E3", fill_type="solid")
|
|
214
|
+
elif "Failed" in label:
|
|
215
|
+
ws[f"B{i}"].fill = PatternFill(start_color="FADBD8", end_color="FADBD8", fill_type="solid")
|
|
216
|
+
elif "Coverage" in label:
|
|
217
|
+
if coverage >= 80:
|
|
218
|
+
ws[f"B{i}"].fill = PatternFill(start_color="D5F5E3", end_color="D5F5E3", fill_type="solid")
|
|
219
|
+
elif coverage >= 60:
|
|
220
|
+
ws[f"B{i}"].fill = PatternFill(start_color="FEF9E7", end_color="FEF9E7", fill_type="solid")
|
|
221
|
+
else:
|
|
222
|
+
ws[f"B{i}"].fill = PatternFill(start_color="FADBD8", end_color="FADBD8", fill_type="solid")
|
|
223
|
+
|
|
224
|
+
# ─── Status Summary ──────────────────────────────────────
|
|
225
|
+
ws.merge_cells("D4:E4")
|
|
226
|
+
ws["D4"] = "OVERALL STATUS"
|
|
227
|
+
ws["D4"].font = header_font
|
|
228
|
+
ws["D4"].fill = header_fill
|
|
229
|
+
ws["E4"].fill = header_fill
|
|
230
|
+
|
|
231
|
+
if coverage >= 80 and failed == 0:
|
|
232
|
+
status = "✅ PASS — Release Ready"
|
|
233
|
+
status_fill = green_fill
|
|
234
|
+
elif coverage >= 60:
|
|
235
|
+
status = "⚠️ CONDITIONAL — Review Required"
|
|
236
|
+
status_fill = yellow_fill
|
|
237
|
+
else:
|
|
238
|
+
status = "❌ FAIL — Critical Issues Found"
|
|
239
|
+
status_fill = red_fill
|
|
240
|
+
|
|
241
|
+
ws.merge_cells("D5:E5")
|
|
242
|
+
ws["D5"] = status
|
|
243
|
+
ws["D5"].font = white_font
|
|
244
|
+
ws["D5"].fill = status_fill
|
|
245
|
+
ws["D5"].alignment = Alignment(horizontal="center", vertical="center")
|
|
246
|
+
ws["E5"].fill = status_fill
|
|
247
|
+
ws.row_dimensions[5].height = 35
|
|
248
|
+
|
|
249
|
+
# Risk Level
|
|
250
|
+
ws["D6"] = "Risk Level:"
|
|
251
|
+
ws["D6"].font = label_font
|
|
252
|
+
if failed > 5:
|
|
253
|
+
risk = "🔴 HIGH"
|
|
254
|
+
elif failed > 0:
|
|
255
|
+
risk = "🟡 MEDIUM"
|
|
256
|
+
else:
|
|
257
|
+
risk = "🟢 LOW"
|
|
258
|
+
ws["E6"] = risk
|
|
259
|
+
ws["E6"].font = value_font
|
|
260
|
+
|
|
261
|
+
# Recommendation
|
|
262
|
+
ws["D7"] = "Recommendation:"
|
|
263
|
+
ws["D7"].font = label_font
|
|
264
|
+
if coverage >= 80 and failed == 0:
|
|
265
|
+
rec = "Approve for production release"
|
|
266
|
+
elif coverage >= 60:
|
|
267
|
+
rec = "Fix critical defects before release"
|
|
268
|
+
else:
|
|
269
|
+
rec = "Major rework required - do not release"
|
|
270
|
+
ws.merge_cells("E7:F7")
|
|
271
|
+
ws["E7"] = rec
|
|
272
|
+
ws["E7"].font = value_font
|
|
273
|
+
|
|
274
|
+
# ─── Test Breakdown by Category ──────────────────────────
|
|
275
|
+
ws.merge_cells("A14:H14")
|
|
276
|
+
ws["A14"] = "TEST BREAKDOWN BY CATEGORY"
|
|
277
|
+
ws["A14"].font = header_font
|
|
278
|
+
ws["A14"].fill = header_fill
|
|
279
|
+
for col in range(1, 9):
|
|
280
|
+
ws.cell(row=14, column=col).fill = header_fill
|
|
281
|
+
|
|
282
|
+
headers = ["Category", "Total", "Passed", "Failed", "Skipped", "Pass Rate", "Coverage", "Status"]
|
|
283
|
+
for col, header in enumerate(headers, 1):
|
|
284
|
+
cell = ws.cell(row=15, column=col)
|
|
285
|
+
cell.value = header
|
|
286
|
+
cell.font = Font(bold=True, color="FFFFFF")
|
|
287
|
+
cell.fill = PatternFill(start_color="34495E", end_color="34495E", fill_type="solid")
|
|
288
|
+
cell.border = thin_border
|
|
289
|
+
cell.alignment = Alignment(horizontal="center")
|
|
290
|
+
|
|
291
|
+
categories = results.get("categories", {})
|
|
292
|
+
row = 16
|
|
293
|
+
for cat_name, cat_data in categories.items():
|
|
294
|
+
if isinstance(cat_data, dict):
|
|
295
|
+
cat_total = cat_data.get("total", 0)
|
|
296
|
+
cat_passed = cat_data.get("passed", 0)
|
|
297
|
+
cat_failed = cat_data.get("failed", 0)
|
|
298
|
+
cat_skipped = cat_data.get("skipped", 0)
|
|
299
|
+
cat_pass_rate = f"{(cat_passed/cat_total*100):.1f}%" if cat_total > 0 else "0%"
|
|
300
|
+
cat_coverage = f"{cat_data.get('coverage', 0)}%"
|
|
301
|
+
|
|
302
|
+
ws.cell(row=row, column=1, value=cat_name.replace("_", " ").title()).border = thin_border
|
|
303
|
+
ws.cell(row=row, column=2, value=cat_total).border = thin_border
|
|
304
|
+
ws.cell(row=row, column=2).alignment = Alignment(horizontal="center")
|
|
305
|
+
ws.cell(row=row, column=3, value=cat_passed).border = thin_border
|
|
306
|
+
ws.cell(row=row, column=3).fill = PatternFill(start_color="D5F5E3", end_color="D5F5E3", fill_type="solid")
|
|
307
|
+
ws.cell(row=row, column=3).alignment = Alignment(horizontal="center")
|
|
308
|
+
ws.cell(row=row, column=4, value=cat_failed).border = thin_border
|
|
309
|
+
if cat_failed > 0:
|
|
310
|
+
ws.cell(row=row, column=4).fill = PatternFill(start_color="FADBD8", end_color="FADBD8", fill_type="solid")
|
|
311
|
+
ws.cell(row=row, column=4).alignment = Alignment(horizontal="center")
|
|
312
|
+
ws.cell(row=row, column=5, value=cat_skipped).border = thin_border
|
|
313
|
+
ws.cell(row=row, column=5).alignment = Alignment(horizontal="center")
|
|
314
|
+
ws.cell(row=row, column=6, value=cat_pass_rate).border = thin_border
|
|
315
|
+
ws.cell(row=row, column=6).alignment = Alignment(horizontal="center")
|
|
316
|
+
ws.cell(row=row, column=7, value=cat_coverage).border = thin_border
|
|
317
|
+
ws.cell(row=row, column=7).alignment = Alignment(horizontal="center")
|
|
318
|
+
|
|
319
|
+
status = "✅ Pass" if cat_failed == 0 else "❌ Fail"
|
|
320
|
+
ws.cell(row=row, column=8, value=status).border = thin_border
|
|
321
|
+
ws.cell(row=row, column=8).alignment = Alignment(horizontal="center")
|
|
322
|
+
|
|
323
|
+
row += 1
|
|
324
|
+
|
|
325
|
+
# ─── Target vs Actual ────────────────────────────────────
|
|
326
|
+
row += 2
|
|
327
|
+
ws.merge_cells(f"A{row}:H{row}")
|
|
328
|
+
ws[f"A{row}"] = "COVERAGE TARGET vs ACTUAL"
|
|
329
|
+
ws[f"A{row}"].font = header_font
|
|
330
|
+
ws[f"A{row}"].fill = header_fill
|
|
331
|
+
for col in range(1, 9):
|
|
332
|
+
ws.cell(row=row, column=col).fill = header_fill
|
|
333
|
+
|
|
334
|
+
row += 1
|
|
335
|
+
target_headers = ["Phase", "Target Min", "Target Max", "Actual", "Status", "Gap"]
|
|
336
|
+
for col, h in enumerate(target_headers, 1):
|
|
337
|
+
cell = ws.cell(row=row, column=col)
|
|
338
|
+
cell.value = h
|
|
339
|
+
cell.font = Font(bold=True, color="FFFFFF")
|
|
340
|
+
cell.fill = PatternFill(start_color="34495E", end_color="34495E", fill_type="solid")
|
|
341
|
+
cell.border = thin_border
|
|
342
|
+
cell.alignment = Alignment(horizontal="center")
|
|
343
|
+
|
|
344
|
+
phases = [
|
|
345
|
+
("Smoke Tests", 0, 20),
|
|
346
|
+
("Functional Tests", 20, 40),
|
|
347
|
+
("Integration Tests", 40, 60),
|
|
348
|
+
("Performance & Security", 60, 80),
|
|
349
|
+
("Advanced & Regression", 80, 90),
|
|
350
|
+
]
|
|
351
|
+
|
|
352
|
+
row += 1
|
|
353
|
+
phase_keys = ["smoke", "functional", "integration", "performance", "advanced"]
|
|
354
|
+
for i, (phase_name, target_min, target_max) in enumerate(phases):
|
|
355
|
+
phase_data = categories.get(phase_keys[i], {}) if i < len(phase_keys) else {}
|
|
356
|
+
actual = phase_data.get("coverage", 0) if isinstance(phase_data, dict) else 0
|
|
357
|
+
|
|
358
|
+
ws.cell(row=row, column=1, value=phase_name).border = thin_border
|
|
359
|
+
ws.cell(row=row, column=2, value=f"{target_min}%").border = thin_border
|
|
360
|
+
ws.cell(row=row, column=2).alignment = Alignment(horizontal="center")
|
|
361
|
+
ws.cell(row=row, column=3, value=f"{target_max}%").border = thin_border
|
|
362
|
+
ws.cell(row=row, column=3).alignment = Alignment(horizontal="center")
|
|
363
|
+
ws.cell(row=row, column=4, value=f"{actual}%").border = thin_border
|
|
364
|
+
ws.cell(row=row, column=4).alignment = Alignment(horizontal="center")
|
|
365
|
+
|
|
366
|
+
if actual >= target_max:
|
|
367
|
+
status = "✅ Achieved"
|
|
368
|
+
gap = "—"
|
|
369
|
+
elif actual >= target_min:
|
|
370
|
+
status = "🔄 In Progress"
|
|
371
|
+
gap = f"+{target_max - actual}% needed"
|
|
372
|
+
else:
|
|
373
|
+
status = "⬜ Pending"
|
|
374
|
+
gap = f"+{target_min - actual}% needed"
|
|
375
|
+
|
|
376
|
+
ws.cell(row=row, column=5, value=status).border = thin_border
|
|
377
|
+
ws.cell(row=row, column=5).alignment = Alignment(horizontal="center")
|
|
378
|
+
ws.cell(row=row, column=6, value=gap).border = thin_border
|
|
379
|
+
ws.cell(row=row, column=6).alignment = Alignment(horizontal="center")
|
|
380
|
+
|
|
381
|
+
row += 1
|
|
382
|
+
|
|
383
|
+
# ═══════════════════════════════════════════════════════════════
|
|
384
|
+
# SHEET 2: Test Details
|
|
385
|
+
# ═══════════════════════════════════════════════════════════════
|
|
386
|
+
|
|
387
|
+
def _create_test_details(self, ws, results: Dict, test_type: str):
|
|
388
|
+
"""Create detailed test results sheet"""
|
|
389
|
+
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
|
390
|
+
|
|
391
|
+
thin_border = Border(
|
|
392
|
+
left=Side(style='thin'), right=Side(style='thin'),
|
|
393
|
+
top=Side(style='thin'), bottom=Side(style='thin')
|
|
394
|
+
)
|
|
395
|
+
header_fill = PatternFill(start_color="2E86C1", end_color="2E86C1", fill_type="solid")
|
|
396
|
+
header_font = Font(name="Calibri", size=11, bold=True, color="FFFFFF")
|
|
397
|
+
|
|
398
|
+
# Title
|
|
399
|
+
ws.merge_cells("A1:J1")
|
|
400
|
+
ws["A1"] = f"DETAILED TEST RESULTS — {test_type.upper()}"
|
|
401
|
+
ws["A1"].font = Font(name="Calibri", size=16, bold=True, color="FFFFFF")
|
|
402
|
+
ws["A1"].fill = PatternFill(start_color="1B4F72", end_color="1B4F72", fill_type="solid")
|
|
403
|
+
ws["A1"].alignment = Alignment(horizontal="center", vertical="center")
|
|
404
|
+
ws.row_dimensions[1].height = 35
|
|
405
|
+
|
|
406
|
+
# Headers
|
|
407
|
+
headers = [
|
|
408
|
+
"#", "Test ID", "Test Name", "Category", "Type",
|
|
409
|
+
"Element/Endpoint", "Status", "Duration (ms)",
|
|
410
|
+
"Expected", "Actual", "Notes"
|
|
411
|
+
]
|
|
412
|
+
|
|
413
|
+
for col, header in enumerate(headers, 1):
|
|
414
|
+
cell = ws.cell(row=3, column=col)
|
|
415
|
+
cell.value = header
|
|
416
|
+
cell.font = header_font
|
|
417
|
+
cell.fill = header_fill
|
|
418
|
+
cell.border = thin_border
|
|
419
|
+
cell.alignment = Alignment(horizontal="center", wrap_text=True)
|
|
420
|
+
|
|
421
|
+
# Data rows
|
|
422
|
+
details = results.get("details", [])
|
|
423
|
+
categories = results.get("categories", {})
|
|
424
|
+
|
|
425
|
+
row = 4
|
|
426
|
+
test_num = 1
|
|
427
|
+
|
|
428
|
+
# If we have detailed test cases
|
|
429
|
+
if details:
|
|
430
|
+
for detail in details:
|
|
431
|
+
if isinstance(detail, dict):
|
|
432
|
+
ws.cell(row=row, column=1, value=test_num).border = thin_border
|
|
433
|
+
ws.cell(row=row, column=1).alignment = Alignment(horizontal="center")
|
|
434
|
+
ws.cell(row=row, column=2, value=detail.get("id", f"TC-{test_num:04d}")).border = thin_border
|
|
435
|
+
ws.cell(row=row, column=3, value=detail.get("name", "Unnamed Test")).border = thin_border
|
|
436
|
+
ws.cell(row=row, column=4, value=detail.get("category", test_type)).border = thin_border
|
|
437
|
+
ws.cell(row=row, column=5, value=detail.get("type", "functional")).border = thin_border
|
|
438
|
+
ws.cell(row=row, column=6, value=detail.get("element", detail.get("endpoint", "N/A"))).border = thin_border
|
|
439
|
+
|
|
440
|
+
status = detail.get("status", "unknown")
|
|
441
|
+
ws.cell(row=row, column=7, value=status.upper()).border = thin_border
|
|
442
|
+
ws.cell(row=row, column=7).alignment = Alignment(horizontal="center")
|
|
443
|
+
|
|
444
|
+
if status == "passed":
|
|
445
|
+
ws.cell(row=row, column=7).fill = PatternFill(start_color="D5F5E3", end_color="D5F5E3", fill_type="solid")
|
|
446
|
+
elif status == "failed":
|
|
447
|
+
ws.cell(row=row, column=7).fill = PatternFill(start_color="FADBD8", end_color="FADBD8", fill_type="solid")
|
|
448
|
+
elif status == "skipped":
|
|
449
|
+
ws.cell(row=row, column=7).fill = PatternFill(start_color="FEF9E7", end_color="FEF9E7", fill_type="solid")
|
|
450
|
+
|
|
451
|
+
ws.cell(row=row, column=8, value=detail.get("duration", 0)).border = thin_border
|
|
452
|
+
ws.cell(row=row, column=9, value=detail.get("expected", "")).border = thin_border
|
|
453
|
+
ws.cell(row=row, column=10, value=detail.get("actual", detail.get("error", ""))).border = thin_border
|
|
454
|
+
ws.cell(row=row, column=11, value=detail.get("notes", "")).border = thin_border
|
|
455
|
+
|
|
456
|
+
test_num += 1
|
|
457
|
+
row += 1
|
|
458
|
+
else:
|
|
459
|
+
# Generate from category data
|
|
460
|
+
for cat_name, cat_data in categories.items():
|
|
461
|
+
if isinstance(cat_data, dict):
|
|
462
|
+
for i in range(cat_data.get("passed", 0)):
|
|
463
|
+
ws.cell(row=row, column=1, value=test_num).border = thin_border
|
|
464
|
+
ws.cell(row=row, column=2, value=f"TC-{test_num:04d}").border = thin_border
|
|
465
|
+
ws.cell(row=row, column=3, value=f"{cat_name} test {i+1}").border = thin_border
|
|
466
|
+
ws.cell(row=row, column=4, value=cat_name).border = thin_border
|
|
467
|
+
ws.cell(row=row, column=7, value="PASSED").border = thin_border
|
|
468
|
+
ws.cell(row=row, column=7).fill = PatternFill(start_color="D5F5E3", end_color="D5F5E3", fill_type="solid")
|
|
469
|
+
ws.cell(row=row, column=7).alignment = Alignment(horizontal="center")
|
|
470
|
+
test_num += 1
|
|
471
|
+
row += 1
|
|
472
|
+
|
|
473
|
+
for i in range(cat_data.get("failed", 0)):
|
|
474
|
+
ws.cell(row=row, column=1, value=test_num).border = thin_border
|
|
475
|
+
ws.cell(row=row, column=2, value=f"TC-{test_num:04d}").border = thin_border
|
|
476
|
+
ws.cell(row=row, column=3, value=f"{cat_name} test (failed) {i+1}").border = thin_border
|
|
477
|
+
ws.cell(row=row, column=4, value=cat_name).border = thin_border
|
|
478
|
+
ws.cell(row=row, column=7, value="FAILED").border = thin_border
|
|
479
|
+
ws.cell(row=row, column=7).fill = PatternFill(start_color="FADBD8", end_color="FADBD8", fill_type="solid")
|
|
480
|
+
ws.cell(row=row, column=7).alignment = Alignment(horizontal="center")
|
|
481
|
+
test_num += 1
|
|
482
|
+
row += 1
|
|
483
|
+
|
|
484
|
+
# Summary at bottom
|
|
485
|
+
row += 2
|
|
486
|
+
ws.merge_cells(f"A{row}:C{row}")
|
|
487
|
+
ws[f"A{row}"] = "SUMMARY"
|
|
488
|
+
ws[f"A{row}"].font = Font(bold=True, size=12)
|
|
489
|
+
row += 1
|
|
490
|
+
ws[f"A{row}"] = "Total Test Cases:"
|
|
491
|
+
ws[f"B{row}"] = results.get("total", 0)
|
|
492
|
+
ws[f"B{row}"].font = Font(bold=True)
|
|
493
|
+
row += 1
|
|
494
|
+
ws[f"A{row}"] = "Pass Rate:"
|
|
495
|
+
total = results.get("total", 1)
|
|
496
|
+
ws[f"B{row}"] = f"{(results.get('passed', 0) / total * 100):.1f}%"
|
|
497
|
+
row += 1
|
|
498
|
+
ws[f"A{row}"] = "Overall Coverage:"
|
|
499
|
+
ws[f"B{row}"] = f"{results.get('coverage', 0)}%"
|
|
500
|
+
|
|
501
|
+
# ═══════════════════════════════════════════════════════════════
|
|
502
|
+
# SHEET 3: Coverage Analysis
|
|
503
|
+
# ═══════════════════════════════════════════════════════════════
|
|
504
|
+
|
|
505
|
+
def _create_coverage_analysis(self, ws, results: Dict, test_type: str):
|
|
506
|
+
"""Create coverage analysis sheet"""
|
|
507
|
+
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
|
508
|
+
|
|
509
|
+
thin_border = Border(
|
|
510
|
+
left=Side(style='thin'), right=Side(style='thin'),
|
|
511
|
+
top=Side(style='thin'), bottom=Side(style='thin')
|
|
512
|
+
)
|
|
513
|
+
header_fill = PatternFill(start_color="2E86C1", end_color="2E86C1", fill_type="solid")
|
|
514
|
+
header_font = Font(name="Calibri", size=11, bold=True, color="FFFFFF")
|
|
515
|
+
|
|
516
|
+
# Title
|
|
517
|
+
ws.merge_cells("A1:G1")
|
|
518
|
+
ws["A1"] = "COVERAGE ANALYSIS — 0% → 90% PROGRESS TRACKER"
|
|
519
|
+
ws["A1"].font = Font(name="Calibri", size=16, bold=True, color="FFFFFF")
|
|
520
|
+
ws["A1"].fill = PatternFill(start_color="1B4F72", end_color="1B4F72", fill_type="solid")
|
|
521
|
+
ws["A1"].alignment = Alignment(horizontal="center", vertical="center")
|
|
522
|
+
ws.row_dimensions[1].height = 35
|
|
523
|
+
|
|
524
|
+
# Overall Coverage
|
|
525
|
+
ws.merge_cells("A3:B3")
|
|
526
|
+
ws["A3"] = "Overall Coverage:"
|
|
527
|
+
ws["A3"].font = Font(bold=True, size=14)
|
|
528
|
+
coverage = results.get("coverage", 0)
|
|
529
|
+
ws["C3"] = f"{coverage}%"
|
|
530
|
+
ws["C3"].font = Font(bold=True, size=14,
|
|
531
|
+
color="27AE60" if coverage >= 80 else "F39C12" if coverage >= 60 else "E74C3C")
|
|
532
|
+
|
|
533
|
+
# Phase coverage table
|
|
534
|
+
ws.merge_cells("A5:G5")
|
|
535
|
+
ws["A5"] = "PHASE-WISE COVERAGE BREAKDOWN"
|
|
536
|
+
ws["A5"].font = header_font
|
|
537
|
+
ws["A5"].fill = header_fill
|
|
538
|
+
for col in range(1, 8):
|
|
539
|
+
ws.cell(row=5, column=col).fill = header_fill
|
|
540
|
+
|
|
541
|
+
headers = ["Phase", "Target Range", "Actual Coverage", "Status", "Tests Run", "Tests Passed", "Gap to Next"]
|
|
542
|
+
for col, h in enumerate(headers, 1):
|
|
543
|
+
cell = ws.cell(row=6, column=col)
|
|
544
|
+
cell.value = h
|
|
545
|
+
cell.font = Font(bold=True, color="FFFFFF")
|
|
546
|
+
cell.fill = PatternFill(start_color="34495E", end_color="34495E", fill_type="solid")
|
|
547
|
+
cell.border = thin_border
|
|
548
|
+
cell.alignment = Alignment(horizontal="center")
|
|
549
|
+
|
|
550
|
+
phases_data = [
|
|
551
|
+
("Phase 1: Smoke Tests", "0% - 20%", "smoke"),
|
|
552
|
+
("Phase 2: Functional Tests", "20% - 40%", "functional"),
|
|
553
|
+
("Phase 3: Integration Tests", "40% - 60%", "integration"),
|
|
554
|
+
("Phase 4: Performance & Security", "60% - 80%", "performance"),
|
|
555
|
+
("Phase 5: Advanced & Regression", "80% - 90%", "advanced"),
|
|
556
|
+
]
|
|
557
|
+
|
|
558
|
+
categories = results.get("categories", {})
|
|
559
|
+
row = 7
|
|
560
|
+
|
|
561
|
+
for phase_name, target_range, phase_key in phases_data:
|
|
562
|
+
cat_data = categories.get(phase_key, {})
|
|
563
|
+
actual = cat_data.get("coverage", 0) if isinstance(cat_data, dict) else 0
|
|
564
|
+
total = cat_data.get("total", 0) if isinstance(cat_data, dict) else 0
|
|
565
|
+
passed = cat_data.get("passed", 0) if isinstance(cat_data, dict) else 0
|
|
566
|
+
|
|
567
|
+
ws.cell(row=row, column=1, value=phase_name).border = thin_border
|
|
568
|
+
ws.cell(row=row, column=2, value=target_range).border = thin_border
|
|
569
|
+
ws.cell(row=row, column=2).alignment = Alignment(horizontal="center")
|
|
570
|
+
ws.cell(row=row, column=3, value=f"{actual}%").border = thin_border
|
|
571
|
+
ws.cell(row=row, column=3).alignment = Alignment(horizontal="center")
|
|
572
|
+
|
|
573
|
+
# Color code actual coverage
|
|
574
|
+
if actual >= 80:
|
|
575
|
+
ws.cell(row=row, column=3).fill = PatternFill(start_color="D5F5E3", end_color="D5F5E3", fill_type="solid")
|
|
576
|
+
elif actual >= 40:
|
|
577
|
+
ws.cell(row=row, column=3).fill = PatternFill(start_color="FEF9E7", end_color="FEF9E7", fill_type="solid")
|
|
578
|
+
elif actual > 0:
|
|
579
|
+
ws.cell(row=row, column=3).fill = PatternFill(start_color="FADBD8", end_color="FADBD8", fill_type="solid")
|
|
580
|
+
|
|
581
|
+
# Status
|
|
582
|
+
if actual >= 80:
|
|
583
|
+
status = "✅ Complete"
|
|
584
|
+
elif actual > 0:
|
|
585
|
+
status = "🔄 In Progress"
|
|
586
|
+
else:
|
|
587
|
+
status = "⬜ Not Started"
|
|
588
|
+
ws.cell(row=row, column=4, value=status).border = thin_border
|
|
589
|
+
ws.cell(row=row, column=4).alignment = Alignment(horizontal="center")
|
|
590
|
+
|
|
591
|
+
ws.cell(row=row, column=5, value=total).border = thin_border
|
|
592
|
+
ws.cell(row=row, column=5).alignment = Alignment(horizontal="center")
|
|
593
|
+
ws.cell(row=row, column=6, value=passed).border = thin_border
|
|
594
|
+
ws.cell(row=row, column=6).alignment = Alignment(horizontal="center")
|
|
595
|
+
|
|
596
|
+
gap = max(0, 20 - actual) if actual < 20 else max(0, 40 - actual) if actual < 40 else max(0, 60 - actual) if actual < 60 else max(0, 80 - actual) if actual < 80 else max(0, 90 - actual)
|
|
597
|
+
ws.cell(row=row, column=7, value=f"+{gap}%" if gap > 0 else "✅").border = thin_border
|
|
598
|
+
ws.cell(row=row, column=7).alignment = Alignment(horizontal="center")
|
|
599
|
+
|
|
600
|
+
row += 1
|
|
601
|
+
|
|
602
|
+
# Coverage by Element Type
|
|
603
|
+
row += 2
|
|
604
|
+
ws.merge_cells(f"A{row}:G{row}")
|
|
605
|
+
ws[f"A{row}"] = "COVERAGE BY ELEMENT TYPE"
|
|
606
|
+
ws[f"A{row}"].font = header_font
|
|
607
|
+
ws[f"A{row}"].fill = header_fill
|
|
608
|
+
for col in range(1, 8):
|
|
609
|
+
ws.cell(row=row, column=col).fill = header_fill
|
|
610
|
+
|
|
611
|
+
row += 1
|
|
612
|
+
elem_headers = ["Element Type", "Total Found", "Tested", "Coverage %", "Priority", "Status", "Notes"]
|
|
613
|
+
for col, h in enumerate(elem_headers, 1):
|
|
614
|
+
cell = ws.cell(row=row, column=col)
|
|
615
|
+
cell.value = h
|
|
616
|
+
cell.font = Font(bold=True, color="FFFFFF")
|
|
617
|
+
cell.fill = PatternFill(start_color="34495E", end_color="34495E", fill_type="solid")
|
|
618
|
+
cell.border = thin_border
|
|
619
|
+
cell.alignment = Alignment(horizontal="center")
|
|
620
|
+
|
|
621
|
+
row += 1
|
|
622
|
+
element_types = [
|
|
623
|
+
("Buttons/CTAs", "critical"), ("Input Fields", "critical"),
|
|
624
|
+
("Forms", "critical"), ("Navigation Links", "high"),
|
|
625
|
+
("Images/Media", "medium"), ("Tables/Data", "medium"),
|
|
626
|
+
("Modals/Dialogs", "high"), ("Search Components", "high"),
|
|
627
|
+
("Footer Elements", "low"), ("Third-party Widgets", "medium"),
|
|
628
|
+
]
|
|
629
|
+
|
|
630
|
+
for elem_name, priority in element_types:
|
|
631
|
+
ws.cell(row=row, column=1, value=elem_name).border = thin_border
|
|
632
|
+
ws.cell(row=row, column=5, value=priority).border = thin_border
|
|
633
|
+
ws.cell(row=row, column=5).alignment = Alignment(horizontal="center")
|
|
634
|
+
|
|
635
|
+
if priority == "critical":
|
|
636
|
+
ws.cell(row=row, column=5).fill = PatternFill(start_color="FADBD8", end_color="FADBD8", fill_type="solid")
|
|
637
|
+
elif priority == "high":
|
|
638
|
+
ws.cell(row=row, column=5).fill = PatternFill(start_color="FEF9E7", end_color="FEF9E7", fill_type="solid")
|
|
639
|
+
|
|
640
|
+
row += 1
|
|
641
|
+
|
|
642
|
+
# ═══════════════════════════════════════════════════════════════
|
|
643
|
+
# SHEET 4: Element Report
|
|
644
|
+
# ═══════════════════════════════════════════════════════════════
|
|
645
|
+
|
|
646
|
+
def _create_element_report(self, ws, results: Dict, test_type: str):
|
|
647
|
+
"""Create element/component report"""
|
|
648
|
+
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
|
649
|
+
|
|
650
|
+
thin_border = Border(
|
|
651
|
+
left=Side(style='thin'), right=Side(style='thin'),
|
|
652
|
+
top=Side(style='thin'), bottom=Side(style='thin')
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
ws.merge_cells("A1:I1")
|
|
656
|
+
ws["A1"] = "ELEMENTS & COMPONENTS DETECTION REPORT"
|
|
657
|
+
ws["A1"].font = Font(name="Calibri", size=16, bold=True, color="FFFFFF")
|
|
658
|
+
ws["A1"].fill = PatternFill(start_color="1B4F72", end_color="1B4F72", fill_type="solid")
|
|
659
|
+
ws["A1"].alignment = Alignment(horizontal="center", vertical="center")
|
|
660
|
+
ws.row_dimensions[1].height = 35
|
|
661
|
+
|
|
662
|
+
ws.merge_cells("A2:I2")
|
|
663
|
+
ws["A2"] = "All system elements detected with their purpose and test coverage status"
|
|
664
|
+
ws["A2"].font = Font(italic=True, color="666666")
|
|
665
|
+
ws["A2"].alignment = Alignment(horizontal="center")
|
|
666
|
+
|
|
667
|
+
headers = [
|
|
668
|
+
"#", "Element Name", "Type", "Selector/ID",
|
|
669
|
+
"Description/Purpose", "Test Priority",
|
|
670
|
+
"Tests Applied", "Result", "Recommendation"
|
|
671
|
+
]
|
|
672
|
+
|
|
673
|
+
header_fill = PatternFill(start_color="2E86C1", end_color="2E86C1", fill_type="solid")
|
|
674
|
+
for col, h in enumerate(headers, 1):
|
|
675
|
+
cell = ws.cell(row=4, column=col)
|
|
676
|
+
cell.value = h
|
|
677
|
+
cell.font = Font(bold=True, color="FFFFFF")
|
|
678
|
+
cell.fill = header_fill
|
|
679
|
+
cell.border = thin_border
|
|
680
|
+
cell.alignment = Alignment(horizontal="center", wrap_text=True)
|
|
681
|
+
|
|
682
|
+
# Element data based on test type
|
|
683
|
+
elements = results.get("elements", [])
|
|
684
|
+
|
|
685
|
+
if not elements:
|
|
686
|
+
# Generate standard elements based on test type
|
|
687
|
+
elements = self._get_standard_elements(test_type)
|
|
688
|
+
|
|
689
|
+
row = 5
|
|
690
|
+
for i, elem in enumerate(elements, 1):
|
|
691
|
+
ws.cell(row=row, column=1, value=i).border = thin_border
|
|
692
|
+
ws.cell(row=row, column=2, value=elem.get("name", "")).border = thin_border
|
|
693
|
+
ws.cell(row=row, column=3, value=elem.get("type", "")).border = thin_border
|
|
694
|
+
ws.cell(row=row, column=4, value=elem.get("selector", elem.get("id", ""))).border = thin_border
|
|
695
|
+
ws.cell(row=row, column=5, value=elem.get("description", "")).border = thin_border
|
|
696
|
+
ws.cell(row=row, column=6, value=elem.get("priority", "medium")).border = thin_border
|
|
697
|
+
ws.cell(row=row, column=7, value=elem.get("tests_applied", "")).border = thin_border
|
|
698
|
+
ws.cell(row=row, column=8, value=elem.get("result", "pending")).border = thin_border
|
|
699
|
+
ws.cell(row=row, column=9, value=elem.get("recommendation", "")).border = thin_border
|
|
700
|
+
|
|
701
|
+
# Wrap text for description
|
|
702
|
+
ws.cell(row=row, column=5).alignment = Alignment(wrap_text=True)
|
|
703
|
+
ws.cell(row=row, column=9).alignment = Alignment(wrap_text=True)
|
|
704
|
+
|
|
705
|
+
row += 1
|
|
706
|
+
|
|
707
|
+
def _get_standard_elements(self, test_type: str) -> List[Dict]:
|
|
708
|
+
"""Get standard elements based on test type"""
|
|
709
|
+
web_elements = [
|
|
710
|
+
{"name": "Header/Logo", "type": "header", "selector": "header > .logo", "description": "Company branding and navigation home link", "priority": "high", "tests_applied": "Load, Click, Responsive", "result": "pending", "recommendation": "Verify logo links to homepage"},
|
|
711
|
+
{"name": "Navigation Menu", "type": "nav", "selector": "nav, .navbar", "description": "Main navigation links for site sections", "priority": "critical", "tests_applied": "Links, Hover, Mobile menu", "result": "pending", "recommendation": "Test all nav links, mobile hamburger menu"},
|
|
712
|
+
{"name": "Hero Section", "type": "section", "selector": ".hero, #hero", "description": "Primary landing content with CTA", "priority": "high", "tests_applied": "Visibility, CTA click, Responsive", "result": "pending", "recommendation": "Verify CTA button functionality"},
|
|
713
|
+
{"name": "Login Form", "type": "form", "selector": "form[action*='login']", "description": "User authentication form", "priority": "critical", "tests_applied": "Input validation, Submit, Error states", "result": "pending", "recommendation": "Test empty fields, invalid credentials, CSRF"},
|
|
714
|
+
{"name": "Search Bar", "type": "input", "selector": "input[type='search']", "description": "Site search functionality", "priority": "high", "tests_applied": "Input, Submit, Results, Empty state", "result": "pending", "recommendation": "Test autocomplete, no results, special chars"},
|
|
715
|
+
{"name": "Footer Links", "type": "footer", "selector": "footer a", "description": "Footer navigation and legal links", "priority": "medium", "tests_applied": "Link validation", "result": "pending", "recommendation": "Verify all footer links resolve correctly"},
|
|
716
|
+
{"name": "Contact Form", "type": "form", "selector": "form[action*='contact']", "description": "Contact/submission form", "priority": "critical", "tests_applied": "Validation, Submission, Confirmation", "result": "pending", "recommendation": "Test all field validations and submission"},
|
|
717
|
+
{"name": "Images", "type": "image", "selector": "img", "description": "All page images with alt text", "priority": "medium", "tests_applied": "Load, Alt text, Responsive, Lazy load", "result": "pending", "recommendation": "Verify alt text for accessibility"},
|
|
718
|
+
]
|
|
719
|
+
|
|
720
|
+
mobile_elements = [
|
|
721
|
+
{"name": "App Launcher", "type": "activity", "selector": "MainActivity", "description": "App main activity/launch point", "priority": "critical", "tests_applied": "Launch time, Crash check", "result": "pending", "recommendation": "Verify cold start under 3 seconds"},
|
|
722
|
+
{"name": "Login Screen", "type": "screen", "selector": "LoginActivity", "description": "User authentication screen", "priority": "critical", "tests_applied": "Input, Submit, Error handling", "result": "pending", "recommendation": "Test all error states and keyboard types"},
|
|
723
|
+
{"name": "Navigation Drawer", "type": "component", "selector": "NavigationView", "description": "Side navigation menu", "priority": "high", "tests_applied": "Open/Close, Item click, Back", "result": "pending", "recommendation": "Test swipe gesture and keyboard nav"},
|
|
724
|
+
{"name": "List/RecyclerView", "type": "component", "selector": "RecyclerView", "description": "Scrollable data list", "priority": "high", "tests_applied": "Scroll, Click item, Pull-to-refresh", "result": "pending", "recommendation": "Test with empty, loading, and error states"},
|
|
725
|
+
{"name": "Bottom Navigation", "type": "component", "selector": "BottomNavigationView", "description": "Bottom tab navigation bar", "priority": "critical", "tests_applied": "Tab switch, Badge, Active state", "result": "pending", "recommendation": "Verify all tabs switch correctly"},
|
|
726
|
+
{"name": "Floating Action Button", "type": "button", "selector": "FloatingActionButton", "description": "Primary action button", "priority": "high", "tests_applied": "Click, Animation, Accessibility", "result": "pending", "recommendation": "Verify content description for a11y"},
|
|
727
|
+
]
|
|
728
|
+
|
|
729
|
+
api_elements = [
|
|
730
|
+
{"name": "Health Endpoint", "type": "GET", "selector": "/api/health", "description": "API health check endpoint", "priority": "critical", "tests_applied": "Status 200, Response time, Format", "result": "pending", "recommendation": "Must return 200 with version info"},
|
|
731
|
+
{"name": "Auth Endpoint", "type": "POST", "selector": "/api/auth/login", "description": "Authentication endpoint", "priority": "critical", "tests_applied": "Valid/invalid creds, Token generation", "result": "pending", "recommendation": "Test brute force protection, token expiry"},
|
|
732
|
+
{"name": "CRUD Endpoints", "type": "ALL", "selector": "/api/resources/*", "description": "Create, Read, Update, Delete operations", "priority": "critical", "tests_applied": "All HTTP methods, Schema validation", "result": "pending", "recommendation": "Test all CRUD operations with edge cases"},
|
|
733
|
+
{"name": "Pagination", "type": "GET", "selector": "/api/resources?page=", "description": "Paginated list endpoints", "priority": "high", "tests_applied": "Page navigation, Limits, Sorting", "result": "pending", "recommendation": "Test boundary pages and empty results"},
|
|
734
|
+
{"name": "File Upload", "type": "POST", "selector": "/api/upload", "description": "File upload endpoint", "priority": "high", "tests_applied": "Size limits, Type validation, Storage", "result": "pending", "recommendation": "Test max file size and invalid types"},
|
|
735
|
+
{"name": "WebSocket", "type": "WS", "selector": "ws://api/ws", "description": "Real-time WebSocket connection", "priority": "medium", "tests_applied": "Connect, Send/Receive, Reconnect", "result": "pending", "recommendation": "Test connection timeout and message format"},
|
|
736
|
+
]
|
|
737
|
+
|
|
738
|
+
embedded_elements = [
|
|
739
|
+
{"name": "MQTT Broker", "type": "protocol", "selector": "mqtt://target:1883", "description": "Message broker connection", "priority": "critical", "tests_applied": "Connect, Subscribe, Publish, QoS", "result": "pending", "recommendation": "Verify QoS levels and retain flag"},
|
|
740
|
+
{"name": "Sensor Data", "type": "telemetry", "selector": "topic/sensors/*", "description": "Sensor data topics", "priority": "critical", "tests_applied": "Data format, Range, Frequency", "result": "pending", "recommendation": "Verify data within expected ranges"},
|
|
741
|
+
{"name": "Command Interface", "type": "control", "selector": "topic/commands/*", "description": "Device command topics", "priority": "high", "tests_applied": "Send/Receive, Ack, Timeout", "result": "pending", "recommendation": "Test command acknowledgment and retries"},
|
|
742
|
+
{"name": "OTA Updates", "type": "firmware", "selector": "topic/ota/*", "description": "Over-the-air firmware updates", "priority": "high", "tests_applied": "Version check, Download, Verify, Install", "result": "pending", "recommendation": "Test update rollback on failure"},
|
|
743
|
+
{"name": "Watchdog Timer", "type": "system", "selector": "device/watchdog", "description": "Device health monitoring", "priority": "critical", "tests_applied": "Heartbeat, Timeout, Recovery", "result": "pending", "recommendation": "Verify automatic recovery after timeout"},
|
|
744
|
+
]
|
|
745
|
+
|
|
746
|
+
type_map = {
|
|
747
|
+
"web": web_elements,
|
|
748
|
+
"mobile": mobile_elements,
|
|
749
|
+
"api": api_elements,
|
|
750
|
+
"embedded": embedded_elements,
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
return type_map.get(test_type, web_elements)
|
|
754
|
+
|
|
755
|
+
# ═══════════════════════════════════════════════════════════════
|
|
756
|
+
# SHEET 5: Defect Log
|
|
757
|
+
# ═══════════════════════════════════════════════════════════════
|
|
758
|
+
|
|
759
|
+
def _create_defect_log(self, ws, results: Dict, test_type: str):
|
|
760
|
+
"""Create defect/bug tracking sheet"""
|
|
761
|
+
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
|
762
|
+
|
|
763
|
+
thin_border = Border(
|
|
764
|
+
left=Side(style='thin'), right=Side(style='thin'),
|
|
765
|
+
top=Side(style='thin'), bottom=Side(style='thin')
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
ws.merge_cells("A1:K1")
|
|
769
|
+
ws["A1"] = "DEFECT LOG — Issues Found During Testing"
|
|
770
|
+
ws["A1"].font = Font(name="Calibri", size=16, bold=True, color="FFFFFF")
|
|
771
|
+
ws["A1"].fill = PatternFill(start_color="C0392B", end_color="C0392B", fill_type="solid")
|
|
772
|
+
ws["A1"].alignment = Alignment(horizontal="center", vertical="center")
|
|
773
|
+
ws.row_dimensions[1].height = 35
|
|
774
|
+
|
|
775
|
+
headers = [
|
|
776
|
+
"Defect ID", "Severity", "Priority", "Category",
|
|
777
|
+
"Title", "Description", "Steps to Reproduce",
|
|
778
|
+
"Expected Result", "Actual Result", "Environment",
|
|
779
|
+
"Status"
|
|
780
|
+
]
|
|
781
|
+
|
|
782
|
+
header_fill = PatternFill(start_color="E74C3C", end_color="E74C3C", fill_type="solid")
|
|
783
|
+
for col, h in enumerate(headers, 1):
|
|
784
|
+
cell = ws.cell(row=3, column=col)
|
|
785
|
+
cell.value = h
|
|
786
|
+
cell.font = Font(bold=True, color="FFFFFF")
|
|
787
|
+
cell.fill = header_fill
|
|
788
|
+
cell.border = thin_border
|
|
789
|
+
cell.alignment = Alignment(horizontal="center", wrap_text=True)
|
|
790
|
+
|
|
791
|
+
# Generate defects from failed tests
|
|
792
|
+
details = results.get("details", [])
|
|
793
|
+
defects = [d for d in details if isinstance(d, dict) and d.get("status") == "failed"]
|
|
794
|
+
|
|
795
|
+
row = 4
|
|
796
|
+
for i, defect in enumerate(defects, 1):
|
|
797
|
+
ws.cell(row=row, column=1, value=f"DEF-{i:04d}").border = thin_border
|
|
798
|
+
ws.cell(row=row, column=2, value="High").border = thin_border
|
|
799
|
+
ws.cell(row=row, column=3, value="P1").border = thin_border
|
|
800
|
+
ws.cell(row=row, column=4, value=defect.get("category", test_type)).border = thin_border
|
|
801
|
+
ws.cell(row=row, column=5, value=defect.get("name", "Test Failure")).border = thin_border
|
|
802
|
+
ws.cell(row=row, column=6, value=defect.get("error", defect.get("actual", "Test failed"))).border = thin_border
|
|
803
|
+
ws.cell(row=row, column=7, value=defect.get("steps", "Run automated test suite")).border = thin_border
|
|
804
|
+
ws.cell(row=row, column=8, value=defect.get("expected", "Test should pass")).border = thin_border
|
|
805
|
+
ws.cell(row=row, column=9, value=defect.get("actual", "Test failed")).border = thin_border
|
|
806
|
+
ws.cell(row=row, column=10, value=test_type).border = thin_border
|
|
807
|
+
ws.cell(row=row, column=11, value="Open").border = thin_border
|
|
808
|
+
|
|
809
|
+
# Color severity
|
|
810
|
+
ws.cell(row=row, column=2).fill = PatternFill(start_color="FADBD8", end_color="FADBD8", fill_type="solid")
|
|
811
|
+
ws.cell(row=row, column=11).fill = PatternFill(start_color="FEF9E7", end_color="FEF9E7", fill_type="solid")
|
|
812
|
+
|
|
813
|
+
row += 1
|
|
814
|
+
|
|
815
|
+
if not defects:
|
|
816
|
+
ws.merge_cells(f"A4:K4")
|
|
817
|
+
ws["A4"] = "✅ No defects found — All tests passed!"
|
|
818
|
+
ws["A4"].font = Font(size=14, bold=True, color="27AE60")
|
|
819
|
+
ws["A4"].alignment = Alignment(horizontal="center")
|
|
820
|
+
ws.row_dimensions[4].height = 30
|
|
821
|
+
|
|
822
|
+
# Summary
|
|
823
|
+
row += 2
|
|
824
|
+
ws[f"A{row}"] = "Defect Summary:"
|
|
825
|
+
ws[f"A{row}"].font = Font(bold=True, size=12)
|
|
826
|
+
row += 1
|
|
827
|
+
ws[f"A{row}"] = "Total Defects:"
|
|
828
|
+
ws[f"B{row}"] = len(defects)
|
|
829
|
+
row += 1
|
|
830
|
+
ws[f"A{row}"] = "Critical:"
|
|
831
|
+
ws[f"B{row}"] = sum(1 for d in defects if isinstance(d, dict) and d.get("severity") == "critical")
|
|
832
|
+
row += 1
|
|
833
|
+
ws[f"A{row}"] = "High:"
|
|
834
|
+
ws[f"B{row}"] = len(defects) # All assumed high for now
|
|
835
|
+
row += 1
|
|
836
|
+
ws[f"A{row}"] = "Open:"
|
|
837
|
+
ws[f"B{row}"] = len(defects)
|
|
838
|
+
|
|
839
|
+
# ═══════════════════════════════════════════════════════════════
|
|
840
|
+
# SHEET 6: Performance Metrics
|
|
841
|
+
# ═══════════════════════════════════════════════════════════════
|
|
842
|
+
|
|
843
|
+
def _create_performance_metrics(self, ws, results: Dict, test_type: str):
|
|
844
|
+
"""Create performance metrics sheet"""
|
|
845
|
+
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
|
846
|
+
|
|
847
|
+
thin_border = Border(
|
|
848
|
+
left=Side(style='thin'), right=Side(style='thin'),
|
|
849
|
+
top=Side(style='thin'), bottom=Side(style='thin')
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
ws.merge_cells("A1:F1")
|
|
853
|
+
ws["A1"] = "PERFORMANCE METRICS"
|
|
854
|
+
ws["A1"].font = Font(name="Calibri", size=16, bold=True, color="FFFFFF")
|
|
855
|
+
ws["A1"].fill = PatternFill(start_color="1B4F72", end_color="1B4F72", fill_type="solid")
|
|
856
|
+
ws["A1"].alignment = Alignment(horizontal="center", vertical="center")
|
|
857
|
+
ws.row_dimensions[1].height = 35
|
|
858
|
+
|
|
859
|
+
# Metrics table
|
|
860
|
+
headers = ["Metric", "Value", "Benchmark", "Status", "Trend", "Notes"]
|
|
861
|
+
header_fill = PatternFill(start_color="2E86C1", end_color="2E86C1", fill_type="solid")
|
|
862
|
+
|
|
863
|
+
for col, h in enumerate(headers, 1):
|
|
864
|
+
cell = ws.cell(row=3, column=col)
|
|
865
|
+
cell.value = h
|
|
866
|
+
cell.font = Font(bold=True, color="FFFFFF")
|
|
867
|
+
cell.fill = header_fill
|
|
868
|
+
cell.border = thin_border
|
|
869
|
+
cell.alignment = Alignment(horizontal="center")
|
|
870
|
+
|
|
871
|
+
metrics = self._get_performance_metrics(results, test_type)
|
|
872
|
+
row = 4
|
|
873
|
+
for metric in metrics:
|
|
874
|
+
ws.cell(row=row, column=1, value=metric["name"]).border = thin_border
|
|
875
|
+
ws.cell(row=row, column=2, value=metric["value"]).border = thin_border
|
|
876
|
+
ws.cell(row=row, column=2).alignment = Alignment(horizontal="center")
|
|
877
|
+
ws.cell(row=row, column=3, value=metric["benchmark"]).border = thin_border
|
|
878
|
+
ws.cell(row=row, column=3).alignment = Alignment(horizontal="center")
|
|
879
|
+
ws.cell(row=row, column=4, value=metric["status"]).border = thin_border
|
|
880
|
+
ws.cell(row=row, column=4).alignment = Alignment(horizontal="center")
|
|
881
|
+
|
|
882
|
+
if metric["status"] == "✅ Pass":
|
|
883
|
+
ws.cell(row=row, column=4).fill = PatternFill(start_color="D5F5E3", end_color="D5F5E3", fill_type="solid")
|
|
884
|
+
elif metric["status"] == "❌ Fail":
|
|
885
|
+
ws.cell(row=row, column=4).fill = PatternFill(start_color="FADBD8", end_color="FADBD8", fill_type="solid")
|
|
886
|
+
|
|
887
|
+
ws.cell(row=row, column=5, value=metric.get("trend", "—")).border = thin_border
|
|
888
|
+
ws.cell(row=row, column=5).alignment = Alignment(horizontal="center")
|
|
889
|
+
ws.cell(row=row, column=6, value=metric.get("notes", "")).border = thin_border
|
|
890
|
+
|
|
891
|
+
row += 1
|
|
892
|
+
|
|
893
|
+
def _get_performance_metrics(self, results: Dict, test_type: str) -> List[Dict]:
|
|
894
|
+
"""Get performance metrics based on test type"""
|
|
895
|
+
duration = results.get("duration", 0)
|
|
896
|
+
|
|
897
|
+
base_metrics = [
|
|
898
|
+
{"name": "Total Test Duration", "value": f"{duration:.1f}s", "benchmark": "< 300s",
|
|
899
|
+
"status": "✅ Pass" if duration < 300 else "❌ Fail", "notes": "End-to-end test time"},
|
|
900
|
+
{"name": "Tests Per Second", "value": f"{results.get('total', 0) / max(duration, 1):.1f}",
|
|
901
|
+
"benchmark": "> 1 tps", "status": "✅ Pass", "notes": "Test execution throughput"},
|
|
902
|
+
{"name": "Pass Rate", "value": f"{(results.get('passed', 0) / max(results.get('total', 1), 1) * 100):.1f}%",
|
|
903
|
+
"benchmark": "> 95%", "status": "✅ Pass" if results.get("passed", 0) / max(results.get("total", 1), 1) > 0.95 else "❌ Fail",
|
|
904
|
+
"notes": "Percentage of tests passing"},
|
|
905
|
+
{"name": "Defect Density", "value": f"{results.get('failed', 0)} defects",
|
|
906
|
+
"benchmark": "< 5 defects", "status": "✅ Pass" if results.get("failed", 0) < 5 else "❌ Fail",
|
|
907
|
+
"notes": "Number of defects found"},
|
|
908
|
+
{"name": "Coverage Efficiency", "value": f"{results.get('coverage', 0)}%",
|
|
909
|
+
"benchmark": "> 80%", "status": "✅ Pass" if results.get("coverage", 0) >= 80 else "⚠️ Warning",
|
|
910
|
+
"notes": "Test coverage vs target"},
|
|
911
|
+
]
|
|
912
|
+
|
|
913
|
+
if test_type == "web":
|
|
914
|
+
base_metrics.extend([
|
|
915
|
+
{"name": "Page Load Time", "value": "—", "benchmark": "< 3s", "status": "Pending", "notes": "Run with --performance flag"},
|
|
916
|
+
{"name": "Lighthouse Score", "value": "—", "benchmark": "> 90", "status": "Pending", "notes": "Run with --performance flag"},
|
|
917
|
+
])
|
|
918
|
+
elif test_type == "api":
|
|
919
|
+
base_metrics.extend([
|
|
920
|
+
{"name": "Avg Response Time", "value": "—", "benchmark": "< 500ms", "status": "Pending", "notes": "API response time"},
|
|
921
|
+
{"name": "Throughput", "value": "—", "benchmark": "> 100 req/s", "status": "Pending", "notes": "Requests per second"},
|
|
922
|
+
])
|
|
923
|
+
|
|
924
|
+
return base_metrics
|
|
925
|
+
|
|
926
|
+
# ═══════════════════════════════════════════════════════════════
|
|
927
|
+
# SHEET 7: Charts & Graphs
|
|
928
|
+
# ═══════════════════════════════════════════════════════════════
|
|
929
|
+
|
|
930
|
+
def _create_charts(self, ws, results: Dict, test_type: str):
|
|
931
|
+
"""Create charts and graphs sheet"""
|
|
932
|
+
from openpyxl.chart import BarChart, PieChart, Reference
|
|
933
|
+
from openpyxl.chart.label import DataLabelList
|
|
934
|
+
from openpyxl.chart.series import DataPoint
|
|
935
|
+
from openpyxl.styles import Font, PatternFill, Alignment
|
|
936
|
+
|
|
937
|
+
ws["A1"] = "CHARTS & VISUAL ANALYTICS"
|
|
938
|
+
ws["A1"].font = Font(name="Calibri", size=16, bold=True, color="FFFFFF")
|
|
939
|
+
ws["A1"].fill = PatternFill(start_color="1B4F72", end_color="1B4F72", fill_type="solid")
|
|
940
|
+
|
|
941
|
+
# ─── Pie Chart: Pass/Fail Distribution ───────────────────
|
|
942
|
+
ws["A3"] = "Status"
|
|
943
|
+
ws["B3"] = "Count"
|
|
944
|
+
ws["A4"] = "Passed"
|
|
945
|
+
ws["B4"] = results.get("passed", 0)
|
|
946
|
+
ws["A5"] = "Failed"
|
|
947
|
+
ws["B5"] = results.get("failed", 0)
|
|
948
|
+
ws["A6"] = "Skipped"
|
|
949
|
+
ws["B6"] = results.get("skipped", 0)
|
|
950
|
+
|
|
951
|
+
pie = PieChart()
|
|
952
|
+
pie.title = "Test Results Distribution"
|
|
953
|
+
pie.style = 10
|
|
954
|
+
data = Reference(ws, min_col=2, min_row=3, max_row=6)
|
|
955
|
+
cats = Reference(ws, min_col=1, min_row=4, max_row=6)
|
|
956
|
+
pie.add_data(data, titles_from_data=True)
|
|
957
|
+
pie.set_categories(cats)
|
|
958
|
+
pie.width = 15
|
|
959
|
+
pie.height = 10
|
|
960
|
+
|
|
961
|
+
# Color the slices
|
|
962
|
+
from openpyxl.chart.series import DataPoint
|
|
963
|
+
from openpyxl.drawing.fill import PatternFillProperties, ColorChoice
|
|
964
|
+
passed_pt = DataPoint(idx=0)
|
|
965
|
+
passed_pt.graphicalProperties.solidFill = "27AE60"
|
|
966
|
+
failed_pt = DataPoint(idx=1)
|
|
967
|
+
failed_pt.graphicalProperties.solidFill = "E74C3C"
|
|
968
|
+
skip_pt = DataPoint(idx=2)
|
|
969
|
+
skip_pt.graphicalProperties.solidFill = "F39C12"
|
|
970
|
+
pie.series[0].data_points = [passed_pt, failed_pt, skip_pt]
|
|
971
|
+
|
|
972
|
+
ws.add_chart(pie, "D3")
|
|
973
|
+
|
|
974
|
+
# ─── Bar Chart: Coverage by Phase ────────────────────────
|
|
975
|
+
ws["A9"] = "Phase"
|
|
976
|
+
ws["B9"] = "Coverage %"
|
|
977
|
+
categories = results.get("categories", {})
|
|
978
|
+
phase_names = ["Smoke", "Functional", "Integration", "Performance", "Advanced"]
|
|
979
|
+
phase_keys = ["smoke", "functional", "integration", "performance", "advanced"]
|
|
980
|
+
|
|
981
|
+
row = 10
|
|
982
|
+
for i, (name, key) in enumerate(zip(phase_names, phase_keys)):
|
|
983
|
+
ws.cell(row=row, column=1, value=name)
|
|
984
|
+
cat_data = categories.get(key, {})
|
|
985
|
+
coverage = cat_data.get("coverage", 0) if isinstance(cat_data, dict) else 0
|
|
986
|
+
ws.cell(row=row, column=2, value=coverage)
|
|
987
|
+
row += 1
|
|
988
|
+
|
|
989
|
+
bar = BarChart()
|
|
990
|
+
bar.type = "col"
|
|
991
|
+
bar.title = "Coverage by Phase (Target: 90%)"
|
|
992
|
+
bar.style = 10
|
|
993
|
+
bar.y_axis.title = "Coverage %"
|
|
994
|
+
bar.x_axis.title = "Test Phase"
|
|
995
|
+
|
|
996
|
+
data = Reference(ws, min_col=2, min_row=9, max_row=14)
|
|
997
|
+
cats = Reference(ws, min_col=1, min_row=10, max_row=14)
|
|
998
|
+
bar.add_data(data, titles_from_data=True)
|
|
999
|
+
bar.set_categories(cats)
|
|
1000
|
+
bar.width = 15
|
|
1001
|
+
bar.height = 10
|
|
1002
|
+
|
|
1003
|
+
ws.add_chart(bar, "D19")
|
|
1004
|
+
|
|
1005
|
+
# ═══════════════════════════════════════════════════════════════
|
|
1006
|
+
# SHEET 8: Recommendations
|
|
1007
|
+
# ═══════════════════════════════════════════════════════════════
|
|
1008
|
+
|
|
1009
|
+
def _create_recommendations(self, ws, results: Dict, test_type: str):
|
|
1010
|
+
"""Create recommendations sheet"""
|
|
1011
|
+
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
|
1012
|
+
|
|
1013
|
+
thin_border = Border(
|
|
1014
|
+
left=Side(style='thin'), right=Side(style='thin'),
|
|
1015
|
+
top=Side(style='thin'), bottom=Side(style='thin')
|
|
1016
|
+
)
|
|
1017
|
+
|
|
1018
|
+
ws.merge_cells("A1:F1")
|
|
1019
|
+
ws["A1"] = "RECOMMENDATIONS & ACTION ITEMS"
|
|
1020
|
+
ws["A1"].font = Font(name="Calibri", size=16, bold=True, color="FFFFFF")
|
|
1021
|
+
ws["A1"].fill = PatternFill(start_color="1B4F72", end_color="1B4F72", fill_type="solid")
|
|
1022
|
+
ws["A1"].alignment = Alignment(horizontal="center", vertical="center")
|
|
1023
|
+
ws.row_dimensions[1].height = 35
|
|
1024
|
+
|
|
1025
|
+
headers = ["#", "Category", "Recommendation", "Priority", "Effort", "Impact"]
|
|
1026
|
+
header_fill = PatternFill(start_color="2E86C1", end_color="2E86C1", fill_type="solid")
|
|
1027
|
+
|
|
1028
|
+
for col, h in enumerate(headers, 1):
|
|
1029
|
+
cell = ws.cell(row=3, column=col)
|
|
1030
|
+
cell.value = h
|
|
1031
|
+
cell.font = Font(bold=True, color="FFFFFF")
|
|
1032
|
+
cell.fill = header_fill
|
|
1033
|
+
cell.border = thin_border
|
|
1034
|
+
cell.alignment = Alignment(horizontal="center")
|
|
1035
|
+
|
|
1036
|
+
# Generate recommendations based on results
|
|
1037
|
+
recommendations = self._generate_recommendations(results, test_type)
|
|
1038
|
+
|
|
1039
|
+
row = 4
|
|
1040
|
+
for i, rec in enumerate(recommendations, 1):
|
|
1041
|
+
ws.cell(row=row, column=1, value=i).border = thin_border
|
|
1042
|
+
ws.cell(row=row, column=2, value=rec["category"]).border = thin_border
|
|
1043
|
+
ws.cell(row=row, column=3, value=rec["recommendation"]).border = thin_border
|
|
1044
|
+
ws.cell(row=row, column=3).alignment = Alignment(wrap_text=True)
|
|
1045
|
+
ws.cell(row=row, column=4, value=rec["priority"]).border = thin_border
|
|
1046
|
+
ws.cell(row=row, column=4).alignment = Alignment(horizontal="center")
|
|
1047
|
+
ws.cell(row=row, column=5, value=rec["effort"]).border = thin_border
|
|
1048
|
+
ws.cell(row=row, column=5).alignment = Alignment(horizontal="center")
|
|
1049
|
+
ws.cell(row=row, column=6, value=rec["impact"]).border = thin_border
|
|
1050
|
+
ws.cell(row=row, column=6).alignment = Alignment(horizontal="center")
|
|
1051
|
+
|
|
1052
|
+
# Color code priority
|
|
1053
|
+
if rec["priority"] == "Critical":
|
|
1054
|
+
ws.cell(row=row, column=4).fill = PatternFill(start_color="FADBD8", end_color="FADBD8", fill_type="solid")
|
|
1055
|
+
elif rec["priority"] == "High":
|
|
1056
|
+
ws.cell(row=row, column=4).fill = PatternFill(start_color="FEF9E7", end_color="FEF9E7", fill_type="solid")
|
|
1057
|
+
|
|
1058
|
+
row += 1
|
|
1059
|
+
|
|
1060
|
+
def _generate_recommendations(self, results: Dict, test_type: str) -> List[Dict]:
|
|
1061
|
+
"""Generate recommendations based on test results"""
|
|
1062
|
+
recs = []
|
|
1063
|
+
coverage = results.get("coverage", 0)
|
|
1064
|
+
failed = results.get("failed", 0)
|
|
1065
|
+
total = results.get("total", 1)
|
|
1066
|
+
|
|
1067
|
+
if coverage < 60:
|
|
1068
|
+
recs.append({
|
|
1069
|
+
"category": "Coverage",
|
|
1070
|
+
"recommendation": f"Coverage is at {coverage}%. Add more test cases to reach at least 80% coverage before release.",
|
|
1071
|
+
"priority": "Critical",
|
|
1072
|
+
"effort": "High",
|
|
1073
|
+
"impact": "High",
|
|
1074
|
+
})
|
|
1075
|
+
|
|
1076
|
+
if failed > 0:
|
|
1077
|
+
recs.append({
|
|
1078
|
+
"category": "Defects",
|
|
1079
|
+
"recommendation": f"Fix {failed} failing test(s) before release. Prioritize critical and high severity defects.",
|
|
1080
|
+
"priority": "Critical",
|
|
1081
|
+
"effort": "Medium",
|
|
1082
|
+
"impact": "High",
|
|
1083
|
+
})
|
|
1084
|
+
|
|
1085
|
+
if test_type == "web":
|
|
1086
|
+
recs.append({
|
|
1087
|
+
"category": "Accessibility",
|
|
1088
|
+
"recommendation": "Run WCAG 2.1 AA compliance audit. Ensure all interactive elements have ARIA labels.",
|
|
1089
|
+
"priority": "High",
|
|
1090
|
+
"effort": "Medium",
|
|
1091
|
+
"impact": "High",
|
|
1092
|
+
})
|
|
1093
|
+
recs.append({
|
|
1094
|
+
"category": "Performance",
|
|
1095
|
+
"recommendation": "Run Lighthouse audit for Core Web Vitals. Target LCP < 2.5s, FID < 100ms, CLS < 0.1.",
|
|
1096
|
+
"priority": "Medium",
|
|
1097
|
+
"effort": "Low",
|
|
1098
|
+
"impact": "Medium",
|
|
1099
|
+
})
|
|
1100
|
+
|
|
1101
|
+
if test_type == "api":
|
|
1102
|
+
recs.append({
|
|
1103
|
+
"category": "Security",
|
|
1104
|
+
"recommendation": "Implement rate limiting on all API endpoints. Add request validation middleware.",
|
|
1105
|
+
"priority": "High",
|
|
1106
|
+
"effort": "Medium",
|
|
1107
|
+
"impact": "High",
|
|
1108
|
+
})
|
|
1109
|
+
recs.append({
|
|
1110
|
+
"category": "Documentation",
|
|
1111
|
+
"recommendation": "Ensure OpenAPI spec is up-to-date. All endpoints should have request/response examples.",
|
|
1112
|
+
"priority": "Medium",
|
|
1113
|
+
"effort": "Low",
|
|
1114
|
+
"impact": "Medium",
|
|
1115
|
+
})
|
|
1116
|
+
|
|
1117
|
+
if test_type == "mobile":
|
|
1118
|
+
recs.append({
|
|
1119
|
+
"category": "UX",
|
|
1120
|
+
"recommendation": "Test on physical devices (not just emulators). Verify on low-end devices with limited RAM.",
|
|
1121
|
+
"priority": "High",
|
|
1122
|
+
"effort": "High",
|
|
1123
|
+
"impact": "High",
|
|
1124
|
+
})
|
|
1125
|
+
recs.append({
|
|
1126
|
+
"category": "Performance",
|
|
1127
|
+
"recommendation": "Monitor memory leaks during extended usage. Check battery consumption impact.",
|
|
1128
|
+
"priority": "Medium",
|
|
1129
|
+
"effort": "Medium",
|
|
1130
|
+
"impact": "Medium",
|
|
1131
|
+
})
|
|
1132
|
+
|
|
1133
|
+
if test_type == "embedded":
|
|
1134
|
+
recs.append({
|
|
1135
|
+
"category": "Reliability",
|
|
1136
|
+
"recommendation": "Run 72-hour soak test. Verify watchdog timer recovery and power cycling.",
|
|
1137
|
+
"priority": "Critical",
|
|
1138
|
+
"effort": "High",
|
|
1139
|
+
"impact": "Critical",
|
|
1140
|
+
})
|
|
1141
|
+
recs.append({
|
|
1142
|
+
"category": "Security",
|
|
1143
|
+
"recommendation": "Verify firmware encryption and secure boot. Test OTA update rollback mechanism.",
|
|
1144
|
+
"priority": "High",
|
|
1145
|
+
"effort": "High",
|
|
1146
|
+
"impact": "Critical",
|
|
1147
|
+
})
|
|
1148
|
+
|
|
1149
|
+
# Always add general recommendations
|
|
1150
|
+
recs.append({
|
|
1151
|
+
"category": "CI/CD",
|
|
1152
|
+
"recommendation": "Integrate nextOG tests into CI/CD pipeline. Set coverage gate at 80% minimum.",
|
|
1153
|
+
"priority": "High",
|
|
1154
|
+
"effort": "Medium",
|
|
1155
|
+
"impact": "High",
|
|
1156
|
+
})
|
|
1157
|
+
recs.append({
|
|
1158
|
+
"category": "Regression",
|
|
1159
|
+
"recommendation": "Add failed tests to regression suite. Schedule weekly full regression runs.",
|
|
1160
|
+
"priority": "Medium",
|
|
1161
|
+
"effort": "Low",
|
|
1162
|
+
"impact": "Medium",
|
|
1163
|
+
})
|
|
1164
|
+
recs.append({
|
|
1165
|
+
"category": "Monitoring",
|
|
1166
|
+
"recommendation": "Set up production monitoring with alerts matching test coverage areas.",
|
|
1167
|
+
"priority": "Medium",
|
|
1168
|
+
"effort": "Medium",
|
|
1169
|
+
"impact": "Medium",
|
|
1170
|
+
})
|
|
1171
|
+
|
|
1172
|
+
return recs
|
|
1173
|
+
|
|
1174
|
+
# ═══════════════════════════════════════════════════════════════
|
|
1175
|
+
# SHEET 9: Test Environment
|
|
1176
|
+
# ═══════════════════════════════════════════════════════════════
|
|
1177
|
+
|
|
1178
|
+
def _create_test_environment(self, ws, test_type: str):
|
|
1179
|
+
"""Create test environment documentation sheet"""
|
|
1180
|
+
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
|
1181
|
+
import platform as plat
|
|
1182
|
+
|
|
1183
|
+
try:
|
|
1184
|
+
import psutil
|
|
1185
|
+
except ImportError:
|
|
1186
|
+
psutil = None
|
|
1187
|
+
|
|
1188
|
+
thin_border = Border(
|
|
1189
|
+
left=Side(style='thin'), right=Side(style='thin'),
|
|
1190
|
+
top=Side(style='thin'), bottom=Side(style='thin')
|
|
1191
|
+
)
|
|
1192
|
+
|
|
1193
|
+
ws.merge_cells("A1:D1")
|
|
1194
|
+
ws["A1"] = "TEST ENVIRONMENT DETAILS"
|
|
1195
|
+
ws["A1"].font = Font(name="Calibri", size=16, bold=True, color="FFFFFF")
|
|
1196
|
+
ws["A1"].fill = PatternFill(start_color="1B4F72", end_color="1B4F72", fill_type="solid")
|
|
1197
|
+
ws["A1"].alignment = Alignment(horizontal="center", vertical="center")
|
|
1198
|
+
ws.row_dimensions[1].height = 35
|
|
1199
|
+
|
|
1200
|
+
headers = ["Property", "Value", "Category", "Notes"]
|
|
1201
|
+
header_fill = PatternFill(start_color="2E86C1", end_color="2E86C1", fill_type="solid")
|
|
1202
|
+
|
|
1203
|
+
for col, h in enumerate(headers, 1):
|
|
1204
|
+
cell = ws.cell(row=3, column=col)
|
|
1205
|
+
cell.value = h
|
|
1206
|
+
cell.font = Font(bold=True, color="FFFFFF")
|
|
1207
|
+
cell.fill = header_fill
|
|
1208
|
+
cell.border = thin_border
|
|
1209
|
+
|
|
1210
|
+
env_data = [
|
|
1211
|
+
("Tool", "nextOG CLI v1.0.0", "Software", "QA Automation Tool"),
|
|
1212
|
+
("Python Version", plat.python_version(), "Runtime", "Test execution engine"),
|
|
1213
|
+
("OS", f"{plat.system()} {plat.release()}", "Platform", "Test host OS"),
|
|
1214
|
+
("Machine", plat.machine(), "Hardware", "CPU architecture"),
|
|
1215
|
+
("Processor", plat.processor() or "N/A", "Hardware", "CPU details"),
|
|
1216
|
+
("Total RAM", f"{psutil.virtual_memory().total / (1024**3):.1f} GB" if psutil else "N/A", "Hardware", "System memory"),
|
|
1217
|
+
("Available RAM", f"{psutil.virtual_memory().available / (1024**3):.1f} GB" if psutil else "N/A", "Hardware", "Free memory"),
|
|
1218
|
+
("CPU Cores", str(psutil.cpu_count()) if psutil else str(os.cpu_count() or "N/A"), "Hardware", "Logical processors"),
|
|
1219
|
+
("Test Type", test_type.upper(), "Configuration", "Test suite type"),
|
|
1220
|
+
("Test Date", datetime.now().strftime("%Y-%m-%d"), "Schedule", "Execution date"),
|
|
1221
|
+
("Test Time", datetime.now().strftime("%H:%M:%S"), "Schedule", "Execution time"),
|
|
1222
|
+
("Timezone", datetime.now().astimezone().tzname(), "Schedule", "Local timezone"),
|
|
1223
|
+
("Privacy Mode", "LOCAL ONLY - No External Data Transfer", "Security", "All data stays on device"),
|
|
1224
|
+
("Data Encryption", "AES-256 (Fernet)", "Security", "Local data encryption"),
|
|
1225
|
+
("Report Location", f"~/.nextog/reports/", "Storage", "Local report storage"),
|
|
1226
|
+
]
|
|
1227
|
+
|
|
1228
|
+
row = 4
|
|
1229
|
+
for prop, value, category, notes in env_data:
|
|
1230
|
+
ws.cell(row=row, column=1, value=prop).border = thin_border
|
|
1231
|
+
ws.cell(row=row, column=2, value=value).border = thin_border
|
|
1232
|
+
ws.cell(row=row, column=3, value=category).border = thin_border
|
|
1233
|
+
ws.cell(row=row, column=4, value=notes).border = thin_border
|
|
1234
|
+
row += 1
|
|
1235
|
+
|
|
1236
|
+
# ═══════════════════════════════════════════════════════════════
|
|
1237
|
+
# SHEET 10: Sign-Off Sheet
|
|
1238
|
+
# ═══════════════════════════════════════════════════════════════
|
|
1239
|
+
|
|
1240
|
+
def _create_signoff_sheet(self, ws, results: Dict, test_type: str, timestamp: str):
|
|
1241
|
+
"""Create sign-off/approval sheet"""
|
|
1242
|
+
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
|
1243
|
+
|
|
1244
|
+
thin_border = Border(
|
|
1245
|
+
left=Side(style='thin'), right=Side(style='thin'),
|
|
1246
|
+
top=Side(style='thin'), bottom=Side(style='thin')
|
|
1247
|
+
)
|
|
1248
|
+
|
|
1249
|
+
ws.merge_cells("A1:F1")
|
|
1250
|
+
ws["A1"] = "TEST SIGN-OFF & APPROVAL"
|
|
1251
|
+
ws["A1"].font = Font(name="Calibri", size=16, bold=True, color="FFFFFF")
|
|
1252
|
+
ws["A1"].fill = PatternFill(start_color="1B4F72", end_color="1B4F72", fill_type="solid")
|
|
1253
|
+
ws["A1"].alignment = Alignment(horizontal="center", vertical="center")
|
|
1254
|
+
ws.row_dimensions[1].height = 35
|
|
1255
|
+
|
|
1256
|
+
# Report info
|
|
1257
|
+
ws["A3"] = "Report ID:"
|
|
1258
|
+
ws["A3"].font = Font(bold=True)
|
|
1259
|
+
ws["B3"] = f"nextOG-{test_type.upper()}-{timestamp}"
|
|
1260
|
+
|
|
1261
|
+
ws["A4"] = "Generated By:"
|
|
1262
|
+
ws["A4"].font = Font(bold=True)
|
|
1263
|
+
ws["B4"] = "nextOG CLI v1.0.0 (Automated)"
|
|
1264
|
+
|
|
1265
|
+
ws["A5"] = "Test Type:"
|
|
1266
|
+
ws["A5"].font = Font(bold=True)
|
|
1267
|
+
ws["B5"] = test_type.upper()
|
|
1268
|
+
|
|
1269
|
+
ws["A6"] = "Coverage:"
|
|
1270
|
+
ws["A6"].font = Font(bold=True)
|
|
1271
|
+
ws["B6"] = f"{results.get('coverage', 0)}%"
|
|
1272
|
+
|
|
1273
|
+
ws["A7"] = "Status:"
|
|
1274
|
+
ws["A7"].font = Font(bold=True)
|
|
1275
|
+
coverage = results.get("coverage", 0)
|
|
1276
|
+
failed = results.get("failed", 0)
|
|
1277
|
+
if coverage >= 80 and failed == 0:
|
|
1278
|
+
ws["B7"] = "✅ PASS — Recommended for Release"
|
|
1279
|
+
elif coverage >= 60:
|
|
1280
|
+
ws["B7"] = "⚠️ CONDITIONAL — Requires Review"
|
|
1281
|
+
else:
|
|
1282
|
+
ws["B7"] = "❌ FAIL — Not Recommended for Release"
|
|
1283
|
+
|
|
1284
|
+
# Approval section
|
|
1285
|
+
ws.merge_cells("A9:F9")
|
|
1286
|
+
ws["A9"] = "APPROVAL SIGNATURES"
|
|
1287
|
+
ws["A9"].font = Font(size=14, bold=True, color="FFFFFF")
|
|
1288
|
+
ws["A9"].fill = PatternFill(start_color="2E86C1", end_color="2E86C1", fill_type="solid")
|
|
1289
|
+
ws["A9"].alignment = Alignment(horizontal="center")
|
|
1290
|
+
|
|
1291
|
+
sign_headers = ["Role", "Name", "Signature", "Date", "Comments", "Approved"]
|
|
1292
|
+
for col, h in enumerate(sign_headers, 1):
|
|
1293
|
+
cell = ws.cell(row=10, column=col)
|
|
1294
|
+
cell.value = h
|
|
1295
|
+
cell.font = Font(bold=True, color="FFFFFF")
|
|
1296
|
+
cell.fill = PatternFill(start_color="34495E", end_color="34495E", fill_type="solid")
|
|
1297
|
+
cell.border = thin_border
|
|
1298
|
+
cell.alignment = Alignment(horizontal="center")
|
|
1299
|
+
|
|
1300
|
+
roles = ["QA Lead", "Dev Lead", "Product Manager", "Release Manager"]
|
|
1301
|
+
row = 11
|
|
1302
|
+
for role in roles:
|
|
1303
|
+
ws.cell(row=row, column=1, value=role).border = thin_border
|
|
1304
|
+
ws.cell(row=row, column=2).border = thin_border
|
|
1305
|
+
ws.cell(row=row, column=3).border = thin_border
|
|
1306
|
+
ws.cell(row=row, column=4).border = thin_border
|
|
1307
|
+
ws.cell(row=row, column=5).border = thin_border
|
|
1308
|
+
ws.cell(row=row, column=6).border = thin_border
|
|
1309
|
+
ws.row_dimensions[row].height = 30
|
|
1310
|
+
row += 1
|
|
1311
|
+
|
|
1312
|
+
# Footer
|
|
1313
|
+
row += 2
|
|
1314
|
+
ws.merge_cells(f"A{row}:F{row}")
|
|
1315
|
+
ws[f"A{row}"] = "This report was generated automatically by nextOG CLI. All data is stored locally with AES-256 encryption. No external data transfer occurred."
|
|
1316
|
+
ws[f"A{row}"].font = Font(italic=True, size=9, color="666666")
|
|
1317
|
+
ws[f"A{row}"].alignment = Alignment(horizontal="center", wrap_text=True)
|
|
1318
|
+
|
|
1319
|
+
# ═══════════════════════════════════════════════════════════════
|
|
1320
|
+
# UTILITY METHODS
|
|
1321
|
+
# ═══════════════════════════════════════════════════════════════
|
|
1322
|
+
|
|
1323
|
+
def _detect_test_type(self, results: Dict) -> str:
|
|
1324
|
+
"""Auto-detect test type from results"""
|
|
1325
|
+
engine = results.get("engine", "")
|
|
1326
|
+
if engine:
|
|
1327
|
+
return engine
|
|
1328
|
+
|
|
1329
|
+
# Try to detect from structure
|
|
1330
|
+
if "url" in results:
|
|
1331
|
+
return "web"
|
|
1332
|
+
elif "app" in results or "platform" in results:
|
|
1333
|
+
return "mobile"
|
|
1334
|
+
elif "base_url" in results or "spec" in results:
|
|
1335
|
+
return "api"
|
|
1336
|
+
elif "target" in results or "protocol" in results:
|
|
1337
|
+
return "embedded"
|
|
1338
|
+
|
|
1339
|
+
return "full"
|
|
1340
|
+
|
|
1341
|
+
def _auto_fit_columns(self, ws):
|
|
1342
|
+
"""Auto-fit column widths"""
|
|
1343
|
+
from openpyxl.utils import get_column_letter
|
|
1344
|
+
|
|
1345
|
+
for column in ws.columns:
|
|
1346
|
+
max_length = 0
|
|
1347
|
+
column_letter = get_column_letter(column[0].column)
|
|
1348
|
+
|
|
1349
|
+
for cell in column:
|
|
1350
|
+
try:
|
|
1351
|
+
if cell.value:
|
|
1352
|
+
cell_len = len(str(cell.value))
|
|
1353
|
+
if cell_len > max_length:
|
|
1354
|
+
max_length = cell_len
|
|
1355
|
+
except Exception:
|
|
1356
|
+
pass
|
|
1357
|
+
|
|
1358
|
+
adjusted_width = min(max_length + 4, 50)
|
|
1359
|
+
ws.column_dimensions[column_letter].width = max(adjusted_width, 12)
|