test-reporting 1.2.1__tar.gz → 1.2.2__tar.gz
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.
- {test_reporting-1.2.1 → test_reporting-1.2.2}/PKG-INFO +1 -1
- {test_reporting-1.2.1 → test_reporting-1.2.2}/reporting/cli.py +4 -2
- {test_reporting-1.2.1 → test_reporting-1.2.2}/reporting/dashboard_generator.py +84 -6
- {test_reporting-1.2.1 → test_reporting-1.2.2}/reporting/dashboard_generator_v2.py +84 -6
- {test_reporting-1.2.1 → test_reporting-1.2.2}/reporting/history_dashboard.py +2 -2
- {test_reporting-1.2.1 → test_reporting-1.2.2}/reporting/overview_dashboard.py +89 -11
- {test_reporting-1.2.1 → test_reporting-1.2.2}/reporting/projects_dashboard.py +77 -1
- {test_reporting-1.2.1 → test_reporting-1.2.2}/reporting/storage.py +166 -78
- {test_reporting-1.2.1 → test_reporting-1.2.2}/setup.py +1 -1
- {test_reporting-1.2.1 → test_reporting-1.2.2}/test_reporting.egg-info/PKG-INFO +1 -1
- {test_reporting-1.2.1 → test_reporting-1.2.2}/LICENSE +0 -0
- {test_reporting-1.2.1 → test_reporting-1.2.2}/README.md +0 -0
- {test_reporting-1.2.1 → test_reporting-1.2.2}/reporting/__init__.py +0 -0
- {test_reporting-1.2.1 → test_reporting-1.2.2}/reporting/classifier.py +0 -0
- {test_reporting-1.2.1 → test_reporting-1.2.2}/reporting/config.py +0 -0
- {test_reporting-1.2.1 → test_reporting-1.2.2}/reporting/modern_design_system.py +0 -0
- {test_reporting-1.2.1 → test_reporting-1.2.2}/reporting/plugin.py +0 -0
- {test_reporting-1.2.1 → test_reporting-1.2.2}/setup.cfg +0 -0
- {test_reporting-1.2.1 → test_reporting-1.2.2}/test_reporting.egg-info/SOURCES.txt +0 -0
- {test_reporting-1.2.1 → test_reporting-1.2.2}/test_reporting.egg-info/dependency_links.txt +0 -0
- {test_reporting-1.2.1 → test_reporting-1.2.2}/test_reporting.egg-info/entry_points.txt +0 -0
- {test_reporting-1.2.1 → test_reporting-1.2.2}/test_reporting.egg-info/requires.txt +0 -0
- {test_reporting-1.2.1 → test_reporting-1.2.2}/test_reporting.egg-info/top_level.txt +0 -0
|
@@ -97,9 +97,11 @@ def show_stats():
|
|
|
97
97
|
def show_suites():
|
|
98
98
|
"""Show statistics for all test suites."""
|
|
99
99
|
from .storage import TestResultStorage
|
|
100
|
+
from .config import ReportingConfig
|
|
100
101
|
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
config = ReportingConfig()
|
|
103
|
+
storage = TestResultStorage(config=config)
|
|
104
|
+
suites = storage.get_suite_statistics(project_name=config.PROJECT_NAME)
|
|
103
105
|
|
|
104
106
|
if not suites:
|
|
105
107
|
print("[ERROR] No suite data found. Run test suites first.")
|
|
@@ -46,11 +46,11 @@ class DashboardGenerator:
|
|
|
46
46
|
|
|
47
47
|
def _get_trend_data(self) -> List[Dict[str, Any]]:
|
|
48
48
|
"""Get trend data from database."""
|
|
49
|
-
return self.storage.get_trend_data(days=30)
|
|
49
|
+
return self.storage.get_trend_data(days=30, project_name=self.config.PROJECT_NAME)
|
|
50
50
|
|
|
51
51
|
def _get_failure_summary(self) -> Dict[str, Any]:
|
|
52
52
|
"""Get failure summary from database."""
|
|
53
|
-
return self.storage.get_failure_summary(days=30)
|
|
53
|
+
return self.storage.get_failure_summary(days=30, project_name=self.config.PROJECT_NAME)
|
|
54
54
|
|
|
55
55
|
def _generate_html(self, latest_run: Dict, trend_data: List, failure_summary: Dict) -> str:
|
|
56
56
|
"""Generate complete HTML dashboard that loads data dynamically."""
|
|
@@ -177,6 +177,47 @@ class DashboardGenerator:
|
|
|
177
177
|
.metric-card.warning .value {{ color: #f59e0b; }}
|
|
178
178
|
.metric-card.danger .value {{ color: #ef4444; }}
|
|
179
179
|
|
|
180
|
+
.metric-card .label {{
|
|
181
|
+
position: relative;
|
|
182
|
+
cursor: help;
|
|
183
|
+
}}
|
|
184
|
+
|
|
185
|
+
.tooltip {{
|
|
186
|
+
position: absolute;
|
|
187
|
+
background: #1f2937;
|
|
188
|
+
color: #f9fafb;
|
|
189
|
+
padding: 12px 16px;
|
|
190
|
+
border-radius: 8px;
|
|
191
|
+
font-size: 13px;
|
|
192
|
+
line-height: 1.5;
|
|
193
|
+
max-width: 300px;
|
|
194
|
+
z-index: 1000;
|
|
195
|
+
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.4);
|
|
196
|
+
pointer-events: none;
|
|
197
|
+
opacity: 0;
|
|
198
|
+
transition: opacity 0.2s ease;
|
|
199
|
+
white-space: normal;
|
|
200
|
+
text-transform: none;
|
|
201
|
+
letter-spacing: normal;
|
|
202
|
+
font-weight: 400;
|
|
203
|
+
}}
|
|
204
|
+
|
|
205
|
+
.tooltip.show {{
|
|
206
|
+
opacity: 1;
|
|
207
|
+
}}
|
|
208
|
+
|
|
209
|
+
.tooltip::before {{
|
|
210
|
+
content: '';
|
|
211
|
+
position: absolute;
|
|
212
|
+
top: -6px;
|
|
213
|
+
left: 20px;
|
|
214
|
+
width: 0;
|
|
215
|
+
height: 0;
|
|
216
|
+
border-left: 6px solid transparent;
|
|
217
|
+
border-right: 6px solid transparent;
|
|
218
|
+
border-bottom: 6px solid #1f2937;
|
|
219
|
+
}}
|
|
220
|
+
|
|
180
221
|
.charts-grid {{
|
|
181
222
|
display: grid;
|
|
182
223
|
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
|
@@ -306,25 +347,25 @@ class DashboardGenerator:
|
|
|
306
347
|
|
|
307
348
|
<div class="metrics-grid">
|
|
308
349
|
<div class="metric-card ${{metrics.health_score >= 90 ? 'success' : metrics.health_score >= 70 ? 'warning' : 'danger'}}">
|
|
309
|
-
<div class="label">Health Score</div>
|
|
350
|
+
<div class="label" data-tooltip="Based on Functional Pass Rate with penalties for soft failures (timeouts, flaky tests, environment issues). Calculation: Functional Pass Rate - (soft failures × 2, max -10 points)">Health Score</div>
|
|
310
351
|
<div class="value">${{metrics.health_score || 0}}</div>
|
|
311
352
|
<div class="trend">Overall system health</div>
|
|
312
353
|
</div>
|
|
313
354
|
|
|
314
355
|
<div class="metric-card success">
|
|
315
|
-
<div class="label">Pass Rate</div>
|
|
356
|
+
<div class="label" data-tooltip="Percentage of all tests that passed. Calculation: (Passed Tests / Total Tests) × 100. Includes all test results regardless of failure type.">Pass Rate</div>
|
|
316
357
|
<div class="value">${{metrics.pass_rate || 0}}%</div>
|
|
317
358
|
<div class="trend">${{metrics.passed || 0}} passed, ${{metrics.failed || 0}} failed</div>
|
|
318
359
|
</div>
|
|
319
360
|
|
|
320
361
|
<div class="metric-card">
|
|
321
|
-
<div class="label">Functional Pass Rate</div>
|
|
362
|
+
<div class="label" data-tooltip="Pass rate excluding soft failures (timeouts, flaky tests, environment issues). Calculation: (Total - Skipped - Hard Failures) / (Total - Skipped) × 100. Focuses on actual functional bugs.">Functional Pass Rate</div>
|
|
322
363
|
<div class="value">${{metrics.functional_pass_rate || 0}}%</div>
|
|
323
364
|
<div class="trend">Excluding timeouts</div>
|
|
324
365
|
</div>
|
|
325
366
|
|
|
326
367
|
<div class="metric-card">
|
|
327
|
-
<div class="label">Total Tests</div>
|
|
368
|
+
<div class="label" data-tooltip="Total number of tests executed in this run. ✓ = Passed, ✗ = Failed, ⊘ = Skipped">Total Tests</div>
|
|
328
369
|
<div class="value">${{metrics.total_tests || 0}}</div>
|
|
329
370
|
<div class="trend">${{metrics.passed || 0}}✓ ${{metrics.failed || 0}}✗ ${{metrics.skipped || 0}}⊘</div>
|
|
330
371
|
</div>
|
|
@@ -442,8 +483,45 @@ class DashboardGenerator:
|
|
|
442
483
|
document.getElementById('failuresContent').innerHTML = html;
|
|
443
484
|
}}
|
|
444
485
|
|
|
486
|
+
// Tooltip functionality
|
|
487
|
+
function initTooltips() {{
|
|
488
|
+
const labels = document.querySelectorAll('[data-tooltip]');
|
|
489
|
+
|
|
490
|
+
labels.forEach(label => {{
|
|
491
|
+
label.addEventListener('mouseenter', function(e) {{
|
|
492
|
+
const tooltipText = this.getAttribute('data-tooltip');
|
|
493
|
+
const tooltip = document.createElement('div');
|
|
494
|
+
tooltip.className = 'tooltip show';
|
|
495
|
+
tooltip.textContent = tooltipText;
|
|
496
|
+
tooltip.id = 'active-tooltip';
|
|
497
|
+
|
|
498
|
+
document.body.appendChild(tooltip);
|
|
499
|
+
|
|
500
|
+
const rect = this.getBoundingClientRect();
|
|
501
|
+
tooltip.style.left = rect.left + 'px';
|
|
502
|
+
tooltip.style.top = (rect.bottom + 10) + 'px';
|
|
503
|
+
|
|
504
|
+
// Adjust if tooltip goes off screen
|
|
505
|
+
const tooltipRect = tooltip.getBoundingClientRect();
|
|
506
|
+
if (tooltipRect.right > window.innerWidth) {{
|
|
507
|
+
tooltip.style.left = (window.innerWidth - tooltipRect.width - 20) + 'px';
|
|
508
|
+
}}
|
|
509
|
+
}});
|
|
510
|
+
|
|
511
|
+
label.addEventListener('mouseleave', function() {{
|
|
512
|
+
const tooltip = document.getElementById('active-tooltip');
|
|
513
|
+
if (tooltip) {{
|
|
514
|
+
tooltip.remove();
|
|
515
|
+
}}
|
|
516
|
+
}});
|
|
517
|
+
}});
|
|
518
|
+
}}
|
|
519
|
+
|
|
445
520
|
// Render dashboard on load
|
|
446
521
|
renderDashboard();
|
|
522
|
+
|
|
523
|
+
// Initialize tooltips after content is rendered
|
|
524
|
+
setTimeout(initTooltips, 100);
|
|
447
525
|
</script>
|
|
448
526
|
</body>
|
|
449
527
|
</html>'''
|
|
@@ -40,7 +40,7 @@ class EnhancedDashboardGenerator:
|
|
|
40
40
|
def _get_latest_run_with_tests(self) -> Dict[str, Any]:
|
|
41
41
|
"""Get latest run data with full test breakdown."""
|
|
42
42
|
# Get latest run
|
|
43
|
-
runs = self.storage.get_latest_runs(limit=1)
|
|
43
|
+
runs = self.storage.get_latest_runs(limit=1, project_name=self.config.PROJECT_NAME)
|
|
44
44
|
if not runs:
|
|
45
45
|
return {}
|
|
46
46
|
|
|
@@ -87,7 +87,7 @@ class EnhancedDashboardGenerator:
|
|
|
87
87
|
|
|
88
88
|
def _get_trend_data(self) -> List[Dict[str, Any]]:
|
|
89
89
|
"""Get trend data from database."""
|
|
90
|
-
return self.storage.get_trend_data(days=30)
|
|
90
|
+
return self.storage.get_trend_data(days=30, project_name=self.config.PROJECT_NAME)
|
|
91
91
|
|
|
92
92
|
def _generate_html(self, latest_run_data: Dict, trend_data: List) -> str:
|
|
93
93
|
"""Generate complete HTML dashboard."""
|
|
@@ -262,6 +262,47 @@ class EnhancedDashboardGenerator:
|
|
|
262
262
|
.metric-card.warning .value {{ color: #d97706; border-left-color: #f59e0b; }}
|
|
263
263
|
.metric-card.danger .value {{ color: #dc2626; border-left-color: #ef4444; }}
|
|
264
264
|
|
|
265
|
+
.metric-card .label {{
|
|
266
|
+
position: relative;
|
|
267
|
+
cursor: help;
|
|
268
|
+
}}
|
|
269
|
+
|
|
270
|
+
.tooltip {{
|
|
271
|
+
position: absolute;
|
|
272
|
+
background: #1f2937;
|
|
273
|
+
color: #f9fafb;
|
|
274
|
+
padding: 12px 16px;
|
|
275
|
+
border-radius: 8px;
|
|
276
|
+
font-size: 13px;
|
|
277
|
+
line-height: 1.5;
|
|
278
|
+
max-width: 300px;
|
|
279
|
+
z-index: 1000;
|
|
280
|
+
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.4);
|
|
281
|
+
pointer-events: none;
|
|
282
|
+
opacity: 0;
|
|
283
|
+
transition: opacity 0.2s ease;
|
|
284
|
+
white-space: normal;
|
|
285
|
+
text-transform: none;
|
|
286
|
+
letter-spacing: normal;
|
|
287
|
+
font-weight: 400;
|
|
288
|
+
}}
|
|
289
|
+
|
|
290
|
+
.tooltip.show {{
|
|
291
|
+
opacity: 1;
|
|
292
|
+
}}
|
|
293
|
+
|
|
294
|
+
.tooltip::before {{
|
|
295
|
+
content: '';
|
|
296
|
+
position: absolute;
|
|
297
|
+
top: -6px;
|
|
298
|
+
left: 20px;
|
|
299
|
+
width: 0;
|
|
300
|
+
height: 0;
|
|
301
|
+
border-left: 6px solid transparent;
|
|
302
|
+
border-right: 6px solid transparent;
|
|
303
|
+
border-bottom: 6px solid #1f2937;
|
|
304
|
+
}}
|
|
305
|
+
|
|
265
306
|
.charts-grid {{
|
|
266
307
|
display: grid;
|
|
267
308
|
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
|
@@ -545,25 +586,25 @@ class EnhancedDashboardGenerator:
|
|
|
545
586
|
|
|
546
587
|
<div class="metrics-grid">
|
|
547
588
|
<div class="metric-card ${{healthScore >= 90 ? 'success' : healthScore >= 70 ? 'warning' : 'danger'}}">
|
|
548
|
-
<div class="label">Health Score</div>
|
|
589
|
+
<div class="label" data-tooltip="Based on Functional Pass Rate with penalties for soft failures (timeouts, flaky tests, environment issues). Calculation: Functional Pass Rate - (soft failures × 2, max -10 points)">Health Score</div>
|
|
549
590
|
<div class="value">${{healthScore}}</div>
|
|
550
591
|
<div class="trend">Overall system health</div>
|
|
551
592
|
</div>
|
|
552
593
|
|
|
553
594
|
<div class="metric-card success">
|
|
554
|
-
<div class="label">Pass Rate</div>
|
|
595
|
+
<div class="label" data-tooltip="Percentage of all tests that passed. Calculation: (Passed Tests / Total Tests) × 100. Includes all test results regardless of failure type.">Pass Rate</div>
|
|
555
596
|
<div class="value">${{passRate.toFixed(1)}}%</div>
|
|
556
597
|
<div class="trend">${{run.passed || 0}} passed, ${{run.failed || 0}} failed</div>
|
|
557
598
|
</div>
|
|
558
599
|
|
|
559
600
|
<div class="metric-card">
|
|
560
|
-
<div class="label">Functional Pass Rate</div>
|
|
601
|
+
<div class="label" data-tooltip="Pass rate excluding soft failures (timeouts, flaky tests, environment issues). Calculation: (Total - Skipped - Hard Failures) / (Total - Skipped) × 100. Focuses on actual functional bugs.">Functional Pass Rate</div>
|
|
561
602
|
<div class="value">${{functionalPassRate.toFixed(1)}}%</div>
|
|
562
603
|
<div class="trend">Excluding timeouts</div>
|
|
563
604
|
</div>
|
|
564
605
|
|
|
565
606
|
<div class="metric-card">
|
|
566
|
-
<div class="label">Total Tests</div>
|
|
607
|
+
<div class="label" data-tooltip="Total number of tests executed in this run. ✓ = Passed, ✗ = Failed, ⊘ = Skipped">Total Tests</div>
|
|
567
608
|
<div class="value">${{run.total_tests || 0}}</div>
|
|
568
609
|
<div class="trend">${{run.passed || 0}}✓ ${{run.failed || 0}}✗ ${{run.skipped || 0}}⊘</div>
|
|
569
610
|
</div>
|
|
@@ -797,8 +838,45 @@ class EnhancedDashboardGenerator:
|
|
|
797
838
|
}}
|
|
798
839
|
}}
|
|
799
840
|
|
|
841
|
+
// Tooltip functionality
|
|
842
|
+
function initTooltips() {{
|
|
843
|
+
const labels = document.querySelectorAll('[data-tooltip]');
|
|
844
|
+
|
|
845
|
+
labels.forEach(label => {{
|
|
846
|
+
label.addEventListener('mouseenter', function(e) {{
|
|
847
|
+
const tooltipText = this.getAttribute('data-tooltip');
|
|
848
|
+
const tooltip = document.createElement('div');
|
|
849
|
+
tooltip.className = 'tooltip show';
|
|
850
|
+
tooltip.textContent = tooltipText;
|
|
851
|
+
tooltip.id = 'active-tooltip';
|
|
852
|
+
|
|
853
|
+
document.body.appendChild(tooltip);
|
|
854
|
+
|
|
855
|
+
const rect = this.getBoundingClientRect();
|
|
856
|
+
tooltip.style.left = rect.left + 'px';
|
|
857
|
+
tooltip.style.top = (rect.bottom + 10) + 'px';
|
|
858
|
+
|
|
859
|
+
// Adjust if tooltip goes off screen
|
|
860
|
+
const tooltipRect = tooltip.getBoundingClientRect();
|
|
861
|
+
if (tooltipRect.right > window.innerWidth) {{
|
|
862
|
+
tooltip.style.left = (window.innerWidth - tooltipRect.width - 20) + 'px';
|
|
863
|
+
}}
|
|
864
|
+
}});
|
|
865
|
+
|
|
866
|
+
label.addEventListener('mouseleave', function() {{
|
|
867
|
+
const tooltip = document.getElementById('active-tooltip');
|
|
868
|
+
if (tooltip) {{
|
|
869
|
+
tooltip.remove();
|
|
870
|
+
}}
|
|
871
|
+
}});
|
|
872
|
+
}});
|
|
873
|
+
}}
|
|
874
|
+
|
|
800
875
|
// Render dashboard on load
|
|
801
876
|
renderDashboard();
|
|
877
|
+
|
|
878
|
+
// Initialize tooltips after content is rendered
|
|
879
|
+
setTimeout(initTooltips, 100);
|
|
802
880
|
</script>
|
|
803
881
|
</body>
|
|
804
882
|
</html>'''
|
|
@@ -21,8 +21,8 @@ class HistoryDashboardGenerator:
|
|
|
21
21
|
output_path = output_path or (self.config.DASHBOARD_DIR / 'history.html')
|
|
22
22
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
23
23
|
|
|
24
|
-
# Get ALL test runs (no limit)
|
|
25
|
-
all_runs = self.storage.get_all_runs_with_details()
|
|
24
|
+
# Get ALL test runs (no limit) for current project
|
|
25
|
+
all_runs = self.storage.get_all_runs_with_details(project_name=self.config.PROJECT_NAME)
|
|
26
26
|
|
|
27
27
|
# Generate HTML
|
|
28
28
|
html = self._generate_html(all_runs)
|
|
@@ -43,13 +43,13 @@ class OverviewDashboardGenerator:
|
|
|
43
43
|
# Top 10 Quick Win Metrics
|
|
44
44
|
retry_analysis = self.storage.get_retry_analysis(project_name=filter_project)
|
|
45
45
|
failure_types = self.storage.get_failure_type_breakdown(project_name=filter_project)
|
|
46
|
-
time_to_recovery = self.storage.get_time_to_recovery(limit=10)
|
|
47
|
-
performance_regressions = self.storage.get_performance_regressions()
|
|
48
|
-
failure_correlations = self.storage.get_failure_correlations()
|
|
49
|
-
error_messages = self.storage.get_error_messages(limit=20)
|
|
50
|
-
test_age_info = self.storage.get_test_age_info()
|
|
51
|
-
soft_fail_analysis = self.storage.get_soft_fail_analysis()
|
|
52
|
-
step_failures = self.storage.get_step_failures(limit=15)
|
|
46
|
+
time_to_recovery = self.storage.get_time_to_recovery(limit=10, project_name=filter_project)
|
|
47
|
+
performance_regressions = self.storage.get_performance_regressions(project_name=filter_project)
|
|
48
|
+
failure_correlations = self.storage.get_failure_correlations(project_name=filter_project)
|
|
49
|
+
error_messages = self.storage.get_error_messages(limit=20, project_name=filter_project)
|
|
50
|
+
test_age_info = self.storage.get_test_age_info(project_name=filter_project)
|
|
51
|
+
soft_fail_analysis = self.storage.get_soft_fail_analysis(project_name=filter_project)
|
|
52
|
+
step_failures = self.storage.get_step_failures(limit=15, project_name=filter_project)
|
|
53
53
|
|
|
54
54
|
# Generate HTML
|
|
55
55
|
html = self._generate_html(
|
|
@@ -267,6 +267,47 @@ class OverviewDashboardGenerator:
|
|
|
267
267
|
.metric-card.warning .value {{ color: #d97706; border-left-color: #f59e0b; }}
|
|
268
268
|
.metric-card.danger .value {{ color: #dc2626; border-left-color: #ef4444; }}
|
|
269
269
|
|
|
270
|
+
.metric-card .label {{
|
|
271
|
+
position: relative;
|
|
272
|
+
cursor: help;
|
|
273
|
+
}}
|
|
274
|
+
|
|
275
|
+
.tooltip {{
|
|
276
|
+
position: absolute;
|
|
277
|
+
background: #1f2937;
|
|
278
|
+
color: #f9fafb;
|
|
279
|
+
padding: 12px 16px;
|
|
280
|
+
border-radius: 8px;
|
|
281
|
+
font-size: 13px;
|
|
282
|
+
line-height: 1.5;
|
|
283
|
+
max-width: 300px;
|
|
284
|
+
z-index: 1000;
|
|
285
|
+
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.4);
|
|
286
|
+
pointer-events: none;
|
|
287
|
+
opacity: 0;
|
|
288
|
+
transition: opacity 0.2s ease;
|
|
289
|
+
white-space: normal;
|
|
290
|
+
text-transform: none;
|
|
291
|
+
letter-spacing: normal;
|
|
292
|
+
font-weight: 400;
|
|
293
|
+
}}
|
|
294
|
+
|
|
295
|
+
.tooltip.show {{
|
|
296
|
+
opacity: 1;
|
|
297
|
+
}}
|
|
298
|
+
|
|
299
|
+
.tooltip::before {{
|
|
300
|
+
content: '';
|
|
301
|
+
position: absolute;
|
|
302
|
+
top: -6px;
|
|
303
|
+
left: 20px;
|
|
304
|
+
width: 0;
|
|
305
|
+
height: 0;
|
|
306
|
+
border-left: 6px solid transparent;
|
|
307
|
+
border-right: 6px solid transparent;
|
|
308
|
+
border-bottom: 6px solid #1f2937;
|
|
309
|
+
}}
|
|
310
|
+
|
|
270
311
|
.section {{
|
|
271
312
|
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
|
|
272
313
|
padding: 32px;
|
|
@@ -537,25 +578,25 @@ class OverviewDashboardGenerator:
|
|
|
537
578
|
const html = `
|
|
538
579
|
<div class="metrics-grid">
|
|
539
580
|
<div class="metric-card ${{healthClass}}">
|
|
540
|
-
<div class="label">Health Score</div>
|
|
581
|
+
<div class="label" data-tooltip="Based on Functional Pass Rate with penalties for soft failures (timeouts, flaky tests, environment issues). Calculation: Functional Pass Rate - (soft failures × 2, max -10 points)">Health Score</div>
|
|
541
582
|
<div class="value">${{healthScore}}/100</div>
|
|
542
583
|
<div class="trend">${{healthScore >= 95 ? 'Excellent!' : healthScore >= 85 ? 'Good' : 'Needs attention'}}</div>
|
|
543
584
|
</div>
|
|
544
585
|
|
|
545
586
|
<div class="metric-card">
|
|
546
|
-
<div class="label">Tests Passing</div>
|
|
587
|
+
<div class="label" data-tooltip="Average percentage of all tests that passed across all runs. Calculation: (Total Passed / Total Tests) × 100. Includes all test results regardless of failure type.">Tests Passing</div>
|
|
547
588
|
<div class="value">${{health.avg_pass_rate}}%</div>
|
|
548
589
|
<div class="trend">${{health.total_passed}} of ${{health.total_tests}} tests passed</div>
|
|
549
590
|
</div>
|
|
550
591
|
|
|
551
592
|
<div class="metric-card">
|
|
552
|
-
<div class="label">Average Time</div>
|
|
593
|
+
<div class="label" data-tooltip="Average duration per test run. Calculated by averaging the total execution time across all test runs.">Average Time</div>
|
|
553
594
|
<div class="value">${{durationText}}</div>
|
|
554
595
|
<div class="trend">Per test run • ${{health.total_runs}} runs total</div>
|
|
555
596
|
</div>
|
|
556
597
|
|
|
557
598
|
<div class="metric-card">
|
|
558
|
-
<div class="label">Test Runs</div>
|
|
599
|
+
<div class="label" data-tooltip="Total number of test runs recorded in the system. Each run represents a complete execution of the test suite.">Test Runs</div>
|
|
559
600
|
<div class="value">${{health.total_runs}}</div>
|
|
560
601
|
<div class="trend">${{health.total_tests}} tests executed</div>
|
|
561
602
|
</div>
|
|
@@ -1275,9 +1316,46 @@ class OverviewDashboardGenerator:
|
|
|
1275
1316
|
document.getElementById('stepFailuresContainer').innerHTML = html;
|
|
1276
1317
|
}}
|
|
1277
1318
|
|
|
1319
|
+
// Tooltip functionality
|
|
1320
|
+
function initTooltips() {{
|
|
1321
|
+
const labels = document.querySelectorAll('[data-tooltip]');
|
|
1322
|
+
|
|
1323
|
+
labels.forEach(label => {{
|
|
1324
|
+
label.addEventListener('mouseenter', function(e) {{
|
|
1325
|
+
const tooltipText = this.getAttribute('data-tooltip');
|
|
1326
|
+
const tooltip = document.createElement('div');
|
|
1327
|
+
tooltip.className = 'tooltip show';
|
|
1328
|
+
tooltip.textContent = tooltipText;
|
|
1329
|
+
tooltip.id = 'active-tooltip';
|
|
1330
|
+
|
|
1331
|
+
document.body.appendChild(tooltip);
|
|
1332
|
+
|
|
1333
|
+
const rect = this.getBoundingClientRect();
|
|
1334
|
+
tooltip.style.left = rect.left + 'px';
|
|
1335
|
+
tooltip.style.top = (rect.bottom + 10) + 'px';
|
|
1336
|
+
|
|
1337
|
+
// Adjust if tooltip goes off screen
|
|
1338
|
+
const tooltipRect = tooltip.getBoundingClientRect();
|
|
1339
|
+
if (tooltipRect.right > window.innerWidth) {{
|
|
1340
|
+
tooltip.style.left = (window.innerWidth - tooltipRect.width - 20) + 'px';
|
|
1341
|
+
}}
|
|
1342
|
+
}});
|
|
1343
|
+
|
|
1344
|
+
label.addEventListener('mouseleave', function() {{
|
|
1345
|
+
const tooltip = document.getElementById('active-tooltip');
|
|
1346
|
+
if (tooltip) {{
|
|
1347
|
+
tooltip.remove();
|
|
1348
|
+
}}
|
|
1349
|
+
}});
|
|
1350
|
+
}});
|
|
1351
|
+
}}
|
|
1352
|
+
|
|
1278
1353
|
// Update navigation and render dashboard on load
|
|
1279
1354
|
updateNavigation();
|
|
1280
1355
|
renderDashboard();
|
|
1356
|
+
|
|
1357
|
+
// Initialize tooltips after content is rendered
|
|
1358
|
+
setTimeout(initTooltips, 100);
|
|
1281
1359
|
</script>
|
|
1282
1360
|
</body>
|
|
1283
1361
|
</html>'''
|
|
@@ -167,6 +167,45 @@ class ProjectsDashboardGenerator:
|
|
|
167
167
|
.score-value.warning {{ color: #f59e0b; }}
|
|
168
168
|
.score-value.critical {{ color: #ef4444; }}
|
|
169
169
|
|
|
170
|
+
.score-label {{
|
|
171
|
+
position: relative;
|
|
172
|
+
cursor: help;
|
|
173
|
+
}}
|
|
174
|
+
|
|
175
|
+
.tooltip {{
|
|
176
|
+
position: absolute;
|
|
177
|
+
background: #1f2937;
|
|
178
|
+
color: #f9fafb;
|
|
179
|
+
padding: 12px 16px;
|
|
180
|
+
border-radius: 8px;
|
|
181
|
+
font-size: 13px;
|
|
182
|
+
line-height: 1.5;
|
|
183
|
+
max-width: 300px;
|
|
184
|
+
z-index: 1000;
|
|
185
|
+
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.4);
|
|
186
|
+
pointer-events: none;
|
|
187
|
+
opacity: 0;
|
|
188
|
+
transition: opacity 0.2s ease;
|
|
189
|
+
white-space: normal;
|
|
190
|
+
font-weight: 400;
|
|
191
|
+
}}
|
|
192
|
+
|
|
193
|
+
.tooltip.show {{
|
|
194
|
+
opacity: 1;
|
|
195
|
+
}}
|
|
196
|
+
|
|
197
|
+
.tooltip::before {{
|
|
198
|
+
content: '';
|
|
199
|
+
position: absolute;
|
|
200
|
+
top: -6px;
|
|
201
|
+
left: 20px;
|
|
202
|
+
width: 0;
|
|
203
|
+
height: 0;
|
|
204
|
+
border-left: 6px solid transparent;
|
|
205
|
+
border-right: 6px solid transparent;
|
|
206
|
+
border-bottom: 6px solid #1f2937;
|
|
207
|
+
}}
|
|
208
|
+
|
|
170
209
|
.stats-grid {{
|
|
171
210
|
display: grid;
|
|
172
211
|
grid-template-columns: repeat(2, 1fr);
|
|
@@ -284,7 +323,7 @@ class ProjectsDashboardGenerator:
|
|
|
284
323
|
</div>
|
|
285
324
|
|
|
286
325
|
<div class="health-score">
|
|
287
|
-
<span class="score-label">Health Score</span>
|
|
326
|
+
<span class="score-label" data-tooltip="Based on Functional Pass Rate with penalties for soft failures (timeouts, flaky tests, environment issues). Calculation: Functional Pass Rate - (soft failures × 2, max -10 points)">Health Score</span>
|
|
288
327
|
<span class="score-value ${{healthClass}}">${{Math.round(passRate)}}%</span>
|
|
289
328
|
</div>
|
|
290
329
|
|
|
@@ -326,8 +365,45 @@ class ProjectsDashboardGenerator:
|
|
|
326
365
|
window.location.href = `overview.html?project=${{encodeURIComponent(projectName)}}`;
|
|
327
366
|
}}
|
|
328
367
|
|
|
368
|
+
// Tooltip functionality
|
|
369
|
+
function initTooltips() {{
|
|
370
|
+
const labels = document.querySelectorAll('[data-tooltip]');
|
|
371
|
+
|
|
372
|
+
labels.forEach(label => {{
|
|
373
|
+
label.addEventListener('mouseenter', function(e) {{
|
|
374
|
+
const tooltipText = this.getAttribute('data-tooltip');
|
|
375
|
+
const tooltip = document.createElement('div');
|
|
376
|
+
tooltip.className = 'tooltip show';
|
|
377
|
+
tooltip.textContent = tooltipText;
|
|
378
|
+
tooltip.id = 'active-tooltip';
|
|
379
|
+
|
|
380
|
+
document.body.appendChild(tooltip);
|
|
381
|
+
|
|
382
|
+
const rect = this.getBoundingClientRect();
|
|
383
|
+
tooltip.style.left = rect.left + 'px';
|
|
384
|
+
tooltip.style.top = (rect.bottom + 10) + 'px';
|
|
385
|
+
|
|
386
|
+
// Adjust if tooltip goes off screen
|
|
387
|
+
const tooltipRect = tooltip.getBoundingClientRect();
|
|
388
|
+
if (tooltipRect.right > window.innerWidth) {{
|
|
389
|
+
tooltip.style.left = (window.innerWidth - tooltipRect.width - 20) + 'px';
|
|
390
|
+
}}
|
|
391
|
+
}});
|
|
392
|
+
|
|
393
|
+
label.addEventListener('mouseleave', function() {{
|
|
394
|
+
const tooltip = document.getElementById('active-tooltip');
|
|
395
|
+
if (tooltip) {{
|
|
396
|
+
tooltip.remove();
|
|
397
|
+
}}
|
|
398
|
+
}});
|
|
399
|
+
}});
|
|
400
|
+
}}
|
|
401
|
+
|
|
329
402
|
// Render on load
|
|
330
403
|
renderProjects();
|
|
404
|
+
|
|
405
|
+
// Initialize tooltips after content is rendered
|
|
406
|
+
setTimeout(initTooltips, 100);
|
|
331
407
|
</script>
|
|
332
408
|
</body>
|
|
333
409
|
</html>'''
|
|
@@ -196,16 +196,23 @@ class TestResultStorage:
|
|
|
196
196
|
))
|
|
197
197
|
conn.commit()
|
|
198
198
|
|
|
199
|
-
def get_all_runs_with_details(self) -> List[Dict[str, Any]]:
|
|
199
|
+
def get_all_runs_with_details(self, project_name: str = None) -> List[Dict[str, Any]]:
|
|
200
200
|
"""Get all test runs with test file details (no limit)."""
|
|
201
201
|
with sqlite3.connect(self.db_path) as conn:
|
|
202
202
|
conn.row_factory = sqlite3.Row
|
|
203
203
|
|
|
204
|
+
where_clause = ""
|
|
205
|
+
params = []
|
|
206
|
+
if project_name:
|
|
207
|
+
where_clause = "WHERE project_name = ?"
|
|
208
|
+
params.append(project_name)
|
|
209
|
+
|
|
204
210
|
# Get all runs
|
|
205
|
-
runs_cursor = conn.execute('''
|
|
211
|
+
runs_cursor = conn.execute(f'''
|
|
206
212
|
SELECT * FROM test_runs
|
|
213
|
+
{where_clause}
|
|
207
214
|
ORDER BY timestamp DESC
|
|
208
|
-
''')
|
|
215
|
+
''', params)
|
|
209
216
|
|
|
210
217
|
runs = []
|
|
211
218
|
for run_row in runs_cursor.fetchall():
|
|
@@ -266,11 +273,21 @@ class TestResultStorage:
|
|
|
266
273
|
''', (result_id,))
|
|
267
274
|
return [dict(row) for row in cursor.fetchall()]
|
|
268
275
|
|
|
269
|
-
def get_trend_data(self, days: int = 30) -> List[Dict[str, Any]]:
|
|
276
|
+
def get_trend_data(self, days: int = 30, project_name: str = None) -> List[Dict[str, Any]]:
|
|
270
277
|
"""Get trend data for the last N days."""
|
|
271
278
|
with sqlite3.connect(self.db_path) as conn:
|
|
272
279
|
conn.row_factory = sqlite3.Row
|
|
273
|
-
|
|
280
|
+
|
|
281
|
+
where_clauses = ["timestamp >= datetime('now', '-' || ? || ' days')"]
|
|
282
|
+
params = [days]
|
|
283
|
+
|
|
284
|
+
if project_name:
|
|
285
|
+
where_clauses.append("project_name = ?")
|
|
286
|
+
params.append(project_name)
|
|
287
|
+
|
|
288
|
+
where_clause = " AND ".join(where_clauses)
|
|
289
|
+
|
|
290
|
+
cursor = conn.execute(f'''
|
|
274
291
|
SELECT
|
|
275
292
|
timestamp,
|
|
276
293
|
pass_rate,
|
|
@@ -280,41 +297,51 @@ class TestResultStorage:
|
|
|
280
297
|
failed,
|
|
281
298
|
duration_seconds
|
|
282
299
|
FROM test_runs
|
|
283
|
-
WHERE
|
|
300
|
+
WHERE {where_clause}
|
|
284
301
|
ORDER BY timestamp ASC
|
|
285
|
-
''',
|
|
302
|
+
''', params)
|
|
286
303
|
return [dict(row) for row in cursor.fetchall()]
|
|
287
304
|
|
|
288
|
-
def get_failure_summary(self, days: int = 30) -> Dict[str, Any]:
|
|
305
|
+
def get_failure_summary(self, days: int = 30, project_name: str = None) -> Dict[str, Any]:
|
|
289
306
|
"""Get failure summary for the last N days."""
|
|
290
307
|
with sqlite3.connect(self.db_path) as conn:
|
|
291
308
|
conn.row_factory = sqlite3.Row
|
|
292
309
|
|
|
310
|
+
where_clauses = [
|
|
311
|
+
"status = 'failed'",
|
|
312
|
+
"timestamp >= datetime('now', '-' || ? || ' days')"
|
|
313
|
+
]
|
|
314
|
+
params = [days]
|
|
315
|
+
|
|
316
|
+
if project_name:
|
|
317
|
+
where_clauses.append("project_name = ?")
|
|
318
|
+
params.append(project_name)
|
|
319
|
+
|
|
320
|
+
where_clause = " AND ".join(where_clauses)
|
|
321
|
+
|
|
293
322
|
# Get failures by type
|
|
294
|
-
cursor = conn.execute('''
|
|
323
|
+
cursor = conn.execute(f'''
|
|
295
324
|
SELECT
|
|
296
325
|
failure_type,
|
|
297
326
|
COUNT(*) as count
|
|
298
327
|
FROM test_results
|
|
299
|
-
WHERE
|
|
300
|
-
AND timestamp >= datetime('now', '-' || ? || ' days')
|
|
328
|
+
WHERE {where_clause}
|
|
301
329
|
GROUP BY failure_type
|
|
302
330
|
ORDER BY count DESC
|
|
303
|
-
''',
|
|
331
|
+
''', params)
|
|
304
332
|
by_type = {row['failure_type']: row['count'] for row in cursor.fetchall()}
|
|
305
333
|
|
|
306
334
|
# Get failures by file
|
|
307
|
-
cursor = conn.execute('''
|
|
335
|
+
cursor = conn.execute(f'''
|
|
308
336
|
SELECT
|
|
309
337
|
file_name,
|
|
310
338
|
COUNT(*) as count
|
|
311
339
|
FROM test_results
|
|
312
|
-
WHERE
|
|
313
|
-
AND timestamp >= datetime('now', '-' || ? || ' days')
|
|
340
|
+
WHERE {where_clause}
|
|
314
341
|
GROUP BY file_name
|
|
315
342
|
ORDER BY count DESC
|
|
316
343
|
LIMIT 10
|
|
317
|
-
''',
|
|
344
|
+
''', params)
|
|
318
345
|
by_file = [{'file': row['file_name'], 'count': row['count']} for row in cursor.fetchall()]
|
|
319
346
|
|
|
320
347
|
return {
|
|
@@ -438,11 +465,21 @@ class TestResultStorage:
|
|
|
438
465
|
|
|
439
466
|
return modules
|
|
440
467
|
|
|
441
|
-
def get_suite_statistics(self) -> List[Dict[str, Any]]:
|
|
468
|
+
def get_suite_statistics(self, project_name: str = None) -> List[Dict[str, Any]]:
|
|
442
469
|
"""Get statistics per test suite."""
|
|
443
470
|
with sqlite3.connect(self.db_path) as conn:
|
|
444
471
|
conn.row_factory = sqlite3.Row
|
|
445
|
-
|
|
472
|
+
|
|
473
|
+
where_clauses = ["suite_name IS NOT NULL", "suite_name != ''"]
|
|
474
|
+
params = []
|
|
475
|
+
|
|
476
|
+
if project_name:
|
|
477
|
+
where_clauses.append("project_name = ?")
|
|
478
|
+
params.append(project_name)
|
|
479
|
+
|
|
480
|
+
where_clause = " AND ".join(where_clauses)
|
|
481
|
+
|
|
482
|
+
cursor = conn.execute(f'''
|
|
446
483
|
SELECT
|
|
447
484
|
suite_name,
|
|
448
485
|
COUNT(*) as total_runs,
|
|
@@ -453,10 +490,10 @@ class TestResultStorage:
|
|
|
453
490
|
MIN(timestamp) as first_run,
|
|
454
491
|
MAX(timestamp) as last_run
|
|
455
492
|
FROM test_runs
|
|
456
|
-
WHERE
|
|
493
|
+
WHERE {where_clause}
|
|
457
494
|
GROUP BY suite_name
|
|
458
495
|
ORDER BY last_run DESC
|
|
459
|
-
''')
|
|
496
|
+
''', params)
|
|
460
497
|
|
|
461
498
|
suites = []
|
|
462
499
|
for row in cursor.fetchall():
|
|
@@ -829,11 +866,19 @@ class TestResultStorage:
|
|
|
829
866
|
''', params)
|
|
830
867
|
return [dict(row) for row in cursor.fetchall()]
|
|
831
868
|
|
|
832
|
-
def get_time_to_recovery(self, limit: int = 10) -> List[Dict[str, Any]]:
|
|
869
|
+
def get_time_to_recovery(self, limit: int = 10, project_name: str = None) -> List[Dict[str, Any]]:
|
|
833
870
|
"""Get time to recovery for tests that failed then passed."""
|
|
834
871
|
with sqlite3.connect(self.db_path) as conn:
|
|
835
872
|
conn.row_factory = sqlite3.Row
|
|
836
|
-
|
|
873
|
+
|
|
874
|
+
project_filter = ""
|
|
875
|
+
params = []
|
|
876
|
+
if project_name:
|
|
877
|
+
project_filter = "AND tr.project_name = ?"
|
|
878
|
+
params.append(project_name)
|
|
879
|
+
params.append(limit)
|
|
880
|
+
|
|
881
|
+
cursor = conn.execute(f'''
|
|
837
882
|
WITH failures AS (
|
|
838
883
|
SELECT
|
|
839
884
|
test_name,
|
|
@@ -844,7 +889,7 @@ class TestResultStorage:
|
|
|
844
889
|
ROW_NUMBER() OVER (PARTITION BY test_name, file_name ORDER BY timestamp) as rn
|
|
845
890
|
FROM test_results tr
|
|
846
891
|
JOIN test_runs r ON tr.run_id = r.id
|
|
847
|
-
WHERE status IN ('failed', 'passed')
|
|
892
|
+
WHERE status IN ('failed', 'passed') {project_filter}
|
|
848
893
|
),
|
|
849
894
|
recovery_times AS (
|
|
850
895
|
SELECT
|
|
@@ -865,55 +910,58 @@ class TestResultStorage:
|
|
|
865
910
|
WHERE minutes_to_recover IS NOT NULL
|
|
866
911
|
ORDER BY minutes_to_recover DESC
|
|
867
912
|
LIMIT ?
|
|
868
|
-
''',
|
|
913
|
+
''', params)
|
|
869
914
|
return [dict(row) for row in cursor.fetchall()]
|
|
870
915
|
|
|
871
|
-
def get_error_messages(self, search: str = None, limit: int = 50) -> List[Dict[str, Any]]:
|
|
916
|
+
def get_error_messages(self, search: str = None, limit: int = 50, project_name: str = None) -> List[Dict[str, Any]]:
|
|
872
917
|
"""Get error messages with optional search."""
|
|
873
918
|
with sqlite3.connect(self.db_path) as conn:
|
|
874
919
|
conn.row_factory = sqlite3.Row
|
|
875
920
|
|
|
921
|
+
where_clauses = ["error_message IS NOT NULL"]
|
|
922
|
+
params = []
|
|
923
|
+
|
|
876
924
|
if search:
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
FROM test_results tr
|
|
903
|
-
JOIN test_runs r ON tr.run_id = r.id
|
|
904
|
-
WHERE error_message IS NOT NULL
|
|
905
|
-
GROUP BY test_name, file_name, error_message, error_type
|
|
906
|
-
ORDER BY occurrence_count DESC
|
|
907
|
-
LIMIT ?
|
|
908
|
-
''', (limit,))
|
|
925
|
+
where_clauses.append("(error_message LIKE ? OR error_type LIKE ?)")
|
|
926
|
+
params.extend([f'%{search}%', f'%{search}%'])
|
|
927
|
+
|
|
928
|
+
if project_name:
|
|
929
|
+
where_clauses.append("tr.project_name = ?")
|
|
930
|
+
params.append(project_name)
|
|
931
|
+
|
|
932
|
+
params.append(limit)
|
|
933
|
+
where_clause = " AND ".join(where_clauses)
|
|
934
|
+
|
|
935
|
+
cursor = conn.execute(f'''
|
|
936
|
+
SELECT
|
|
937
|
+
test_name,
|
|
938
|
+
file_name,
|
|
939
|
+
error_message,
|
|
940
|
+
error_type,
|
|
941
|
+
COUNT(*) as occurrence_count,
|
|
942
|
+
MAX(r.timestamp) as last_seen
|
|
943
|
+
FROM test_results tr
|
|
944
|
+
JOIN test_runs r ON tr.run_id = r.id
|
|
945
|
+
WHERE {where_clause}
|
|
946
|
+
GROUP BY test_name, file_name, error_message, error_type
|
|
947
|
+
ORDER BY occurrence_count DESC
|
|
948
|
+
LIMIT ?
|
|
949
|
+
''', params)
|
|
909
950
|
|
|
910
951
|
return [dict(row) for row in cursor.fetchall()]
|
|
911
952
|
|
|
912
|
-
def get_test_age_info(self) -> List[Dict[str, Any]]:
|
|
953
|
+
def get_test_age_info(self, project_name: str = None) -> List[Dict[str, Any]]:
|
|
913
954
|
"""Get test age information."""
|
|
914
955
|
with sqlite3.connect(self.db_path) as conn:
|
|
915
956
|
conn.row_factory = sqlite3.Row
|
|
916
|
-
|
|
957
|
+
|
|
958
|
+
where_clause = ""
|
|
959
|
+
params = []
|
|
960
|
+
if project_name:
|
|
961
|
+
where_clause = "WHERE tr.project_name = ?"
|
|
962
|
+
params.append(project_name)
|
|
963
|
+
|
|
964
|
+
cursor = conn.execute(f'''
|
|
917
965
|
SELECT
|
|
918
966
|
test_name,
|
|
919
967
|
file_name,
|
|
@@ -923,29 +971,42 @@ class TestResultStorage:
|
|
|
923
971
|
CAST((julianday('now') - julianday(MAX(r.timestamp))) AS INTEGER) as days_since_last_run
|
|
924
972
|
FROM test_results tr
|
|
925
973
|
JOIN test_runs r ON tr.run_id = r.id
|
|
974
|
+
{where_clause}
|
|
926
975
|
GROUP BY test_name, file_name
|
|
927
976
|
ORDER BY days_since_last_run DESC
|
|
928
|
-
''')
|
|
977
|
+
''', params)
|
|
929
978
|
return [dict(row) for row in cursor.fetchall()]
|
|
930
979
|
|
|
931
|
-
def get_soft_fail_analysis(self) -> Dict[str, Any]:
|
|
980
|
+
def get_soft_fail_analysis(self, project_name: str = None) -> Dict[str, Any]:
|
|
932
981
|
"""Get soft fail vs hard fail analysis."""
|
|
933
982
|
with sqlite3.connect(self.db_path) as conn:
|
|
934
983
|
conn.row_factory = sqlite3.Row
|
|
935
984
|
|
|
985
|
+
where_clauses = ["status = 'failed'"]
|
|
986
|
+
params = []
|
|
987
|
+
if project_name:
|
|
988
|
+
where_clauses.append("project_name = ?")
|
|
989
|
+
params.append(project_name)
|
|
990
|
+
where_clause = " AND ".join(where_clauses)
|
|
991
|
+
|
|
936
992
|
# Overall stats
|
|
937
|
-
cursor = conn.execute('''
|
|
993
|
+
cursor = conn.execute(f'''
|
|
938
994
|
SELECT
|
|
939
995
|
COUNT(*) as total_failures,
|
|
940
996
|
SUM(CASE WHEN is_soft_fail = 1 THEN 1 ELSE 0 END) as soft_fails,
|
|
941
997
|
SUM(CASE WHEN is_soft_fail = 0 OR is_soft_fail IS NULL THEN 1 ELSE 0 END) as hard_fails
|
|
942
998
|
FROM test_results
|
|
943
|
-
WHERE
|
|
944
|
-
''')
|
|
999
|
+
WHERE {where_clause}
|
|
1000
|
+
''', params)
|
|
945
1001
|
overall = dict(cursor.fetchone())
|
|
946
1002
|
|
|
947
1003
|
# Tests with most soft fails
|
|
948
|
-
|
|
1004
|
+
where_clauses_soft = ["is_soft_fail = 1"]
|
|
1005
|
+
if project_name:
|
|
1006
|
+
where_clauses_soft.append("tr.project_name = ?")
|
|
1007
|
+
where_clause_soft = " AND ".join(where_clauses_soft)
|
|
1008
|
+
|
|
1009
|
+
cursor = conn.execute(f'''
|
|
949
1010
|
SELECT
|
|
950
1011
|
test_name,
|
|
951
1012
|
file_name,
|
|
@@ -953,11 +1014,11 @@ class TestResultStorage:
|
|
|
953
1014
|
MAX(r.timestamp) as last_soft_fail
|
|
954
1015
|
FROM test_results tr
|
|
955
1016
|
JOIN test_runs r ON tr.run_id = r.id
|
|
956
|
-
WHERE
|
|
1017
|
+
WHERE {where_clause_soft}
|
|
957
1018
|
GROUP BY test_name, file_name
|
|
958
1019
|
ORDER BY soft_fail_count DESC
|
|
959
1020
|
LIMIT 10
|
|
960
|
-
''')
|
|
1021
|
+
''', params)
|
|
961
1022
|
top_soft_fails = [dict(row) for row in cursor.fetchall()]
|
|
962
1023
|
|
|
963
1024
|
return {
|
|
@@ -965,11 +1026,20 @@ class TestResultStorage:
|
|
|
965
1026
|
'top_soft_fails': top_soft_fails
|
|
966
1027
|
}
|
|
967
1028
|
|
|
968
|
-
def get_step_failures(self, limit: int = 20) -> List[Dict[str, Any]]:
|
|
1029
|
+
def get_step_failures(self, limit: int = 20, project_name: str = None) -> List[Dict[str, Any]]:
|
|
969
1030
|
"""Get most common failing steps."""
|
|
970
1031
|
with sqlite3.connect(self.db_path) as conn:
|
|
971
1032
|
conn.row_factory = sqlite3.Row
|
|
972
|
-
|
|
1033
|
+
|
|
1034
|
+
where_clauses = ["ts.status = 'failed'"]
|
|
1035
|
+
params = []
|
|
1036
|
+
if project_name:
|
|
1037
|
+
where_clauses.append("tr.project_name = ?")
|
|
1038
|
+
params.append(project_name)
|
|
1039
|
+
params.append(limit)
|
|
1040
|
+
where_clause = " AND ".join(where_clauses)
|
|
1041
|
+
|
|
1042
|
+
cursor = conn.execute(f'''
|
|
973
1043
|
SELECT
|
|
974
1044
|
ts.description,
|
|
975
1045
|
COUNT(*) as failure_count,
|
|
@@ -977,18 +1047,26 @@ class TestResultStorage:
|
|
|
977
1047
|
COUNT(DISTINCT tr.test_name) as affected_tests
|
|
978
1048
|
FROM test_steps ts
|
|
979
1049
|
JOIN test_results tr ON ts.result_id = tr.id
|
|
980
|
-
WHERE
|
|
1050
|
+
WHERE {where_clause}
|
|
981
1051
|
GROUP BY ts.description
|
|
982
1052
|
ORDER BY failure_count DESC
|
|
983
1053
|
LIMIT ?
|
|
984
|
-
''',
|
|
1054
|
+
''', params)
|
|
985
1055
|
return [dict(row) for row in cursor.fetchall()]
|
|
986
1056
|
|
|
987
|
-
def get_performance_regressions(self, threshold_percent: float = 20.0) -> List[Dict[str, Any]]:
|
|
1057
|
+
def get_performance_regressions(self, threshold_percent: float = 20.0, project_name: str = None) -> List[Dict[str, Any]]:
|
|
988
1058
|
"""Detect tests that are getting slower."""
|
|
989
1059
|
with sqlite3.connect(self.db_path) as conn:
|
|
990
1060
|
conn.row_factory = sqlite3.Row
|
|
991
|
-
|
|
1061
|
+
|
|
1062
|
+
project_filter = ""
|
|
1063
|
+
params = []
|
|
1064
|
+
if project_name:
|
|
1065
|
+
project_filter = "AND tr.project_name = ?"
|
|
1066
|
+
params.append(project_name)
|
|
1067
|
+
params.append(threshold_percent)
|
|
1068
|
+
|
|
1069
|
+
cursor = conn.execute(f'''
|
|
992
1070
|
WITH recent_avg AS (
|
|
993
1071
|
SELECT
|
|
994
1072
|
tr.test_name,
|
|
@@ -998,6 +1076,7 @@ class TestResultStorage:
|
|
|
998
1076
|
JOIN test_runs r ON tr.run_id = r.id
|
|
999
1077
|
WHERE r.timestamp >= datetime('now', '-7 days')
|
|
1000
1078
|
AND tr.duration_seconds > 0
|
|
1079
|
+
{project_filter}
|
|
1001
1080
|
GROUP BY tr.test_name, tr.file_name
|
|
1002
1081
|
),
|
|
1003
1082
|
historical_avg AS (
|
|
@@ -1010,6 +1089,7 @@ class TestResultStorage:
|
|
|
1010
1089
|
WHERE r.timestamp < datetime('now', '-7 days')
|
|
1011
1090
|
AND r.timestamp >= datetime('now', '-30 days')
|
|
1012
1091
|
AND tr.duration_seconds > 0
|
|
1092
|
+
{project_filter}
|
|
1013
1093
|
GROUP BY tr.test_name, tr.file_name
|
|
1014
1094
|
)
|
|
1015
1095
|
SELECT
|
|
@@ -1022,18 +1102,26 @@ class TestResultStorage:
|
|
|
1022
1102
|
JOIN historical_avg h ON r.test_name = h.test_name AND r.file_name = h.file_name
|
|
1023
1103
|
WHERE ((r.recent_avg - h.historical_avg) / h.historical_avg) * 100 > ?
|
|
1024
1104
|
ORDER BY percent_slower DESC
|
|
1025
|
-
''',
|
|
1105
|
+
''', params)
|
|
1026
1106
|
return [dict(row) for row in cursor.fetchall()]
|
|
1027
1107
|
|
|
1028
|
-
def get_failure_correlations(self, min_correlation: int = 3) -> List[Dict[str, Any]]:
|
|
1108
|
+
def get_failure_correlations(self, min_correlation: int = 3, project_name: str = None) -> List[Dict[str, Any]]:
|
|
1029
1109
|
"""Find tests that frequently fail together."""
|
|
1030
1110
|
with sqlite3.connect(self.db_path) as conn:
|
|
1031
1111
|
conn.row_factory = sqlite3.Row
|
|
1032
|
-
|
|
1112
|
+
|
|
1113
|
+
where_clause = "status = 'failed'"
|
|
1114
|
+
params = []
|
|
1115
|
+
if project_name:
|
|
1116
|
+
where_clause += " AND project_name = ?"
|
|
1117
|
+
params.append(project_name)
|
|
1118
|
+
params.append(min_correlation)
|
|
1119
|
+
|
|
1120
|
+
cursor = conn.execute(f'''
|
|
1033
1121
|
WITH failed_runs AS (
|
|
1034
1122
|
SELECT DISTINCT run_id, test_name, file_name
|
|
1035
1123
|
FROM test_results
|
|
1036
|
-
WHERE
|
|
1124
|
+
WHERE {where_clause}
|
|
1037
1125
|
)
|
|
1038
1126
|
SELECT
|
|
1039
1127
|
f1.test_name as test1_name,
|
|
@@ -1048,7 +1136,7 @@ class TestResultStorage:
|
|
|
1048
1136
|
HAVING fail_together_count >= ?
|
|
1049
1137
|
ORDER BY fail_together_count DESC
|
|
1050
1138
|
LIMIT 20
|
|
1051
|
-
''',
|
|
1139
|
+
''', params)
|
|
1052
1140
|
return [dict(row) for row in cursor.fetchall()]
|
|
1053
1141
|
|
|
1054
1142
|
# ===== MULTI-PROJECT QUERIES =====
|
|
@@ -9,7 +9,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
|
|
9
9
|
|
|
10
10
|
setup(
|
|
11
11
|
name='test-reporting',
|
|
12
|
-
version='1.2.
|
|
12
|
+
version='1.2.2',
|
|
13
13
|
description='Beautiful multi-project test reporting dashboard system',
|
|
14
14
|
long_description=long_description,
|
|
15
15
|
long_description_content_type="text/markdown",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|