#!/usr/bin/env python3 """ Testing Efficiency Optimizer - Specialized agent for optimizing test execution efficiency. This tool addresses Issue #57 by diagnosing pytest issues, optimizing test execution, and enhancing TDD8 workflow integration for better red-green iteration cycles. """ import json import os import re import subprocess import sys import time from dataclasses import dataclass, asdict from pathlib import Path from typing import Dict, List, Optional, Set, Tuple import argparse from datetime import datetime @dataclass class PytestIssue: """Represents a pytest issue or problem.""" type: str description: str file_path: Optional[str] line_number: Optional[int] severity: str # 'low', 'medium', 'high', 'critical' solution: str command_suggestion: Optional[str] @dataclass class TestPerformanceMetrics: """Test execution performance metrics.""" total_tests: int execution_time: float passed_tests: int failed_tests: int skipped_tests: int slowest_tests: List[Tuple[str, float]] cache_hit_rate: Optional[float] parallel_efficiency: Optional[float] @dataclass class TestOptimizationReport: """Comprehensive test optimization report.""" timestamp: str current_performance: TestPerformanceMetrics identified_issues: List[PytestIssue] optimization_recommendations: List[str] tdd_workflow_improvements: List[str] agent_integration_suggestions: List[str] class TestExecutionAnalyzer: """Analyzes test execution patterns and identifies optimization opportunities.""" def __init__(self, repo_path: str = "."): self.repo_path = Path(repo_path) def analyze_pytest_issues(self) -> List[PytestIssue]: """Identify and analyze pytest-related issues.""" issues = [] # Check for common pytest configuration issues issues.extend(self._check_pytest_configuration()) # Check for import path issues issues.extend(self._check_import_path_issues()) # Check for cache-related issues issues.extend(self._check_cache_issues()) # Check for test discovery issues issues.extend(self._check_test_discovery_issues()) # Check for recent test failures issues.extend(self._check_recent_test_failures()) return issues def measure_test_performance(self) -> TestPerformanceMetrics: """Measure current test execution performance.""" try: # Run tests with timing and capture output start_time = time.time() result = subprocess.run( ['make', 'test'], capture_output=True, text=True, cwd=self.repo_path, timeout=600 # 10 minute timeout ) execution_time = time.time() - start_time # Parse test results output = result.stdout + result.stderr metrics = self._parse_test_output(output, execution_time) return metrics except subprocess.TimeoutExpired: return TestPerformanceMetrics( total_tests=0, execution_time=600.0, passed_tests=0, failed_tests=0, skipped_tests=0, slowest_tests=[], cache_hit_rate=None, parallel_efficiency=None ) except Exception as e: print(f"Error measuring test performance: {e}") return TestPerformanceMetrics( total_tests=0, execution_time=0.0, passed_tests=0, failed_tests=0, skipped_tests=0, slowest_tests=[], cache_hit_rate=None, parallel_efficiency=None ) def identify_slow_tests(self) -> List[Tuple[str, float]]: """Identify the slowest tests for optimization.""" try: # Run tests with duration reporting result = subprocess.run( ['python', '-m', 'pytest', '--durations=10', 'tests/'], capture_output=True, text=True, cwd=self.repo_path, env={**os.environ, 'PYTHONPATH': '.'} ) slow_tests = [] output = result.stdout + result.stderr # Parse duration output duration_pattern = r'(\d+\.\d+)s\s+(.+?)(?:\s+|$)' for match in re.finditer(duration_pattern, output): duration = float(match.group(1)) test_name = match.group(2).strip() if duration > 1.0: # Tests slower than 1 second slow_tests.append((test_name, duration)) return sorted(slow_tests, key=lambda x: x[1], reverse=True) except Exception as e: print(f"Error identifying slow tests: {e}") return [] def _check_pytest_configuration(self) -> List[PytestIssue]: """Check for pytest configuration issues.""" issues = [] # Check for pytest.ini pytest_ini = self.repo_path / "pytest.ini" if not pytest_ini.exists(): issues.append(PytestIssue( type="configuration", description="Missing pytest.ini configuration file", file_path=None, line_number=None, severity="medium", solution="Create pytest.ini with proper configuration", command_suggestion="Create pytest.ini with testpaths, markers, and addopts" )) # Check for proper test path configuration pyproject_toml = self.repo_path / "pyproject.toml" if pyproject_toml.exists(): try: with open(pyproject_toml, 'r') as f: content = f.read() if '[tool.pytest' not in content: issues.append(PytestIssue( type="configuration", description="Missing pytest configuration in pyproject.toml", file_path=str(pyproject_toml), line_number=None, severity="low", solution="Add [tool.pytest.ini_options] section", command_suggestion=None )) except Exception: pass return issues def _check_import_path_issues(self) -> List[PytestIssue]: """Check for Python import path issues.""" issues = [] # Check if PYTHONPATH is needed try: result = subprocess.run( ['python', '-c', 'import markitect'], capture_output=True, text=True, cwd=self.repo_path ) if result.returncode != 0: issues.append(PytestIssue( type="import_path", description="Module import fails without PYTHONPATH", file_path=None, line_number=None, severity="high", solution="Ensure PYTHONPATH=. is set for test execution", command_suggestion="PYTHONPATH=. python -m pytest" )) except Exception: pass # Check for relative imports in tests test_files = list(self.repo_path.glob("tests/**/*.py")) for test_file in test_files: try: with open(test_file, 'r') as f: content = f.read() if 'from markitect' in content or 'import markitect' in content: # This is good - absolute imports continue elif 'from ..' in content: issues.append(PytestIssue( type="import_path", description="Relative imports found in test file", file_path=str(test_file), line_number=None, severity="medium", solution="Use absolute imports instead of relative imports", command_suggestion=None )) except Exception: continue return issues def _check_cache_issues(self) -> List[PytestIssue]: """Check for pytest cache-related issues.""" issues = [] # Check for corrupted cache cache_dir = self.repo_path / ".pytest_cache" if cache_dir.exists(): cache_size = sum(f.stat().st_size for f in cache_dir.rglob('*') if f.is_file()) if cache_size > 100 * 1024 * 1024: # 100MB issues.append(PytestIssue( type="cache", description="Pytest cache is very large (>100MB)", file_path=str(cache_dir), line_number=None, severity="medium", solution="Clean pytest cache to improve performance", command_suggestion="make test-cache-clean" )) # Check for __pycache__ accumulation pycache_dirs = list(self.repo_path.rglob("__pycache__")) if len(pycache_dirs) > 50: issues.append(PytestIssue( type="cache", description=f"Many __pycache__ directories found ({len(pycache_dirs)})", file_path=None, line_number=None, severity="low", solution="Clean Python cache directories", command_suggestion="find . -name '__pycache__' -type d -exec rm -rf {} +" )) return issues def _check_test_discovery_issues(self) -> List[PytestIssue]: """Check for test discovery problems.""" issues = [] # Check for test files that might not be discovered test_files = list(self.repo_path.glob("tests/**/*.py")) discovered_pattern_files = [ f for f in test_files if f.name.startswith('test_') or f.name.endswith('_test.py') ] non_discovered_files = [ f for f in test_files if f not in discovered_pattern_files and f.name != '__init__.py' ] if non_discovered_files: issues.append(PytestIssue( type="test_discovery", description=f"Test files may not be discovered: {[f.name for f in non_discovered_files]}", file_path=None, line_number=None, severity="medium", solution="Rename files to follow test_*.py or *_test.py pattern", command_suggestion=None )) return issues def _check_recent_test_failures(self) -> List[PytestIssue]: """Check for patterns in recent test failures.""" issues = [] try: # Check git log for test-related commits result = subprocess.run( ['git', 'log', '--oneline', '-10'], capture_output=True, text=True, cwd=self.repo_path ) commits = result.stdout.strip().split('\n') test_related_commits = [c for c in commits if 'test' in c.lower() or 'fix' in c.lower()] if len(test_related_commits) > 5: issues.append(PytestIssue( type="test_reliability", description="High frequency of test-related commits suggests test instability", file_path=None, line_number=None, severity="medium", solution="Review test reliability and stability patterns", command_suggestion="make test-arch" )) except Exception: pass return issues def _parse_test_output(self, output: str, execution_time: float) -> TestPerformanceMetrics: """Parse pytest output to extract performance metrics.""" # Initialize default values total_tests = 0 passed_tests = 0 failed_tests = 0 skipped_tests = 0 slowest_tests = [] # Parse test summary summary_pattern = r'(\d+) passed' match = re.search(summary_pattern, output) if match: passed_tests = int(match.group(1)) total_tests += passed_tests failed_pattern = r'(\d+) failed' match = re.search(failed_pattern, output) if match: failed_tests = int(match.group(1)) total_tests += failed_tests skipped_pattern = r'(\d+) skipped' match = re.search(skipped_pattern, output) if match: skipped_tests = int(match.group(1)) total_tests += skipped_tests # Parse slowest tests if available duration_pattern = r'(\d+\.\d+)s\s+(.+?)(?:\s+|$)' for match in re.finditer(duration_pattern, output): duration = float(match.group(1)) test_name = match.group(2).strip() slowest_tests.append((test_name, duration)) # Sort and limit to top 5 slowest_tests = sorted(slowest_tests, key=lambda x: x[1], reverse=True)[:5] return TestPerformanceMetrics( total_tests=total_tests, execution_time=execution_time, passed_tests=passed_tests, failed_tests=failed_tests, skipped_tests=skipped_tests, slowest_tests=slowest_tests, cache_hit_rate=None, # Would need specific pytest plugin parallel_efficiency=None # Would need specific analysis ) class TDD8WorkflowOptimizer: """Optimizes test execution for TDD8 red-green cycles.""" def __init__(self, repo_path: str = "."): self.repo_path = Path(repo_path) def generate_tdd_optimizations(self) -> List[str]: """Generate TDD workflow optimization recommendations.""" optimizations = [] # Fast test execution for red phase optimizations.append( "Implement fast test execution for TDD red phase: " "Use 'make test-quick' or 'make test-changed' for rapid feedback" ) # Smart test selection optimizations.append( "Implement smart test selection: " "Run only tests affected by current changes using git diff analysis" ) # Parallel execution optimization optimizations.append( "Optimize parallel test execution: " "Configure pytest-xdist for multi-core test execution" ) # Cache optimization optimizations.append( "Implement test result caching: " "Cache test results for unchanged code to speed up iterations" ) # Test prioritization optimizations.append( "Implement test prioritization: " "Run fast, critical tests first, slower integration tests later" ) return optimizations def create_smart_test_commands(self) -> Dict[str, str]: """Create optimized test commands for different scenarios.""" commands = { # Red phase - fast failure detection "red_phase": "PYTHONPATH=. python -m pytest tests/ -x --maxfail=1 --tb=short", # Green phase - comprehensive validation "green_phase": "PYTHONPATH=. python -m pytest tests/ --tb=short", # Changed files only "changed_only": "PYTHONPATH=. python -m pytest $(git diff --name-only HEAD~1 | grep test_ | tr '\\n' ' ') -v", # Fast subset "fast_subset": "PYTHONPATH=. python -m pytest tests/ -m 'not slow' --maxfail=3", # Architecture tests "architecture": "PYTHONPATH=. python -m pytest tests/ -k 'arch' --tb=short", # Unit tests only "unit_only": "PYTHONPATH=. python -m pytest tests/ -m 'unit' --tb=short", } return commands class TestInfrastructureEnhancer: """Enhances test infrastructure for reliability and performance.""" def __init__(self, repo_path: str = "."): self.repo_path = Path(repo_path) def generate_pytest_config_recommendations(self) -> str: """Generate optimized pytest configuration.""" config = """ [tool:pytest] minversion = 6.0 addopts = --strict-markers --strict-config --disable-warnings --tb=short --maxfail=5 --timeout=300 -ra --durations=10 testpaths = tests python_files = test_*.py python_classes = Test* python_functions = test_* markers = slow: marks tests as slow (deselect with '-m \"not slow\"') integration: marks tests as integration tests unit: marks tests as unit tests smoke: marks tests as smoke tests arch: marks tests as architecture tests timeout = 300 """ return config.strip() def generate_makefile_enhancements(self) -> str: """Generate enhanced Makefile targets for test optimization.""" makefile_content = """ # Enhanced test targets for Issue #57 # Fast test execution for TDD red phase test-red: @echo "๐Ÿ”ด TDD Red Phase - Fast test execution..." PYTHONPATH=. python -m pytest tests/ -x --maxfail=1 --tb=short -q # Comprehensive test execution for TDD green phase test-green: @echo "๐ŸŸข TDD Green Phase - Comprehensive validation..." PYTHONPATH=. python -m pytest tests/ --tb=short # Smart test selection - changed files only test-smart: @echo "๐Ÿง  Smart test selection - changed files only..." @changed_tests=$$(git diff --name-only HEAD~1 | grep test_ | tr '\\n' ' '); \\ if [ -n "$$changed_tests" ]; then \\ PYTHONPATH=. python -m pytest $$changed_tests -v; \\ else \\ echo "No test files changed, running fast subset"; \\ $(MAKE) test-fast; \\ fi # Ultra-fast test execution test-ultra-fast: @echo "โšก Ultra-fast test execution..." PYTHONPATH=. python -m pytest tests/ -m "not slow" --maxfail=1 -x -q # Test with performance monitoring test-perf: @echo "๐Ÿ“Š Test execution with performance monitoring..." PYTHONPATH=. python -m pytest tests/ --durations=10 --tb=short # Clean all test caches test-cache-clean: @echo "๐Ÿงน Cleaning test caches..." find . -name '.pytest_cache' -type d -exec rm -rf {} + 2>/dev/null || true find . -name '__pycache__' -type d -exec rm -rf {} + 2>/dev/null || true find . -name '*.pyc' -delete 2>/dev/null || true # Test health check test-health: @echo "๐Ÿฅ Test infrastructure health check..." @python tools/testing_efficiency_optimizer.py diagnose # TDD workflow optimization test-tdd-optimize: @echo "๐Ÿ”ง Optimizing TDD workflow..." @python tools/testing_efficiency_optimizer.py optimize-tdd """ return makefile_content.strip() def main(): """Main entry point for the testing efficiency optimizer.""" parser = argparse.ArgumentParser(description="Testing Efficiency Optimizer") parser.add_argument("command", choices=["diagnose", "optimize", "optimize-tdd", "performance", "report"], help="Command to execute") parser.add_argument("--format", choices=["json", "markdown", "text"], default="markdown", help="Output format") parser.add_argument("--output", help="Output file (default: stdout)") args = parser.parse_args() # Initialize components analyzer = TestExecutionAnalyzer() tdd_optimizer = TDD8WorkflowOptimizer() infrastructure_enhancer = TestInfrastructureEnhancer() if args.command == "diagnose": # Diagnose pytest issues issues = analyzer.analyze_pytest_issues() if args.format == "json": output = json.dumps([asdict(issue) for issue in issues], indent=2) else: output = f"# Pytest Issues Diagnosis\n\nFound {len(issues)} issues:\n\n" for i, issue in enumerate(issues, 1): output += f"## Issue {i}: {issue.type.title()}\n" output += f"- **Severity**: {issue.severity}\n" output += f"- **Description**: {issue.description}\n" if issue.file_path: output += f"- **File**: {issue.file_path}\n" output += f"- **Solution**: {issue.solution}\n" if issue.command_suggestion: output += f"- **Command**: `{issue.command_suggestion}`\n" output += "\n" elif args.command == "performance": # Measure test performance metrics = analyzer.measure_test_performance() slow_tests = analyzer.identify_slow_tests() if args.format == "json": data = asdict(metrics) data['slow_tests'] = slow_tests output = json.dumps(data, indent=2) else: output = f"# Test Performance Analysis\n\n" output += f"- **Total Tests**: {metrics.total_tests}\n" output += f"- **Execution Time**: {metrics.execution_time:.2f} seconds\n" output += f"- **Passed**: {metrics.passed_tests}\n" output += f"- **Failed**: {metrics.failed_tests}\n" output += f"- **Skipped**: {metrics.skipped_tests}\n\n" if slow_tests: output += "## Slowest Tests\n\n" for test_name, duration in slow_tests[:5]: output += f"- {test_name}: {duration:.2f}s\n" elif args.command == "optimize": # Generate optimization recommendations issues = analyzer.analyze_pytest_issues() output = "# Testing Infrastructure Optimization\n\n" # Configuration recommendations output += "## Recommended pytest.ini Configuration\n\n" output += "```ini\n" output += infrastructure_enhancer.generate_pytest_config_recommendations() output += "\n```\n\n" # Makefile enhancements output += "## Enhanced Makefile Targets\n\n" output += "```makefile\n" output += infrastructure_enhancer.generate_makefile_enhancements() output += "\n```\n\n" # Issue-specific recommendations if issues: output += "## Issue-Specific Recommendations\n\n" for issue in issues: output += f"- **{issue.type.title()}**: {issue.solution}\n" elif args.command == "optimize-tdd": # TDD workflow optimization optimizations = tdd_optimizer.generate_tdd_optimizations() commands = tdd_optimizer.create_smart_test_commands() output = "# TDD8 Workflow Optimization\n\n" output += "## Optimization Recommendations\n\n" for opt in optimizations: output += f"- {opt}\n" output += "\n## Optimized Test Commands\n\n" for scenario, command in commands.items(): output += f"### {scenario.replace('_', ' ').title()}\n" output += f"```bash\n{command}\n```\n\n" elif args.command == "report": # Generate comprehensive report issues = analyzer.analyze_pytest_issues() metrics = analyzer.measure_test_performance() optimizations = tdd_optimizer.generate_tdd_optimizations() report = TestOptimizationReport( timestamp=datetime.now().isoformat(), current_performance=metrics, identified_issues=issues, optimization_recommendations=optimizations, tdd_workflow_improvements=[ "Implement fast red-phase testing with make test-red", "Use smart test selection for changed files", "Optimize test caching for faster iterations", "Implement parallel test execution" ], agent_integration_suggestions=[ "Use 'make test' as primary test command", "Use 'make test-quick' for TDD red phase", "Use 'make test-changed' for incremental testing", "Always set PYTHONPATH=. for reliable imports" ] ) if args.format == "json": output = json.dumps(asdict(report), indent=2) else: output = f"# Testing Efficiency Optimization Report\n\n" output += f"**Generated**: {report.timestamp}\n\n" output += "## Current Performance\n" output += f"- Total Tests: {metrics.total_tests}\n" output += f"- Execution Time: {metrics.execution_time:.2f}s\n" output += f"- Success Rate: {(metrics.passed_tests/max(metrics.total_tests,1)*100):.1f}%\n\n" output += f"## Issues Found ({len(issues)})\n" for issue in issues[:5]: # Top 5 issues output += f"- **{issue.type.title()}**: {issue.description}\n" output += "\n## Key Recommendations\n" for rec in optimizations[:3]: # Top 3 recommendations output += f"- {rec}\n" output += "\n## Agent Integration\n" for suggestion in report.agent_integration_suggestions: output += f"- {suggestion}\n" # Output results if args.output: with open(args.output, 'w') as f: f.write(output) print(f"Output written to {args.output}") else: print(output) if __name__ == "__main__": main()