test-reporting 3.2.6__tar.gz → 3.2.7__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.2.6/test_reporting.egg-info → test_reporting-3.2.7}/PKG-INFO +1 -1
  2. {test_reporting-3.2.6 → test_reporting-3.2.7}/reporting/plugin.py +87 -4
  3. {test_reporting-3.2.6 → test_reporting-3.2.7}/reporting/storage.py +91 -4
  4. {test_reporting-3.2.6 → test_reporting-3.2.7}/setup.py +1 -1
  5. {test_reporting-3.2.6 → test_reporting-3.2.7/test_reporting.egg-info}/PKG-INFO +1 -1
  6. {test_reporting-3.2.6 → test_reporting-3.2.7}/LICENSE +0 -0
  7. {test_reporting-3.2.6 → test_reporting-3.2.7}/README.md +0 -0
  8. {test_reporting-3.2.6 → test_reporting-3.2.7}/reporting/__init__.py +0 -0
  9. {test_reporting-3.2.6 → test_reporting-3.2.7}/reporting/classifier.py +0 -0
  10. {test_reporting-3.2.6 → test_reporting-3.2.7}/reporting/cli.py +0 -0
  11. {test_reporting-3.2.6 → test_reporting-3.2.7}/reporting/config.py +0 -0
  12. {test_reporting-3.2.6 → test_reporting-3.2.7}/reporting/publisher.py +0 -0
  13. {test_reporting-3.2.6 → test_reporting-3.2.7}/reporting/templates/index.html +0 -0
  14. {test_reporting-3.2.6 → test_reporting-3.2.7}/reporting/templates/project.html +0 -0
  15. {test_reporting-3.2.6 → test_reporting-3.2.7}/reporting/templates/run.html +0 -0
  16. {test_reporting-3.2.6 → test_reporting-3.2.7}/setup.cfg +0 -0
  17. {test_reporting-3.2.6 → test_reporting-3.2.7}/test_reporting.egg-info/SOURCES.txt +0 -0
  18. {test_reporting-3.2.6 → test_reporting-3.2.7}/test_reporting.egg-info/dependency_links.txt +0 -0
  19. {test_reporting-3.2.6 → test_reporting-3.2.7}/test_reporting.egg-info/entry_points.txt +0 -0
  20. {test_reporting-3.2.6 → test_reporting-3.2.7}/test_reporting.egg-info/requires.txt +0 -0
  21. {test_reporting-3.2.6 → test_reporting-3.2.7}/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.2.6
3
+ Version: 3.2.7
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
@@ -5,9 +5,10 @@ Automatically collects test data during pytest execution.
5
5
  import os
6
6
  import json
7
7
  import logging
8
+ import uuid
8
9
  from datetime import datetime
9
10
  from pathlib import Path
10
- from typing import Dict, Any, List
11
+ from typing import Dict, Any, List, Optional
11
12
  import pytest
12
13
  from .config import ReportingConfig
13
14
  from .classifier import FailureClassifier
@@ -21,6 +22,12 @@ class TestReportingPlugin:
21
22
  def __init__(self):
22
23
  self.config = ReportingConfig()
23
24
  self.storage = TestResultStorage(config=self.config)
25
+
26
+ # Session ID for multi-run support
27
+ self.session_id = os.getenv('TEST_SESSION_ID', None)
28
+ self.append_mode = os.getenv('TEST_APPEND_MODE', 'false').lower() == 'true'
29
+ self.attempt_number = int(os.getenv('TEST_ATTEMPT_NUMBER', '1'))
30
+
24
31
  self.run_data = {
25
32
  'project_name': self.config.PROJECT_NAME,
26
33
  'timestamp': datetime.now().isoformat(),
@@ -33,10 +40,13 @@ class TestReportingPlugin:
33
40
  'failed': 0,
34
41
  'skipped': 0,
35
42
  'duration_seconds': 0,
43
+ 'session_id': self.session_id,
44
+ 'is_append_mode': self.append_mode,
36
45
  }
37
46
  self.run_id = None
38
47
  self.test_start_times = {}
39
48
  self.test_logs = {} # Store logs per test
49
+ self.existing_results = {} # Store existing test results in append mode
40
50
 
41
51
  @pytest.hookimpl(tryfirst=True)
42
52
  def pytest_sessionstart(self, session):
@@ -48,7 +58,22 @@ class TestReportingPlugin:
48
58
  first_arg = session.config.args[0]
49
59
  self.run_data['suite_name'] = Path(first_arg).stem
50
60
 
51
- logging.info(f"[Reporting] Starting test run: {self.run_data['suite_name']}")
61
+ # Handle append mode - find existing run or create session ID
62
+ if self.append_mode and self.session_id:
63
+ existing_run_id = self.storage.find_run_by_session_id(self.session_id)
64
+ if existing_run_id:
65
+ self.run_id = existing_run_id
66
+ self.existing_results = self.storage.get_existing_test_results(self.run_id)
67
+ logging.info(f"[Reporting] Appending to existing run ID: {self.run_id} (session: {self.session_id}, attempt: {self.attempt_number})")
68
+ else:
69
+ logging.info(f"[Reporting] Starting new multi-run session: {self.session_id} (attempt: {self.attempt_number})")
70
+ elif not self.session_id:
71
+ # Auto-generate session ID if not provided
72
+ self.session_id = str(uuid.uuid4())
73
+ self.run_data['session_id'] = self.session_id
74
+ logging.info(f"[Reporting] Starting test run: {self.run_data['suite_name']} (session: {self.session_id})")
75
+ else:
76
+ logging.info(f"[Reporting] Starting test run: {self.run_data['suite_name']}")
52
77
 
53
78
  @pytest.hookimpl(hookwrapper=True)
54
79
  def pytest_runtest_protocol(self, item, nextitem):
@@ -136,6 +161,9 @@ class TestReportingPlugin:
136
161
  # Get logs for this test
137
162
  test_log = self.test_logs.get(item.nodeid, '')
138
163
 
164
+ # Determine if this is a retry
165
+ is_retry = self.attempt_number > 1
166
+
139
167
  # Store test result
140
168
  test_result = {
141
169
  'project_name': self.config.PROJECT_NAME,
@@ -149,6 +177,8 @@ class TestReportingPlugin:
149
177
  'screenshot_path': screenshot_path,
150
178
  'trace_path': trace_path,
151
179
  'logs': test_log[:5000] if test_log else None, # Limit log size to 5000 chars
180
+ 'attempt_number': self.attempt_number,
181
+ 'is_retry': is_retry,
152
182
  }
153
183
 
154
184
  # Add failure classification if available
@@ -181,6 +211,10 @@ class TestReportingPlugin:
181
211
  @pytest.hookimpl(trylast=True)
182
212
  def pytest_sessionfinish(self, session, exitstatus):
183
213
  """Called at the end of the test session."""
214
+ # In append mode, merge with existing results
215
+ if self.append_mode and self.run_id and self.existing_results:
216
+ self._merge_with_existing_results()
217
+
184
218
  # Calculate final metrics
185
219
  total = self.run_data['total_tests']
186
220
 
@@ -210,10 +244,22 @@ class TestReportingPlugin:
210
244
  self.run_data['functional_pass_rate'] = 0
211
245
 
212
246
  # Save to database
213
- self.run_id = self.storage.save_test_run(self.run_data)
247
+ if self.append_mode and self.run_id:
248
+ # Update existing run
249
+ self.storage.update_run_metrics(self.run_id, self.run_data)
250
+ else:
251
+ # Create new run
252
+ self.run_id = self.storage.save_test_run(self.run_data)
214
253
 
215
- # Save individual test results
254
+ # Save individual test results (only new ones in append mode)
216
255
  for test_result in self.run_data['tests']:
256
+ # Skip if this test already exists and hasn't changed
257
+ if self.append_mode and test_result['full_name'] in self.existing_results:
258
+ existing = self.existing_results[test_result['full_name']]
259
+ # Only save if it's a new attempt
260
+ if test_result.get('attempt_number', 1) <= existing.get('attempt_number', 1):
261
+ continue
262
+
217
263
  result_id = self.storage.save_test_result(self.run_id, test_result)
218
264
 
219
265
  # TODO: Extract and save steps from logs
@@ -222,6 +268,8 @@ class TestReportingPlugin:
222
268
  self._save_latest_json()
223
269
 
224
270
  logging.info(f"[Reporting] Test run complete. Run ID: {self.run_id}")
271
+ if self.append_mode:
272
+ logging.info(f"[Reporting] Append mode: attempt {self.attempt_number}")
225
273
  logging.info(f"[Reporting] Pass Rate: {self.run_data['pass_rate']:.1f}%")
226
274
  logging.info(f"[Reporting] Functional Pass Rate: {self.run_data['functional_pass_rate']:.1f}%")
227
275
 
@@ -234,6 +282,41 @@ class TestReportingPlugin:
234
282
  except Exception as e:
235
283
  logging.warning(f"[Reporting] Auto-publish failed: {e}")
236
284
 
285
+ def _merge_with_existing_results(self):
286
+ """Merge current test results with existing results from previous attempts."""
287
+ # Create a map of current test results by full_name
288
+ current_tests = {t['full_name']: t for t in self.run_data['tests']}
289
+
290
+ # Add existing tests that weren't run in this attempt
291
+ for full_name, existing_test in self.existing_results.items():
292
+ if full_name not in current_tests:
293
+ # Convert DB result to test result format
294
+ test_result = {
295
+ 'project_name': existing_test['project_name'],
296
+ 'file_name': existing_test['file_name'],
297
+ 'test_name': existing_test['test_name'],
298
+ 'full_name': existing_test['full_name'],
299
+ 'status': existing_test['status'],
300
+ 'duration_seconds': existing_test['duration_seconds'],
301
+ 'error_message': existing_test.get('error_message'),
302
+ 'error_type': existing_test.get('error_type'),
303
+ 'screenshot_path': existing_test.get('screenshot_path'),
304
+ 'trace_path': existing_test.get('trace_path'),
305
+ 'logs': existing_test.get('logs'),
306
+ 'attempt_number': existing_test.get('attempt_number', 1),
307
+ 'is_retry': existing_test.get('is_retry', False),
308
+ 'failure_type': existing_test.get('failure_type'),
309
+ 'failure_category': existing_test.get('failure_category'),
310
+ 'is_soft_fail': existing_test.get('is_soft_fail', False),
311
+ }
312
+ self.run_data['tests'].append(test_result)
313
+
314
+ # Recalculate totals
315
+ self.run_data['total_tests'] = len(self.run_data['tests'])
316
+ self.run_data['passed'] = sum(1 for t in self.run_data['tests'] if t['status'] == 'passed')
317
+ self.run_data['failed'] = sum(1 for t in self.run_data['tests'] if t['status'] == 'failed')
318
+ self.run_data['skipped'] = sum(1 for t in self.run_data['tests'] if t['status'] == 'skipped')
319
+
237
320
  def _save_latest_json(self):
238
321
  """Save latest run data as JSON for dashboard."""
239
322
  output = {
@@ -46,6 +46,8 @@ class TestResultStorage:
46
46
  duration_seconds REAL,
47
47
  pass_rate REAL,
48
48
  functional_pass_rate REAL,
49
+ session_id TEXT,
50
+ is_append_mode BOOLEAN DEFAULT 0,
49
51
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
50
52
  )
51
53
  ''')
@@ -68,6 +70,8 @@ class TestResultStorage:
68
70
  screenshot_path TEXT,
69
71
  trace_path TEXT,
70
72
  retry_count INTEGER DEFAULT 0,
73
+ attempt_number INTEGER DEFAULT 1,
74
+ is_retry BOOLEAN DEFAULT 0,
71
75
  logs TEXT,
72
76
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
73
77
  FOREIGN KEY (run_id) REFERENCES test_runs(id)
@@ -122,6 +126,34 @@ class TestResultStorage:
122
126
  except sqlite3.OperationalError:
123
127
  conn.execute('ALTER TABLE test_results ADD COLUMN project_name TEXT')
124
128
  print("[Reporting] Migration: Added 'project_name' column to test_results")
129
+
130
+ # Migration: Add session_id to test_runs
131
+ try:
132
+ conn.execute('SELECT session_id FROM test_runs LIMIT 1')
133
+ except sqlite3.OperationalError:
134
+ conn.execute('ALTER TABLE test_runs ADD COLUMN session_id TEXT')
135
+ print("[Reporting] Migration: Added 'session_id' column to test_runs")
136
+
137
+ # Migration: Add is_append_mode to test_runs
138
+ try:
139
+ conn.execute('SELECT is_append_mode FROM test_runs LIMIT 1')
140
+ except sqlite3.OperationalError:
141
+ conn.execute('ALTER TABLE test_runs ADD COLUMN is_append_mode BOOLEAN DEFAULT 0')
142
+ print("[Reporting] Migration: Added 'is_append_mode' column to test_runs")
143
+
144
+ # Migration: Add attempt_number to test_results
145
+ try:
146
+ conn.execute('SELECT attempt_number FROM test_results LIMIT 1')
147
+ except sqlite3.OperationalError:
148
+ conn.execute('ALTER TABLE test_results ADD COLUMN attempt_number INTEGER DEFAULT 1')
149
+ print("[Reporting] Migration: Added 'attempt_number' column to test_results")
150
+
151
+ # Migration: Add is_retry to test_results
152
+ try:
153
+ conn.execute('SELECT is_retry FROM test_results LIMIT 1')
154
+ except sqlite3.OperationalError:
155
+ conn.execute('ALTER TABLE test_results ADD COLUMN is_retry BOOLEAN DEFAULT 0')
156
+ print("[Reporting] Migration: Added 'is_retry' column to test_results")
125
157
 
126
158
  def save_test_run(self, run_data: Dict[str, Any]) -> int:
127
159
  """Save a test run and return the run ID."""
@@ -130,8 +162,9 @@ class TestResultStorage:
130
162
  INSERT INTO test_runs (
131
163
  project_name, timestamp, suite_name, build_number, build_url,
132
164
  total_tests, passed, failed, skipped,
133
- duration_seconds, pass_rate, functional_pass_rate
134
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
165
+ duration_seconds, pass_rate, functional_pass_rate,
166
+ session_id, is_append_mode
167
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
135
168
  ''', (
136
169
  run_data.get('project_name', self.config.PROJECT_NAME),
137
170
  run_data.get('timestamp', datetime.now().isoformat()),
@@ -145,6 +178,8 @@ class TestResultStorage:
145
178
  run_data.get('duration_seconds', 0),
146
179
  run_data.get('pass_rate', 0),
147
180
  run_data.get('functional_pass_rate', 0),
181
+ run_data.get('session_id'),
182
+ run_data.get('is_append_mode', False),
148
183
  ))
149
184
  conn.commit()
150
185
  return cursor.lastrowid
@@ -157,8 +192,8 @@ class TestResultStorage:
157
192
  run_id, project_name, file_name, test_name, full_name, status,
158
193
  failure_type, failure_category, is_soft_fail,
159
194
  duration_seconds, error_message, error_type,
160
- screenshot_path, trace_path, retry_count, logs
161
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
195
+ screenshot_path, trace_path, retry_count, attempt_number, is_retry, logs
196
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
162
197
  ''', (
163
198
  run_id,
164
199
  result_data.get('project_name', self.config.PROJECT_NAME),
@@ -175,6 +210,8 @@ class TestResultStorage:
175
210
  result_data.get('screenshot_path'),
176
211
  result_data.get('trace_path'),
177
212
  result_data.get('retry_count', 0),
213
+ result_data.get('attempt_number', 1),
214
+ result_data.get('is_retry', False),
178
215
  result_data.get('logs'),
179
216
  ))
180
217
  conn.commit()
@@ -196,6 +233,56 @@ class TestResultStorage:
196
233
  ))
197
234
  conn.commit()
198
235
 
236
+ def find_run_by_session_id(self, session_id: str) -> Optional[int]:
237
+ """Find an existing run by session_id. Returns run_id or None."""
238
+ if not session_id:
239
+ return None
240
+
241
+ with sqlite3.connect(self.db_path) as conn:
242
+ cursor = conn.execute('''
243
+ SELECT id FROM test_runs
244
+ WHERE session_id = ?
245
+ ORDER BY created_at DESC
246
+ LIMIT 1
247
+ ''', (session_id,))
248
+ row = cursor.fetchone()
249
+ return row[0] if row else None
250
+
251
+ def update_run_metrics(self, run_id: int, run_data: Dict[str, Any]):
252
+ """Update metrics for an existing test run (used in append mode)."""
253
+ with sqlite3.connect(self.db_path) as conn:
254
+ conn.execute('''
255
+ UPDATE test_runs
256
+ SET total_tests = ?,
257
+ passed = ?,
258
+ failed = ?,
259
+ skipped = ?,
260
+ duration_seconds = ?,
261
+ pass_rate = ?,
262
+ functional_pass_rate = ?
263
+ WHERE id = ?
264
+ ''', (
265
+ run_data.get('total_tests', 0),
266
+ run_data.get('passed', 0),
267
+ run_data.get('failed', 0),
268
+ run_data.get('skipped', 0),
269
+ run_data.get('duration_seconds', 0),
270
+ run_data.get('pass_rate', 0),
271
+ run_data.get('functional_pass_rate', 0),
272
+ run_id,
273
+ ))
274
+ conn.commit()
275
+
276
+ def get_existing_test_results(self, run_id: int) -> Dict[str, Dict[str, Any]]:
277
+ """Get existing test results for a run, keyed by full_name."""
278
+ with sqlite3.connect(self.db_path) as conn:
279
+ conn.row_factory = sqlite3.Row
280
+ cursor = conn.execute('''
281
+ SELECT * FROM test_results
282
+ WHERE run_id = ?
283
+ ''', (run_id,))
284
+ return {row['full_name']: dict(row) for row in cursor.fetchall()}
285
+
199
286
  def get_all_runs_with_details(self, project_name: str = None) -> List[Dict[str, Any]]:
200
287
  """Get all test runs with test file details (no limit)."""
201
288
  with sqlite3.connect(self.db_path) as conn:
@@ -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.2.6',
12
+ version='3.2.7',
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.2.6
3
+ Version: 3.2.7
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