Some syntax errors fixed. Syntax checking added. Release checklist created.
This commit is contained in:
459
tools/validate_release.py
Executable file
459
tools/validate_release.py
Executable file
@ -0,0 +1,459 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Fenrir Release Validation Tool
|
||||
|
||||
Comprehensive validation suite for Fenrir releases, including syntax validation,
|
||||
dependency checking, import testing, and basic functionality validation.
|
||||
|
||||
Usage:
|
||||
python3 tools/validate_release.py # Full validation
|
||||
python3 tools/validate_release.py --quick # Skip slow tests
|
||||
python3 tools/validate_release.py --fix # Auto-fix issues where possible
|
||||
"""
|
||||
|
||||
import ast
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import subprocess
|
||||
import tempfile
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import time
|
||||
|
||||
|
||||
class ReleaseValidator:
|
||||
def __init__(self, verbose=True):
|
||||
self.verbose = verbose
|
||||
self.errors = []
|
||||
self.warnings = []
|
||||
self.fixes_applied = []
|
||||
self.tests_run = 0
|
||||
self.tests_passed = 0
|
||||
|
||||
def log(self, message, level="INFO"):
|
||||
"""Log a message with appropriate formatting."""
|
||||
if not self.verbose and level == "INFO":
|
||||
return
|
||||
|
||||
colors = {
|
||||
"INFO": "\033[0;36m", # Cyan
|
||||
"SUCCESS": "\033[0;32m", # Green
|
||||
"WARNING": "\033[1;33m", # Yellow
|
||||
"ERROR": "\033[0;31m", # Red
|
||||
"HEADER": "\033[1;34m", # Bold Blue
|
||||
}
|
||||
|
||||
reset = "\033[0m"
|
||||
color = colors.get(level, "")
|
||||
|
||||
if level == "HEADER":
|
||||
print(f"\n{color}{'='*60}")
|
||||
print(f"{message}")
|
||||
print(f"{'='*60}{reset}")
|
||||
else:
|
||||
symbol = {
|
||||
"SUCCESS": "✓",
|
||||
"ERROR": "✗",
|
||||
"WARNING": "⚠",
|
||||
"INFO": "•"
|
||||
}.get(level, "•")
|
||||
|
||||
print(f"{color}{symbol} {message}{reset}")
|
||||
|
||||
def run_test(self, name, test_func, *args, **kwargs):
|
||||
"""Run a test and track results."""
|
||||
self.tests_run += 1
|
||||
try:
|
||||
result = test_func(*args, **kwargs)
|
||||
if result:
|
||||
self.tests_passed += 1
|
||||
self.log(f"{name}: PASSED", "SUCCESS")
|
||||
else:
|
||||
self.log(f"{name}: FAILED", "ERROR")
|
||||
return result
|
||||
except Exception as e:
|
||||
self.log(f"{name}: ERROR - {e}", "ERROR")
|
||||
self.errors.append(f"{name}: {e}")
|
||||
return False
|
||||
|
||||
def validate_python_syntax(self, directory, fix_mode=False):
|
||||
"""Validate Python syntax across all files."""
|
||||
python_files = list(Path(directory).rglob("*.py"))
|
||||
# Filter out cache and build directories
|
||||
python_files = [f for f in python_files if not any(part.startswith(('__pycache__', '.git', 'build', 'dist')) for part in f.parts)]
|
||||
|
||||
syntax_errors = []
|
||||
fixed_files = []
|
||||
|
||||
for filepath in python_files:
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
ast.parse(content, filename=str(filepath))
|
||||
except SyntaxError as e:
|
||||
syntax_errors.append((filepath, e))
|
||||
|
||||
if fix_mode:
|
||||
# Try to fix common f-string issues
|
||||
fixed_content = self.fix_fstring_issues(content)
|
||||
if fixed_content != content:
|
||||
try:
|
||||
ast.parse(fixed_content, filename=str(filepath))
|
||||
# Fix worked, write it back
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(fixed_content)
|
||||
fixed_files.append(filepath)
|
||||
syntax_errors.pop() # Remove from errors
|
||||
except SyntaxError:
|
||||
pass # Fix didn't work
|
||||
except Exception as e:
|
||||
syntax_errors.append((filepath, e))
|
||||
|
||||
if fixed_files:
|
||||
self.fixes_applied.extend([f"Fixed f-string syntax in {f}" for f in fixed_files])
|
||||
|
||||
if syntax_errors:
|
||||
for filepath, error in syntax_errors[:5]: # Show only first 5
|
||||
if isinstance(error, SyntaxError):
|
||||
self.errors.append(f"Syntax error in {filepath}:{error.lineno}: {error.msg}")
|
||||
else:
|
||||
self.errors.append(f"Error in {filepath}: {error}")
|
||||
if len(syntax_errors) > 5:
|
||||
self.errors.append(f"... and {len(syntax_errors) - 5} more syntax errors")
|
||||
|
||||
return len(syntax_errors) == 0
|
||||
|
||||
def fix_fstring_issues(self, content):
|
||||
"""Fix common f-string syntax issues."""
|
||||
lines = content.split('\n')
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
# Look for f-strings that span multiple lines incorrectly
|
||||
if ('f"' in line and line.count('"') % 2 == 1 and
|
||||
i + 1 < len(lines) and lines[i + 1].strip()):
|
||||
|
||||
next_line = lines[i + 1]
|
||||
# Common patterns to fix
|
||||
if (next_line.strip().endswith('}"') or
|
||||
'str(e)}' in next_line or
|
||||
next_line.strip().startswith(('fenrirVersion.', 'self.'))):
|
||||
|
||||
# Merge the lines properly
|
||||
fixed_line = line.rstrip() + next_line.strip()
|
||||
lines[i] = fixed_line
|
||||
lines[i + 1] = ''
|
||||
|
||||
return '\n'.join(line for line in lines if line.strip() or not line)
|
||||
|
||||
def validate_dependencies(self):
|
||||
"""Run the existing dependency checker."""
|
||||
try:
|
||||
result = subprocess.run([
|
||||
sys.executable, "check-dependencies.py"
|
||||
], capture_output=True, text=True, timeout=30)
|
||||
|
||||
if result.returncode == 0:
|
||||
return True
|
||||
else:
|
||||
self.errors.append(f"Dependency check failed: {result.stderr}")
|
||||
return False
|
||||
except subprocess.TimeoutExpired:
|
||||
self.errors.append("Dependency check timed out")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.errors.append(f"Could not run dependency check: {e}")
|
||||
return False
|
||||
|
||||
def validate_core_imports(self):
|
||||
"""Test importing core Fenrir modules."""
|
||||
# Change to src directory for imports
|
||||
original_path = sys.path.copy()
|
||||
src_dir = Path.cwd() / "src"
|
||||
if src_dir.exists():
|
||||
sys.path.insert(0, str(src_dir))
|
||||
|
||||
core_modules = [
|
||||
"fenrirscreenreader.core.fenrirManager",
|
||||
"fenrirscreenreader.core.commandManager",
|
||||
"fenrirscreenreader.core.eventManager",
|
||||
"fenrirscreenreader.core.screenManager",
|
||||
"fenrirscreenreader.core.inputManager",
|
||||
"fenrirscreenreader.core.outputManager",
|
||||
]
|
||||
|
||||
import_failures = []
|
||||
|
||||
for module_name in core_modules:
|
||||
try:
|
||||
importlib.import_module(module_name)
|
||||
except ImportError as e:
|
||||
import_failures.append(f"{module_name}: {e}")
|
||||
except Exception as e:
|
||||
import_failures.append(f"{module_name}: Unexpected error: {e}")
|
||||
|
||||
# Restore path
|
||||
sys.path = original_path
|
||||
|
||||
if import_failures:
|
||||
self.errors.extend(import_failures)
|
||||
return False
|
||||
return True
|
||||
|
||||
def validate_command_structure(self):
|
||||
"""Validate command file structure and naming."""
|
||||
commands_dir = Path("src/fenrirscreenreader/commands")
|
||||
if not commands_dir.exists():
|
||||
self.errors.append("Commands directory not found")
|
||||
return False
|
||||
|
||||
issues = []
|
||||
|
||||
# Check command directories
|
||||
expected_dirs = ["commands", "onHeartBeat", "onKeyInput", "onCursorChange",
|
||||
"onScreenUpdate", "onScreenChanged", "vmenu-profiles"]
|
||||
|
||||
for expected_dir in expected_dirs:
|
||||
if not (commands_dir / expected_dir).exists():
|
||||
issues.append(f"Missing expected directory: {expected_dir}")
|
||||
|
||||
# Check for critical issues only (skip template files and base classes)
|
||||
for py_file in commands_dir.rglob("*.py"):
|
||||
if (py_file.name.startswith("__") or
|
||||
"template" in py_file.name.lower() or
|
||||
"base" in py_file.name.lower()):
|
||||
continue
|
||||
|
||||
try:
|
||||
with open(py_file, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Critical structure checks only
|
||||
if "class command" not in content:
|
||||
issues.append(f"{py_file}: Missing 'class command' definition")
|
||||
|
||||
# Skip method checks for files that inherit from base classes
|
||||
if ("super().__init__" in content or
|
||||
"importlib.util" in content or
|
||||
"_base.py" in content):
|
||||
continue # These inherit methods from base classes
|
||||
|
||||
# Only check direct implementations
|
||||
# Special case: Application profile commands use load/unload instead of run
|
||||
if "onSwitchApplicationProfile" in str(py_file):
|
||||
if "def load" not in content and "def unload" not in content:
|
||||
issues.append(f"{py_file}: Missing 'load' or 'unload' method")
|
||||
else:
|
||||
critical_methods = ["run"] # Focus on the most critical method
|
||||
for method in critical_methods:
|
||||
if (f"def {method}" not in content and
|
||||
"super()" not in content): # Skip if uses inheritance
|
||||
issues.append(f"{py_file}: Missing '{method}' method")
|
||||
|
||||
except Exception as e:
|
||||
issues.append(f"{py_file}: Could not validate structure: {e}")
|
||||
|
||||
# Only report critical issues, not template/base class warnings
|
||||
critical_issues = [issue for issue in issues if not any(skip in issue.lower()
|
||||
for skip in ["template", "base", "missing 'initialize'", "missing 'shutdown'"])]
|
||||
|
||||
if critical_issues:
|
||||
self.warnings.extend(critical_issues[:5]) # Limit warnings
|
||||
if len(critical_issues) > 5:
|
||||
self.warnings.append(f"... and {len(critical_issues) - 5} more critical command structure issues")
|
||||
|
||||
# Return success if no critical issues (warnings are acceptable)
|
||||
return len(critical_issues) == 0
|
||||
|
||||
def validate_configuration_files(self):
|
||||
"""Validate configuration file structure."""
|
||||
config_dir = Path("config")
|
||||
if not config_dir.exists():
|
||||
self.errors.append("Config directory not found")
|
||||
return False
|
||||
|
||||
required_configs = [
|
||||
"settings/settings.conf",
|
||||
"keyboard/desktop.conf",
|
||||
"punctuation/default.conf"
|
||||
]
|
||||
|
||||
missing_configs = []
|
||||
for config_file in required_configs:
|
||||
if not (config_dir / config_file).exists():
|
||||
missing_configs.append(config_file)
|
||||
|
||||
if missing_configs:
|
||||
self.errors.extend([f"Missing config file: {f}" for f in missing_configs])
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def validate_installation_scripts(self):
|
||||
"""Validate installation and setup scripts."""
|
||||
required_scripts = ["setup.py", "install.sh", "uninstall.sh"]
|
||||
missing_scripts = []
|
||||
|
||||
for script in required_scripts:
|
||||
if not Path(script).exists():
|
||||
missing_scripts.append(script)
|
||||
|
||||
if missing_scripts:
|
||||
self.warnings.extend([f"Missing installation script: {s}" for s in missing_scripts])
|
||||
|
||||
# Check setup.py syntax if it exists
|
||||
if Path("setup.py").exists():
|
||||
try:
|
||||
with open("setup.py", 'r') as f:
|
||||
content = f.read()
|
||||
ast.parse(content, filename="setup.py")
|
||||
except SyntaxError as e:
|
||||
self.errors.append(f"setup.py syntax error: {e}")
|
||||
return False
|
||||
|
||||
return len(missing_scripts) == 0
|
||||
|
||||
def validate_repository_cleanliness(self):
|
||||
"""Check for cache files and other artifacts that shouldn't be in git."""
|
||||
# Check for Python cache files in git tracking
|
||||
try:
|
||||
result = subprocess.run([
|
||||
"git", "ls-files", "--cached"
|
||||
], capture_output=True, text=True, timeout=10)
|
||||
|
||||
if result.returncode == 0:
|
||||
tracked_files = result.stdout.strip().split('\n')
|
||||
cache_files = [f for f in tracked_files if '__pycache__' in f or f.endswith('.pyc')]
|
||||
|
||||
if cache_files:
|
||||
self.errors.extend([f"Python cache file tracked in git: {f}" for f in cache_files[:5]])
|
||||
if len(cache_files) > 5:
|
||||
self.errors.append(f"... and {len(cache_files) - 5} more cache files in git")
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
else:
|
||||
self.warnings.append("Could not check git tracked files")
|
||||
return True
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
self.warnings.append("Git check timed out")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.warnings.append(f"Could not check repository cleanliness: {e}")
|
||||
return True
|
||||
|
||||
def generate_report(self):
|
||||
"""Generate final validation report."""
|
||||
self.log("FENRIR RELEASE VALIDATION REPORT", "HEADER")
|
||||
|
||||
# Test Summary
|
||||
success_rate = (self.tests_passed / self.tests_run * 100) if self.tests_run > 0 else 0
|
||||
self.log(f"Tests run: {self.tests_run}")
|
||||
self.log(f"Tests passed: {self.tests_passed}")
|
||||
self.log(f"Success rate: {success_rate:.1f}%")
|
||||
|
||||
# Fixes Applied
|
||||
if self.fixes_applied:
|
||||
self.log("\nAUTO-FIXES APPLIED:", "HEADER")
|
||||
for fix in self.fixes_applied:
|
||||
self.log(fix, "SUCCESS")
|
||||
|
||||
# Errors
|
||||
if self.errors:
|
||||
self.log(f"\nERRORS ({len(self.errors)}):", "HEADER")
|
||||
for error in self.errors:
|
||||
self.log(error, "ERROR")
|
||||
|
||||
# Warnings
|
||||
if self.warnings:
|
||||
self.log(f"\nWARNINGS ({len(self.warnings)}):", "HEADER")
|
||||
for warning in self.warnings:
|
||||
self.log(warning, "WARNING")
|
||||
|
||||
# Final Status
|
||||
if not self.errors and success_rate >= 80:
|
||||
self.log("\n🎉 RELEASE VALIDATION PASSED", "SUCCESS")
|
||||
self.log("The codebase appears ready for release", "SUCCESS")
|
||||
return True
|
||||
elif not self.errors:
|
||||
self.log("\n⚠️ RELEASE VALIDATION PASSED WITH WARNINGS", "WARNING")
|
||||
self.log("Release is possible but issues should be addressed", "WARNING")
|
||||
return True
|
||||
else:
|
||||
self.log("\n❌ RELEASE VALIDATION FAILED", "ERROR")
|
||||
self.log("Critical issues must be fixed before release", "ERROR")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Comprehensive Fenrir release validation')
|
||||
parser.add_argument('--quick', action='store_true',
|
||||
help='Skip slow tests (dependency checking)')
|
||||
parser.add_argument('--fix', action='store_true',
|
||||
help='Attempt to fix issues automatically where possible')
|
||||
parser.add_argument('--quiet', action='store_true',
|
||||
help='Reduce output verbosity')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Ensure we're in the project root
|
||||
if not Path("src/fenrirscreenreader").exists():
|
||||
print("Error: Must be run from Fenrir project root directory")
|
||||
sys.exit(1)
|
||||
|
||||
validator = ReleaseValidator(verbose=not args.quiet)
|
||||
|
||||
validator.log("FENRIR RELEASE VALIDATION STARTING", "HEADER")
|
||||
start_time = time.time()
|
||||
|
||||
# Run validation tests
|
||||
validator.run_test(
|
||||
"Python syntax validation",
|
||||
validator.validate_python_syntax,
|
||||
"src/fenrirscreenreader",
|
||||
args.fix
|
||||
)
|
||||
|
||||
if not args.quick:
|
||||
validator.run_test(
|
||||
"Dependency validation",
|
||||
validator.validate_dependencies
|
||||
)
|
||||
|
||||
validator.run_test(
|
||||
"Core module imports",
|
||||
validator.validate_core_imports
|
||||
)
|
||||
|
||||
validator.run_test(
|
||||
"Command structure validation",
|
||||
validator.validate_command_structure
|
||||
)
|
||||
|
||||
validator.run_test(
|
||||
"Configuration files validation",
|
||||
validator.validate_configuration_files
|
||||
)
|
||||
|
||||
validator.run_test(
|
||||
"Installation scripts validation",
|
||||
validator.validate_installation_scripts
|
||||
)
|
||||
|
||||
validator.run_test(
|
||||
"Repository cleanliness validation",
|
||||
validator.validate_repository_cleanliness
|
||||
)
|
||||
|
||||
# Generate final report
|
||||
elapsed_time = time.time() - start_time
|
||||
validator.log(f"\nValidation completed in {elapsed_time:.1f} seconds")
|
||||
|
||||
success = validator.generate_report()
|
||||
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
Reference in New Issue
Block a user