Spaces:
Sleeping
Sleeping
| """ | |
| Industry-level code validator for UVM testbench generation. | |
| Validates generated SystemVerilog code for: | |
| 1. Basic syntax correctness | |
| 2. Spec compliance (signals, registers, interfaces used) | |
| 3. UVM best practices | |
| 4. Common error patterns | |
| 5. Compilation readiness | |
| Provides detailed validation reports with: | |
| - Errors (blocking issues) | |
| - Warnings (potential issues) | |
| - Info (best practice suggestions) | |
| - Auto-fix suggestions | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| import re | |
| from dataclasses import dataclass, field | |
| from enum import Enum | |
| from typing import Any, Dict, List, Optional, Set, Tuple, Pattern | |
| logger = logging.getLogger("uvmgen.validator") | |
| class ValidationSeverity(Enum): | |
| ERROR = "error" | |
| WARNING = "warning" | |
| INFO = "info" | |
| STYLE = "style" | |
| class ValidationIssue: | |
| """Single validation issue.""" | |
| severity: ValidationSeverity | |
| code: str | |
| message: str | |
| line_number: Optional[int] = None | |
| context: Optional[str] = None | |
| suggestion: Optional[str] = None | |
| auto_fixable: bool = False | |
| def to_dict(self) -> Dict[str, Any]: | |
| return { | |
| "severity": self.severity.value, | |
| "code": self.code, | |
| "message": self.message, | |
| "line_number": self.line_number, | |
| "context": self.context, | |
| "suggestion": self.suggestion, | |
| "auto_fixable": self.auto_fixable, | |
| } | |
| class FileValidationResult: | |
| """Validation result for a single file.""" | |
| filename: str | |
| file_type: str | |
| passed: bool | |
| issues: List[ValidationIssue] = field(default_factory=list) | |
| checks_run: int = 0 | |
| checks_passed: int = 0 | |
| def error_count(self) -> int: | |
| return sum(1 for i in self.issues if i.severity == ValidationSeverity.ERROR) | |
| def warning_count(self) -> int: | |
| return sum(1 for i in self.issues if i.severity == ValidationSeverity.WARNING) | |
| def info_count(self) -> int: | |
| return sum(1 for i in self.issues if i.severity == ValidationSeverity.INFO) | |
| def to_dict(self) -> Dict[str, Any]: | |
| return { | |
| "filename": self.filename, | |
| "file_type": self.file_type, | |
| "passed": self.passed, | |
| "error_count": self.error_count, | |
| "warning_count": self.warning_count, | |
| "info_count": self.info_count, | |
| "checks_run": self.checks_run, | |
| "checks_passed": self.checks_passed, | |
| "issues": [i.to_dict() for i in self.issues], | |
| } | |
| class ValidationReport: | |
| """Complete validation report for a generation run.""" | |
| design_name: str | |
| overall_passed: bool | |
| files: List[FileValidationResult] = field(default_factory=list) | |
| timestamp: str = "" | |
| def total_errors(self) -> int: | |
| return sum(f.error_count for f in self.files) | |
| def total_warnings(self) -> int: | |
| return sum(f.warning_count for f in self.files) | |
| def total_checks_run(self) -> int: | |
| return sum(f.checks_run for f in self.files) | |
| def total_checks_passed(self) -> int: | |
| return sum(f.checks_passed for f in self.files) | |
| def pass_rate(self) -> float: | |
| if self.total_checks_run == 0: | |
| return 1.0 | |
| return self.total_checks_passed / self.total_checks_run | |
| def to_dict(self) -> Dict[str, Any]: | |
| return { | |
| "design_name": self.design_name, | |
| "overall_passed": self.overall_passed, | |
| "total_errors": self.total_errors, | |
| "total_warnings": self.total_warnings, | |
| "total_checks_run": self.total_checks_run, | |
| "total_checks_passed": self.total_checks_passed, | |
| "pass_rate": round(self.pass_rate * 100, 1), | |
| "files": [f.to_dict() for f in self.files], | |
| } | |
| SV_KEYWORDS = { | |
| "module", "endmodule", "interface", "endinterface", "class", "endclass", | |
| "input", "output", "inout", "logic", "reg", "wire", "bit", "int", "integer", | |
| "always", "initial", "assign", "begin", "end", "case", "endcase", "if", "else", | |
| "for", "while", "repeat", "forever", "task", "endtask", "function", "endfunction", | |
| "parameter", "localparam", "defparam", "typedef", "struct", "union", "enum", | |
| "posedge", "negedge", "or", "and", "not", "default", "none", | |
| "import", "export", "package", "endpackage", "include", "define", | |
| "uvm_object_utils", "uvm_component_utils", "uvm_field_utils", | |
| "virtual", "rand", "randc", "constraint", "extends", "implements", | |
| } | |
| UVM_BASE_CLASSES = { | |
| "uvm_test", "uvm_env", "uvm_agent", "uvm_driver", "uvm_monitor", | |
| "uvm_sequencer", "uvm_sequence", "uvm_sequence_item", "uvm_scoreboard", | |
| "uvm_subscriber", "uvm_reg_block", "uvm_reg", "uvm_reg_field", | |
| "uvm_reg_map", "uvm_reg_adapter", "uvm_reg_predictor", | |
| "uvm_analysis_port", "uvm_analysis_imp", "uvm_tlm_fifo", | |
| "uvm_component", "uvm_object", "uvm_report_object", | |
| } | |
| class SystemVerilogSyntaxChecker: | |
| """Basic but effective SystemVerilog syntax checker.""" | |
| PAIR_CHECKS = [ | |
| ("module", ["endmodule"]), | |
| ("interface", ["endinterface"]), | |
| ("class", ["endclass"]), | |
| ("function", ["endfunction"]), | |
| ("task", ["endtask"]), | |
| ("case", ["endcase"]), | |
| ("begin", ["end"]), | |
| ("fork", ["join", "join_any", "join_none"]), | |
| ] | |
| def __init__(self): | |
| self._patterns: Dict[str, Pattern] = {} | |
| self._compile_patterns() | |
| def _compile_patterns(self) -> None: | |
| self._patterns = { | |
| "comment_single": re.compile(r'//.*$', re.MULTILINE), | |
| "comment_multi": re.compile(r'/\*.*?\*/', re.DOTALL), | |
| "string_lit": re.compile(r'"[^"]*"'), | |
| "module_decl": re.compile(r'\bmodule\s+(\w+)\s*[#(;]'), | |
| "interface_decl": re.compile(r'\binterface\s+(\w+)\s*[#(;]'), | |
| "class_decl": re.compile(r'\bclass\s+(\w+)\s*(?:#\s*\(|extends|implements|;|{)'), | |
| "port_list": re.compile(r'\(([^)]+)\)'), | |
| "unbalanced_paren": re.compile(r'[()]'), | |
| "unbalanced_bracket": re.compile(r'[\[\]]'), | |
| "unbalanced_brace": re.compile(r'[{}]'), | |
| "semicolon": re.compile(r';\s*$'), | |
| } | |
| def _strip_comments_and_strings(self, content: str) -> str: | |
| """Remove comments and strings for analysis.""" | |
| result = content | |
| result = self._patterns["comment_multi"].sub(" ", result) | |
| result = self._patterns["comment_single"].sub(" ", result) | |
| result = self._patterns["string_lit"].sub("\"STR\"", result) | |
| return result | |
| def check_balance(self, content: str) -> List[ValidationIssue]: | |
| """Check balanced delimiters (heuristic, warnings only).""" | |
| issues: List[ValidationIssue] = [] | |
| stripped = self._strip_comments_and_strings(content) | |
| checks = [ | |
| ("()", "parentheses"), | |
| ("[]", "brackets"), | |
| ("{}", "braces"), | |
| ] | |
| for pair, name in checks: | |
| count_open = stripped.count(pair[0]) | |
| count_close = stripped.count(pair[1]) | |
| if count_open != count_close: | |
| issues.append(ValidationIssue( | |
| severity=ValidationSeverity.WARNING, | |
| code=f"SV-SYN-001-{name}", | |
| message=f"Possibly unbalanced {name}: {count_open} '{pair[0]}' vs {count_close} '{pair[1]}'", | |
| auto_fixable=False, | |
| )) | |
| return issues | |
| def check_begin_end_pairs(self, content: str) -> List[ValidationIssue]: | |
| """Check begin/end and other block pairs (heuristic, warnings only).""" | |
| issues: List[ValidationIssue] = [] | |
| stripped = self._strip_comments_and_strings(content) | |
| lines = stripped.split('\n') | |
| for open_kw, close_kws in self.PAIR_CHECKS: | |
| close_kws_set = set(close_kws) | |
| close_kw_display = close_kws[0] if len(close_kws) == 1 else f"{close_kws[0]}/..." | |
| stack: List[int] = [] | |
| for line_num, line in enumerate(lines, 1): | |
| words = re.findall(r'\b\w+\b', line.lower()) | |
| for word in words: | |
| if word == open_kw: | |
| stack.append(line_num) | |
| elif word in close_kws_set: | |
| if stack: | |
| stack.pop() | |
| for line_num in stack: | |
| issues.append(ValidationIssue( | |
| severity=ValidationSeverity.WARNING, | |
| code="SV-SYN-003", | |
| message=f"'{open_kw}' at line {line_num} may have no matching '{close_kw_display}'", | |
| line_number=line_num, | |
| auto_fixable=False, | |
| )) | |
| return issues | |
| def check_semicolons(self, content: str) -> List[ValidationIssue]: | |
| """Check for missing semicolons (heuristic).""" | |
| issues: List[ValidationIssue] = [] | |
| lines = content.split('\n') | |
| statement_keywords = { | |
| "logic", "reg", "wire", "bit", "int", "input", "output", "inout", | |
| "parameter", "localparam", "typedef", "import", "assign", | |
| } | |
| block_starters = { | |
| "module", "interface", "class", "function", "task", "case", | |
| "begin", "fork", "if", "else", "for", "while", "repeat", "forever", | |
| } | |
| block_enders = { | |
| "endmodule", "endinterface", "endclass", "endfunction", "endtask", | |
| "endcase", "end", "join", "join_any", "join_none", | |
| } | |
| for line_num, line in enumerate(lines, 1): | |
| stripped = line.strip() | |
| if not stripped: | |
| continue | |
| if stripped.startswith('//'): | |
| continue | |
| if stripped.startswith('`'): | |
| continue | |
| first_word = stripped.split()[0].lower() if stripped.split() else "" | |
| if first_word in block_enders: | |
| continue | |
| if first_word in block_starters: | |
| if stripped.rstrip().endswith((':', 'begin', '{')): | |
| continue | |
| if first_word in statement_keywords: | |
| if not stripped.rstrip().endswith(';') and not stripped.rstrip().endswith(')'): | |
| issues.append(ValidationIssue( | |
| severity=ValidationSeverity.WARNING, | |
| code="SV-SYN-004", | |
| message="Possible missing semicolon", | |
| line_number=line_num, | |
| context=stripped[:60], | |
| suggestion="Add ';' at end of statement", | |
| auto_fixable=True, | |
| )) | |
| return issues | |
| def check(self, content: str) -> List[ValidationIssue]: | |
| """Run all syntax checks.""" | |
| issues: List[ValidationIssue] = [] | |
| issues.extend(self.check_balance(content)) | |
| issues.extend(self.check_begin_end_pairs(content)) | |
| issues.extend(self.check_semicolons(content)) | |
| return issues | |
| class SpecComplianceChecker: | |
| """Check that generated code matches the design spec.""" | |
| def __init__(self, spec_dict: Dict[str, Any]): | |
| self.spec = spec_dict | |
| self.design_name = spec_dict.get("design_name", "unknown") | |
| self._extract_signals() | |
| self._extract_registers() | |
| def _extract_signals(self) -> None: | |
| """Extract all signals from spec.""" | |
| self.all_signals: Set[str] = set() | |
| self.signals_by_direction: Dict[str, Set[str]] = { | |
| "input": set(), | |
| "output": set(), | |
| "inout": set(), | |
| } | |
| self.signal_widths: Dict[str, int] = {} | |
| for iface in self.spec.get("interfaces", []): | |
| for sig in iface.get("signals", []): | |
| name = sig.get("name", "") | |
| if name: | |
| self.all_signals.add(name) | |
| direction = sig.get("direction", "input") | |
| self.signals_by_direction.get(direction, set()).add(name) | |
| self.signal_widths[name] = sig.get("width", 1) | |
| def _extract_registers(self) -> None: | |
| """Extract all registers from spec.""" | |
| self.all_registers: Set[str] = set() | |
| self.register_addresses: Dict[str, str] = {} | |
| self.register_fields: Dict[str, Set[str]] = {} | |
| for reg in self.spec.get("registers", []): | |
| name = reg.get("name", "") | |
| if name: | |
| self.all_registers.add(name) | |
| self.register_addresses[name] = reg.get("address", "") | |
| self.register_fields[name] = { | |
| f.get("name", "") for f in reg.get("fields", []) if f.get("name") | |
| } | |
| def check_signals_in_code( | |
| self, | |
| content: str, | |
| file_type: str, | |
| ) -> List[ValidationIssue]: | |
| """Check that spec signals are referenced in code.""" | |
| issues: List[ValidationIssue] = [] | |
| stripped = self._strip_for_analysis(content) | |
| found_signals: Set[str] = set() | |
| for sig in self.all_signals: | |
| if re.search(r'\b' + re.escape(sig) + r'\b', stripped, re.IGNORECASE): | |
| found_signals.add(sig) | |
| if file_type in ("interface", "testbench"): | |
| missing_signals = self.all_signals - found_signals | |
| for sig in sorted(missing_signals): | |
| issues.append(ValidationIssue( | |
| severity=ValidationSeverity.ERROR, | |
| code="SPEC-001", | |
| message=f"Signal '{sig}' defined in spec but not found in {file_type}", | |
| suggestion=f"Ensure signal '{sig}' is declared in the {file_type}", | |
| auto_fixable=False, | |
| )) | |
| for sig in sorted(found_signals & self.all_signals): | |
| issues.append(ValidationIssue( | |
| severity=ValidationSeverity.INFO, | |
| code="SPEC-002", | |
| message=f"Signal '{sig}' from spec is properly referenced", | |
| auto_fixable=False, | |
| )) | |
| return issues | |
| def check_registers_in_code( | |
| self, | |
| content: str, | |
| file_type: str, | |
| ) -> List[ValidationIssue]: | |
| """Check that spec registers are referenced in code.""" | |
| issues: List[ValidationIssue] = [] | |
| if file_type not in ("ral_model", "test", "sequence", "scoreboard", "coverage"): | |
| return issues | |
| if not self.all_registers: | |
| return issues | |
| stripped = self._strip_for_analysis(content) | |
| found_registers: Set[str] = set() | |
| for reg in self.all_registers: | |
| if re.search(r'\b' + re.escape(reg) + r'\b', stripped, re.IGNORECASE): | |
| found_registers.add(reg) | |
| if file_type == "ral_model": | |
| missing_regs = self.all_registers - found_registers | |
| for reg in sorted(missing_regs): | |
| issues.append(ValidationIssue( | |
| severity=ValidationSeverity.ERROR, | |
| code="SPEC-003", | |
| message=f"Register '{reg}' defined in spec but not found in RAL model", | |
| auto_fixable=False, | |
| )) | |
| return issues | |
| def check_clock_reset( | |
| self, | |
| content: str, | |
| file_type: str, | |
| ) -> List[ValidationIssue]: | |
| """Check clock/reset signals are present.""" | |
| issues: List[ValidationIssue] = [] | |
| if file_type not in ("interface", "testbench"): | |
| return issues | |
| cr = self.spec.get("clock_reset", {}) | |
| clock = cr.get("clock", "clk") | |
| reset = cr.get("reset", "rst_n") | |
| stripped = self._strip_for_analysis(content) | |
| if not re.search(r'\b' + re.escape(clock) + r'\b', stripped, re.IGNORECASE): | |
| issues.append(ValidationIssue( | |
| severity=ValidationSeverity.ERROR, | |
| code="SPEC-004", | |
| message=f"Clock signal '{clock}' not found in {file_type}", | |
| auto_fixable=False, | |
| )) | |
| if not re.search(r'\b' + re.escape(reset) + r'\b', stripped, re.IGNORECASE): | |
| issues.append(ValidationIssue( | |
| severity=ValidationSeverity.ERROR, | |
| code="SPEC-005", | |
| message=f"Reset signal '{reset}' not found in {file_type}", | |
| auto_fixable=False, | |
| )) | |
| return issues | |
| def _strip_for_analysis(content: str) -> str: | |
| """Strip comments and strings for analysis.""" | |
| result = content | |
| result = re.sub(r'/\*.*?\*/', ' ', result, flags=re.DOTALL) | |
| result = re.sub(r'//.*$', ' ', result, flags=re.MULTILINE) | |
| result = re.sub(r'"[^"]*"', 'STR', result) | |
| return result | |
| class UVMBestPracticesChecker: | |
| """Check UVM best practices and common patterns.""" | |
| def check(self, content: str, file_type: str) -> List[ValidationIssue]: | |
| """Run UVM best practice checks.""" | |
| issues: List[ValidationIssue] = [] | |
| lines = content.split('\n') | |
| checks: Dict[str, List[Tuple[Pattern, ValidationSeverity, str, Optional[str]]]] = { | |
| "driver": [ | |
| (re.compile(r'\bseq_item_port\.(get|next_item)\b'), | |
| ValidationSeverity.INFO, "UVM-DRV-001", "Uses proper sequence item retrieval"), | |
| (re.compile(r'\bseq_item_port\.item_done\b'), | |
| ValidationSeverity.INFO, "UVM-DRV-002", "Properly completes items"), | |
| ], | |
| "monitor": [ | |
| (re.compile(r'\banalysis_port\s*<'), | |
| ValidationSeverity.INFO, "UVM-MON-001", "Has analysis port"), | |
| (re.compile(r'\bwrite\s*\('), | |
| ValidationSeverity.INFO, "UVM-MON-002", "Writes to analysis port"), | |
| ], | |
| "agent": [ | |
| (re.compile(r'\b(driver|monitor|sequencer)\s*=\s*'), | |
| ValidationSeverity.INFO, "UVM-AGT-001", "Creates agent components"), | |
| (re.compile(r'\bget_is_active\b'), | |
| ValidationSeverity.INFO, "UVM-AGT-002", "Checks active/passive mode"), | |
| ], | |
| "scoreboard": [ | |
| (re.compile(r'\buvm_analysis_imp\s*<'), | |
| ValidationSeverity.INFO, "UVM-SCB-001", "Has analysis exports"), | |
| (re.compile(r'\bwrite\s*\(\s*\w+\s+(\w+)\)'), | |
| ValidationSeverity.INFO, "UVM-SCB-002", "Implements write methods"), | |
| ], | |
| "test": [ | |
| (re.compile(r'\buvm_top\.(finish|stop|objection)'), | |
| ValidationSeverity.INFO, "UVM-TEST-001", "Proper objection handling"), | |
| (re.compile(r'\braise_objection\b'), | |
| ValidationSeverity.INFO, "UVM-TEST-002", "Raises objections"), | |
| (re.compile(r'\bdrop_objection\b'), | |
| ValidationSeverity.INFO, "UVM-TEST-003", "Drops objections"), | |
| ], | |
| "sequence": [ | |
| (re.compile(r'\bstart_item\b'), | |
| ValidationSeverity.INFO, "UVM-SEQ-001", "Uses start_item"), | |
| (re.compile(r'\bfinish_item\b'), | |
| ValidationSeverity.INFO, "UVM-SEQ-002", "Uses finish_item"), | |
| ], | |
| "any": [ | |
| (re.compile(r'\b`uvm_(component|object)_utils\b'), | |
| ValidationSeverity.INFO, "UVM-ANY-001", "Uses UVM factory registration"), | |
| (re.compile(r'\buvm_info\s*\('), | |
| ValidationSeverity.INFO, "UVM-ANY-002", "Has UVM messaging"), | |
| ], | |
| } | |
| error_patterns = [ | |
| (re.compile(r'\buvm_error\s*\(\s*"[^"]*"\s*,\s*"[^"]*"\s*,\s*UVM_(LOW|MEDIUM|HIGH|FULL|DEBUG)\)'), | |
| ValidationSeverity.INFO, "UVM-ANY-003", "Proper uvm_error usage"), | |
| ] | |
| relevant_checks = checks.get(file_type, []) + checks.get("any", []) | |
| for pattern, severity, code, message in relevant_checks: | |
| if pattern.search(content): | |
| issues.append(ValidationIssue( | |
| severity=severity, | |
| code=code, | |
| message=message, | |
| auto_fixable=False, | |
| )) | |
| is_uvm = any(uvm_base in content for uvm_base in UVM_BASE_CLASSES) | |
| if is_uvm and file_type in ("test", "env", "sequence"): | |
| if not re.search(r'\b(raise|drop)_objection\b', content): | |
| issues.append(ValidationIssue( | |
| severity=ValidationSeverity.WARNING, | |
| code="UVM-WARN-001", | |
| message="UVM test/sequence without objection handling", | |
| suggestion="Consider adding raise_objection/drop_objection for proper test termination", | |
| auto_fixable=False, | |
| )) | |
| return issues | |
| class CodeValidator: | |
| """ | |
| Industry-level code validator for UVM testbench generation. | |
| Provides comprehensive validation with: | |
| - Syntax checking | |
| - Spec compliance | |
| - UVM best practices | |
| - Detailed reporting | |
| """ | |
| FILE_TYPE_DETECTORS = [ | |
| (r'ral_model', "ral_model"), | |
| (r'scoreboard', "scoreboard"), | |
| (r'driver', "driver"), | |
| (r'monitor', "monitor"), | |
| (r'agent', "agent"), | |
| (r'sequence_item', "sequence_item"), | |
| (r'_sequence', "sequence"), | |
| (r'regression', "sequence"), | |
| (r'coverage_collector', "coverage"), | |
| (r'protocol_checker', "checker"), | |
| (r'_test', "test"), | |
| (r'environment|env_', "env"), | |
| (r'testbench', "testbench"), | |
| (r'interface', "interface"), | |
| (r'serial_monitor', "monitor"), | |
| ] | |
| NON_SV_EXTENSIONS = {'.f', '.tcl', '.core', '.json', '.yaml', '.yml', '.md', '.txt'} | |
| def __init__(self, spec_dict: Optional[Dict[str, Any]] = None): | |
| self.spec_dict = spec_dict | |
| self._syntax_checker = SystemVerilogSyntaxChecker() | |
| self._spec_checker = SpecComplianceChecker(spec_dict) if spec_dict else None | |
| self._uvm_checker = UVMBestPracticesChecker() | |
| def _is_sv_file(cls, filename: str) -> bool: | |
| """Check if file is a SystemVerilog/Verilog file.""" | |
| fname_lower = filename.lower() | |
| for ext in cls.NON_SV_EXTENSIONS: | |
| if fname_lower.endswith(ext): | |
| return False | |
| if fname_lower.endswith(('.sv', '.v', '.svh', '.vh')): | |
| return True | |
| if '/' in fname_lower or '\\' in fname_lower: | |
| base = fname_lower.replace('\\', '/').split('/')[-1] | |
| if '.' not in base: | |
| return True | |
| return True | |
| def detect_file_type(cls, filename: str) -> str: | |
| """Detect the type of SystemVerilog file from its name.""" | |
| fname_lower = filename.lower() | |
| for pattern, file_type in cls.FILE_TYPE_DETECTORS: | |
| if re.search(pattern, fname_lower): | |
| return file_type | |
| return "unknown" | |
| def validate_file( | |
| self, | |
| filename: str, | |
| content: str, | |
| file_type: Optional[str] = None, | |
| ) -> FileValidationResult: | |
| """Validate a single file. Skip non-SV files.""" | |
| if not self._is_sv_file(filename): | |
| return FileValidationResult( | |
| filename=filename, | |
| file_type="skipped", | |
| passed=True, | |
| issues=[], | |
| checks_run=0, | |
| checks_passed=0, | |
| ) | |
| if file_type is None: | |
| file_type = self.detect_file_type(filename) | |
| issues: List[ValidationIssue] = [] | |
| checks_run = 0 | |
| checks_passed = 0 | |
| syntax_issues = self._syntax_checker.check(content) | |
| issues.extend(syntax_issues) | |
| checks_run += 3 | |
| syntax_errors = sum(1 for i in syntax_issues if i.severity == ValidationSeverity.ERROR) | |
| checks_passed += (3 - min(syntax_errors, 3)) | |
| if self._spec_checker: | |
| spec_issues = self._spec_checker.check_signals_in_code(content, file_type) | |
| issues.extend(spec_issues) | |
| checks_run += 2 | |
| reg_issues = self._spec_checker.check_registers_in_code(content, file_type) | |
| issues.extend(reg_issues) | |
| cr_issues = self._spec_checker.check_clock_reset(content, file_type) | |
| issues.extend(cr_issues) | |
| spec_errors = sum(1 for i in spec_issues + reg_issues + cr_issues | |
| if i.severity == ValidationSeverity.ERROR) | |
| checks_passed += max(0, 2 - spec_errors) | |
| if file_type != "unknown": | |
| uvm_issues = self._uvm_checker.check(content, file_type) | |
| issues.extend(uvm_issues) | |
| errors = sum(1 for i in issues if i.severity == ValidationSeverity.ERROR) | |
| passed = errors == 0 | |
| return FileValidationResult( | |
| filename=filename, | |
| file_type=file_type, | |
| passed=passed, | |
| issues=issues, | |
| checks_run=checks_run, | |
| checks_passed=checks_passed, | |
| ) | |
| def validate_files( | |
| self, | |
| files: Dict[str, str], | |
| design_name: str = "", | |
| ) -> ValidationReport: | |
| """Validate multiple files and generate a report.""" | |
| file_results: List[FileValidationResult] = [] | |
| for filename, content in files.items(): | |
| result = self.validate_file(filename, content) | |
| file_results.append(result) | |
| total_errors = sum(f.error_count for f in file_results) | |
| overall_passed = total_errors == 0 | |
| import datetime | |
| report = ValidationReport( | |
| design_name=design_name, | |
| overall_passed=overall_passed, | |
| files=file_results, | |
| timestamp=datetime.datetime.now().isoformat(), | |
| ) | |
| return report | |
| def validate_files_by_path( | |
| self, | |
| file_paths: Dict[str, str], | |
| design_name: str = "", | |
| ) -> ValidationReport: | |
| """Validate files given as path mappings.""" | |
| content_map: Dict[str, str] = {} | |
| for filename, path in file_paths.items(): | |
| try: | |
| with open(path, "r", encoding="utf-8") as f: | |
| content_map[filename] = f.read() | |
| except Exception as e: | |
| logger.warning("Failed to read %s: %s", path, e) | |
| content_map[filename] = "" | |
| return self.validate_files(content_map, design_name) | |