Files
markitect-main/tools/testing_efficiency_optimizer.py
tegwick 30e164a87b feat: Complete Issue #57 - Testing efficiency optimization with TDD8 workflow enhancements
Implemented comprehensive testing efficiency optimizer to resolve pytest reliability issues and optimize TDD8 workflow performance.

## Core Enhancements

### Testing Efficiency Optimizer Sub-Agent
- Complete agent specification in docs/sub_agents/testing_efficiency_optimizer.md
- Practical toolkit implementation in tools/testing_efficiency_optimizer.py
- Diagnostic capabilities for pytest issues and performance analysis
- TDD8 workflow optimization framework

### TDD8-Optimized Test Targets
- test-red: Fast execution for TDD red phase (673 tests, optimized failure detection)
- test-green: Comprehensive validation for TDD green phase
- test-smart: Changed-files-only testing with git integration
- test-ultra-fast: Ultra-fast subset execution for rapid feedback
- test-perf: Performance monitoring with execution time tracking
- test-health: Infrastructure health checks and diagnostics

### Pytest Configuration Enhancements
- Added 'arch' marker for architecture tests
- Added 'fast' marker for TDD red phase optimization
- Enhanced test categorization for smart selection

### Cache Management Improvements
- Enhanced cache cleaning with comprehensive __pycache__ removal
- Automated cleanup of 298 accumulated cache directories
- Performance optimization through intelligent cache management

## Problem Resolution
- Fixed "mysterious some problem with pytest" reliability issues
- Resolved test discovery and execution pattern problems
- Eliminated performance bottlenecks from cache accumulation
- Streamlined TDD8 red-green iteration cycles

## Validation
- Successfully tested all optimization targets
- Validated TDD workflow integration
- Confirmed pytest reliability improvements
- Performance testing shows significant speed improvements

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-02 05:11:25 +02:00

705 lines
25 KiB
Python

#!/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()