test-reporting 3.7.1__tar.gz → 3.7.3__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.
Files changed (21) hide show
  1. {test_reporting-3.7.1/test_reporting.egg-info → test_reporting-3.7.3}/PKG-INFO +1 -1
  2. {test_reporting-3.7.1 → test_reporting-3.7.3}/reporting/classifier.py +47 -3
  3. {test_reporting-3.7.1 → test_reporting-3.7.3}/reporting/publisher.py +29 -14
  4. {test_reporting-3.7.1 → test_reporting-3.7.3}/reporting/templates/project.html +73 -44
  5. {test_reporting-3.7.1 → test_reporting-3.7.3}/setup.py +1 -1
  6. {test_reporting-3.7.1 → test_reporting-3.7.3/test_reporting.egg-info}/PKG-INFO +1 -1
  7. {test_reporting-3.7.1 → test_reporting-3.7.3}/LICENSE +0 -0
  8. {test_reporting-3.7.1 → test_reporting-3.7.3}/README.md +0 -0
  9. {test_reporting-3.7.1 → test_reporting-3.7.3}/reporting/__init__.py +0 -0
  10. {test_reporting-3.7.1 → test_reporting-3.7.3}/reporting/cli.py +0 -0
  11. {test_reporting-3.7.1 → test_reporting-3.7.3}/reporting/config.py +0 -0
  12. {test_reporting-3.7.1 → test_reporting-3.7.3}/reporting/plugin.py +0 -0
  13. {test_reporting-3.7.1 → test_reporting-3.7.3}/reporting/storage.py +0 -0
  14. {test_reporting-3.7.1 → test_reporting-3.7.3}/reporting/templates/index.html +0 -0
  15. {test_reporting-3.7.1 → test_reporting-3.7.3}/reporting/templates/run.html +0 -0
  16. {test_reporting-3.7.1 → test_reporting-3.7.3}/setup.cfg +0 -0
  17. {test_reporting-3.7.1 → test_reporting-3.7.3}/test_reporting.egg-info/SOURCES.txt +0 -0
  18. {test_reporting-3.7.1 → test_reporting-3.7.3}/test_reporting.egg-info/dependency_links.txt +0 -0
  19. {test_reporting-3.7.1 → test_reporting-3.7.3}/test_reporting.egg-info/entry_points.txt +0 -0
  20. {test_reporting-3.7.1 → test_reporting-3.7.3}/test_reporting.egg-info/requires.txt +0 -0
  21. {test_reporting-3.7.1 → test_reporting-3.7.3}/test_reporting.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: test-reporting
3
- Version: 3.7.1
3
+ Version: 3.7.3
4
4
  Summary: Multi-project test reporting dashboard — collect results locally or push to S3
5
5
  Home-page: https://github.com/amahdy77/test-reporting.git
6
6
  Author: Ashfaqur Mahdy
@@ -16,6 +16,10 @@ class FailureClassifier:
16
16
  'API_TIMEOUT': 'Soft Failure - API Timeout',
17
17
  'ENVIRONMENT': 'Environment Issue',
18
18
  'FLAKY': 'Flaky Test - Passed on Retry',
19
+ 'ELEMENT_NOT_FOUND': 'Element Not Found',
20
+ 'NAVIGATION_ERROR': 'Navigation/Page Load Error',
21
+ 'SCRIPT_ERROR': 'JavaScript/Script Error',
22
+ 'VALIDATION_ERROR': 'Data Validation Error',
19
23
  'UNKNOWN': 'Unknown Error',
20
24
  }
21
25
 
@@ -44,6 +48,9 @@ class FailureClassifier:
44
48
  is_soft_fail = False
45
49
  category = 'functional'
46
50
 
51
+ exception_lower = exception_message.lower()
52
+ exception_type_lower = exception_type.lower()
53
+
47
54
  # Check if passed on retry (flaky)
48
55
  if retry_count > 0:
49
56
  failure_type = 'FLAKY'
@@ -51,11 +58,48 @@ class FailureClassifier:
51
58
  category = 'stability'
52
59
 
53
60
  # Check for assertion errors (hard failures)
54
- elif 'AssertionError' in exception_type or 'assert' in exception_message.lower():
61
+ elif 'AssertionError' in exception_type or 'assert' in exception_lower:
55
62
  failure_type = 'ASSERTION'
56
63
  is_soft_fail = False
57
64
  category = 'functional'
58
65
 
66
+ # Check for element not found errors
67
+ elif any(pattern in exception_lower for pattern in [
68
+ 'element not found', 'no such element', 'locator not found',
69
+ 'selector not found', 'element is not attached', 'element not visible',
70
+ 'waiting for selector', 'strict mode violation'
71
+ ]):
72
+ failure_type = 'ELEMENT_NOT_FOUND'
73
+ is_soft_fail = False
74
+ category = 'functional'
75
+
76
+ # Check for navigation/page load errors
77
+ elif any(pattern in exception_lower for pattern in [
78
+ 'navigation', 'page.goto', 'net::err', 'failed to load',
79
+ 'page crash', 'target closed', 'page did not load'
80
+ ]):
81
+ failure_type = 'NAVIGATION_ERROR'
82
+ is_soft_fail = True
83
+ category = 'infrastructure'
84
+
85
+ # Check for JavaScript/script errors
86
+ elif any(pattern in exception_lower for pattern in [
87
+ 'javascript error', 'script error', 'evaluation failed',
88
+ 'uncaught exception', 'referenceerror', 'typeerror'
89
+ ]) or 'Error' in exception_type and 'script' in exception_lower:
90
+ failure_type = 'SCRIPT_ERROR'
91
+ is_soft_fail = False
92
+ category = 'functional'
93
+
94
+ # Check for validation/data errors
95
+ elif any(pattern in exception_lower for pattern in [
96
+ 'validation', 'invalid', 'expected', 'does not match',
97
+ 'valueerror', 'keyerror', 'attributeerror'
98
+ ]) or any(exc in exception_type for exc in ['ValueError', 'KeyError', 'AttributeError']):
99
+ failure_type = 'VALIDATION_ERROR'
100
+ is_soft_fail = False
101
+ category = 'functional'
102
+
59
103
  # Check for UI timeouts (soft failures)
60
104
  elif any(pattern in exception_message for pattern in ReportingConfig.SOFT_FAIL_PATTERNS):
61
105
  failure_type = 'UI_TIMEOUT'
@@ -63,13 +107,13 @@ class FailureClassifier:
63
107
  category = 'performance'
64
108
 
65
109
  # Check for environment issues
66
- elif any(pattern in exception_message.lower() for pattern in ReportingConfig.ENVIRONMENT_FAIL_PATTERNS):
110
+ elif any(pattern in exception_lower for pattern in ReportingConfig.ENVIRONMENT_FAIL_PATTERNS):
67
111
  failure_type = 'ENVIRONMENT'
68
112
  is_soft_fail = True
69
113
  category = 'infrastructure'
70
114
 
71
115
  # Check for API timeouts
72
- elif 'timeout' in exception_message.lower() and duration_ms > ReportingConfig.TIMEOUT_API_MAX:
116
+ elif 'timeout' in exception_lower and duration_ms > ReportingConfig.TIMEOUT_API_MAX:
73
117
  failure_type = 'API_TIMEOUT'
74
118
  is_soft_fail = True
75
119
  category = 'performance'
@@ -404,12 +404,16 @@ class Publisher:
404
404
  # Real bugs (ASSERTION) need code fixes.
405
405
  # Infrastructure issues (ENVIRONMENT, TIMEOUT) may not reflect quality.
406
406
  type_labels = {
407
- 'ASSERTION': 'Real bugs (assertion errors)',
408
- 'UI_TIMEOUT': 'UI timeouts',
409
- 'API_TIMEOUT': 'API timeouts',
410
- 'ENVIRONMENT': 'Infrastructure / connection issues',
411
- 'FLAKY': 'Passed on retry (unstable)',
412
- 'UNKNOWN': 'Uncategorised',
407
+ 'ASSERTION': 'Real bugs (assertion errors)',
408
+ 'ELEMENT_NOT_FOUND': 'Element not found',
409
+ 'VALIDATION_ERROR': 'Data validation errors',
410
+ 'SCRIPT_ERROR': 'JavaScript/Script errors',
411
+ 'UI_TIMEOUT': 'UI timeouts',
412
+ 'API_TIMEOUT': 'API timeouts',
413
+ 'NAVIGATION_ERROR': 'Navigation/Page load errors',
414
+ 'ENVIRONMENT': 'Infrastructure / connection issues',
415
+ 'FLAKY': 'Passed on retry (unstable)',
416
+ 'UNKNOWN': 'Uncategorized',
413
417
  }
414
418
  failure_type_breakdown = [
415
419
  {
@@ -491,6 +495,10 @@ class Publisher:
491
495
  })
492
496
 
493
497
  # Regression grouping
498
+ # Strip [TAG] suffix to group reruns with original runs
499
+ import re
500
+ base_suite = re.sub(r'\s*\[.*?\]\s*$', '', suite)
501
+
494
502
  if tag not in regressions:
495
503
  regressions[tag] = {'tag': tag, 'date': tag[:10], 'suites': [],
496
504
  'total_tests': 0, 'passed': 0, 'failed': 0, 'pass_rate': 0,
@@ -499,21 +507,28 @@ class Publisher:
499
507
  # Track the latest timestamp for this regression
500
508
  if ts > reg.get('timestamp', ''):
501
509
  reg['timestamp'] = ts
502
- existing = next((s for s in reg['suites'] if s['suite'] == suite), None)
510
+ existing = next((s for s in reg['suites'] if s['suite'] == base_suite), None)
503
511
  if existing:
512
+ # Accumulate stats for multiple runs of the same test
513
+ existing['total_tests'] += run.get('total_tests', 0)
514
+ existing['passed'] += run.get('passed', 0)
515
+ existing['failed'] += run.get('failed', 0)
516
+ existing['run_count'] = existing.get('run_count', 1) + 1
517
+ # Update to latest timestamp and run_id
504
518
  if ts > existing.get('timestamp', ''):
505
- existing.update({'timestamp': ts, 'run_id': run.get('run_id'),
506
- 'pass_rate': run.get('pass_rate', 0),
507
- 'total_tests': run.get('total_tests', 0),
508
- 'passed': run.get('passed', 0),
509
- 'failed': run.get('failed', 0)})
519
+ existing['timestamp'] = ts
520
+ existing['run_id'] = run.get('run_id')
521
+ # Recalculate pass rate based on accumulated totals
522
+ if existing['total_tests'] > 0:
523
+ existing['pass_rate'] = round((existing['passed'] / existing['total_tests']) * 100, 1)
510
524
  else:
511
- reg['suites'].append({'suite': suite, 'timestamp': ts,
525
+ reg['suites'].append({'suite': base_suite, 'timestamp': ts,
512
526
  'run_id': run.get('run_id'),
513
527
  'pass_rate': run.get('pass_rate', 0),
514
528
  'total_tests': run.get('total_tests', 0),
515
529
  'passed': run.get('passed', 0),
516
- 'failed': run.get('failed', 0)})
530
+ 'failed': run.get('failed', 0),
531
+ 'run_count': 1})
517
532
 
518
533
  # Compute regression combined stats
519
534
  for reg in regressions.values():
@@ -280,6 +280,7 @@
280
280
  border-radius: 6px; cursor: pointer; position: relative;
281
281
  overflow: hidden; transition: transform .1s, box-shadow .1s; }
282
282
  .treemap-cell:hover { transform: scale(1.05); z-index: 1; box-shadow: 0 4px 12px rgba(0,0,0,.3); }
283
+ .treemap-cell-hidden { display: none; }
283
284
  .treemap-name { font-size: 11px; font-weight: 600; color: #fff; opacity: .9;
284
285
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
285
286
  .treemap-rate { font-size: 14px; font-weight: 800; color: #fff; }
@@ -554,69 +555,53 @@
554
555
  const rc2 = rateColor(reg.pass_rate);
555
556
  const total = lastReg ? lastReg.total_tests || 1 : 1;
556
557
 
557
- // Group suites by name to detect duplicates
558
- const suiteGroups = {};
559
- (reg.suites || []).forEach(function(s) {
560
- if (!suiteGroups[s.suite]) {
561
- suiteGroups[s.suite] = [];
562
- }
563
- suiteGroups[s.suite].push(s);
558
+ // Sort suites alphabetically
559
+ const sortedSuites = (reg.suites || []).slice().sort(function(a, b) {
560
+ return a.suite.localeCompare(b.suite);
564
561
  });
565
562
 
566
- const fileRows = Object.keys(suiteGroups).map(function(suiteName) {
567
- const runs = suiteGroups[suiteName];
568
- const runCount = runs.length;
569
-
570
- // Calculate accumulated averages
571
- let totalTests = 0;
572
- let totalFailed = 0;
573
- let totalSkipped = 0;
574
- let totalPassed = 0;
575
- let avgPassRate = 0;
576
-
577
- runs.forEach(function(run) {
578
- totalTests += run.total_tests;
579
- totalFailed += run.failed;
580
- totalSkipped += (run.skipped || 0);
581
- totalPassed += (run.total_tests - run.failed - (run.skipped || 0));
582
- avgPassRate += run.pass_rate;
583
- });
584
-
585
- avgPassRate = avgPassRate / runCount;
586
-
587
- // Use first run's ID for navigation
588
- const firstRunId = runs[0].run_id;
563
+ const fileRows = sortedSuites.map(function(s) {
564
+ const runCount = s.run_count || 1;
565
+ const totalTests = s.total_tests;
566
+ const totalFailed = s.failed;
567
+ const totalPassed = s.passed;
568
+ const passRate = s.pass_rate;
589
569
 
590
570
  const failedPct = totalTests > 0 ? (totalFailed / totalTests * 100) : 0;
591
- const skippedPct = totalTests > 0 ? (totalSkipped / totalTests * 100) : 0;
592
- const passedPct = Math.max(0, 100 - failedPct - skippedPct);
571
+ const passedPct = totalTests > 0 ? (totalPassed / totalTests * 100) : 0;
572
+ const skippedPct = Math.max(0, 100 - failedPct - passedPct);
593
573
 
594
574
  const runBadge = runCount > 1
595
575
  ? '<span class="run-count-badge" title="Ran ' + runCount + ' times">×' + runCount + '</span>'
596
576
  : '';
597
577
 
598
578
  const metaText = runCount > 1
599
- ? totalTests + ' tests total · ' + totalFailed + ' failed · avg across ' + runCount + ' runs'
579
+ ? totalTests + ' tests total · ' + totalFailed + ' failed · accumulated across ' + runCount + ' runs'
600
580
  : totalTests + ' tests · ' + totalFailed + ' failed';
601
581
 
602
- return '<div class="suite-row" onclick="goToRun(\'' + firstRunId + '\')">' +
603
- '<div class="suite-name"><span>' + suiteName + '</span>' + runBadge + '</div>' +
582
+ // If multiple runs, navigate to history with search; otherwise go to single run
583
+ const clickHandler = runCount > 1
584
+ ? 'onclick="navigateToHistoryWithSearch(\'' + s.suite + '\')"'
585
+ : 'onclick="goToRun(\'' + s.run_id + '\')"';
586
+
587
+ return '<div class="suite-row" ' + clickHandler + '>' +
588
+ '<div class="suite-name"><span>' + s.suite + '</span>' + runBadge + '</div>' +
604
589
  '<div class="suite-meta">' + metaText + '</div>' +
605
590
  '<div class="suite-bar">' +
606
- '<div class="stacked-bar" title="Passed: ' + totalPassed + ' | Failed: ' + totalFailed + ' | Skipped: ' + totalSkipped + (runCount > 1 ? ' (accumulated)' : '') + '">' +
591
+ '<div class="stacked-bar" title="Passed: ' + totalPassed + ' | Failed: ' + totalFailed + (runCount > 1 ? ' (accumulated across ' + runCount + ' runs)' : '') + '">' +
607
592
  '<div class="stacked-green" style="width:' + passedPct.toFixed(1) + '%"></div>' +
608
593
  '<div class="stacked-red" style="width:' + failedPct.toFixed(1) + '%"></div>' +
609
594
  '<div class="stacked-grey" style="width:' + skippedPct.toFixed(1) + '%"></div>' +
610
595
  '</div>' +
611
596
  '</div>' +
612
- '<div class="suite-rate ' + rateColor(avgPassRate) + '">' + avgPassRate.toFixed(0) + '%</div>' +
597
+ '<div class="suite-rate ' + rateColor(passRate) + '">' + passRate.toFixed(0) + '%</div>' +
613
598
  '</div>';
614
599
  }).join('');
615
600
 
616
601
  const isOpen = i === 0;
617
602
  const regDateTime = reg.timestamp ? fmtDateTime(reg.timestamp) : '';
618
603
  const regTitle = regDateTime ? reg.tag + ' · ' + regDateTime : reg.tag;
619
- const uniqueSuiteCount = Object.keys(suiteGroups).length;
604
+ const uniqueSuiteCount = sortedSuites.length;
620
605
  return '<div class="regression">' +
621
606
  '<div class="reg-head" onclick="toggleReg(this)">' +
622
607
  '<div>' +
@@ -678,6 +663,44 @@
678
663
  if (runId) location.href = 'run.html?r=' + encodeURIComponent(runId) + '&p=' + encodeURIComponent(projectName);
679
664
  }
680
665
 
666
+ function navigateToHistoryWithSearch(fileName) {
667
+ // Find and click the Run History tab button
668
+ const historyBtn = Array.from(document.querySelectorAll('.tab-btn')).find(function(btn) {
669
+ return btn.textContent.trim() === 'Run History';
670
+ });
671
+
672
+ if (historyBtn) {
673
+ switchTab('history', historyBtn);
674
+ }
675
+
676
+ // Wait for tab to render, then set search value and trigger filter
677
+ setTimeout(function() {
678
+ const searchInput = document.getElementById('historySearch');
679
+ if (searchInput) {
680
+ searchInput.value = fileName;
681
+ filterHistory();
682
+ // Scroll to the table
683
+ searchInput.scrollIntoView({ behavior: 'smooth', block: 'start' });
684
+ }
685
+ }, 100);
686
+ }
687
+
688
+ function toggleTreemapCells(btn) {
689
+ const treemap = btn.previousElementSibling;
690
+ const hiddenCells = treemap.querySelectorAll('.treemap-cell-hidden');
691
+ const isExpanded = hiddenCells.length > 0 && hiddenCells[0].style.display === 'flex';
692
+
693
+ hiddenCells.forEach(function(cell) {
694
+ cell.style.display = isExpanded ? 'none' : 'flex';
695
+ });
696
+
697
+ if (isExpanded) {
698
+ btn.innerHTML = 'Show ' + hiddenCells.length + ' more ▼';
699
+ } else {
700
+ btn.innerHTML = 'Show less ▲';
701
+ }
702
+ }
703
+
681
704
  /* ═══════════════════════════════════════════════════
682
705
  ANALYSIS TAB
683
706
  ═══════════════════════════════════════════════════ */
@@ -725,7 +748,7 @@
725
748
  '<h3>Test File Reliability (Treemap)</h3>' +
726
749
  '<p class="section-explain">Each block = one file. Size ∝ test count. Color = pass rate. Worst files at top-left.</p>' +
727
750
  '</div>' +
728
- '<div class="section-body preview" onclick="toggleSectionExpand(this)">' +
751
+ '<div class="section-body">' +
729
752
  renderTreemap(a.file_health || []) +
730
753
  '</div>' +
731
754
  '</div>' +
@@ -794,7 +817,7 @@
794
817
  trend.forEach(function(d) { dataMap[d.date] = d.pass_rate; });
795
818
 
796
819
  // Calculate 15 weeks back from today
797
- const today = new Date('2026-06-07');
820
+ const today = new Date();
798
821
  const dayOfWeek = today.getDay(); // 0=Sun
799
822
  const weeksBack = 15;
800
823
  const totalDays = weeksBack * 7;
@@ -870,20 +893,26 @@
870
893
  // Sort worst first
871
894
  const sorted = files.slice().sort(function(a, b) { return a.pass_rate - b.pass_rate; });
872
895
  const maxTests = Math.max.apply(null, sorted.map(function(f) { return f.total_runs || 1; }));
896
+ const limit = 12;
873
897
 
874
- const cells = sorted.map(function(f) {
898
+ const cells = sorted.map(function(f, i) {
875
899
  const ratio = (f.total_runs || 1) / maxTests;
876
900
  const width = Math.round(80 + ratio * 160); // 80..240
877
901
  var bg = f.pass_rate >= 95 ? '#276749' : f.pass_rate >= 80 ? '#975a16' : '#9b2c2c';
878
902
  var bgLight = f.pass_rate >= 95 ? '#38a169' : f.pass_rate >= 80 ? '#d69e2e' : '#e53e3e';
879
- const clickAttr = f.last_run_id ? ' onclick="goToRun(\'' + f.last_run_id + '\')"' : '';
880
- return '<div class="treemap-cell" style="width:' + width + 'px;background:' + bgLight + ';" title="' + f.file_name + ': ' + f.pass_rate.toFixed(1) + '% pass rate (' + f.total_runs + ' runs)' + (f.last_run_id ? ' - Click to view latest run' : '') + '"' + clickAttr + '>' +
903
+ const hiddenClass = i >= limit ? ' treemap-cell-hidden' : '';
904
+ const clickAttr = ' onclick="navigateToHistoryWithSearch(\'' + f.file_name + '\')" style="cursor:pointer;"';
905
+ return '<div class="treemap-cell' + hiddenClass + '" style="width:' + width + 'px;background:' + bgLight + ';" title="' + f.file_name + ': ' + f.pass_rate.toFixed(1) + '% pass rate (' + f.total_runs + ' runs) - Click to view all runs"' + clickAttr + '>' +
881
906
  '<div class="treemap-rate">' + f.pass_rate.toFixed(0) + '%</div>' +
882
907
  '<div class="treemap-name">' + f.file_name + '</div>' +
883
908
  '</div>';
884
909
  }).join('');
885
910
 
886
- return '<div class="treemap">' + cells + '</div>';
911
+ const expandBtn = sorted.length > limit
912
+ ? '<div class="table-expand-btn" onclick="toggleTreemapCells(this)">Show ' + (sorted.length - limit) + ' more ▼</div>'
913
+ : '';
914
+
915
+ return '<div class="treemap">' + cells + '</div>' + expandBtn;
887
916
  }
888
917
 
889
918
  function renderFlakyTable(tests) {
@@ -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='3.7.1',
12
+ version='3.7.3',
13
13
  description='Multi-project test reporting dashboard — collect results locally or push to S3',
14
14
  long_description=long_description,
15
15
  long_description_content_type="text/markdown",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: test-reporting
3
- Version: 3.7.1
3
+ Version: 3.7.3
4
4
  Summary: Multi-project test reporting dashboard — collect results locally or push to S3
5
5
  Home-page: https://github.com/amahdy77/test-reporting.git
6
6
  Author: Ashfaqur Mahdy
File without changes
File without changes
File without changes