Python日志解析与报告生成:正则+pandas解析Vivado日志,自动生成测试报告
在FPGA开发中,需要分析Vivado编译日志、仿真日志、测试报告等。Python可以高效地解析这些日志,提取关键信息,并生成可读性强的报告。
1. 日志解析基础
import re
from pathlib import Path
from datetime import datetime
from typing import List, Dict, Optional
from dataclasses import dataclass, field
@dataclass
class LogEntry:
"""日志条目"""
timestamp: Optional[datetime]
level: str
source: str
message: str
line_number: int
class LogParser:
"""通用日志解析器"""
# 常见日志格式
PATTERNS = {
'standard': r'\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] \[(\w+)\] \[(\w+)\] (.+)',
'simple': r'(\w+): (.+)',
'vivado': r'(INFO|WARNING|ERROR|CRITICAL): \[([^\]]+)\] (.+)',
}
def __init__(self, pattern_name: str = 'standard'):
self.pattern = re.compile(self.PATTERNS.get(pattern_name, pattern_name))
self.entries: List[LogEntry] = []
def parse_file(self, filepath: str) -> List[LogEntry]:
"""解析日志文件"""
self.entries = []
content = Path(filepath).read_text(encoding='utf-8', errors='ignore')
for i, line in enumerate(content.split('\n'), 1):
entry = self.parse_line(line, i)
if entry:
self.entries.append(entry)
return self.entries
def parse_line(self, line: str, line_number: int = 0) -> Optional[LogEntry]:
"""解析单行日志"""
match = self.pattern.match(line.strip())
if not match:
return None
groups = match.groups()
# 尝试解析时间戳
timestamp = None
try:
timestamp = datetime.strptime(groups[0], '%Y-%m-%d %H:%M:%S')
except:
pass
return LogEntry(
timestamp=timestamp,
level=groups[1] if len(groups) > 1 else 'INFO',
source=groups[2] if len(groups) > 2 else '',
message=groups[-1],
line_number=line_number
)
def filter_by_level(self, level: str) -> List[LogEntry]:
"""按级别过滤"""
return [e for e in self.entries if e.level.upper() == level.upper()]
def filter_by_keyword(self, keyword: str) -> List[LogEntry]:
"""按关键字过滤"""
return [e for e in self.entries if keyword.lower() in e.message.lower()]
def get_summary(self) -> Dict[str, int]:
"""获取统计摘要"""
summary = {}
for entry in self.entries:
level = entry.level.upper()
summary[level] = summary.get(level, 0) + 1
return summary
# 使用示例
parser = LogParser('vivado')
# entries = parser.parse_file('vivado.log')
# print(parser.get_summary())
2. Vivado日志解析
import re
from pathlib import Path
from dataclasses import dataclass, field
from typing import List, Dict, Optional
from datetime import datetime
@dataclass
class VivadoMessage:
"""Vivado消息"""
level: str # INFO, WARNING, ERROR, CRITICAL
id: str # 消息ID,如 [Synth 8-3331]
message: str # 消息内容
file: Optional[str] = None
line: Optional[int] = None
@dataclass
class VivadoBuildResult:
"""Vivado构建结果"""
success: bool
start_time: Optional[datetime] = None
end_time: Optional[datetime] = None
duration_seconds: float = 0
messages: List[VivadoMessage] = field(default_factory=list)
warnings_count: int = 0
errors_count: int = 0
critical_count: int = 0
# 时序结果
wns: float = 0
tns: float = 0
whs: float = 0
ths: float = 0
timing_met: bool = True
# 资源利用
lut_used: int = 0
lut_available: int = 0
ff_used: int = 0
ff_available: int = 0
bram_used: float = 0
bram_available: int = 0
dsp_used: int = 0
dsp_available: int = 0
class VivadoLogParser:
"""Vivado日志解析器"""
def __init__(self):
self.result = VivadoBuildResult(success=False)
# 消息模式
self.msg_pattern = re.compile(
r'(INFO|WARNING|ERROR|CRITICAL WARNING): \[([^\]]+)\] (.+)'
)
# 时序模式
self.timing_patterns = {
'wns': re.compile(r'WNS\(ns\)\s*:\s*([-\d.]+)'),
'tns': re.compile(r'TNS\(ns\)\s*:\s*([-\d.]+)'),
'whs': re.compile(r'WHS\(ns\)\s*:\s*([-\d.]+)'),
'ths': re.compile(r'THS\(ns\)\s*:\s*([-\d.]+)'),
}
# 资源模式
self.util_patterns = {
'lut': re.compile(r'Slice LUTs\*?\s*\|\s*(\d+)\s*\|\s*(\d+)'),
'ff': re.compile(r'Slice Registers\s*\|\s*(\d+)\s*\|\s*(\d+)'),
'bram': re.compile(r'Block RAM Tile\s*\|\s*([\d.]+)\s*\|\s*(\d+)'),
'dsp': re.compile(r'DSPs\s*\|\s*(\d+)\s*\|\s*(\d+)'),
}
def parse(self, log_file: str) -> VivadoBuildResult:
"""解析Vivado日志"""
content = Path(log_file).read_text(encoding='utf-8', errors='ignore')
self.result = VivadoBuildResult(success=True)
# 解析消息
self._parse_messages(content)
# 解析时序
self._parse_timing(content)
# 解析资源
self._parse_utilization(content)
# 判断成功
self.result.success = (self.result.errors_count == 0 and
self.result.critical_count == 0)
return self.result
def _parse_messages(self, content: str):
"""解析消息"""
for match in self.msg_pattern.finditer(content):
level, msg_id, message = match.groups()
msg = VivadoMessage(
level=level,
id=msg_id,
message=message.strip()
)
self.result.messages.append(msg)
if level == 'WARNING':
self.result.warnings_count += 1
elif level == 'ERROR':
self.result.errors_count += 1
elif level == 'CRITICAL WARNING':
self.result.critical_count += 1
def _parse_timing(self, content: str):
"""解析时序"""
for name, pattern in self.timing_patterns.items():
match = pattern.search(content)
if match:
setattr(self.result, name, float(match.group(1)))
self.result.timing_met = (self.result.wns >= 0 and self.result.whs >= 0)
def _parse_utilization(self, content: str):
"""解析资源利用率"""
for name, pattern in self.util_patterns.items():
match = pattern.search(content)
if match:
used = float(match.group(1))
available = int(match.group(2))
setattr(self.result, f'{name}_used', used)
setattr(self.result, f'{name}_available', available)
def get_errors(self) -> List[VivadoMessage]:
"""获取所有错误"""
return [m for m in self.result.messages if m.level == 'ERROR']
def get_warnings(self) -> List[VivadoMessage]:
"""获取所有警告"""
return [m for m in self.result.messages
if m.level in ('WARNING', 'CRITICAL WARNING')]
def print_summary(self):
"""打印摘要"""
r = self.result
print("="*60)
print("Vivado构建摘要")
print("="*60)
print(f"状态: {'成功' if r.success else '失败'}")
print(f"错误: {r.errors_count}, 严重警告: {r.critical_count}, 警告: {r.warnings_count}")
print()
print("时序:")
print(f" WNS: {r.wns:.3f} ns")
print(f" TNS: {r.tns:.3f} ns")
print(f" WHS: {r.whs:.3f} ns")
print(f" 时序满足: {'是' if r.timing_met else '否'}")
print()
print("资源利用率:")
if r.lut_available > 0:
print(f" LUT: {r.lut_used}/{r.lut_available} ({100*r.lut_used/r.lut_available:.1f}%)")
if r.ff_available > 0:
print(f" FF: {r.ff_used}/{r.ff_available} ({100*r.ff_used/r.ff_available:.1f}%)")
if r.bram_available > 0:
print(f" BRAM: {r.bram_used}/{r.bram_available} ({100*r.bram_used/r.bram_available:.1f}%)")
if r.dsp_available > 0:
print(f" DSP: {r.dsp_used}/{r.dsp_available} ({100*r.dsp_used/r.dsp_available:.1f}%)")
print("="*60)
# 使用
# parser = VivadoLogParser()
# result = parser.parse('vivado.log')
# parser.print_summary()
3. 仿真日志解析
import re
from pathlib import Path
from dataclasses import dataclass, field
from typing import List, Dict, Optional
from enum import Enum
class TestStatus(Enum):
PASS = "PASS"
FAIL = "FAIL"
ERROR = "ERROR"
SKIP = "SKIP"
@dataclass
class TestCase:
"""测试用例"""
name: str
status: TestStatus
duration_ms: float = 0
message: str = ""
assertions_passed: int = 0
assertions_failed: int = 0
@dataclass
class SimulationResult:
"""仿真结果"""
total_tests: int = 0
passed: int = 0
failed: int = 0
errors: int = 0
skipped: int = 0
duration_ms: float = 0
test_cases: List[TestCase] = field(default_factory=list)
coverage: Dict[str, float] = field(default_factory=dict)
class SimulationLogParser:
"""仿真日志解析器"""
def __init__(self):
self.result = SimulationResult()
# 常见仿真器输出模式
self.patterns = {
# ModelSim/QuestaSim
'modelsim_test': re.compile(r'# \*\* (Note|Warning|Error|Fatal): (.+)'),
'modelsim_time': re.compile(r'# Time: (\d+) (\w+)'),
# Vivado Simulator
'xsim_test': re.compile(r'\[(\d+)\] (PASS|FAIL|ERROR): (.+)'),
'xsim_assert': re.compile(r'(ASSERTION|CHECK) (PASSED|FAILED): (.+)'),
# 通用测试输出
'test_result': re.compile(r'TEST\s+(\w+)\s*:\s*(PASS|FAIL|ERROR)'),
'assertion': re.compile(r'(ASSERT|CHECK)\s+(PASS|FAIL):\s*(.+)'),
}
def parse(self, log_file: str) -> SimulationResult:
"""解析仿真日志"""
content = Path(log_file).read_text(encoding='utf-8', errors='ignore')
self.result = SimulationResult()
# 解析测试结果
self._parse_tests(content)
# 解析覆盖率
self._parse_coverage(content)
# 计算统计
self._calculate_stats()
return self.result
def _parse_tests(self, content: str):
"""解析测试结果"""
# 查找测试结果
for match in self.patterns['test_result'].finditer(content):
name, status = match.groups()
test = TestCase(
name=name,
status=TestStatus[status]
)
self.result.test_cases.append(test)
# 查找断言
for match in self.patterns['assertion'].finditer(content):
_, status, message = match.groups()
if self.result.test_cases:
if status == 'PASS':
self.result.test_cases[-1].assertions_passed += 1
else:
self.result.test_cases[-1].assertions_failed += 1
def _parse_coverage(self, content: str):
"""解析覆盖率"""
coverage_patterns = {
'line': re.compile(r'Line Coverage:\s*([\d.]+)%'),
'branch': re.compile(r'Branch Coverage:\s*([\d.]+)%'),
'toggle': re.compile(r'Toggle Coverage:\s*([\d.]+)%'),
'fsm': re.compile(r'FSM Coverage:\s*([\d.]+)%'),
}
for name, pattern in coverage_patterns.items():
match = pattern.search(content)
if match:
self.result.coverage[name] = float(match.group(1))
def _calculate_stats(self):
"""计算统计"""
self.result.total_tests = len(self.result.test_cases)
self.result.passed = sum(1 for t in self.result.test_cases
if t.status == TestStatus.PASS)
self.result.failed = sum(1 for t in self.result.test_cases
if t.status == TestStatus.FAIL)
self.result.errors = sum(1 for t in self.result.test_cases
if t.status == TestStatus.ERROR)
self.result.skipped = sum(1 for t in self.result.test_cases
if t.status == TestStatus.SKIP)
def get_failed_tests(self) -> List[TestCase]:
"""获取失败的测试"""
return [t for t in self.result.test_cases
if t.status in (TestStatus.FAIL, TestStatus.ERROR)]
def print_summary(self):
"""打印摘要"""
r = self.result
print("="*60)
print("仿真测试摘要")
print("="*60)
print(f"总测试数: {r.total_tests}")
print(f"通过: {r.passed}")
print(f"失败: {r.failed}")
print(f"错误: {r.errors}")
print(f"跳过: {r.skipped}")
if r.coverage:
print("\n覆盖率:")
for name, value in r.coverage.items():
print(f" {name}: {value:.1f}%")
if r.failed > 0 or r.errors > 0:
print("\n失败的测试:")
for test in self.get_failed_tests():
print(f" - {test.name}: {test.status.value}")
if test.message:
print(f" {test.message}")
print("="*60)
4. 报告生成
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Any
from dataclasses import dataclass
import json
@dataclass
class ReportSection:
"""报告章节"""
title: str
content: str
level: int = 1
class ReportGenerator:
"""报告生成器"""
def __init__(self, title: str):
self.title = title
self.sections: List[ReportSection] = []
self.metadata: Dict[str, Any] = {
'generated_at': datetime.now().isoformat(),
'generator': 'Python Report Generator'
}
def add_section(self, title: str, content: str, level: int = 1):
"""添加章节"""
self.sections.append(ReportSection(title, content, level))
def add_table(self, title: str, headers: List[str],
rows: List[List[Any]], level: int = 1):
"""添加表格"""
# 生成Markdown表格
lines = []
lines.append('| ' + ' | '.join(headers) + ' |')
lines.append('| ' + ' | '.join(['---'] * len(headers)) + ' |')
for row in rows:
lines.append('| ' + ' | '.join(str(cell) for cell in row) + ' |')
self.add_section(title, '\n'.join(lines), level)
def add_key_value(self, title: str, data: Dict[str, Any], level: int = 1):
"""添加键值对"""
lines = []
for key, value in data.items():
lines.append(f"- **{key}**: {value}")
self.add_section(title, '\n'.join(lines), level)
def to_markdown(self) -> str:
"""生成Markdown报告"""
lines = [f"# {self.title}", ""]
# 元数据
lines.append(f"*生成时间: {self.metadata['generated_at']}*")
lines.append("")
# 目录
lines.append("## 目录")
for i, section in enumerate(self.sections, 1):
indent = " " * (section.level - 1)
lines.append(f"{indent}- [{section.title}](#{section.title.lower().replace(' ', '-')})")
lines.append("")
# 内容
for section in self.sections:
prefix = "#" * (section.level + 1)
lines.append(f"{prefix} {section.title}")
lines.append("")
lines.append(section.content)
lines.append("")
return '\n'.join(lines)
def to_text(self) -> str:
"""生成纯文本报告"""
lines = ["=" * 60, self.title, "=" * 60, ""]
for section in self.sections:
lines.append("-" * 40)
lines.append(section.title)
lines.append("-" * 40)
lines.append(section.content)
lines.append("")
return '\n'.join(lines)
def save(self, filepath: str, format: str = 'markdown'):
"""保存报告"""
if format == 'markdown':
content = self.to_markdown()
elif format == 'text':
content = self.to_text()
elif format == 'json':
content = json.dumps({
'title': self.title,
'metadata': self.metadata,
'sections': [
{'title': s.title, 'content': s.content, 'level': s.level}
for s in self.sections
]
}, indent=2, ensure_ascii=False)
else:
content = self.to_markdown()
Path(filepath).write_text(content, encoding='utf-8')
print(f"报告已保存到: {filepath}")
# 使用示例
def generate_build_report(vivado_result: VivadoBuildResult):
"""生成构建报告"""
report = ReportGenerator("FPGA构建报告")
# 概述
status = "✅ 成功" if vivado_result.success else "❌ 失败"
report.add_key_value("构建概述", {
"状态": status,
"错误数": vivado_result.errors_count,
"警告数": vivado_result.warnings_count,
})
# 时序
timing_status = "✅ 满足" if vivado_result.timing_met else "❌ 不满足"
report.add_key_value("时序分析", {
"状态": timing_status,
"WNS": f"{vivado_result.wns:.3f} ns",
"TNS": f"{vivado_result.tns:.3f} ns",
"WHS": f"{vivado_result.whs:.3f} ns",
})
# 资源
report.add_table("资源利用率",
["资源", "已用", "可用", "利用率"],
[
["LUT", vivado_result.lut_used, vivado_result.lut_available,
f"{100*vivado_result.lut_used/max(1,vivado_result.lut_available):.1f}%"],
["FF", vivado_result.ff_used, vivado_result.ff_available,
f"{100*vivado_result.ff_used/max(1,vivado_result.ff_available):.1f}%"],
["BRAM", vivado_result.bram_used, vivado_result.bram_available,
f"{100*vivado_result.bram_used/max(1,vivado_result.bram_available):.1f}%"],
["DSP", vivado_result.dsp_used, vivado_result.dsp_available,
f"{100*vivado_result.dsp_used/max(1,vivado_result.dsp_available):.1f}%"],
]
)
return report
5. HTML报告
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Any
class HTMLReportGenerator:
"""HTML报告生成器"""
CSS_STYLE = """
<style>
body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }
.container { max-width: 1200px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
h1 { color: #333; border-bottom: 2px solid #4472C4; padding-bottom: 10px; }
h2 { color: #4472C4; margin-top: 30px; }
.summary-box { display: flex; gap: 20px; margin: 20px 0; }
.summary-item { flex: 1; padding: 15px; border-radius: 8px; text-align: center; }
.summary-item.success { background: #d4edda; border: 1px solid #c3e6cb; }
.summary-item.warning { background: #fff3cd; border: 1px solid #ffeeba; }
.summary-item.error { background: #f8d7da; border: 1px solid #f5c6cb; }
.summary-item.info { background: #d1ecf1; border: 1px solid #bee5eb; }
.summary-value { font-size: 24px; font-weight: bold; }
.summary-label { font-size: 12px; color: #666; }
table { width: 100%; border-collapse: collapse; margin: 15px 0; }
th, td { padding: 10px; text-align: left; border: 1px solid #ddd; }
th { background: #4472C4; color: white; }
tr:nth-child(even) { background: #f9f9f9; }
.pass { color: #28a745; font-weight: bold; }
.fail { color: #dc3545; font-weight: bold; }
.progress-bar { height: 20px; background: #e9ecef; border-radius: 4px; overflow: hidden; }
.progress-fill { height: 100%; background: #4472C4; transition: width 0.3s; }
.timestamp { color: #666; font-size: 12px; }
</style>
"""
def __init__(self, title: str):
self.title = title
self.content = []
def add_summary_boxes(self, items: List[Dict]):
"""添加摘要框"""
html = '<div class="summary-box">'
for item in items:
css_class = item.get('class', 'info')
html += f'''
<div class="summary-item {css_class}">
<div class="summary-value">{item['value']}</div>
<div class="summary-label">{item['label']}</div>
</div>
'''
html += '</div>'
self.content.append(html)
def add_section(self, title: str, content: str):
"""添加章节"""
self.content.append(f'<h2>{title}</h2>')
self.content.append(content)
def add_table(self, headers: List[str], rows: List[List[Any]]):
"""添加表格"""
html = '<table><thead><tr>'
for h in headers:
html += f'<th>{h}</th>'
html += '</tr></thead><tbody>'
for row in rows:
html += '<tr>'
for cell in row:
html += f'<td>{cell}</td>'
html += '</tr>'
html += '</tbody></table>'
self.content.append(html)
def add_progress_bar(self, label: str, value: float, max_value: float = 100):
"""添加进度条"""
percentage = min(100, value / max_value * 100)
html = f'''
<div style="margin: 10px 0;">
<div style="display: flex; justify-content: space-between;">
<span>{label}</span>
<span>{value:.1f}%</span>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: {percentage}%;"></div>
</div>
</div>
'''
self.content.append(html)
def generate(self) -> str:
"""生成HTML"""
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
html = f'''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{self.title}</title>
{self.CSS_STYLE}
</head>
<body>
<div class="container">
<h1>{self.title}</h1>
<p class="timestamp">生成时间: {timestamp}</p>
{''.join(self.content)}
</div>
</body>
</html>
'''
return html
def save(self, filepath: str):
"""保存HTML文件"""
Path(filepath).write_text(self.generate(), encoding='utf-8')
print(f"HTML报告已保存到: {filepath}")
# 使用示例
def generate_html_report(vivado_result, sim_result=None):
"""生成HTML报告"""
report = HTMLReportGenerator("FPGA项目报告")
# 摘要框
status_class = 'success' if vivado_result.success else 'error'
timing_class = 'success' if vivado_result.timing_met else 'error'
report.add_summary_boxes([
{'value': '✓' if vivado_result.success else '✗',
'label': '构建状态', 'class': status_class},
{'value': '✓' if vivado_result.timing_met else '✗',
'label': '时序状态', 'class': timing_class},
{'value': vivado_result.errors_count,
'label': '错误', 'class': 'error' if vivado_result.errors_count > 0 else 'success'},
{'value': vivado_result.warnings_count,
'label': '警告', 'class': 'warning' if vivado_result.warnings_count > 0 else 'info'},
])
# 时序
report.add_section("时序分析", "")
report.add_table(
['指标', '值', '状态'],
[
['WNS', f'{vivado_result.wns:.3f} ns',
'<span class="pass">PASS</span>' if vivado_result.wns >= 0 else '<span class="fail">FAIL</span>'],
['TNS', f'{vivado_result.tns:.3f} ns', '-'],
['WHS', f'{vivado_result.whs:.3f} ns',
'<span class="pass">PASS</span>' if vivado_result.whs >= 0 else '<span class="fail">FAIL</span>'],
]
)
# 资源利用率
report.add_section("资源利用率", "")
if vivado_result.lut_available > 0:
report.add_progress_bar('LUT', 100 * vivado_result.lut_used / vivado_result.lut_available)
if vivado_result.ff_available > 0:
report.add_progress_bar('FF', 100 * vivado_result.ff_used / vivado_result.ff_available)
if vivado_result.bram_available > 0:
report.add_progress_bar('BRAM', 100 * vivado_result.bram_used / vivado_result.bram_available)
if vivado_result.dsp_available > 0:
report.add_progress_bar('DSP', 100 * vivado_result.dsp_used / vivado_result.dsp_available)
return report
6. 自动化报告系统
from pathlib import Path
from datetime import datetime
import json
class AutoReportSystem:
"""自动化报告系统"""
def __init__(self, project_dir: str):
self.project_dir = Path(project_dir)
self.reports_dir = self.project_dir / 'reports'
self.reports_dir.mkdir(exist_ok=True)
def generate_full_report(self) -> str:
"""生成完整报告"""
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
# 解析Vivado日志
vivado_parser = VivadoLogParser()
vivado_log = self.project_dir / 'build' / 'vivado.log'
if vivado_log.exists():
vivado_result = vivado_parser.parse(str(vivado_log))
else:
vivado_result = VivadoBuildResult(success=False)
# 解析仿真日志
sim_parser = SimulationLogParser()
sim_log = self.project_dir / 'sim' / 'simulation.log'
if sim_log.exists():
sim_result = sim_parser.parse(str(sim_log))
else:
sim_result = None
# 生成Markdown报告
md_report = self._generate_markdown(vivado_result, sim_result)
md_file = self.reports_dir / f'report_{timestamp}.md'
md_file.write_text(md_report, encoding='utf-8')
# 生成HTML报告
html_gen = generate_html_report(vivado_result, sim_result)
html_file = self.reports_dir / f'report_{timestamp}.html'
html_gen.save(str(html_file))
# 生成JSON数据
json_data = self._generate_json(vivado_result, sim_result)
json_file = self.reports_dir / f'report_{timestamp}.json'
json_file.write_text(json.dumps(json_data, indent=2, ensure_ascii=False))
print(f"报告已生成:")
print(f" Markdown: {md_file}")
print(f" HTML: {html_file}")
print(f" JSON: {json_file}")
return str(html_file)
def _generate_markdown(self, vivado_result, sim_result) -> str:
"""生成Markdown报告"""
report = ReportGenerator("FPGA项目报告")
# 构建结果
report.add_key_value("构建结果", {
"状态": "成功" if vivado_result.success else "失败",
"错误": vivado_result.errors_count,
"警告": vivado_result.warnings_count,
})
# 时序
report.add_key_value("时序分析", {
"WNS": f"{vivado_result.wns:.3f} ns",
"TNS": f"{vivado_result.tns:.3f} ns",
"时序满足": "是" if vivado_result.timing_met else "否",
})
# 仿真结果
if sim_result:
report.add_key_value("仿真结果", {
"总测试": sim_result.total_tests,
"通过": sim_result.passed,
"失败": sim_result.failed,
})
return report.to_markdown()
def _generate_json(self, vivado_result, sim_result) -> dict:
"""生成JSON数据"""
data = {
'timestamp': datetime.now().isoformat(),
'build': {
'success': vivado_result.success,
'errors': vivado_result.errors_count,
'warnings': vivado_result.warnings_count,
},
'timing': {
'wns': vivado_result.wns,
'tns': vivado_result.tns,
'met': vivado_result.timing_met,
},
'utilization': {
'lut': {'used': vivado_result.lut_used, 'available': vivado_result.lut_available},
'ff': {'used': vivado_result.ff_used, 'available': vivado_result.ff_available},
}
}
if sim_result:
data['simulation'] = {
'total': sim_result.total_tests,
'passed': sim_result.passed,
'failed': sim_result.failed,
}
return data
7. 实战案例
案例:FPGA项目CI报告生成器
"""
实战案例:FPGA项目CI报告生成器
用于持续集成环境,自动解析日志并生成报告
"""
import argparse
from pathlib import Path
from datetime import datetime
import sys
class CIReportGenerator:
"""CI报告生成器"""
def __init__(self, project_dir: str):
self.project_dir = Path(project_dir)
self.success = True
self.results = {}
def run(self) -> int:
"""运行报告生成"""
print("="*60)
print("FPGA CI 报告生成器")
print("="*60)
# 1. 解析构建日志
self._parse_build()
# 2. 解析仿真日志
self._parse_simulation()
# 3. 生成报告
self._generate_reports()
# 4. 输出摘要
self._print_summary()
return 0 if self.success else 1
def _parse_build(self):
"""解析构建日志"""
print("\n[1/3] 解析构建日志...")
log_patterns = [
self.project_dir / 'build' / 'vivado.log',
self.project_dir / 'vivado.log',
]
for log_file in log_patterns:
if log_file.exists():
parser = VivadoLogParser()
self.results['build'] = parser.parse(str(log_file))
print(f" 已解析: {log_file}")
if not self.results['build'].success:
self.success = False
return
print(" 警告: 未找到构建日志")
self.results['build'] = None
def _parse_simulation(self):
"""解析仿真日志"""
print("\n[2/3] 解析仿真日志...")
log_patterns = [
self.project_dir / 'sim' / 'simulation.log',
self.project_dir / 'simulation.log',
]
for log_file in log_patterns:
if log_file.exists():
parser = SimulationLogParser()
self.results['simulation'] = parser.parse(str(log_file))
print(f" 已解析: {log_file}")
if self.results['simulation'].failed > 0:
self.success = False
return
print(" 警告: 未找到仿真日志")
self.results['simulation'] = None
def _generate_reports(self):
"""生成报告"""
print("\n[3/3] 生成报告...")
reports_dir = self.project_dir / 'reports'
reports_dir.mkdir(exist_ok=True)
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
# HTML报告
if self.results.get('build'):
html_gen = HTMLReportGenerator("FPGA CI 报告")
# 构建状态
build = self.results['build']
html_gen.add_summary_boxes([
{'value': '✓' if build.success else '✗',
'label': '构建', 'class': 'success' if build.success else 'error'},
{'value': '✓' if build.timing_met else '✗',
'label': '时序', 'class': 'success' if build.timing_met else 'error'},
{'value': build.errors_count,
'label': '错误', 'class': 'error' if build.errors_count > 0 else 'success'},
{'value': build.warnings_count,
'label': '警告', 'class': 'warning' if build.warnings_count > 0 else 'info'},
])
# 时序详情
html_gen.add_section("时序分析", "")
html_gen.add_table(
['指标', '值', '状态'],
[
['WNS', f'{build.wns:.3f} ns',
'<span class="pass">PASS</span>' if build.wns >= 0 else '<span class="fail">FAIL</span>'],
['TNS', f'{build.tns:.3f} ns', '-'],
['WHS', f'{build.whs:.3f} ns',
'<span class="pass">PASS</span>' if build.whs >= 0 else '<span class="fail">FAIL</span>'],
]
)
# 资源利用率
html_gen.add_section("资源利用率", "")
if build.lut_available > 0:
html_gen.add_progress_bar('LUT', 100 * build.lut_used / build.lut_available)
if build.ff_available > 0:
html_gen.add_progress_bar('FF', 100 * build.ff_used / build.ff_available)
# 仿真结果
if self.results.get('simulation'):
sim = self.results['simulation']
html_gen.add_section("仿真测试", "")
html_gen.add_table(
['指标', '值'],
[
['总测试', sim.total_tests],
['通过', f'<span class="pass">{sim.passed}</span>'],
['失败', f'<span class="fail">{sim.failed}</span>' if sim.failed > 0 else '0'],
]
)
html_file = reports_dir / f'ci_report_{timestamp}.html'
html_gen.save(str(html_file))
print(f" HTML: {html_file}")
# 生成badge数据(用于README显示)
badge_data = {
'build': 'passing' if self.success else 'failing',
'timing': 'met' if (self.results.get('build') and
self.results['build'].timing_met) else 'not met',
}
badge_file = reports_dir / 'badge.json'
import json
badge_file.write_text(json.dumps(badge_data, indent=2))
print(f" Badge: {badge_file}")
def _print_summary(self):
"""打印摘要"""
print("\n" + "="*60)
print("CI 结果摘要")
print("="*60)
if self.results.get('build'):
build = self.results['build']
print(f"构建: {'✓ 成功' if build.success else '✗ 失败'}")
print(f"时序: {'✓ 满足' if build.timing_met else '✗ 不满足'} (WNS={build.wns:.3f}ns)")
print(f"消息: {build.errors_count} 错误, {build.warnings_count} 警告")
if self.results.get('simulation'):
sim = self.results['simulation']
print(f"仿真: {sim.passed}/{sim.total_tests} 通过")
print("="*60)
print(f"总体结果: {'✓ 通过' if self.success else '✗ 失败'}")
print("="*60)
def main():
parser = argparse.ArgumentParser(description='FPGA CI报告生成器')
parser.add_argument('project_dir', nargs='?', default='.',
help='项目目录')
args = parser.parse_args()
generator = CIReportGenerator(args.project_dir)
sys.exit(generator.run())
if __name__ == '__main__':
main()
8. 总结
🔑 核心要点
| 知识点 | 要点 |
|---|---|
| 日志解析 | 正则表达式匹配,状态机解析 |
| Vivado日志 | 消息、时序、资源利用率 |
| 仿真日志 | 测试结果、覆盖率 |
| 报告格式 | Markdown、HTML、JSON |
| 自动化 | CI集成、批量处理 |
✅ 学习检查清单
- 能解析Vivado构建日志
- 能解析仿真测试日志
- 能生成Markdown报告
- 能生成HTML报告
- 能集成到CI流程
📖 下一步学习
FPGA开发辅助部分学习完成!接下来进入实用技巧篇:
常见问题 FAQ
💬 日志文件太大怎么处理?
用生成器逐行读取,不要一次性read()。配合正则表达式边读边匹配,内存占用极小。几个GB的日志文件也能秒级处理。
💬 怎么生成好看的HTML报告?
用Jinja2模板引擎,把数据填入HTML模板。也可以用pandas的to_html()直接生成表格。加上CSS样式就是一份专业的测试报告。
📘 系列导航
- 上一篇:27 - Python生成测试向量
- 当前:28 - Python日志解析与报告生成
- 下一篇:29 - Python代码调试技巧