test-reporting 3.3.0__tar.gz → 3.5.0__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.3.0 → test_reporting-3.5.0}/PKG-INFO +1 -1
- {test_reporting-3.3.0 → test_reporting-3.5.0}/reporting/plugin.py +18 -87
- {test_reporting-3.3.0 → test_reporting-3.5.0}/reporting/storage.py +5 -8
- {test_reporting-3.3.0 → test_reporting-3.5.0}/reporting/templates/project.html +7 -0
- {test_reporting-3.3.0 → test_reporting-3.5.0}/setup.py +1 -1
- {test_reporting-3.3.0 → test_reporting-3.5.0}/test_reporting.egg-info/PKG-INFO +1 -1
- {test_reporting-3.3.0 → test_reporting-3.5.0}/LICENSE +0 -0
- {test_reporting-3.3.0 → test_reporting-3.5.0}/README.md +0 -0
- {test_reporting-3.3.0 → test_reporting-3.5.0}/reporting/__init__.py +0 -0
- {test_reporting-3.3.0 → test_reporting-3.5.0}/reporting/classifier.py +0 -0
- {test_reporting-3.3.0 → test_reporting-3.5.0}/reporting/cli.py +0 -0
- {test_reporting-3.3.0 → test_reporting-3.5.0}/reporting/config.py +0 -0
- {test_reporting-3.3.0 → test_reporting-3.5.0}/reporting/publisher.py +0 -0
- {test_reporting-3.3.0 → test_reporting-3.5.0}/reporting/templates/index.html +0 -0
- {test_reporting-3.3.0 → test_reporting-3.5.0}/reporting/templates/run.html +0 -0
- {test_reporting-3.3.0 → test_reporting-3.5.0}/setup.cfg +0 -0
- {test_reporting-3.3.0 → test_reporting-3.5.0}/test_reporting.egg-info/SOURCES.txt +0 -0
- {test_reporting-3.3.0 → test_reporting-3.5.0}/test_reporting.egg-info/dependency_links.txt +0 -0
- {test_reporting-3.3.0 → test_reporting-3.5.0}/test_reporting.egg-info/entry_points.txt +0 -0
- {test_reporting-3.3.0 → test_reporting-3.5.0}/test_reporting.egg-info/requires.txt +0 -0
- {test_reporting-3.3.0 → test_reporting-3.5.0}/test_reporting.egg-info/top_level.txt +0 -0
|
@@ -23,13 +23,8 @@ class TestReportingPlugin:
|
|
|
23
23
|
self.config = ReportingConfig()
|
|
24
24
|
self.storage = TestResultStorage(config=self.config)
|
|
25
25
|
|
|
26
|
-
#
|
|
27
|
-
self.
|
|
28
|
-
self.append_mode = os.getenv('TEST_APPEND_MODE', 'false').lower() == 'true'
|
|
29
|
-
self.attempt_number = int(os.getenv('TEST_ATTEMPT_NUMBER', '1'))
|
|
30
|
-
|
|
31
|
-
# Debug logging
|
|
32
|
-
logging.info(f"[Reporting] Plugin initialized - Session ID: {self.session_id}, Append Mode: {self.append_mode}, Attempt: {self.attempt_number}")
|
|
26
|
+
# Check for run tag (e.g., 'RERUN')
|
|
27
|
+
self.run_tag = os.getenv('TEST_RUN_TAG', None)
|
|
33
28
|
|
|
34
29
|
self.run_data = {
|
|
35
30
|
'project_name': self.config.PROJECT_NAME,
|
|
@@ -37,49 +32,35 @@ class TestReportingPlugin:
|
|
|
37
32
|
'suite_name': None,
|
|
38
33
|
'build_number': os.getenv('BUILD_NUMBER', 'local'),
|
|
39
34
|
'build_url': os.getenv('BUILD_URL', ''),
|
|
35
|
+
'run_tag': self.run_tag, # Add tag to run data
|
|
40
36
|
'tests': [],
|
|
41
37
|
'total_tests': 0,
|
|
42
38
|
'passed': 0,
|
|
43
39
|
'failed': 0,
|
|
44
40
|
'skipped': 0,
|
|
45
41
|
'duration_seconds': 0,
|
|
46
|
-
'session_id': self.session_id,
|
|
47
|
-
'is_append_mode': self.append_mode,
|
|
48
42
|
}
|
|
49
43
|
self.run_id = None
|
|
50
44
|
self.test_start_times = {}
|
|
51
45
|
self.test_logs = {} # Store logs per test
|
|
52
|
-
self.existing_results = {} # Store existing test results in append mode
|
|
53
46
|
|
|
54
47
|
@pytest.hookimpl(tryfirst=True)
|
|
55
48
|
def pytest_sessionstart(self, session):
|
|
56
49
|
"""Called at the start of the test session."""
|
|
57
50
|
self.config.ensure_directories()
|
|
58
51
|
|
|
59
|
-
# Determine suite name from
|
|
60
|
-
|
|
61
|
-
if suite_name_from_env:
|
|
62
|
-
self.run_data['suite_name'] = suite_name_from_env
|
|
63
|
-
elif hasattr(session.config, 'args') and session.config.args:
|
|
52
|
+
# Determine suite name from pytest argument
|
|
53
|
+
if hasattr(session.config, 'args') and session.config.args:
|
|
64
54
|
first_arg = session.config.args[0]
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
self.run_id = existing_run_id
|
|
72
|
-
self.existing_results = self.storage.get_existing_test_results(self.run_id)
|
|
73
|
-
logging.info(f"[Reporting] Appending to existing run ID: {self.run_id} (session: {self.session_id}, attempt: {self.attempt_number})")
|
|
55
|
+
suite_name = Path(first_arg).stem
|
|
56
|
+
|
|
57
|
+
# Add tag to suite name if present (e.g., "test_payments [RERUN]")
|
|
58
|
+
if self.run_tag:
|
|
59
|
+
self.run_data['suite_name'] = f"{suite_name} [{self.run_tag}]"
|
|
60
|
+
logging.info(f"[Reporting] Starting test run: {suite_name} (tagged as {self.run_tag})")
|
|
74
61
|
else:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
# Auto-generate session ID if not provided
|
|
78
|
-
self.session_id = str(uuid.uuid4())
|
|
79
|
-
self.run_data['session_id'] = self.session_id
|
|
80
|
-
logging.info(f"[Reporting] Starting test run: {self.run_data['suite_name']} (session: {self.session_id})")
|
|
81
|
-
else:
|
|
82
|
-
logging.info(f"[Reporting] Starting test run: {self.run_data['suite_name']}")
|
|
62
|
+
self.run_data['suite_name'] = suite_name
|
|
63
|
+
logging.info(f"[Reporting] Starting test run: {suite_name}")
|
|
83
64
|
|
|
84
65
|
@pytest.hookimpl(hookwrapper=True)
|
|
85
66
|
def pytest_runtest_protocol(self, item, nextitem):
|
|
@@ -217,10 +198,6 @@ class TestReportingPlugin:
|
|
|
217
198
|
@pytest.hookimpl(trylast=True)
|
|
218
199
|
def pytest_sessionfinish(self, session, exitstatus):
|
|
219
200
|
"""Called at the end of the test session."""
|
|
220
|
-
# In append mode, merge with existing results
|
|
221
|
-
if self.append_mode and self.run_id and self.existing_results:
|
|
222
|
-
self._merge_with_existing_results()
|
|
223
|
-
|
|
224
201
|
# Calculate final metrics
|
|
225
202
|
total = self.run_data['total_tests']
|
|
226
203
|
|
|
@@ -249,22 +226,11 @@ class TestReportingPlugin:
|
|
|
249
226
|
self.run_data['pass_rate'] = 0
|
|
250
227
|
self.run_data['functional_pass_rate'] = 0
|
|
251
228
|
|
|
252
|
-
# Save to database
|
|
253
|
-
|
|
254
|
-
# Update existing run
|
|
255
|
-
self.storage.update_run_metrics(self.run_id, self.run_data)
|
|
256
|
-
else:
|
|
257
|
-
# Create new run
|
|
258
|
-
self.run_id = self.storage.save_test_run(self.run_data)
|
|
229
|
+
# Save to database - always create a new run
|
|
230
|
+
self.run_id = self.storage.save_test_run(self.run_data)
|
|
259
231
|
|
|
260
|
-
# Save individual test results
|
|
232
|
+
# Save individual test results
|
|
261
233
|
for test_result in self.run_data['tests']:
|
|
262
|
-
# Skip if this test already exists and hasn't changed
|
|
263
|
-
if self.append_mode and test_result['full_name'] in self.existing_results:
|
|
264
|
-
existing = self.existing_results[test_result['full_name']]
|
|
265
|
-
# Only save if it's a new attempt
|
|
266
|
-
if test_result.get('attempt_number', 1) <= existing.get('attempt_number', 1):
|
|
267
|
-
continue
|
|
268
234
|
|
|
269
235
|
result_id = self.storage.save_test_result(self.run_id, test_result)
|
|
270
236
|
|
|
@@ -274,8 +240,8 @@ class TestReportingPlugin:
|
|
|
274
240
|
self._save_latest_json()
|
|
275
241
|
|
|
276
242
|
logging.info(f"[Reporting] Test run complete. Run ID: {self.run_id}")
|
|
277
|
-
if self.
|
|
278
|
-
logging.info(f"[Reporting]
|
|
243
|
+
if self.run_tag:
|
|
244
|
+
logging.info(f"[Reporting] Run tagged as: {self.run_tag}")
|
|
279
245
|
logging.info(f"[Reporting] Pass Rate: {self.run_data['pass_rate']:.1f}%")
|
|
280
246
|
logging.info(f"[Reporting] Functional Pass Rate: {self.run_data['functional_pass_rate']:.1f}%")
|
|
281
247
|
|
|
@@ -288,41 +254,6 @@ class TestReportingPlugin:
|
|
|
288
254
|
except Exception as e:
|
|
289
255
|
logging.warning(f"[Reporting] Auto-publish failed: {e}")
|
|
290
256
|
|
|
291
|
-
def _merge_with_existing_results(self):
|
|
292
|
-
"""Merge current test results with existing results from previous attempts."""
|
|
293
|
-
# Create a map of current test results by full_name
|
|
294
|
-
current_tests = {t['full_name']: t for t in self.run_data['tests']}
|
|
295
|
-
|
|
296
|
-
# Add existing tests that weren't run in this attempt
|
|
297
|
-
for full_name, existing_test in self.existing_results.items():
|
|
298
|
-
if full_name not in current_tests:
|
|
299
|
-
# Convert DB result to test result format
|
|
300
|
-
test_result = {
|
|
301
|
-
'project_name': existing_test['project_name'],
|
|
302
|
-
'file_name': existing_test['file_name'],
|
|
303
|
-
'test_name': existing_test['test_name'],
|
|
304
|
-
'full_name': existing_test['full_name'],
|
|
305
|
-
'status': existing_test['status'],
|
|
306
|
-
'duration_seconds': existing_test['duration_seconds'],
|
|
307
|
-
'error_message': existing_test.get('error_message'),
|
|
308
|
-
'error_type': existing_test.get('error_type'),
|
|
309
|
-
'screenshot_path': existing_test.get('screenshot_path'),
|
|
310
|
-
'trace_path': existing_test.get('trace_path'),
|
|
311
|
-
'logs': existing_test.get('logs'),
|
|
312
|
-
'attempt_number': existing_test.get('attempt_number', 1),
|
|
313
|
-
'is_retry': existing_test.get('is_retry', False),
|
|
314
|
-
'failure_type': existing_test.get('failure_type'),
|
|
315
|
-
'failure_category': existing_test.get('failure_category'),
|
|
316
|
-
'is_soft_fail': existing_test.get('is_soft_fail', False),
|
|
317
|
-
}
|
|
318
|
-
self.run_data['tests'].append(test_result)
|
|
319
|
-
|
|
320
|
-
# Recalculate totals
|
|
321
|
-
self.run_data['total_tests'] = len(self.run_data['tests'])
|
|
322
|
-
self.run_data['passed'] = sum(1 for t in self.run_data['tests'] if t['status'] == 'passed')
|
|
323
|
-
self.run_data['failed'] = sum(1 for t in self.run_data['tests'] if t['status'] == 'failed')
|
|
324
|
-
self.run_data['skipped'] = sum(1 for t in self.run_data['tests'] if t['status'] == 'skipped')
|
|
325
|
-
|
|
326
257
|
def _save_latest_json(self):
|
|
327
258
|
"""Save latest run data as JSON for dashboard."""
|
|
328
259
|
output = {
|
|
@@ -39,6 +39,7 @@ class TestResultStorage:
|
|
|
39
39
|
suite_name TEXT,
|
|
40
40
|
build_number TEXT,
|
|
41
41
|
build_url TEXT,
|
|
42
|
+
run_tag TEXT,
|
|
42
43
|
total_tests INTEGER,
|
|
43
44
|
passed INTEGER,
|
|
44
45
|
failed INTEGER,
|
|
@@ -46,8 +47,6 @@ class TestResultStorage:
|
|
|
46
47
|
duration_seconds REAL,
|
|
47
48
|
pass_rate REAL,
|
|
48
49
|
functional_pass_rate REAL,
|
|
49
|
-
session_id TEXT,
|
|
50
|
-
is_append_mode BOOLEAN DEFAULT 0,
|
|
51
50
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
52
51
|
)
|
|
53
52
|
''')
|
|
@@ -160,17 +159,17 @@ class TestResultStorage:
|
|
|
160
159
|
with sqlite3.connect(self.db_path) as conn:
|
|
161
160
|
cursor = conn.execute('''
|
|
162
161
|
INSERT INTO test_runs (
|
|
163
|
-
project_name, timestamp, suite_name, build_number, build_url,
|
|
162
|
+
project_name, timestamp, suite_name, build_number, build_url, run_tag,
|
|
164
163
|
total_tests, passed, failed, skipped,
|
|
165
|
-
duration_seconds, pass_rate, functional_pass_rate
|
|
166
|
-
|
|
167
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
164
|
+
duration_seconds, pass_rate, functional_pass_rate
|
|
165
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
168
166
|
''', (
|
|
169
167
|
run_data.get('project_name', self.config.PROJECT_NAME),
|
|
170
168
|
run_data.get('timestamp', datetime.now().isoformat()),
|
|
171
169
|
run_data.get('suite_name'),
|
|
172
170
|
run_data.get('build_number'),
|
|
173
171
|
run_data.get('build_url'),
|
|
172
|
+
run_data.get('run_tag'),
|
|
174
173
|
run_data.get('total_tests', 0),
|
|
175
174
|
run_data.get('passed', 0),
|
|
176
175
|
run_data.get('failed', 0),
|
|
@@ -178,8 +177,6 @@ class TestResultStorage:
|
|
|
178
177
|
run_data.get('duration_seconds', 0),
|
|
179
178
|
run_data.get('pass_rate', 0),
|
|
180
179
|
run_data.get('functional_pass_rate', 0),
|
|
181
|
-
run_data.get('session_id'),
|
|
182
|
-
run_data.get('is_append_mode', False),
|
|
183
180
|
))
|
|
184
181
|
conn.commit()
|
|
185
182
|
return cursor.lastrowid
|
|
@@ -380,6 +380,11 @@
|
|
|
380
380
|
return new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
+
function fmtTime(iso) {
|
|
384
|
+
if (!iso) return '-';
|
|
385
|
+
return new Date(iso).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', hour12: false });
|
|
386
|
+
}
|
|
387
|
+
|
|
383
388
|
function fmtDur(s) {
|
|
384
389
|
if (!s || s === 0) return '-';
|
|
385
390
|
if (s >= 60) return Math.floor(s / 60) + 'm ' + Math.floor(s % 60) + 's';
|
|
@@ -919,6 +924,7 @@
|
|
|
919
924
|
sparklineIds[sparkId] = fileTrends[run.file_name] || [];
|
|
920
925
|
return '<tr>' +
|
|
921
926
|
'<td style="font-size:12px">' + fmtDate(run.timestamp) + '</td>' +
|
|
927
|
+
'<td style="font-size:12px;color:var(--muted)">' + fmtTime(run.timestamp) + '</td>' +
|
|
922
928
|
'<td><div style="font-weight:600;font-size:13px;max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + run.file_name + '">' + run.file_name + '</div></td>' +
|
|
923
929
|
'<td style="font-size:12px">' + (run.regression_tag || '-') + '</td>' +
|
|
924
930
|
'<td><span class="pill ' + pc + '">' + run.pass_rate.toFixed(0) + '%</span></td>' +
|
|
@@ -960,6 +966,7 @@
|
|
|
960
966
|
'<table class="hist-table">' +
|
|
961
967
|
'<thead><tr>' +
|
|
962
968
|
'<th style="width:90px">Date</th>' +
|
|
969
|
+
'<th style="width:60px">Time</th>' +
|
|
963
970
|
'<th>Test File</th>' +
|
|
964
971
|
'<th style="width:100px">Tag</th>' +
|
|
965
972
|
'<th style="width:80px">Pass Rate</th>' +
|
|
@@ -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.
|
|
12
|
+
version='3.5.0',
|
|
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
|