chore: import upstream snapshot with attribution
Build and test / Build and test AMD64 Ubuntu 22.04 (push) Failing after 0s
Publish Builder / amazonlinux2023 (push) Failing after 1s
Build and test / UT for Go (push) Has been skipped
Publish KRTE Images / KRTE (push) Failing after 1s
Build and test / Integration Test (push) Has been skipped
Build and test / Upload Code Coverage (push) Has been skipped
Publish Builder / rockylinux9 (push) Failing after 1s
Publish Builder / ubuntu22.04 (push) Failing after 0s
Publish Builder / ubuntu24.04 (push) Failing after 0s
Publish Gpu Builder / publish-gpu-builder (push) Failing after 1s
Publish Test Images / PyTest (push) Failing after 0s
Build and test / UT for Cpp (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:31:17 +08:00
commit 498b235461
5446 changed files with 2748612 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""Pytest plugins for test logging optimization"""
+846
View File
@@ -0,0 +1,846 @@
"""
Pytest plugin for conditional logging based on test outcome.
This plugin captures logs per test in memory buffers and only persists
detailed logs for failed/xpass tests:
- PASSED tests: Only metadata saved (nodeid, duration, timestamp)
- FAILED tests: Full logs + error traceback + metadata
- SKIPPED tests: Only metadata + skip reason
- XFAIL tests: Only metadata + xfail reason (expected failures)
- XPASS tests: Full logs + xfail reason (unexpected passes - need attention)
Generates two report formats:
- test_report.json: AI-friendly structured format with complete test data
- test_report.html: Human-readable HTML report with color-coded logs
"""
import os
import logging
import json
from datetime import datetime
import pytest
class LogBuffer:
"""Captures logs per test in memory"""
def __init__(self, test_nodeid):
self.test_nodeid = test_nodeid
self.records = [] # List of LogRecord objects
self.start_time = None
self.end_time = None
def emit(self, record):
"""Add a log record to the buffer - must be thread-safe"""
# Make a copy of the record to avoid mutation issues
try:
self.records.append(record)
except Exception:
pass # Silently ignore errors during capture
def get_summary(self, report):
"""Extract test name, duration, outcome"""
duration = report.duration if hasattr(report, 'duration') else 0
status = report.outcome.upper()
return f"[{status}] {self.test_nodeid} ({duration:.2f}s)\n"
def get_full_logs(self, formatter):
"""Format all captured records"""
formatted_logs = []
for record in self.records:
try:
formatted_logs.append(formatter.format(record))
except Exception:
pass # Skip records that can't be formatted
return "\n".join(formatted_logs) if formatted_logs else "(No logs captured)"
def get_structured_logs(self):
"""Get logs structured by level for JSON output"""
structured = {
'debug': [],
'info': [],
'warning': [],
'error': [],
'critical': []
}
for record in self.records:
try:
level = record.levelname.lower()
if level in structured:
structured[level].append({
'message': record.getMessage(),
'timestamp': datetime.fromtimestamp(record.created).isoformat(),
'location': f"{record.filename}:{record.lineno}",
'logger': record.name
})
except Exception:
pass
return structured
class ConditionalLogHandler(logging.Handler):
"""Routes logs to per-test buffers"""
def __init__(self, log_config):
super().__init__()
self.current_test_buffer = None
self.buffers = {} # test_nodeid -> LogBuffer
self.log_config = log_config
self.formatter = logging.Formatter(
"[%(asctime)s - %(levelname)s - %(name)s]: %(message)s (%(filename)s:%(lineno)s)"
)
# Initialize log files
self.report_json = os.path.join(log_config.log_path, "test_report.json")
self.report_html = os.path.join(log_config.log_path, "test_report.html")
# Track test statistics
self.test_stats = {
'passed': [],
'failed': [],
'skipped': [],
'xfail': [], # Expected failures (tests marked xfail that failed as expected)
'xpass': [], # Unexpected passes (tests marked xfail that passed unexpectedly)
'start_time': None,
'end_time': None
}
# Worker mode support
self.worker_id = ''
self.is_worker = False
def emit(self, record):
"""Capture log record into current test buffer - NO LOCKS to avoid deadlock"""
# CRITICAL: No locks here to avoid deadlock with logging system
try:
if self.current_test_buffer:
self.current_test_buffer.emit(record)
except Exception:
# Must silently ignore errors to avoid breaking logging system
pass
def start_test(self, item):
"""Create new buffer for this test"""
buffer = LogBuffer(item.nodeid)
buffer.start_time = datetime.now()
self.buffers[item.nodeid] = buffer
self.current_test_buffer = buffer
def end_test(self, item, report):
"""Process test result and write logs"""
buffer = self.buffers.get(item.nodeid)
if buffer:
buffer.end_time = datetime.now()
# Track statistics with structured data
try:
# Extract test location info
test_parts = item.nodeid.split("::")
file_path = test_parts[0] if test_parts else "unknown"
test_class = test_parts[1] if len(test_parts) > 2 else None
test_function = test_parts[-1] if test_parts else "unknown"
# Check for xfail/xpass first (wasxfail attribute indicates xfail marker)
wasxfail = getattr(report, 'wasxfail', None)
if report.passed and wasxfail:
# xpass: test was expected to fail but passed
self.test_stats['xpass'].append({
'nodeid': item.nodeid,
'file': file_path,
'class': test_class,
'function': test_function,
'duration': report.duration,
'timestamp': datetime.now().isoformat(),
'xfail_reason': wasxfail,
'logs': buffer.get_structured_logs()
})
elif report.skipped and wasxfail:
# xfail: test was expected to fail and did fail (or was skipped due to xfail)
self.test_stats['xfail'].append({
'nodeid': item.nodeid,
'file': file_path,
'class': test_class,
'function': test_function,
'duration': report.duration,
'timestamp': datetime.now().isoformat(),
'xfail_reason': wasxfail
})
elif report.passed:
self.test_stats['passed'].append({
'nodeid': item.nodeid,
'file': file_path,
'class': test_class,
'function': test_function,
'duration': report.duration,
'timestamp': datetime.now().isoformat()
})
elif report.failed:
# Extract error information
error_info = self._extract_error_info(report)
self.test_stats['failed'].append({
'nodeid': item.nodeid,
'file': file_path,
'class': test_class,
'function': test_function,
'duration': report.duration,
'timestamp': datetime.now().isoformat(),
'error': error_info,
'logs': buffer.get_structured_logs()
})
elif report.skipped:
self.test_stats['skipped'].append({
'nodeid': item.nodeid,
'file': file_path,
'class': test_class,
'function': test_function,
'duration': report.duration,
'timestamp': datetime.now().isoformat(),
'reason': str(report.longrepr[2]) if hasattr(report, 'longrepr') and len(report.longrepr) > 2 else 'Unknown'
})
except Exception:
pass # Silently ignore write errors
# Clear buffer to free memory
if item.nodeid in self.buffers:
del self.buffers[item.nodeid]
# Clear current buffer if it matches
if self.current_test_buffer is buffer:
self.current_test_buffer = None
def _extract_error_info(self, report):
"""Extract structured error information from test report"""
error_info = {
'type': 'Unknown',
'message': '',
'traceback': '',
'line_number': None,
'file': None
}
try:
if hasattr(report, 'longreprtext'):
error_info['traceback'] = report.longreprtext
# Try to extract error type and message
lines = report.longreprtext.split('\n')
for line in lines:
if line.startswith('E '):
error_info['message'] = line[8:].strip()
# Try to extract error type
if ':' in error_info['message']:
parts = error_info['message'].split(':', 1)
error_info['type'] = parts[0].strip()
break
# Try to extract file and line number from traceback
for line in lines:
if '.py:' in line and '>' in line:
parts = line.split('.py:')
if len(parts) >= 2:
error_info['file'] = parts[0].split()[-1] + '.py'
line_num = parts[1].split()[0]
try:
error_info['line_number'] = int(line_num)
except:
pass
break
elif hasattr(report, 'longrepr'):
error_info['traceback'] = str(report.longrepr)
error_info['message'] = str(report.longrepr)
except Exception:
pass
return error_info
def _save_worker_data(self, filepath):
"""Save worker's test data to file for later merging"""
try:
# All data is already serializable (no report/buffer objects)
serializable_stats = {
'passed': self.test_stats['passed'],
'skipped': self.test_stats['skipped'],
'failed': self.test_stats['failed'],
'xfail': self.test_stats['xfail'],
'xpass': self.test_stats['xpass'],
'start_time': self.test_stats['start_time'].isoformat() if self.test_stats['start_time'] else None,
'end_time': self.test_stats['end_time'].isoformat() if self.test_stats['end_time'] else None
}
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(serializable_stats, f)
except Exception as e:
print(f"Error saving worker data: {e}")
def _merge_worker_data(self):
"""Merge data from all worker processes"""
try:
import glob
# Find all worker data files
worker_files = glob.glob(os.path.join(self.log_config.log_path, ".worker_*_data.json"))
for worker_file in worker_files:
try:
with open(worker_file, 'r', encoding='utf-8') as f:
worker_data = json.load(f)
# Merge data
self.test_stats['passed'].extend(worker_data.get('passed', []))
self.test_stats['skipped'].extend(worker_data.get('skipped', []))
self.test_stats['failed'].extend(worker_data.get('failed', []))
self.test_stats['xfail'].extend(worker_data.get('xfail', []))
self.test_stats['xpass'].extend(worker_data.get('xpass', []))
# Delete worker file after merging
os.remove(worker_file)
except Exception as e:
print(f"Error merging worker file {worker_file}: {e}")
except Exception as e:
print(f"Error in merge_worker_data: {e}")
def generate_json_report(self):
"""Generate AI-friendly JSON report"""
try:
total = (len(self.test_stats['passed']) + len(self.test_stats['failed']) +
len(self.test_stats['skipped']) + len(self.test_stats['xfail']) +
len(self.test_stats['xpass']))
total_duration = (sum(t['duration'] for t in self.test_stats['passed']) +
sum(t['duration'] for t in self.test_stats['failed']) +
sum(t['duration'] for t in self.test_stats['skipped']) +
sum(t['duration'] for t in self.test_stats['xfail']) +
sum(t['duration'] for t in self.test_stats['xpass']))
# Build JSON structure optimized for AI analysis
report_data = {
'metadata': {
'generated_at': datetime.now().isoformat(),
'log_location': self.log_config.log_path,
'total_tests': total,
'total_duration_seconds': round(total_duration, 2),
'start_time': self.test_stats['start_time'].isoformat() if self.test_stats['start_time'] else None,
'end_time': self.test_stats['end_time'].isoformat() if self.test_stats['end_time'] else None
},
'summary': {
'passed': {
'count': len(self.test_stats['passed']),
'percentage': round(len(self.test_stats['passed']) / total * 100, 1) if total > 0 else 0
},
'failed': {
'count': len(self.test_stats['failed']),
'percentage': round(len(self.test_stats['failed']) / total * 100, 1) if total > 0 else 0
},
'skipped': {
'count': len(self.test_stats['skipped']),
'percentage': round(len(self.test_stats['skipped']) / total * 100, 1) if total > 0 else 0
},
'xfail': {
'count': len(self.test_stats['xfail']),
'percentage': round(len(self.test_stats['xfail']) / total * 100, 1) if total > 0 else 0
},
'xpass': {
'count': len(self.test_stats['xpass']),
'percentage': round(len(self.test_stats['xpass']) / total * 100, 1) if total > 0 else 0
}
},
'tests': {
'passed': [
{
'id': t['nodeid'],
'file': t['file'],
'class': t['class'],
'function': t['function'],
'duration': round(t['duration'], 3),
'timestamp': t['timestamp']
}
for t in self.test_stats['passed']
],
'failed': [
{
'id': t['nodeid'],
'file': t['file'],
'class': t['class'],
'function': t['function'],
'duration': round(t['duration'], 3),
'timestamp': t['timestamp'],
'error': {
'type': t['error']['type'],
'message': t['error']['message'],
'file': t['error']['file'],
'line': t['error']['line_number'],
'traceback': t['error']['traceback']
},
'logs': {
'debug': t['logs']['debug'],
'info': t['logs']['info'],
'warning': t['logs']['warning'],
'error': t['logs']['error'],
'critical': t['logs']['critical']
}
}
for t in self.test_stats['failed']
],
'skipped': [
{
'id': t['nodeid'],
'file': t['file'],
'class': t['class'],
'function': t['function'],
'duration': round(t['duration'], 3),
'timestamp': t['timestamp'],
'reason': t['reason']
}
for t in self.test_stats['skipped']
],
'xfail': [
{
'id': t['nodeid'],
'file': t['file'],
'class': t['class'],
'function': t['function'],
'duration': round(t['duration'], 3),
'timestamp': t['timestamp'],
'xfail_reason': t['xfail_reason']
}
for t in self.test_stats['xfail']
],
'xpass': [
{
'id': t['nodeid'],
'file': t['file'],
'class': t['class'],
'function': t['function'],
'duration': round(t['duration'], 3),
'timestamp': t['timestamp'],
'xfail_reason': t['xfail_reason'],
'logs': {
'debug': t['logs']['debug'],
'info': t['logs']['info'],
'warning': t['logs']['warning'],
'error': t['logs']['error'],
'critical': t['logs']['critical']
}
}
for t in self.test_stats['xpass']
]
}
}
# Write JSON report with pretty printing
with open(self.report_json, 'w', encoding='utf-8') as f:
json.dump(report_data, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"Error generating JSON report: {e}")
def generate_html_report(self):
"""Generate HTML test report for humans"""
try:
total = (len(self.test_stats['passed']) + len(self.test_stats['failed']) +
len(self.test_stats['skipped']) + len(self.test_stats['xfail']) +
len(self.test_stats['xpass']))
total_duration = (sum(t['duration'] for t in self.test_stats['passed']) +
sum(t['duration'] for t in self.test_stats['failed']) +
sum(t['duration'] for t in self.test_stats['skipped']) +
sum(t['duration'] for t in self.test_stats['xfail']) +
sum(t['duration'] for t in self.test_stats['xpass']))
passed_pct = (len(self.test_stats['passed'])/total*100) if total > 0 else 0
failed_pct = (len(self.test_stats['failed'])/total*100) if total > 0 else 0
skipped_pct = (len(self.test_stats['skipped'])/total*100) if total > 0 else 0
xfail_pct = (len(self.test_stats['xfail'])/total*100) if total > 0 else 0
xpass_pct = (len(self.test_stats['xpass'])/total*100) if total > 0 else 0
with open(self.report_html, 'w', encoding='utf-8') as f:
# HTML header
f.write("""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Milvus Python Test Report</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
border-radius: 8px;
margin-bottom: 20px;
}
.summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin: 20px 0;
}
.summary-card {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.summary-card h3 { margin: 0 0 10px 0; color: #666; font-size: 14px; }
.summary-card .value { font-size: 32px; font-weight: bold; }
.passed { color: #10b981; }
.failed { color: #ef4444; }
.skipped { color: #f59e0b; }
.xfail { color: #8b5cf6; }
.xpass { color: #ec4899; }
.test-section {
background: white;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.test-section h2 {
margin-top: 0;
border-bottom: 2px solid #e5e7eb;
padding-bottom: 10px;
}
.test-item {
padding: 10px;
margin: 5px 0;
border-left: 4px solid;
background: #f9fafb;
}
.test-item.passed { border-color: #10b981; }
.test-item.failed { border-color: #ef4444; }
.test-item.skipped { border-color: #f59e0b; }
.test-item.xfail { border-color: #8b5cf6; }
.test-item.xpass { border-color: #ec4899; }
.test-name { font-family: monospace; font-size: 13px; }
.duration { color: #6b7280; font-size: 12px; }
.error-box {
background: #fee2e2;
border: 1px solid #ef4444;
padding: 15px;
margin: 10px 0;
border-radius: 4px;
font-family: monospace;
font-size: 12px;
white-space: pre-wrap;
}
.logs-box {
background: #f3f4f6;
border: 1px solid #d1d5db;
padding: 15px;
margin: 10px 0;
border-radius: 4px;
font-family: monospace;
font-size: 11px;
max-height: 400px;
overflow-y: auto;
}
.log-entry { margin: 3px 0; }
.log-debug { color: #6b7280; }
.log-info { color: #3b82f6; }
.log-warning { color: #f59e0b; }
.log-error { color: #ef4444; }
.log-critical { color: #dc2626; font-weight: bold; }
details { margin: 10px 0; }
summary { cursor: pointer; font-weight: 600; padding: 10px; background: #f9fafb; }
summary:hover { background: #f3f4f6; }
</style>
</head>
<body>
<div class="header">
<h1>Milvus Python Test Report</h1>
<p>Generated at: """ + datetime.now().strftime('%Y-%m-%d %H:%M:%S') + f"""</p>
<p>Log location: {self.log_config.log_path}</p>
</div>
<div class="summary">
<div class="summary-card">
<h3>Total Tests</h3>
<div class="value">{total}</div>
</div>
<div class="summary-card">
<h3>Passed</h3>
<div class="value passed">{len(self.test_stats['passed'])}</div>
<div class="duration">{passed_pct:.1f}%</div>
</div>
<div class="summary-card">
<h3>Failed</h3>
<div class="value failed">{len(self.test_stats['failed'])}</div>
<div class="duration">{failed_pct:.1f}%</div>
</div>
<div class="summary-card">
<h3>Skipped</h3>
<div class="value skipped">{len(self.test_stats['skipped'])}</div>
<div class="duration">{skipped_pct:.1f}%</div>
</div>
<div class="summary-card">
<h3>XFail</h3>
<div class="value xfail">{len(self.test_stats['xfail'])}</div>
<div class="duration">{xfail_pct:.1f}%</div>
</div>
<div class="summary-card">
<h3>XPass</h3>
<div class="value xpass">{len(self.test_stats['xpass'])}</div>
<div class="duration">{xpass_pct:.1f}%</div>
</div>
<div class="summary-card">
<h3>Duration</h3>
<div class="value">{total_duration:.2f}s</div>
</div>
</div>
""")
# Failed tests section
if self.test_stats['failed']:
f.write(' <div class="test-section">\n')
f.write(' <h2>❌ Failed Tests</h2>\n')
for test_info in self.test_stats['failed']:
f.write(f' <details open>\n')
f.write(f' <summary class="test-item failed">\n')
f.write(f' <span class="test-name">{test_info["nodeid"]}</span>\n')
f.write(f' <span class="duration"> ({test_info["duration"]:.2f}s)</span>\n')
f.write(f' </summary>\n')
# Error information - use data from test_info['error']
error_info = test_info.get('error', {})
if error_info:
f.write(' <div class="error-box">\n')
error_type = error_info.get('type', 'Unknown')
error_msg = error_info.get('message', '')
error_traceback = error_info.get('traceback', '')
# Format error display
if error_traceback:
error_text = error_traceback.replace('<', '&lt;').replace('>', '&gt;')
else:
error_text = f"{error_type}: {error_msg}".replace('<', '&lt;').replace('>', '&gt;')
f.write(f'{error_text}\n')
f.write(' </div>\n')
# Captured logs - use data from test_info['logs']
logs = test_info.get('logs', {})
has_logs = any(logs.get(level, []) for level in ['debug', 'info', 'warning', 'error', 'critical'])
if has_logs:
f.write(' <div class="logs-box">\n')
f.write(' <strong>Captured Logs:</strong>\n')
for level in ['debug', 'info', 'warning', 'error', 'critical']:
for log_entry in logs.get(level, []):
msg = log_entry['message'].replace('<', '&lt;').replace('>', '&gt;')
ts = log_entry['timestamp'].split('T')[1].split('.')[0]
loc = log_entry['location']
f.write(f' <div class="log-entry log-{level}">')
f.write(f'[{ts} - {level.upper()}] {msg} ({loc})</div>\n')
f.write(' </div>\n')
f.write(' </details>\n')
f.write(' </div>\n')
# XPass tests section (unexpected passes - need attention)
if self.test_stats['xpass']:
f.write(' <div class="test-section">\n')
f.write(' <h2>⚠️ XPass Tests (Unexpected Passes)</h2>\n')
for test_info in self.test_stats['xpass']:
f.write(f' <details open>\n')
f.write(f' <summary class="test-item xpass">\n')
f.write(f' <span class="test-name">{test_info["nodeid"]}</span>\n')
f.write(f' <span class="duration"> ({test_info["duration"]:.2f}s)</span>\n')
f.write(f' </summary>\n')
# XFail reason
xfail_reason = test_info.get('xfail_reason', 'Unknown')
if xfail_reason:
reason_text = str(xfail_reason).replace('<', '&lt;').replace('>', '&gt;')
f.write(f' <div style="margin:10px 0;padding:10px;background:#fdf4ff;border:1px solid #ec4899;border-radius:4px;">\n')
f.write(f' <strong>Expected to fail:</strong> {reason_text}\n')
f.write(f' </div>\n')
# Captured logs
logs = test_info.get('logs', {})
has_logs = any(logs.get(level, []) for level in ['debug', 'info', 'warning', 'error', 'critical'])
if has_logs:
f.write(' <div class="logs-box">\n')
f.write(' <strong>Captured Logs:</strong>\n')
for level in ['debug', 'info', 'warning', 'error', 'critical']:
for log_entry in logs.get(level, []):
msg = log_entry['message'].replace('<', '&lt;').replace('>', '&gt;')
ts = log_entry['timestamp'].split('T')[1].split('.')[0]
loc = log_entry['location']
f.write(f' <div class="log-entry log-{level}">')
f.write(f'[{ts} - {level.upper()}] {msg} ({loc})</div>\n')
f.write(' </div>\n')
f.write(' </details>\n')
f.write(' </div>\n')
# XFail tests section (expected failures)
if self.test_stats['xfail']:
f.write(' <div class="test-section">\n')
f.write(' <h2>📋 XFail Tests (Expected Failures)</h2>\n')
for test in self.test_stats['xfail']:
f.write(f' <div class="test-item xfail">\n')
f.write(f' <span class="test-name">{test["nodeid"]}</span>\n')
f.write(f' <span class="duration"> ({test["duration"]:.2f}s)</span>\n')
xfail_reason = test.get('xfail_reason', 'Unknown')
if xfail_reason:
reason_text = str(xfail_reason).replace('<', '&lt;').replace('>', '&gt;')
f.write(f' <div style="margin-top:5px;color:#6b7280;font-size:12px;">Expected: {reason_text}</div>\n')
f.write(f' </div>\n')
f.write(' </div>\n')
# Passed tests section
if self.test_stats['passed']:
f.write(' <div class="test-section">\n')
f.write(' <h2>✅ Passed Tests</h2>\n')
for test in self.test_stats['passed']:
f.write(f' <div class="test-item passed">\n')
f.write(f' <span class="test-name">{test["nodeid"]}</span>\n')
f.write(f' <span class="duration"> ({test["duration"]:.2f}s)</span>\n')
f.write(f' </div>\n')
f.write(' </div>\n')
# Skipped tests section
if self.test_stats['skipped']:
f.write(' <div class="test-section">\n')
f.write(' <h2>⊘ Skipped Tests</h2>\n')
for test in self.test_stats['skipped']:
f.write(f' <div class="test-item skipped">\n')
f.write(f' <span class="test-name">{test["nodeid"]}</span>\n')
f.write(f' <span class="duration"> ({test["duration"]:.2f}s)</span>\n')
if 'reason' in test:
reason = test['reason'].replace('<', '&lt;').replace('>', '&gt;')
f.write(f' <div style="margin-top:5px;color:#6b7280;font-size:12px;">Reason: {reason}</div>\n')
f.write(f' </div>\n')
f.write(' </div>\n')
f.write("""
</body>
</html>
""")
except Exception as e:
print(f"Error generating HTML report: {e}")
def generate_report(self):
"""Generate unified test report (JSON and HTML)"""
# Generate JSON report for AI
self.generate_json_report()
# Generate HTML report for humans
self.generate_html_report()
# Global handler instance
_conditional_handler = None
def pytest_configure(config):
"""Initialize the conditional log handler"""
global _conditional_handler
# Only initialize if not already done
if _conditional_handler is None:
try:
from config.log_config import log_config
_conditional_handler = ConditionalLogHandler(log_config)
_conditional_handler.setLevel(logging.DEBUG)
# Record start time
_conditional_handler.test_stats['start_time'] = datetime.now()
# Detect if running in xdist worker mode
worker_id = os.environ.get('PYTEST_XDIST_WORKER', '')
_conditional_handler.worker_id = worker_id
_conditional_handler.is_worker = bool(worker_id)
# Pre-create log files
for log_file in [_conditional_handler.report_json,
_conditional_handler.report_html]:
if not os.path.exists(log_file):
try:
with open(log_file, 'w', encoding='utf-8') as f:
pass # Create empty file
except Exception:
pass # Ignore errors
# Add handler to root logger
root_logger = logging.getLogger()
root_logger.addHandler(_conditional_handler)
except Exception:
# If plugin initialization fails, don't break pytest
pass
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_setup(item):
"""Hook before test setup"""
global _conditional_handler
if _conditional_handler:
try:
_conditional_handler.start_test(item)
except Exception:
pass # Don't break test execution
yield
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
"""Hook after each test phase (setup, call, teardown)"""
outcome = yield
report = outcome.get_result()
# Only process after the main test execution (call phase)
if report.when == "call":
global _conditional_handler
if _conditional_handler:
try:
_conditional_handler.end_test(item, report)
except Exception:
pass # Don't break test execution
def pytest_sessionfinish(session, exitstatus):
"""Hook at the end of test session"""
global _conditional_handler
if _conditional_handler:
try:
# Record end time
_conditional_handler.test_stats['end_time'] = datetime.now()
if _conditional_handler.is_worker:
# Worker mode: save data to temporary file
worker_data_file = os.path.join(
_conditional_handler.log_config.log_path,
f".worker_{_conditional_handler.worker_id}_data.json"
)
_conditional_handler._save_worker_data(worker_data_file)
else:
# Main process or single process mode
# Collect data from workers if any
_conditional_handler._merge_worker_data()
# Generate unified report
_conditional_handler.generate_report()
# Clear any remaining references
_conditional_handler.current_test_buffer = None
except Exception as e:
print(f"Error in pytest_sessionfinish: {e}")