Veloris.
返回索引
设计实战 2026-02-14

Python日志解析与报告生成:正则+pandas解析Vivado日志,自动生成测试报告

2 分钟
458 words

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样式就是一份专业的测试报告。


📘 系列导航

End of file.