test-reporting 3.6.1__tar.gz → 3.6.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.
- {test_reporting-3.6.1/test_reporting.egg-info → test_reporting-3.6.3}/PKG-INFO +1 -1
- {test_reporting-3.6.1 → test_reporting-3.6.3}/reporting/plugin.py +3 -0
- {test_reporting-3.6.1 → test_reporting-3.6.3}/reporting/publisher.py +15 -5
- {test_reporting-3.6.1 → test_reporting-3.6.3}/reporting/storage.py +43 -3
- {test_reporting-3.6.1 → test_reporting-3.6.3}/reporting/templates/project.html +14 -2
- {test_reporting-3.6.1 → test_reporting-3.6.3}/reporting/templates/run.html +3 -1
- {test_reporting-3.6.1 → test_reporting-3.6.3}/setup.py +1 -1
- {test_reporting-3.6.1 → test_reporting-3.6.3/test_reporting.egg-info}/PKG-INFO +1 -1
- {test_reporting-3.6.1 → test_reporting-3.6.3}/LICENSE +0 -0
- {test_reporting-3.6.1 → test_reporting-3.6.3}/README.md +0 -0
- {test_reporting-3.6.1 → test_reporting-3.6.3}/reporting/__init__.py +0 -0
- {test_reporting-3.6.1 → test_reporting-3.6.3}/reporting/classifier.py +0 -0
- {test_reporting-3.6.1 → test_reporting-3.6.3}/reporting/cli.py +0 -0
- {test_reporting-3.6.1 → test_reporting-3.6.3}/reporting/config.py +0 -0
- {test_reporting-3.6.1 → test_reporting-3.6.3}/reporting/templates/index.html +0 -0
- {test_reporting-3.6.1 → test_reporting-3.6.3}/setup.cfg +0 -0
- {test_reporting-3.6.1 → test_reporting-3.6.3}/test_reporting.egg-info/SOURCES.txt +0 -0
- {test_reporting-3.6.1 → test_reporting-3.6.3}/test_reporting.egg-info/dependency_links.txt +0 -0
- {test_reporting-3.6.1 → test_reporting-3.6.3}/test_reporting.egg-info/entry_points.txt +0 -0
- {test_reporting-3.6.1 → test_reporting-3.6.3}/test_reporting.egg-info/requires.txt +0 -0
- {test_reporting-3.6.1 → test_reporting-3.6.3}/test_reporting.egg-info/top_level.txt +0 -0
|
@@ -248,6 +248,9 @@ class TestReportingPlugin:
|
|
|
248
248
|
logging.info(f"[Reporting] Published run: {run_id}")
|
|
249
249
|
except Exception as e:
|
|
250
250
|
logging.warning(f"[Reporting] Auto-publish failed: {e}")
|
|
251
|
+
|
|
252
|
+
# Close database connection to prevent ResourceWarning
|
|
253
|
+
self.storage.close()
|
|
251
254
|
|
|
252
255
|
def _save_latest_json(self):
|
|
253
256
|
"""Save latest run data as JSON for dashboard."""
|
|
@@ -493,8 +493,12 @@ class Publisher:
|
|
|
493
493
|
# Regression grouping
|
|
494
494
|
if tag not in regressions:
|
|
495
495
|
regressions[tag] = {'tag': tag, 'date': tag[:10], 'suites': [],
|
|
496
|
-
'total_tests': 0, 'passed': 0, 'failed': 0, 'pass_rate': 0
|
|
496
|
+
'total_tests': 0, 'passed': 0, 'failed': 0, 'pass_rate': 0,
|
|
497
|
+
'timestamp': ''}
|
|
497
498
|
reg = regressions[tag]
|
|
499
|
+
# Track the latest timestamp for this regression
|
|
500
|
+
if ts > reg.get('timestamp', ''):
|
|
501
|
+
reg['timestamp'] = ts
|
|
498
502
|
existing = next((s for s in reg['suites'] if s['suite'] == suite), None)
|
|
499
503
|
if existing:
|
|
500
504
|
if ts > existing.get('timestamp', ''):
|
|
@@ -525,8 +529,10 @@ class Publisher:
|
|
|
525
529
|
s['runs'] = sorted(s['runs'], key=lambda r: r['timestamp'], reverse=True)
|
|
526
530
|
s['runs'] = s['runs'][:self.config.DASHBOARD_MAX_RUNS_PER_SUITE]
|
|
527
531
|
|
|
528
|
-
# Sort regressions newest first
|
|
529
|
-
regressions = dict(sorted(regressions.items(),
|
|
532
|
+
# Sort regressions by timestamp (newest first), fallback to tag name
|
|
533
|
+
regressions = dict(sorted(regressions.items(),
|
|
534
|
+
key=lambda x: (x[1].get('timestamp', ''), x[0]),
|
|
535
|
+
reverse=True))
|
|
530
536
|
|
|
531
537
|
# Overall health from latest regression
|
|
532
538
|
last_reg = next(iter(regressions.values()), None) if regressions else None
|
|
@@ -745,7 +751,9 @@ class Publisher:
|
|
|
745
751
|
s3.put_object(Bucket=bucket, Key=dest_key, Body=f.read(),
|
|
746
752
|
ContentType='image/png')
|
|
747
753
|
print(f"[Reporting] ✓ Uploaded screenshot: {src.name}")
|
|
748
|
-
|
|
754
|
+
# Use full S3 URL so it works when dashboard is served from S3
|
|
755
|
+
s3_url = f"https://{bucket}.s3.{self.config.S3_REGION}.amazonaws.com/{dest_key}"
|
|
756
|
+
test['screenshot_path'] = s3_url
|
|
749
757
|
artifact_count += 1
|
|
750
758
|
else:
|
|
751
759
|
print(f"[Reporting] ✗ Screenshot not found: {src}")
|
|
@@ -761,7 +769,9 @@ class Publisher:
|
|
|
761
769
|
s3.put_object(Bucket=bucket, Key=dest_key, Body=f.read(),
|
|
762
770
|
ContentType='application/zip')
|
|
763
771
|
print(f"[Reporting] ✓ Uploaded trace: {src.name}")
|
|
764
|
-
|
|
772
|
+
# Use full S3 URL so it works when dashboard is served from S3
|
|
773
|
+
s3_url = f"https://{bucket}.s3.{self.config.S3_REGION}.amazonaws.com/{dest_key}"
|
|
774
|
+
test['trace_path'] = s3_url
|
|
765
775
|
artifact_count += 1
|
|
766
776
|
else:
|
|
767
777
|
print(f"[Reporting] ✗ Trace not found: {src}")
|
|
@@ -18,15 +18,49 @@ class TestResultStorage:
|
|
|
18
18
|
self.config = config or ReportingConfig()
|
|
19
19
|
self.db_path = db_path or self.config.DB_PATH
|
|
20
20
|
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
self._connection = None
|
|
21
22
|
self._init_database()
|
|
22
23
|
|
|
24
|
+
def __enter__(self):
|
|
25
|
+
"""Context manager entry."""
|
|
26
|
+
return self
|
|
27
|
+
|
|
28
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
29
|
+
"""Context manager exit - ensure connection is closed."""
|
|
30
|
+
self.close()
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
def close(self):
|
|
34
|
+
"""Close any open database connections."""
|
|
35
|
+
if self._connection is not None:
|
|
36
|
+
try:
|
|
37
|
+
self._connection.close()
|
|
38
|
+
except Exception:
|
|
39
|
+
pass
|
|
40
|
+
finally:
|
|
41
|
+
self._connection = None
|
|
42
|
+
|
|
43
|
+
def _execute_with_connection(self, func):
|
|
44
|
+
"""Execute a function with a database connection, ensuring it's closed."""
|
|
45
|
+
conn = sqlite3.connect(self.db_path)
|
|
46
|
+
try:
|
|
47
|
+
result = func(conn)
|
|
48
|
+
conn.commit()
|
|
49
|
+
return result
|
|
50
|
+
finally:
|
|
51
|
+
conn.close()
|
|
52
|
+
|
|
23
53
|
def _init_database(self):
|
|
24
54
|
"""Initialize database with WAL mode and create tables."""
|
|
25
|
-
|
|
55
|
+
conn = sqlite3.connect(self.db_path)
|
|
56
|
+
try:
|
|
26
57
|
# Enable WAL mode for concurrent access
|
|
27
58
|
conn.execute('PRAGMA journal_mode=WAL')
|
|
28
59
|
conn.execute('PRAGMA busy_timeout=5000') # Wait 5s if locked
|
|
29
60
|
self._create_tables(conn)
|
|
61
|
+
conn.commit()
|
|
62
|
+
finally:
|
|
63
|
+
conn.close()
|
|
30
64
|
|
|
31
65
|
def _create_tables(self, conn: sqlite3.Connection):
|
|
32
66
|
"""Create database tables if they don't exist."""
|
|
@@ -156,7 +190,8 @@ class TestResultStorage:
|
|
|
156
190
|
|
|
157
191
|
def save_test_run(self, run_data: Dict[str, Any]) -> int:
|
|
158
192
|
"""Save a test run and return the run ID."""
|
|
159
|
-
|
|
193
|
+
conn = sqlite3.connect(self.db_path)
|
|
194
|
+
try:
|
|
160
195
|
cursor = conn.execute('''
|
|
161
196
|
INSERT INTO test_runs (
|
|
162
197
|
project_name, timestamp, suite_name, build_number, build_url, run_tag,
|
|
@@ -180,10 +215,13 @@ class TestResultStorage:
|
|
|
180
215
|
))
|
|
181
216
|
conn.commit()
|
|
182
217
|
return cursor.lastrowid
|
|
218
|
+
finally:
|
|
219
|
+
conn.close()
|
|
183
220
|
|
|
184
221
|
def save_test_result(self, run_id: int, result_data: Dict[str, Any]) -> int:
|
|
185
222
|
"""Save a test result and return the result ID."""
|
|
186
|
-
|
|
223
|
+
conn = sqlite3.connect(self.db_path)
|
|
224
|
+
try:
|
|
187
225
|
cursor = conn.execute('''
|
|
188
226
|
INSERT INTO test_results (
|
|
189
227
|
run_id, project_name, file_name, test_name, full_name, status,
|
|
@@ -211,6 +249,8 @@ class TestResultStorage:
|
|
|
211
249
|
))
|
|
212
250
|
conn.commit()
|
|
213
251
|
return cursor.lastrowid
|
|
252
|
+
finally:
|
|
253
|
+
conn.close()
|
|
214
254
|
|
|
215
255
|
def save_test_step(self, result_id: int, step_data: Dict[str, Any]):
|
|
216
256
|
"""Save a test step."""
|
|
@@ -392,6 +392,14 @@
|
|
|
392
392
|
return new Date(iso).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', hour12: false });
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
+
function fmtDateTime(iso) {
|
|
396
|
+
if (!iso) return '-';
|
|
397
|
+
const date = new Date(iso);
|
|
398
|
+
const dateStr = date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
|
|
399
|
+
const timeStr = date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', hour12: false });
|
|
400
|
+
return dateStr + ' ' + timeStr;
|
|
401
|
+
}
|
|
402
|
+
|
|
395
403
|
function fmtDur(s) {
|
|
396
404
|
if (!s || s === 0) return '-';
|
|
397
405
|
if (s >= 60) return Math.floor(s / 60) + 'm ' + Math.floor(s % 60) + 's';
|
|
@@ -553,10 +561,12 @@
|
|
|
553
561
|
}).join('');
|
|
554
562
|
|
|
555
563
|
const isOpen = i === 0;
|
|
564
|
+
const regDateTime = reg.timestamp ? fmtDateTime(reg.timestamp) : '';
|
|
565
|
+
const regTitle = regDateTime ? reg.tag + ' · ' + regDateTime : reg.tag;
|
|
556
566
|
return '<div class="regression">' +
|
|
557
567
|
'<div class="reg-head" onclick="toggleReg(this)">' +
|
|
558
568
|
'<div>' +
|
|
559
|
-
'<div class="reg-title">' +
|
|
569
|
+
'<div class="reg-title">' + regTitle + '</div>' +
|
|
560
570
|
'<div class="reg-sub">' + reg.suites.length + ' test ' + plural(reg.suites.length, 'file') + ' · ' + reg.total_tests + ' tests</div>' +
|
|
561
571
|
'</div>' +
|
|
562
572
|
'<div class="reg-right">' +
|
|
@@ -1207,7 +1217,9 @@
|
|
|
1207
1217
|
}
|
|
1208
1218
|
|
|
1209
1219
|
document.getElementById('pageTitle').textContent = proj.name;
|
|
1210
|
-
document.getElementById('lastRun')
|
|
1220
|
+
const lastRunEl = document.getElementById('lastRun');
|
|
1221
|
+
lastRunEl.textContent = 'Last run ' + relTime(proj.last_run);
|
|
1222
|
+
lastRunEl.title = 'All times shown in your local timezone';
|
|
1211
1223
|
document.title = proj.name + ' - Test Dashboard';
|
|
1212
1224
|
|
|
1213
1225
|
const analytics = proj.analytics || {};
|
|
@@ -413,7 +413,9 @@
|
|
|
413
413
|
document.title = (run.suite_name || 'Run') + ' - Test Dashboard';
|
|
414
414
|
document.getElementById('pageTitle').textContent =
|
|
415
415
|
(run.suite_name || 'Run') + ' · ' + (run.project_name || '');
|
|
416
|
-
document.getElementById('runTime')
|
|
416
|
+
var runTimeEl = document.getElementById('runTime');
|
|
417
|
+
runTimeEl.textContent = fmtDate(run.timestamp);
|
|
418
|
+
runTimeEl.title = 'Time shown in your local timezone';
|
|
417
419
|
|
|
418
420
|
var backHref = projectName
|
|
419
421
|
? 'project.html?p=' + encodeURIComponent(projectName)
|
|
@@ -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.6.
|
|
12
|
+
version='3.6.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",
|
|
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
|