PyGithub MCP Server

""" Data models for coverage analysis. This module contains the dataclasses that represent coverage data and test failures for reporting purposes. """ from typing import List, Set, Dict, Any from dataclasses import dataclass, field # ANSI color codes for terminal output COLORS = { "red": "\033[31m", "green": "\033[32m", "yellow": "\033[33m", "blue": "\033[34m", "magenta": "\033[35m", "cyan": "\033[36m", "white": "\033[37m", "reset": "\033[0m", "bold": "\033[1m", } @dataclass class ModuleCoverage: """Coverage information for a module.""" name: str statements: int missing: int branches: int branch_missing: int coverage: float missing_lines: str # Additional processed fields missing_line_ranges: List[str] = field(default_factory=list) parsed_missing_lines: Set[int] = field(default_factory=set) @property def priority(self) -> str: """Determine testing priority based on coverage.""" if self.coverage < 70: return "High" elif self.coverage < 85: return "Medium" else: return "Low" @property def priority_color(self) -> str: """Get color for priority level.""" if self.priority == "High": return COLORS["red"] elif self.priority == "Medium": return COLORS["yellow"] else: return COLORS["green"] def parse_missing_lines(self) -> None: """Parse missing lines string into a set of individual line numbers and ranges.""" if not self.missing_lines: self.parsed_missing_lines = set() return parts = self.missing_lines.split(", ") line_set = set() range_list = [] for part in parts: # Handle ranges like "10-20" if "->" in part: # Handle branch coverage notation "10->12" start, end = part.split("->") line_set.add(int(start)) range_list.append(f"{start} (branch)") elif "-" in part: # Handle line ranges "10-20" start, end = part.split("-") line_set.update(range(int(start), int(end) + 1)) range_list.append(f"{start}-{end}") else: # Handle individual lines try: line_set.add(int(part)) except ValueError: # Skip any non-integer parts pass self.parsed_missing_lines = line_set self.missing_line_ranges = range_list @dataclass class ModulePriority: """Priority group for modules.""" name: str modules: List[ModuleCoverage] = field(default_factory=list) @property def count(self) -> int: return len(self.modules) @property def total_missing_lines(self) -> int: return sum(m.missing for m in self.modules) @dataclass class CoverageReport: """Complete coverage analysis report.""" timestamp: str overall_coverage: float total_statements: int total_missing: int modules_count: int high_priority: ModulePriority = field(default_factory=lambda: ModulePriority("High")) medium_priority: ModulePriority = field(default_factory=lambda: ModulePriority("Medium")) low_priority: ModulePriority = field(default_factory=lambda: ModulePriority("Low")) def add_module(self, module: ModuleCoverage) -> None: """Add a module to the appropriate priority group.""" if module.priority == "High": self.high_priority.modules.append(module) elif module.priority == "Medium": self.medium_priority.modules.append(module) else: self.low_priority.modules.append(module) def sort_modules(self) -> None: """Sort modules within each priority group by coverage (ascending).""" self.high_priority.modules.sort(key=lambda m: (m.coverage, m.name)) self.medium_priority.modules.sort(key=lambda m: (m.coverage, m.name)) self.low_priority.modules.sort(key=lambda m: (m.coverage, m.name)) def to_dict(self) -> Dict[str, Any]: """Convert the report to a dictionary for JSON serialization.""" return { "timestamp": self.timestamp, "summary": { "overall_coverage": self.overall_coverage, "total_statements": self.total_statements, "total_missing": self.total_missing, "modules_count": self.modules_count, "high_priority_count": self.high_priority.count, "medium_priority_count": self.medium_priority.count, "low_priority_count": self.low_priority.count, }, "high_priority_modules": [ { "name": m.name, "coverage": m.coverage, "statements": m.statements, "missing": m.missing, "missing_lines": m.missing_lines, "parsed_lines": list(m.parsed_missing_lines), "missing_ranges": m.missing_line_ranges } for m in self.high_priority.modules ], "medium_priority_modules": [ { "name": m.name, "coverage": m.coverage, "statements": m.statements, "missing": m.missing, "missing_lines": m.missing_lines, "parsed_lines": list(m.parsed_missing_lines), "missing_ranges": m.missing_line_ranges } for m in self.medium_priority.modules ], "low_priority_modules": [ { "name": m.name, "coverage": m.coverage, "statements": m.statements, "missing": m.missing, "missing_lines": m.missing_lines, "parsed_lines": list(m.parsed_missing_lines), "missing_ranges": m.missing_line_ranges } for m in self.low_priority.modules ] } def print_summary(self) -> None: """Print a colorful summary of the coverage report to the console.""" print(f"\n{COLORS['bold']}=== Coverage Analysis Report ==={COLORS['reset']}") print(f"Generated on: {self.timestamp}") print(f"Overall coverage: {self.overall_coverage_colored}") print(f"Total statements: {self.total_statements}") print(f"Missing statements: {self.total_missing}") print(f"Total modules: {self.modules_count}") print(f"\n{COLORS['bold']}Priority Groups:{COLORS['reset']}") print(f" {COLORS['red']}High Priority{COLORS['reset']}: {self.high_priority.count} modules ({self.high_priority.total_missing_lines} missing lines)") print(f" {COLORS['yellow']}Medium Priority{COLORS['reset']}: {self.medium_priority.count} modules ({self.medium_priority.total_missing_lines} missing lines)") print(f" {COLORS['green']}Low Priority{COLORS['reset']}: {self.low_priority.count} modules ({self.low_priority.total_missing_lines} missing lines)") if self.high_priority.count > 0: print(f"\n{COLORS['bold']}{COLORS['red']}Top High Priority Modules:{COLORS['reset']}") for module in self.high_priority.modules[:5]: # Only show top 5 print(f" {module.name}: {module.coverage}% coverage ({module.missing} missing lines)") if self.medium_priority.count > 0: print(f"\n{COLORS['bold']}{COLORS['yellow']}Top Medium Priority Modules:{COLORS['reset']}") for module in self.medium_priority.modules[:5]: # Only show top 5 print(f" {module.name}: {module.coverage}% coverage ({module.missing} missing lines)") @property def overall_coverage_colored(self) -> str: """Get the overall coverage percentage with appropriate color.""" if self.overall_coverage < 70: return f"{COLORS['red']}{self.overall_coverage:.2f}%{COLORS['reset']}" elif self.overall_coverage < 85: return f"{COLORS['yellow']}{self.overall_coverage:.2f}%{COLORS['reset']}" else: return f"{COLORS['green']}{self.overall_coverage:.2f}%{COLORS['reset']}" @dataclass class TestFailure: """Information about a test failure.""" name: str outcome: str message: str duration: float = 0.0 file: str = "" line: int = 0 @property def category(self) -> str: """Classify the failure based on the error message.""" if "SyntaxError" in self.message: return "syntax_error" elif "AssertionError" in self.message: return "assertion_failure" elif any(err in self.message for err in ["ImportError", "ModuleNotFoundError"]): return "import_error" elif "timeout" in self.message.lower(): return "timeout_error" else: return "other" @property def module_name(self) -> str: """Extract module name from test path.""" parts = self.name.split('::')[0].split('/') if len(parts) > 1: return parts[-1].replace('test_', '').replace('.py', '') return "" def get_color_for_coverage(coverage: float) -> str: """Return an appropriate color for the given coverage percentage.""" if coverage < 70: return "#d9534f" # Red elif coverage < 85: return "#f0ad4e" # Yellow else: return "#5cb85c" # Green