test-reporting 3.7.2__tar.gz → 3.7.4__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-3.7.2 → test_reporting-3.7.4}/PKG-INFO +1 -1
- {test_reporting-3.7.2 → test_reporting-3.7.4}/reporting/publisher.py +35 -17
- {test_reporting-3.7.2 → test_reporting-3.7.4}/reporting/templates/project.html +45 -52
- {test_reporting-3.7.2 → test_reporting-3.7.4}/setup.py +1 -1
- {test_reporting-3.7.2 → test_reporting-3.7.4}/test_reporting.egg-info/PKG-INFO +1 -1
- {test_reporting-3.7.2 → test_reporting-3.7.4}/LICENSE +0 -0
- {test_reporting-3.7.2 → test_reporting-3.7.4}/README.md +0 -0
- {test_reporting-3.7.2 → test_reporting-3.7.4}/reporting/__init__.py +0 -0
- {test_reporting-3.7.2 → test_reporting-3.7.4}/reporting/classifier.py +0 -0
- {test_reporting-3.7.2 → test_reporting-3.7.4}/reporting/cli.py +0 -0
- {test_reporting-3.7.2 → test_reporting-3.7.4}/reporting/config.py +0 -0
- {test_reporting-3.7.2 → test_reporting-3.7.4}/reporting/plugin.py +0 -0
- {test_reporting-3.7.2 → test_reporting-3.7.4}/reporting/storage.py +0 -0
- {test_reporting-3.7.2 → test_reporting-3.7.4}/reporting/templates/index.html +0 -0
- {test_reporting-3.7.2 → test_reporting-3.7.4}/reporting/templates/run.html +0 -0
- {test_reporting-3.7.2 → test_reporting-3.7.4}/setup.cfg +0 -0
- {test_reporting-3.7.2 → test_reporting-3.7.4}/test_reporting.egg-info/SOURCES.txt +0 -0
- {test_reporting-3.7.2 → test_reporting-3.7.4}/test_reporting.egg-info/dependency_links.txt +0 -0
- {test_reporting-3.7.2 → test_reporting-3.7.4}/test_reporting.egg-info/entry_points.txt +0 -0
- {test_reporting-3.7.2 → test_reporting-3.7.4}/test_reporting.egg-info/requires.txt +0 -0
- {test_reporting-3.7.2 → test_reporting-3.7.4}/test_reporting.egg-info/top_level.txt +0 -0
|
@@ -473,7 +473,7 @@ class Publisher:
|
|
|
473
473
|
tag = run.get('regression_tag') or self._date_tag(run.get('timestamp'))
|
|
474
474
|
ts = run.get('timestamp', '')
|
|
475
475
|
|
|
476
|
-
# Suite summary
|
|
476
|
+
# Suite summary (kept for backwards compatibility)
|
|
477
477
|
if suite not in suites:
|
|
478
478
|
suites[suite] = {'name': suite, 'last_run': ts,
|
|
479
479
|
'last_pass_rate': 0, 'runs': []}
|
|
@@ -494,7 +494,7 @@ class Publisher:
|
|
|
494
494
|
'duration_seconds': run.get('duration_seconds', 0),
|
|
495
495
|
})
|
|
496
496
|
|
|
497
|
-
# Regression grouping
|
|
497
|
+
# Regression grouping - group by actual test files, not suite names
|
|
498
498
|
if tag not in regressions:
|
|
499
499
|
regressions[tag] = {'tag': tag, 'date': tag[:10], 'suites': [],
|
|
500
500
|
'total_tests': 0, 'passed': 0, 'failed': 0, 'pass_rate': 0,
|
|
@@ -503,21 +503,39 @@ class Publisher:
|
|
|
503
503
|
# Track the latest timestamp for this regression
|
|
504
504
|
if ts > reg.get('timestamp', ''):
|
|
505
505
|
reg['timestamp'] = ts
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
506
|
+
|
|
507
|
+
# Process each test file in this run
|
|
508
|
+
for test_file in run.get('test_files', []):
|
|
509
|
+
file_name = test_file.get('file_name', 'unknown')
|
|
510
|
+
# Strip .py extension for consistency
|
|
511
|
+
base_file_name = file_name.replace('.py', '')
|
|
512
|
+
|
|
513
|
+
existing = next((s for s in reg['suites'] if s['suite'] == base_file_name), None)
|
|
514
|
+
if existing:
|
|
515
|
+
# Accumulate stats for multiple runs of the same file
|
|
516
|
+
existing['total_tests'] += test_file.get('total', 0)
|
|
517
|
+
existing['passed'] += test_file.get('passed', 0)
|
|
518
|
+
existing['failed'] += test_file.get('failed', 0)
|
|
519
|
+
existing['run_count'] = existing.get('run_count', 1) + 1
|
|
520
|
+
# Update to latest timestamp and run_id
|
|
521
|
+
if ts > existing.get('timestamp', ''):
|
|
522
|
+
existing['timestamp'] = ts
|
|
523
|
+
existing['run_id'] = run.get('run_id')
|
|
524
|
+
# Recalculate pass rate based on accumulated totals
|
|
525
|
+
if existing['total_tests'] > 0:
|
|
526
|
+
existing['pass_rate'] = round((existing['passed'] / existing['total_tests']) * 100, 1)
|
|
527
|
+
else:
|
|
528
|
+
file_total = test_file.get('total', 0)
|
|
529
|
+
file_passed = test_file.get('passed', 0)
|
|
530
|
+
file_failed = test_file.get('failed', 0)
|
|
531
|
+
file_pass_rate = round((file_passed / file_total * 100) if file_total > 0 else 0, 1)
|
|
532
|
+
reg['suites'].append({'suite': base_file_name, 'timestamp': ts,
|
|
533
|
+
'run_id': run.get('run_id'),
|
|
534
|
+
'pass_rate': file_pass_rate,
|
|
535
|
+
'total_tests': file_total,
|
|
536
|
+
'passed': file_passed,
|
|
537
|
+
'failed': file_failed,
|
|
538
|
+
'run_count': 1})
|
|
521
539
|
|
|
522
540
|
# Compute regression combined stats
|
|
523
541
|
for reg in regressions.values():
|
|
@@ -555,75 +555,53 @@
|
|
|
555
555
|
const rc2 = rateColor(reg.pass_rate);
|
|
556
556
|
const total = lastReg ? lastReg.total_tests || 1 : 1;
|
|
557
557
|
|
|
558
|
-
//
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
(reg.suites || []).forEach(function(s) {
|
|
562
|
-
// Remove [TAG] suffix like [RERUN], [RETRY], etc.
|
|
563
|
-
const baseName = s.suite.replace(/\s*\[.*?\]\s*$/, '');
|
|
564
|
-
if (!suiteGroups[baseName]) {
|
|
565
|
-
suiteGroups[baseName] = [];
|
|
566
|
-
}
|
|
567
|
-
suiteGroups[baseName].push(s);
|
|
558
|
+
// Sort suites alphabetically
|
|
559
|
+
const sortedSuites = (reg.suites || []).slice().sort(function(a, b) {
|
|
560
|
+
return a.suite.localeCompare(b.suite);
|
|
568
561
|
});
|
|
569
562
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
const
|
|
575
|
-
const
|
|
576
|
-
|
|
577
|
-
// Calculate accumulated averages
|
|
578
|
-
let totalTests = 0;
|
|
579
|
-
let totalFailed = 0;
|
|
580
|
-
let totalSkipped = 0;
|
|
581
|
-
let totalPassed = 0;
|
|
582
|
-
let avgPassRate = 0;
|
|
583
|
-
|
|
584
|
-
runs.forEach(function(run) {
|
|
585
|
-
totalTests += run.total_tests;
|
|
586
|
-
totalFailed += run.failed;
|
|
587
|
-
totalSkipped += (run.skipped || 0);
|
|
588
|
-
totalPassed += (run.total_tests - run.failed - (run.skipped || 0));
|
|
589
|
-
avgPassRate += run.pass_rate;
|
|
590
|
-
});
|
|
591
|
-
|
|
592
|
-
avgPassRate = avgPassRate / runCount;
|
|
593
|
-
|
|
594
|
-
// Use first run's ID for navigation
|
|
595
|
-
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;
|
|
596
569
|
|
|
597
570
|
const failedPct = totalTests > 0 ? (totalFailed / totalTests * 100) : 0;
|
|
598
|
-
const
|
|
599
|
-
const
|
|
571
|
+
const passedPct = totalTests > 0 ? (totalPassed / totalTests * 100) : 0;
|
|
572
|
+
const skippedPct = Math.max(0, 100 - failedPct - passedPct);
|
|
600
573
|
|
|
601
574
|
const runBadge = runCount > 1
|
|
602
|
-
? '<span class="run-count-badge" title="Ran ' + runCount + ' times">×' + runCount + '</span>'
|
|
575
|
+
? '<span class="run-count-badge" title="Ran ' + runCount + ' times in this regression">×' + runCount + '</span>'
|
|
603
576
|
: '';
|
|
604
577
|
|
|
605
578
|
const metaText = runCount > 1
|
|
606
|
-
? totalTests + ' tests total · ' + totalFailed + ' failed ·
|
|
579
|
+
? totalTests + ' tests total · ' + totalFailed + ' failed · accumulated across ' + runCount + ' runs'
|
|
607
580
|
: totalTests + ' tests · ' + totalFailed + ' failed';
|
|
608
581
|
|
|
609
|
-
|
|
610
|
-
|
|
582
|
+
// If multiple runs, navigate to history with search; otherwise go to single run
|
|
583
|
+
const clickHandler = runCount > 1
|
|
584
|
+
? 'onclick="navigateToHistoryWithSearch(\'' + s.suite + '\', \'' + reg.tag + '\')"'
|
|
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>' +
|
|
611
589
|
'<div class="suite-meta">' + metaText + '</div>' +
|
|
612
590
|
'<div class="suite-bar">' +
|
|
613
|
-
'<div class="stacked-bar" title="Passed: ' + totalPassed + ' | Failed: ' + totalFailed +
|
|
591
|
+
'<div class="stacked-bar" title="Passed: ' + totalPassed + ' | Failed: ' + totalFailed + (runCount > 1 ? ' (accumulated across ' + runCount + ' runs)' : '') + '">' +
|
|
614
592
|
'<div class="stacked-green" style="width:' + passedPct.toFixed(1) + '%"></div>' +
|
|
615
593
|
'<div class="stacked-red" style="width:' + failedPct.toFixed(1) + '%"></div>' +
|
|
616
594
|
'<div class="stacked-grey" style="width:' + skippedPct.toFixed(1) + '%"></div>' +
|
|
617
595
|
'</div>' +
|
|
618
596
|
'</div>' +
|
|
619
|
-
'<div class="suite-rate ' + rateColor(
|
|
597
|
+
'<div class="suite-rate ' + rateColor(passRate) + '">' + passRate.toFixed(0) + '%</div>' +
|
|
620
598
|
'</div>';
|
|
621
599
|
}).join('');
|
|
622
600
|
|
|
623
601
|
const isOpen = i === 0;
|
|
624
602
|
const regDateTime = reg.timestamp ? fmtDateTime(reg.timestamp) : '';
|
|
625
603
|
const regTitle = regDateTime ? reg.tag + ' · ' + regDateTime : reg.tag;
|
|
626
|
-
const uniqueSuiteCount =
|
|
604
|
+
const uniqueSuiteCount = sortedSuites.length;
|
|
627
605
|
return '<div class="regression">' +
|
|
628
606
|
'<div class="reg-head" onclick="toggleReg(this)">' +
|
|
629
607
|
'<div>' +
|
|
@@ -685,14 +663,23 @@
|
|
|
685
663
|
if (runId) location.href = 'run.html?r=' + encodeURIComponent(runId) + '&p=' + encodeURIComponent(projectName);
|
|
686
664
|
}
|
|
687
665
|
|
|
688
|
-
function navigateToHistoryWithSearch(fileName) {
|
|
689
|
-
//
|
|
690
|
-
|
|
666
|
+
function navigateToHistoryWithSearch(fileName, tag) {
|
|
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
|
+
|
|
691
676
|
// Wait for tab to render, then set search value and trigger filter
|
|
692
677
|
setTimeout(function() {
|
|
693
678
|
const searchInput = document.getElementById('historySearch');
|
|
694
679
|
if (searchInput) {
|
|
695
|
-
|
|
680
|
+
// Combine file name and tag for search
|
|
681
|
+
const searchValue = tag ? fileName + ' ' + tag : fileName;
|
|
682
|
+
searchInput.value = searchValue;
|
|
696
683
|
filterHistory();
|
|
697
684
|
// Scroll to the table
|
|
698
685
|
searchInput.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
@@ -916,7 +903,9 @@
|
|
|
916
903
|
var bg = f.pass_rate >= 95 ? '#276749' : f.pass_rate >= 80 ? '#975a16' : '#9b2c2c';
|
|
917
904
|
var bgLight = f.pass_rate >= 95 ? '#38a169' : f.pass_rate >= 80 ? '#d69e2e' : '#e53e3e';
|
|
918
905
|
const hiddenClass = i >= limit ? ' treemap-cell-hidden' : '';
|
|
919
|
-
|
|
906
|
+
// Strip .py extension for search to match display format
|
|
907
|
+
const searchName = f.file_name.replace(/\.py$/, '');
|
|
908
|
+
const clickAttr = ' onclick="navigateToHistoryWithSearch(\'' + searchName + '\')" style="cursor:pointer;"';
|
|
920
909
|
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 + '>' +
|
|
921
910
|
'<div class="treemap-rate">' + f.pass_rate.toFixed(0) + '%</div>' +
|
|
922
911
|
'<div class="treemap-name">' + f.file_name + '</div>' +
|
|
@@ -1048,7 +1037,7 @@
|
|
|
1048
1037
|
'<span style="font-size:13px;color:var(--muted)"><span id="visibleRunCount">' + recentRuns.length + '</span> of ' + recentRuns.length + ' runs across ' + names.length + ' test ' + plural(names.length, 'file') + '</span>' +
|
|
1049
1038
|
'</div>' +
|
|
1050
1039
|
'<div class="search-wrap">' +
|
|
1051
|
-
'<input type="text" id="historySearch" class="search-input" placeholder="Search by test file name..." oninput="filterHistory()" />' +
|
|
1040
|
+
'<input type="text" id="historySearch" class="search-input" placeholder="Search by test file name or tag..." oninput="filterHistory()" />' +
|
|
1052
1041
|
'</div>' +
|
|
1053
1042
|
'<div class="section-body-flush">' +
|
|
1054
1043
|
'<table class="hist-table" id="historyTable">' +
|
|
@@ -1126,9 +1115,13 @@
|
|
|
1126
1115
|
|
|
1127
1116
|
rows.forEach(function(row) {
|
|
1128
1117
|
const fileNameCell = row.querySelector('td:nth-child(3)');
|
|
1118
|
+
const tagCell = row.querySelector('td:nth-child(4)');
|
|
1129
1119
|
if (fileNameCell) {
|
|
1130
1120
|
const fileName = fileNameCell.textContent.toLowerCase();
|
|
1131
|
-
|
|
1121
|
+
const tag = tagCell ? tagCell.textContent.toLowerCase() : '';
|
|
1122
|
+
// Match if search term is in file name OR tag
|
|
1123
|
+
const combinedText = fileName + ' ' + tag;
|
|
1124
|
+
if (combinedText.includes(searchTerm)) {
|
|
1132
1125
|
row.style.display = '';
|
|
1133
1126
|
visibleCount++;
|
|
1134
1127
|
} else {
|
|
@@ -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.
|
|
12
|
+
version='3.7.4',
|
|
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",
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|