Some syntax errors fixed. Syntax checking added. Release checklist created.
This commit is contained in:
191
RELEASE_CHECKLIST.md
Normal file
191
RELEASE_CHECKLIST.md
Normal file
@ -0,0 +1,191 @@
|
||||
# Fenrir Release Validation Checklist
|
||||
|
||||
This checklist ensures thorough validation before releasing Fenrir packages.
|
||||
|
||||
## 🔧 Setup Tools (One-time setup)
|
||||
|
||||
### Install Pre-commit Hook
|
||||
```bash
|
||||
# Safely install composite hook (preserves existing version management)
|
||||
./tools/install_validation_hook.sh
|
||||
|
||||
# Test the hook
|
||||
./.git/hooks/pre-commit
|
||||
```
|
||||
|
||||
### Validation Scripts
|
||||
- `tools/validate_syntax.py` - Python syntax validation
|
||||
- `tools/validate_release.py` - Comprehensive release validation
|
||||
- `tools/cleanup_cache.py` - Remove Python cache files and directories
|
||||
- `tools/pre-commit-hook` - Git pre-commit validation
|
||||
|
||||
## 📋 Pre-Release Checklist
|
||||
|
||||
### 1. Code Quality Validation ✅
|
||||
```bash
|
||||
# Comprehensive release validation (includes syntax, imports, structure)
|
||||
python3 tools/validate_release.py
|
||||
|
||||
# If issues found, try auto-fix
|
||||
python3 tools/validate_release.py --fix
|
||||
|
||||
# Quick validation (skips slow dependency checks)
|
||||
python3 tools/validate_release.py --quick
|
||||
```
|
||||
|
||||
**Expected Result**: All tests pass, no syntax errors
|
||||
|
||||
### 2. Dependency Validation ✅
|
||||
```bash
|
||||
# Validate all dependencies are available
|
||||
python3 check-dependencies.py
|
||||
```
|
||||
|
||||
**Expected Result**: All required dependencies reported as available
|
||||
|
||||
### 3. Core Functionality Test ✅
|
||||
```bash
|
||||
# Test core imports (safe to run without sudo)
|
||||
cd src
|
||||
python3 -c "
|
||||
import fenrirscreenreader.core.fenrirManager
|
||||
import fenrirscreenreader.core.commandManager
|
||||
import fenrirscreenreader.core.eventManager
|
||||
print('Core imports successful')
|
||||
"
|
||||
cd ..
|
||||
```
|
||||
|
||||
**Expected Result**: No import errors
|
||||
|
||||
### 4. Installation Script Validation ✅
|
||||
```bash
|
||||
# Validate setup.py syntax
|
||||
python3 -m py_compile setup.py
|
||||
|
||||
# Check setup.py can be parsed
|
||||
python3 setup.py --help-commands >/dev/null
|
||||
```
|
||||
|
||||
**Expected Result**: No syntax errors, setup.py functional
|
||||
|
||||
### 5. Configuration Validation ✅
|
||||
```bash
|
||||
# Verify config files exist and are parseable
|
||||
ls -la config/settings/settings.conf
|
||||
ls -la config/keyboard/desktop.conf
|
||||
ls -la config/punctuation/default.conf
|
||||
```
|
||||
|
||||
**Expected Result**: All core config files present
|
||||
|
||||
### 6. Manual Testing (User/Package Maintainer) ⚠️
|
||||
|
||||
**Important**: These require user interaction as they need sudo access or specific hardware.
|
||||
|
||||
```bash
|
||||
# Test basic functionality (ask user to run)
|
||||
sudo ./src/fenrir --help
|
||||
|
||||
# Test in emulation mode (safer for desktop environments)
|
||||
sudo ./src/fenrir -e --version
|
||||
|
||||
# Quick functionality test (3-5 seconds)
|
||||
sudo timeout 5 ./src/fenrir -e -f || echo "Timeout reached (expected)"
|
||||
```
|
||||
|
||||
**Expected Result**: No immediate crashes, basic help/version output works
|
||||
|
||||
### 7. Package-Specific Validation ✅
|
||||
```bash
|
||||
# Test the same compilation process used by package managers
|
||||
python3 -m compileall src/fenrirscreenreader/ -q
|
||||
|
||||
# Verify no __pycache__ permission issues
|
||||
find src/ -name "*.pyc" -delete
|
||||
find src/ -name "__pycache__" -delete
|
||||
```
|
||||
|
||||
**Expected Result**: Clean compilation, no permission errors
|
||||
|
||||
## 🚨 Known Issue Categories
|
||||
|
||||
### Critical Issues (Block Release)
|
||||
- **Python syntax errors** (SyntaxError, unterminated strings)
|
||||
- **Missing core dependencies** (dbus-python, evdev, etc.)
|
||||
- **Import failures in core modules** (fenrirManager, commandManager)
|
||||
- **Missing critical config files** (settings.conf, desktop.conf)
|
||||
|
||||
### Warning Issues (Address if Possible)
|
||||
- **PEP8 violations** (cosmetic, don't block release)
|
||||
- **Missing optional dependencies** (for specific features)
|
||||
- **Command structure issues** (missing methods in command files)
|
||||
- **Very long lines** (>120 characters)
|
||||
|
||||
## 🔍 Root Cause Analysis
|
||||
|
||||
### Why These Errors Weren't Caught Previously
|
||||
|
||||
1. **No automated syntax validation** - The codebase relied on manual testing
|
||||
2. **No pre-commit hooks** - Syntax errors could be committed
|
||||
3. **No CI/CD pipeline** - Package compilation happens only during release
|
||||
4. **Manual PEP8 cleanup** - F-string refactoring introduced syntax errors during batch cleanup
|
||||
|
||||
## 📖 Usage Instructions
|
||||
|
||||
### For Developers
|
||||
```bash
|
||||
# Before committing changes
|
||||
git add .
|
||||
git commit # Pre-commit hook will run automatically
|
||||
|
||||
# Before creating tags/releases
|
||||
python3 tools/validate_release.py
|
||||
```
|
||||
|
||||
### For Package Maintainers
|
||||
```bash
|
||||
# Before packaging
|
||||
python3 tools/validate_release.py
|
||||
|
||||
# If validation fails
|
||||
python3 tools/validate_release.py --fix
|
||||
|
||||
# Quick check (if dependencies are known good)
|
||||
python3 tools/validate_release.py --quick
|
||||
```
|
||||
|
||||
### For Release Managers
|
||||
```bash
|
||||
# Complete validation before tagging
|
||||
python3 tools/validate_release.py
|
||||
|
||||
# Manual verification (requires sudo)
|
||||
sudo ./src/fenrir --version
|
||||
|
||||
# Tag release only after all validations pass
|
||||
git tag -a v2.x.x -m "Release v2.x.x"
|
||||
```
|
||||
|
||||
## 🎯 Future Improvements
|
||||
|
||||
### Recommended Additions
|
||||
1. **GitHub Actions CI/CD** - Automated validation on every push
|
||||
2. **Automated testing** - Unit tests for core functionality
|
||||
3. **Integration testing** - Test driver interactions
|
||||
4. **Package testing** - Validate actual package installation
|
||||
|
||||
### Modern Python Packaging
|
||||
- Consider migrating to `pyproject.toml` (PEP 621)
|
||||
- Use `build` instead of `setup.py` directly
|
||||
- Add `tox.ini` for multi-environment testing
|
||||
|
||||
## 📞 Support
|
||||
|
||||
If validation fails and auto-fix doesn't resolve issues:
|
||||
|
||||
1. **Check the specific error messages** in validation output
|
||||
2. **Review recent commits** that might have introduced issues
|
||||
3. **Run individual validation steps** to isolate problems
|
||||
|
||||
Remember: **Working code is better than perfect code** - especially for accessibility software where reliability is critical.
|
@ -24,9 +24,8 @@ class command:
|
||||
def run(self):
|
||||
try:
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
f"Fenrir screen reader version {
|
||||
fenrirVersion.version}-{
|
||||
fenrirVersion.code_name}",
|
||||
f"Fenrir screen reader version "
|
||||
f"{fenrirVersion.version}-{fenrirVersion.code_name}",
|
||||
interrupt=True,
|
||||
)
|
||||
except Exception as e:
|
||||
|
@ -59,8 +59,7 @@ class command(config_command):
|
||||
|
||||
except Exception as e:
|
||||
self.present_text(
|
||||
f"Failed to reset configuration: {
|
||||
str(e)}",
|
||||
f"Failed to reset configuration: {str(e)}",
|
||||
interrupt=False,
|
||||
flush=False,
|
||||
)
|
||||
|
@ -34,8 +34,7 @@ class command:
|
||||
SpeechDriver.initialize(self.env)
|
||||
except Exception as e:
|
||||
print(
|
||||
f"revert_to_saved SpeechDriver: Error reinitializing speech driver: {
|
||||
str(e)}"
|
||||
f"revert_to_saved SpeechDriver: Error reinitializing speech driver: {str(e)}"
|
||||
)
|
||||
|
||||
# Reinitialize sound system with restored settings
|
||||
|
@ -45,8 +45,7 @@ class command:
|
||||
self.env["runtime"]["SpeechDriver"].set_rate(new_rate)
|
||||
except Exception as e:
|
||||
print(
|
||||
f"adjust_speech_rate set_rate: Error setting speech rate: {
|
||||
str(e)}"
|
||||
f"adjust_speech_rate set_rate: Error setting speech rate: {str(e)}"
|
||||
)
|
||||
|
||||
new_percent = int(new_rate * 100)
|
||||
|
@ -29,9 +29,7 @@ class DynamicVoiceCommand:
|
||||
def run(self):
|
||||
try:
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
f"Testing voice {
|
||||
self.voice} from {
|
||||
self.module}. Please wait.",
|
||||
f"Testing voice {self.voice} from {self.module}. Please wait.",
|
||||
interrupt=True,
|
||||
)
|
||||
|
||||
|
@ -4,6 +4,6 @@
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
version = "2025.07.23"
|
||||
version = "2025.07.24"
|
||||
codeName = "testing"
|
||||
code_name = "testing"
|
||||
|
288
tools/cleanup_cache.py
Executable file
288
tools/cleanup_cache.py
Executable file
@ -0,0 +1,288 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Fenrir Cache Cleanup Tool
|
||||
|
||||
Removes Python cache files and directories from the repository.
|
||||
These files should never be committed and can cause issues.
|
||||
|
||||
Usage:
|
||||
python3 tools/cleanup_cache.py # Show what would be removed
|
||||
python3 tools/cleanup_cache.py --remove # Actually remove cache files
|
||||
python3 tools/cleanup_cache.py --check # Exit with error if cache files found
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class CacheCleanup:
|
||||
def __init__(self, verbose=True):
|
||||
self.verbose = verbose
|
||||
self.cache_dirs = []
|
||||
self.cache_files = []
|
||||
|
||||
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 find_cache_files(self, directory):
|
||||
"""Find all Python cache files and directories."""
|
||||
directory = Path(directory)
|
||||
|
||||
for root, dirs, files in os.walk(directory):
|
||||
root_path = Path(root)
|
||||
|
||||
# Skip .git directory entirely
|
||||
if '.git' in root_path.parts:
|
||||
continue
|
||||
|
||||
# Find __pycache__ directories
|
||||
if '__pycache__' in dirs:
|
||||
cache_dir = root_path / '__pycache__'
|
||||
self.cache_dirs.append(cache_dir)
|
||||
# Don't traverse into __pycache__ directories
|
||||
dirs.remove('__pycache__')
|
||||
|
||||
# Find .pyc files outside of __pycache__
|
||||
for file in files:
|
||||
if file.endswith('.pyc'):
|
||||
cache_file = root_path / file
|
||||
self.cache_files.append(cache_file)
|
||||
|
||||
def show_findings(self):
|
||||
"""Display what cache files were found."""
|
||||
total_items = len(self.cache_dirs) + len(self.cache_files)
|
||||
|
||||
if total_items == 0:
|
||||
self.log("No Python cache files found", "SUCCESS")
|
||||
return True
|
||||
|
||||
self.log(f"Found {total_items} cache items:", "WARNING")
|
||||
|
||||
if self.cache_dirs:
|
||||
self.log(f"\n__pycache__ directories ({len(self.cache_dirs)}):", "WARNING")
|
||||
for cache_dir in sorted(self.cache_dirs):
|
||||
# Show size of directory
|
||||
size = self.get_directory_size(cache_dir)
|
||||
self.log(f" {cache_dir} ({size} files)", "WARNING")
|
||||
|
||||
if self.cache_files:
|
||||
self.log(f"\nLoose .pyc files ({len(self.cache_files)}):", "WARNING")
|
||||
for cache_file in sorted(self.cache_files):
|
||||
# Show file size
|
||||
try:
|
||||
size = cache_file.stat().st_size
|
||||
size_str = self.format_size(size)
|
||||
self.log(f" {cache_file} ({size_str})", "WARNING")
|
||||
except OSError:
|
||||
self.log(f" {cache_file} (size unknown)", "WARNING")
|
||||
|
||||
return False
|
||||
|
||||
def get_directory_size(self, directory):
|
||||
"""Get the number of files in a directory."""
|
||||
try:
|
||||
return len(list(directory.rglob('*')))
|
||||
except OSError:
|
||||
return 0
|
||||
|
||||
def format_size(self, size_bytes):
|
||||
"""Format file size in human-readable format."""
|
||||
if size_bytes < 1024:
|
||||
return f"{size_bytes} B"
|
||||
elif size_bytes < 1024 * 1024:
|
||||
return f"{size_bytes // 1024} KB"
|
||||
else:
|
||||
return f"{size_bytes // (1024 * 1024)} MB"
|
||||
|
||||
def remove_cache_files(self):
|
||||
"""Actually remove the cache files and directories."""
|
||||
removed_count = 0
|
||||
errors = []
|
||||
|
||||
# Remove __pycache__ directories
|
||||
for cache_dir in self.cache_dirs:
|
||||
try:
|
||||
if cache_dir.exists():
|
||||
shutil.rmtree(cache_dir)
|
||||
self.log(f"Removed directory: {cache_dir}", "SUCCESS")
|
||||
removed_count += 1
|
||||
except OSError as e:
|
||||
error_msg = f"Failed to remove {cache_dir}: {e}"
|
||||
errors.append(error_msg)
|
||||
self.log(error_msg, "ERROR")
|
||||
|
||||
# Remove .pyc files
|
||||
for cache_file in self.cache_files:
|
||||
try:
|
||||
if cache_file.exists():
|
||||
cache_file.unlink()
|
||||
self.log(f"Removed file: {cache_file}", "SUCCESS")
|
||||
removed_count += 1
|
||||
except OSError as e:
|
||||
error_msg = f"Failed to remove {cache_file}: {e}"
|
||||
errors.append(error_msg)
|
||||
self.log(error_msg, "ERROR")
|
||||
|
||||
if errors:
|
||||
self.log(f"Encountered {len(errors)} errors during cleanup", "ERROR")
|
||||
return False
|
||||
else:
|
||||
self.log(f"Successfully removed {removed_count} cache items", "SUCCESS")
|
||||
return True
|
||||
|
||||
def check_gitignore(self):
|
||||
"""Check if .gitignore properly excludes cache files."""
|
||||
gitignore_path = Path('.gitignore')
|
||||
if not gitignore_path.exists():
|
||||
self.log("Warning: No .gitignore file found", "WARNING")
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(gitignore_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
has_pycache = '__pycache__' in content or '__pycache__/' in content
|
||||
has_pyc = '*.pyc' in content
|
||||
|
||||
if has_pycache and has_pyc:
|
||||
self.log("✓ .gitignore properly excludes Python cache files", "SUCCESS")
|
||||
return True
|
||||
else:
|
||||
missing = []
|
||||
if not has_pycache:
|
||||
missing.append("__pycache__/")
|
||||
if not has_pyc:
|
||||
missing.append("*.pyc")
|
||||
|
||||
self.log(f"Warning: .gitignore missing: {', '.join(missing)}", "WARNING")
|
||||
return False
|
||||
|
||||
except OSError as e:
|
||||
self.log(f"Could not read .gitignore: {e}", "ERROR")
|
||||
return False
|
||||
|
||||
def suggest_gitignore_additions(self):
|
||||
"""Suggest additions to .gitignore."""
|
||||
self.log("\nRecommended .gitignore entries for Python:", "INFO")
|
||||
print("""
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
""")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Clean Python cache files from Fenrir repository')
|
||||
parser.add_argument('--remove', action='store_true',
|
||||
help='Actually remove cache files (default is dry-run)')
|
||||
parser.add_argument('--check', action='store_true',
|
||||
help='Exit with non-zero code if cache files found')
|
||||
parser.add_argument('--quiet', action='store_true',
|
||||
help='Reduce output verbosity')
|
||||
parser.add_argument('--directory', default='.',
|
||||
help='Directory to scan (default: current directory)')
|
||||
|
||||
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)
|
||||
|
||||
cleanup = CacheCleanup(verbose=not args.quiet)
|
||||
|
||||
cleanup.log("FENRIR CACHE CLEANUP", "HEADER")
|
||||
cleanup.log(f"Scanning directory: {Path(args.directory).absolute()}")
|
||||
|
||||
# Find cache files
|
||||
cleanup.find_cache_files(args.directory)
|
||||
|
||||
# Show what we found
|
||||
no_cache_found = cleanup.show_findings()
|
||||
|
||||
if no_cache_found:
|
||||
# Check .gitignore anyway
|
||||
cleanup.check_gitignore()
|
||||
cleanup.log("\n✅ Repository is clean of Python cache files", "SUCCESS")
|
||||
sys.exit(0)
|
||||
|
||||
# Check .gitignore
|
||||
gitignore_ok = cleanup.check_gitignore()
|
||||
if not gitignore_ok:
|
||||
cleanup.suggest_gitignore_additions()
|
||||
|
||||
# Handle different modes
|
||||
if args.remove:
|
||||
cleanup.log("\n🧹 REMOVING CACHE FILES", "HEADER")
|
||||
success = cleanup.remove_cache_files()
|
||||
if success:
|
||||
cleanup.log("\n✅ Cache cleanup completed successfully", "SUCCESS")
|
||||
sys.exit(0)
|
||||
else:
|
||||
cleanup.log("\n❌ Cache cleanup completed with errors", "ERROR")
|
||||
sys.exit(1)
|
||||
|
||||
elif args.check:
|
||||
cleanup.log("\n❌ Cache files found - validation failed", "ERROR")
|
||||
cleanup.log("Run with --remove to clean up cache files", "INFO")
|
||||
sys.exit(1)
|
||||
|
||||
else:
|
||||
# Dry run mode
|
||||
cleanup.log("\n💡 DRY RUN MODE", "HEADER")
|
||||
cleanup.log("Add --remove to actually delete these files", "INFO")
|
||||
cleanup.log("Add --check to fail if cache files are present", "INFO")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
105
tools/clipboard_sync.sh
Executable file
105
tools/clipboard_sync.sh
Executable file
@ -0,0 +1,105 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Fenrir X11 Clipboard Sync
|
||||
# Synchronizes between X11 clipboard and Fenrir clipboard file
|
||||
# Prevents loops using checksums and timestamps
|
||||
|
||||
# Check for root privileges
|
||||
if [[ $(whoami) != "root" ]]; then
|
||||
echo "Error: This script must be run as root to access Fenrir's clipboard file"
|
||||
echo "Run with: sudo DISPLAY=:0 ./clipboard_sync.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
FENRIR_CLIPBOARD_FILE="${1:-/tmp/fenrirClipboard}"
|
||||
STATE_FILE="/var/tmp/.fenrir_clipboard_state"
|
||||
|
||||
# Simple state tracking without complex locking
|
||||
get_file_checksum() {
|
||||
if [[ -f "$FENRIR_CLIPBOARD_FILE" ]]; then
|
||||
md5sum "$FENRIR_CLIPBOARD_FILE" 2>/dev/null | cut -d' ' -f1
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
get_clipboard_checksum() {
|
||||
xclip -o -selection clipboard 2>/dev/null | md5sum | cut -d' ' -f1
|
||||
}
|
||||
|
||||
# Initialize state
|
||||
rm -f "$STATE_FILE" 2>/dev/null
|
||||
|
||||
echo "Starting Fenrir clipboard sync..."
|
||||
echo "Monitoring file: $FENRIR_CLIPBOARD_FILE"
|
||||
|
||||
# Check dependencies
|
||||
if ! command -v xclip >/dev/null 2>&1; then
|
||||
echo "Error: xclip is required but not installed"
|
||||
echo "Install with: sudo apt install xclip"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v inotifywait >/dev/null 2>&1; then
|
||||
echo "Error: inotify-tools is required but not installed"
|
||||
echo "Install with: sudo apt install inotify-tools"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create clipboard file if it doesn't exist
|
||||
touch "$FENRIR_CLIPBOARD_FILE"
|
||||
|
||||
echo "Starting Fenrir clipboard sync..."
|
||||
|
||||
while true; do
|
||||
# Read last state
|
||||
if [[ -f "$STATE_FILE" ]]; then
|
||||
read -r LAST_FILE_CHECKSUM LAST_CLIPBOARD_CHECKSUM LAST_UPDATE_TIME < "$STATE_FILE"
|
||||
else
|
||||
LAST_FILE_CHECKSUM=""
|
||||
LAST_CLIPBOARD_CHECKSUM=""
|
||||
LAST_UPDATE_TIME="0"
|
||||
fi
|
||||
|
||||
# Get current checksums
|
||||
CURRENT_FILE_CHECKSUM=$(get_file_checksum)
|
||||
CURRENT_CLIPBOARD_CHECKSUM=$(get_clipboard_checksum)
|
||||
CURRENT_TIME=$(date +%s)
|
||||
|
||||
# Skip update if we just made one (prevent immediate loops)
|
||||
TIME_SINCE_LAST=$((CURRENT_TIME - LAST_UPDATE_TIME))
|
||||
if [[ $TIME_SINCE_LAST -lt 3 ]]; then
|
||||
sleep 1
|
||||
continue
|
||||
fi
|
||||
|
||||
# Clipboard changed
|
||||
if [[ "$CURRENT_CLIPBOARD_CHECKSUM" != "$LAST_CLIPBOARD_CHECKSUM" ]]; then
|
||||
echo "X11 clipboard changed, updating file..."
|
||||
if xclip -o -selection clipboard > "$FENRIR_CLIPBOARD_FILE" 2>/dev/null; then
|
||||
CURRENT_FILE_CHECKSUM=$(get_file_checksum)
|
||||
echo "$CURRENT_FILE_CHECKSUM $CURRENT_CLIPBOARD_CHECKSUM $CURRENT_TIME" > "$STATE_FILE"
|
||||
echo "File updated successfully"
|
||||
else
|
||||
echo "Failed to update file from clipboard"
|
||||
fi
|
||||
sleep 1
|
||||
continue
|
||||
fi
|
||||
|
||||
# File changed
|
||||
if [[ "$CURRENT_FILE_CHECKSUM" != "$LAST_FILE_CHECKSUM" ]]; then
|
||||
echo "Fenrir clipboard file changed, updating X11 clipboard..."
|
||||
if cat "$FENRIR_CLIPBOARD_FILE" | xclip -i -selection clipboard 2>/dev/null; then
|
||||
CURRENT_CLIPBOARD_CHECKSUM=$(get_clipboard_checksum)
|
||||
echo "$CURRENT_FILE_CHECKSUM $CURRENT_CLIPBOARD_CHECKSUM $CURRENT_TIME" > "$STATE_FILE"
|
||||
echo "X11 clipboard updated successfully"
|
||||
else
|
||||
echo "Failed to update clipboard from file"
|
||||
fi
|
||||
sleep 1
|
||||
continue
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
done
|
110
tools/install_validation_hook.sh
Executable file
110
tools/install_validation_hook.sh
Executable file
@ -0,0 +1,110 @@
|
||||
#!/bin/bash
|
||||
# Safe Installation of Fenrir Validation Hook
|
||||
#
|
||||
# This script safely installs the composite pre-commit hook that combines
|
||||
# your existing version management with new code quality validation.
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
BLUE='\033[1;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${BLUE}Fenrir Validation Hook Installation${NC}"
|
||||
echo "===================================="
|
||||
|
||||
# Check we're in the right directory
|
||||
if [ ! -f "CLAUDE.md" ] || [ ! -d "src/fenrirscreenreader" ]; then
|
||||
echo -e "${RED}Error: Must be run from Fenrir project root directory${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if there's already a pre-commit hook
|
||||
if [ -f ".git/hooks/pre-commit" ]; then
|
||||
echo -e "\n${YELLOW}Existing pre-commit hook detected${NC}"
|
||||
|
||||
# Check if it's a symlink (our validation hook) or a regular file (version hook)
|
||||
if [ -L ".git/hooks/pre-commit" ]; then
|
||||
echo -e "${YELLOW}Current hook appears to be our validation hook (symlink)${NC}"
|
||||
read -p "Replace with composite hook that includes version management? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo -e "${YELLOW}Installation cancelled${NC}"
|
||||
exit 0
|
||||
fi
|
||||
rm .git/hooks/pre-commit
|
||||
else
|
||||
echo -e "${GREEN}Current hook appears to be the version management hook (regular file)${NC}"
|
||||
|
||||
# Back up the existing hook
|
||||
backup_name=".git/hooks/pre-commit.backup.$(date +%Y%m%d_%H%M%S)"
|
||||
cp .git/hooks/pre-commit "$backup_name"
|
||||
echo -e "${GREEN}✓ Existing hook backed up to: $backup_name${NC}"
|
||||
|
||||
# Verify the backup contains version management code
|
||||
if grep -q "versionFile" "$backup_name"; then
|
||||
echo -e "${GREEN}✓ Backup contains version management logic${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠ Backup doesn't appear to contain version management logic${NC}"
|
||||
echo -e "${YELLOW} You may need to manually restore version management functionality${NC}"
|
||||
fi
|
||||
|
||||
read -p "Install composite hook (version management + validation)? (Y/n): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Nn]$ ]]; then
|
||||
echo -e "${YELLOW}Installation cancelled${NC}"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}No existing pre-commit hook found${NC}"
|
||||
read -p "Install composite hook? (Y/n): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Nn]$ ]]; then
|
||||
echo -e "${YELLOW}Installation cancelled${NC}"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Install the composite hook
|
||||
echo -e "\n${YELLOW}Installing composite pre-commit hook...${NC}"
|
||||
cp tools/pre-commit-composite .git/hooks/pre-commit
|
||||
chmod +x .git/hooks/pre-commit
|
||||
echo -e "${GREEN}✓ Composite hook installed${NC}"
|
||||
|
||||
# Test the hook
|
||||
echo -e "\n${YELLOW}Testing the composite hook...${NC}"
|
||||
if ./.git/hooks/pre-commit >/dev/null 2>&1; then
|
||||
echo -e "${GREEN}✓ Composite hook test passed${NC}"
|
||||
else
|
||||
echo -e "${RED}⚠ Composite hook test found issues (this may be normal)${NC}"
|
||||
echo " Run manually to see details: ./.git/hooks/pre-commit"
|
||||
fi
|
||||
|
||||
# Final instructions
|
||||
echo -e "\n${GREEN}Installation Complete!${NC}"
|
||||
echo ""
|
||||
echo "Your composite pre-commit hook now provides:"
|
||||
echo " 1. ✓ Version management (existing functionality preserved)"
|
||||
echo " 2. ✓ Python syntax validation"
|
||||
echo " 3. ✓ Core module import testing"
|
||||
echo " 4. ✓ Common issue detection"
|
||||
echo ""
|
||||
echo "Development workflow:"
|
||||
echo " • Make your changes"
|
||||
echo " • git add . && git commit"
|
||||
echo " • Hook runs automatically (version update + validation)"
|
||||
echo ""
|
||||
echo "Manual validation (optional):"
|
||||
echo " • python3 tools/validate_syntax.py --fix"
|
||||
echo " • python3 tools/validate_release.py --quick"
|
||||
echo ""
|
||||
echo -e "${BLUE}Environment variables:${NC}"
|
||||
echo -e "${BLUE} SKIP_VERSION_UPDATE=1 Skip version management${NC}"
|
||||
echo ""
|
||||
if [ -f ".git/hooks/pre-commit.backup."* ]; then
|
||||
echo -e "${YELLOW}Note: Your original hook is backed up and can be restored if needed${NC}"
|
||||
fi
|
231
tools/pre-commit-composite
Executable file
231
tools/pre-commit-composite
Executable file
@ -0,0 +1,231 @@
|
||||
#!/bin/bash
|
||||
# Fenrir Composite Pre-commit Hook
|
||||
#
|
||||
# This hook combines version management and code quality validation.
|
||||
# It first runs the version management logic, then runs validation.
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[1;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${BLUE}Fenrir Pre-commit Validation${NC}"
|
||||
echo "=================================="
|
||||
|
||||
# Get the repository root
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
# ============================================================================
|
||||
# PART 1: VERSION MANAGEMENT (existing logic)
|
||||
# ============================================================================
|
||||
|
||||
echo -e "\n${YELLOW}1. Version Management...${NC}"
|
||||
|
||||
# Check if SKIP_VERSION_UPDATE is set
|
||||
if [[ "${SKIP_VERSION_UPDATE}" = "1" ]]; then
|
||||
echo -e "${YELLOW}Notice: Skipping version update due to SKIP_VERSION_UPDATE=1${NC}"
|
||||
else
|
||||
# Verify .git/versionpath exists
|
||||
if [[ ! -f ".git/versionpath" ]]; then
|
||||
echo -e "${RED}Error: .git/versionpath not found. Please create it with contents:${NC}"
|
||||
echo -e "${YELLOW}versionFile=\"path/to/your/version/file\"${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Source the version path file
|
||||
source ".git/versionpath"
|
||||
|
||||
# Validate that versionFile variable was set
|
||||
if [[ -z "$versionFile" ]]; then
|
||||
echo -e "${RED}Error: versionFile variable not set in .git/versionpath${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get current date components
|
||||
year=$(date +%Y)
|
||||
month=$(date +%m)
|
||||
day=$(date +%d)
|
||||
|
||||
# Create new version string
|
||||
newVersion="$year.$month.$day"
|
||||
|
||||
# Get current branch name
|
||||
branchName=$(git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
# Check if we're in the middle of a merge
|
||||
if [[ -f ".git/MERGE_HEAD" ]]; then
|
||||
echo -e "${YELLOW}Warning: In the middle of a merge. Skipping version update.${NC}"
|
||||
else
|
||||
# Check if file exists relative to git root
|
||||
if [[ ! -f "$versionFile" ]]; then
|
||||
echo -e "${RED}Error: Version file not found at $versionFile${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Store original version file content
|
||||
originalContent=$(cat "$versionFile")
|
||||
|
||||
# Check if version actually needs updating
|
||||
if ! grep -q "version = \"$newVersion\"" "$versionFile"; then
|
||||
# Update the version in the file
|
||||
sed -i "s/version = [\"']\{0,1\}[0-9.]\+[\"']\{0,1\}/version = \"$newVersion\"/" "$versionFile"
|
||||
fi
|
||||
|
||||
# Check if codeName exists and isn't "stable"
|
||||
if grep -q "codeName.*=.*\"stable\"" "$versionFile"; then
|
||||
# Don't modify stable codeName
|
||||
:
|
||||
elif grep -q "codeName.*=.*\"$branchName\"" "$versionFile"; then
|
||||
# CodeName already matches branch name, no need to update
|
||||
:
|
||||
elif grep -q "codeName" "$versionFile"; then
|
||||
# Update existing codeName
|
||||
sed -i "s/codeName = [\"']\{0,1\}[^\"']*[\"']\{0,1\}/codeName = \"$branchName\"/" "$versionFile"
|
||||
else
|
||||
# Add codeName after the version line
|
||||
sed -i "/version = / a\codeName = \"$branchName\"" "$versionFile"
|
||||
fi
|
||||
|
||||
# Check if the file was actually modified
|
||||
if [[ "$(cat "$versionFile")" != "$originalContent" ]]; then
|
||||
echo -e "${GREEN}✓ Version file updated to $newVersion${NC}"
|
||||
if ! git diff --cached --quiet "$versionFile"; then
|
||||
echo -e "${YELLOW}Notice: Version file was already staged, updates made to staged version${NC}"
|
||||
else
|
||||
git add "$versionFile"
|
||||
echo -e "${YELLOW}Notice: Version file has been staged${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e "${GREEN}✓ No version updates needed${NC}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# PART 2: CODE QUALITY VALIDATION (our new logic)
|
||||
# ============================================================================
|
||||
|
||||
echo -e "\n${YELLOW}2. Code Quality Validation...${NC}"
|
||||
|
||||
# Track validation results
|
||||
VALIDATION_FAILED=0
|
||||
|
||||
# 2a. Python Syntax Validation
|
||||
echo -e "\n${YELLOW} 2a. Validating Python syntax...${NC}"
|
||||
if python3 tools/validate_syntax.py --check-only >/dev/null 2>&1; then
|
||||
echo -e "${GREEN} ✓ Syntax validation passed${NC}"
|
||||
else
|
||||
echo -e "${RED} ✗ Syntax validation failed${NC}"
|
||||
echo " Run: python3 tools/validate_syntax.py --fix"
|
||||
VALIDATION_FAILED=1
|
||||
fi
|
||||
|
||||
# 2b. Check for common issues in modified files
|
||||
echo -e "\n${YELLOW} 2b. Checking modified files for common issues...${NC}"
|
||||
|
||||
# Get list of staged files (all types)
|
||||
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM || true)
|
||||
STAGED_PYTHON_FILES=$(echo "$STAGED_FILES" | grep '\.py$' || true)
|
||||
|
||||
if [ -n "$STAGED_FILES" ]; then
|
||||
ISSUES_FOUND=0
|
||||
|
||||
# Check for cache files being committed
|
||||
CACHE_FILES=$(echo "$STAGED_FILES" | grep -E '(__pycache__|\.pyc$)' || true)
|
||||
if [ -n "$CACHE_FILES" ]; then
|
||||
echo -e "${RED} ✗ Python cache files staged for commit:${NC}"
|
||||
echo "$CACHE_FILES" | while read cache_file; do
|
||||
echo -e "${RED} $cache_file${NC}"
|
||||
done
|
||||
echo -e "${RED} ✗ Run: python3 tools/cleanup_cache.py --remove${NC}"
|
||||
ISSUES_FOUND=1
|
||||
fi
|
||||
|
||||
# Check Python files for specific issues
|
||||
if [ -n "$STAGED_PYTHON_FILES" ]; then
|
||||
for file in $STAGED_PYTHON_FILES; do
|
||||
if [ -f "$file" ]; then
|
||||
# Check for unterminated strings (the main issue from the email)
|
||||
if grep -n 'f".*{$' "$file" >/dev/null 2>&1; then
|
||||
echo -e "${RED} ✗ $file: Potential unterminated f-string${NC}"
|
||||
ISSUES_FOUND=1
|
||||
fi
|
||||
|
||||
# Check for missing imports that are commonly used
|
||||
if grep -q 'debug\.DebugLevel\.' "$file" && ! grep -q 'from.*debug' "$file" && ! grep -q 'import.*debug' "$file"; then
|
||||
echo -e "${YELLOW} ⚠ $file: Uses debug.DebugLevel but no debug import found${NC}"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ $ISSUES_FOUND -eq 0 ]; then
|
||||
echo -e "${GREEN} ✓ No common issues found in staged files${NC}"
|
||||
else
|
||||
echo -e "${RED} ✗ Common issues found in staged files${NC}"
|
||||
VALIDATION_FAILED=1
|
||||
fi
|
||||
else
|
||||
echo -e "${GREEN} ✓ No files staged for commit${NC}"
|
||||
fi
|
||||
|
||||
# 2c. Quick import test for core modules (informational only)
|
||||
echo -e "\n${YELLOW} 2c. Testing core module imports...${NC}"
|
||||
IMPORT_WARNINGS=0
|
||||
|
||||
# Test core imports that are critical (but don't fail on import issues - might be dependency related)
|
||||
CORE_MODULES=(
|
||||
"src.fenrirscreenreader.core.fenrirManager"
|
||||
"src.fenrirscreenreader.core.commandManager"
|
||||
"src.fenrirscreenreader.core.eventManager"
|
||||
)
|
||||
|
||||
cd src
|
||||
for module in "${CORE_MODULES[@]}"; do
|
||||
if python3 -c "import $module" 2>/dev/null; then
|
||||
echo -e "${GREEN} ✓ $module${NC}"
|
||||
else
|
||||
echo -e "${YELLOW} ⚠ $module (import failed - might be dependency related)${NC}"
|
||||
IMPORT_WARNINGS=1
|
||||
fi
|
||||
done
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
if [ $IMPORT_WARNINGS -eq 1 ]; then
|
||||
echo -e "${YELLOW} ⚠ Some core module imports failed (non-blocking)${NC}"
|
||||
echo -e "${YELLOW} This may be due to missing runtime dependencies${NC}"
|
||||
else
|
||||
echo -e "${GREEN} ✓ Core module imports successful${NC}"
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# FINAL SUMMARY
|
||||
# ============================================================================
|
||||
|
||||
echo -e "\n============================================================"
|
||||
if [ $VALIDATION_FAILED -eq 0 ]; then
|
||||
echo -e "${GREEN}✓ All pre-commit validations passed${NC}"
|
||||
echo -e "${GREEN}✓ Version management completed${NC}"
|
||||
echo -e "${GREEN}✓ Code quality checks passed${NC}"
|
||||
echo -e "${GREEN}Commit allowed to proceed${NC}"
|
||||
|
||||
# Show skip option
|
||||
echo -e "\n${BLUE}Tip: You can skip version updates with SKIP_VERSION_UPDATE=1${NC}"
|
||||
exit 0
|
||||
else
|
||||
echo -e "${RED}✗ Pre-commit validation failed${NC}"
|
||||
echo -e "${RED}Commit blocked - please fix issues above${NC}"
|
||||
echo ""
|
||||
echo "Quick fixes:"
|
||||
echo " • Python syntax: python3 tools/validate_syntax.py --fix"
|
||||
echo " • Review flagged files manually"
|
||||
echo " • Re-run commit after fixes"
|
||||
echo ""
|
||||
echo -e "${BLUE}Note: Version management completed successfully${NC}"
|
||||
exit 1
|
||||
fi
|
143
tools/pre-commit-hook
Executable file
143
tools/pre-commit-hook
Executable file
@ -0,0 +1,143 @@
|
||||
#!/bin/bash
|
||||
# Fenrir Pre-commit Hook
|
||||
#
|
||||
# This hook validates Python syntax and basic code quality before commits.
|
||||
# Install with: ln -sf ../../tools/pre-commit-hook .git/hooks/pre-commit
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${GREEN}Fenrir Pre-commit Validation${NC}"
|
||||
echo "=================================="
|
||||
|
||||
# Get the repository root
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
# Track validation results
|
||||
VALIDATION_FAILED=0
|
||||
|
||||
# 1. Python Syntax Validation
|
||||
echo -e "\n${YELLOW}1. Validating Python syntax...${NC}"
|
||||
if python3 tools/validate_syntax.py --check-only; then
|
||||
echo -e "${GREEN}✓ Syntax validation passed${NC}"
|
||||
else
|
||||
echo -e "${RED}✗ Syntax validation failed${NC}"
|
||||
echo "Run: python3 tools/validate_syntax.py --fix"
|
||||
VALIDATION_FAILED=1
|
||||
fi
|
||||
|
||||
# 2. Check for common issues in modified files
|
||||
echo -e "\n${YELLOW}2. Checking modified files for common issues...${NC}"
|
||||
|
||||
# Get list of staged Python files
|
||||
STAGED_PYTHON_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.py$' || true)
|
||||
|
||||
if [ -n "$STAGED_PYTHON_FILES" ]; then
|
||||
ISSUES_FOUND=0
|
||||
|
||||
for file in $STAGED_PYTHON_FILES; do
|
||||
if [ -f "$file" ]; then
|
||||
# Check for unterminated strings (the main issue from the email)
|
||||
if grep -n 'f".*{$' "$file" >/dev/null 2>&1; then
|
||||
echo -e "${RED}✗ $file: Potential unterminated f-string${NC}"
|
||||
ISSUES_FOUND=1
|
||||
fi
|
||||
|
||||
# Check for missing imports that are commonly used
|
||||
if grep -q 'debug\.DebugLevel\.' "$file" && ! grep -q 'from.*debug' "$file" && ! grep -q 'import.*debug' "$file"; then
|
||||
echo -e "${YELLOW}⚠ $file: Uses debug.DebugLevel but no debug import found${NC}"
|
||||
fi
|
||||
|
||||
# Check for extremely long lines (over 120 chars) that might indicate issues
|
||||
if awk 'length($0) > 120 {print NR ": " $0; exit 1}' "$file" >/dev/null 2>&1; then
|
||||
# Only warn, don't fail
|
||||
line_num=$(awk 'length($0) > 120 {print NR; exit}' "$file")
|
||||
echo -e "${YELLOW}⚠ $file:$line_num: Very long line (>120 chars)${NC}"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $ISSUES_FOUND -eq 0 ]; then
|
||||
echo -e "${GREEN}✓ No common issues found in modified files${NC}"
|
||||
else
|
||||
echo -e "${RED}✗ Common issues found in modified files${NC}"
|
||||
VALIDATION_FAILED=1
|
||||
fi
|
||||
else
|
||||
echo -e "${GREEN}✓ No Python files modified${NC}"
|
||||
fi
|
||||
|
||||
# 3. Quick import test for core modules
|
||||
echo -e "\n${YELLOW}3. Testing core module imports...${NC}"
|
||||
IMPORT_FAILED=0
|
||||
|
||||
# Test core imports that are critical
|
||||
CORE_MODULES=(
|
||||
"src.fenrirscreenreader.core.fenrirManager"
|
||||
"src.fenrirscreenreader.core.commandManager"
|
||||
"src.fenrirscreenreader.core.eventManager"
|
||||
)
|
||||
|
||||
cd src
|
||||
for module in "${CORE_MODULES[@]}"; do
|
||||
if python3 -c "import $module" 2>/dev/null; then
|
||||
echo -e "${GREEN}✓ $module${NC}"
|
||||
else
|
||||
echo -e "${RED}✗ $module (import failed)${NC}"
|
||||
IMPORT_FAILED=1
|
||||
fi
|
||||
done
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
if [ $IMPORT_FAILED -eq 1 ]; then
|
||||
echo -e "${RED}✗ Core module import test failed${NC}"
|
||||
VALIDATION_FAILED=1
|
||||
else
|
||||
echo -e "${GREEN}✓ Core module imports successful${NC}"
|
||||
fi
|
||||
|
||||
# 4. Check for secrets or sensitive data
|
||||
echo -e "\n${YELLOW}4. Checking for potential secrets...${NC}"
|
||||
SECRETS_FOUND=0
|
||||
|
||||
if [ -n "$STAGED_PYTHON_FILES" ]; then
|
||||
for file in $STAGED_PYTHON_FILES; do
|
||||
if [ -f "$file" ]; then
|
||||
# Check for potential passwords, keys, tokens
|
||||
if grep -i -E '(password|passwd|pwd|key|token|secret|api_key).*=.*["\'][^"\']{8,}["\']' "$file" >/dev/null 2>&1; then
|
||||
echo -e "${RED}✗ $file: Potential hardcoded secret detected${NC}"
|
||||
SECRETS_FOUND=1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ $SECRETS_FOUND -eq 0 ]; then
|
||||
echo -e "${GREEN}✓ No potential secrets found${NC}"
|
||||
else
|
||||
echo -e "${RED}✗ Potential secrets found - please review${NC}"
|
||||
VALIDATION_FAILED=1
|
||||
fi
|
||||
|
||||
# Summary
|
||||
echo -e "\n${'='*50}"
|
||||
if [ $VALIDATION_FAILED -eq 0 ]; then
|
||||
echo -e "${GREEN}✓ All pre-commit validations passed${NC}"
|
||||
echo -e "${GREEN}Commit allowed to proceed${NC}"
|
||||
exit 0
|
||||
else
|
||||
echo -e "${RED}✗ Pre-commit validation failed${NC}"
|
||||
echo -e "${RED}Commit blocked - please fix issues above${NC}"
|
||||
echo ""
|
||||
echo "Quick fixes:"
|
||||
echo " • Python syntax: python3 tools/validate_syntax.py --fix"
|
||||
echo " • Review flagged files manually"
|
||||
echo " • Re-run commit after fixes"
|
||||
exit 1
|
||||
fi
|
97
tools/setup_validation.sh
Executable file
97
tools/setup_validation.sh
Executable file
@ -0,0 +1,97 @@
|
||||
#!/bin/bash
|
||||
# Fenrir Validation Setup Script
|
||||
#
|
||||
# Sets up the validation tools and pre-commit hooks for Fenrir development.
|
||||
# Run this once after cloning the repository.
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${GREEN}Fenrir Development Environment Setup${NC}"
|
||||
echo "======================================"
|
||||
|
||||
# Check we're in the right directory
|
||||
if [ ! -f "CLAUDE.md" ] || [ ! -d "src/fenrirscreenreader" ]; then
|
||||
echo -e "${RED}Error: Must be run from Fenrir project root directory${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Make validation scripts executable
|
||||
echo -e "\n${YELLOW}1. Making validation scripts executable...${NC}"
|
||||
chmod +x tools/validate_syntax.py
|
||||
chmod +x tools/validate_release.py
|
||||
chmod +x tools/cleanup_cache.py
|
||||
chmod +x tools/pre-commit-hook
|
||||
chmod +x tools/install_validation_hook.sh
|
||||
chmod +x tools/pre-commit-composite
|
||||
echo -e "${GREEN}✓ Scripts are now executable${NC}"
|
||||
|
||||
# Install pre-commit hook
|
||||
echo -e "\n${YELLOW}2. Installing composite pre-commit hook...${NC}"
|
||||
echo -e "${YELLOW}This preserves existing version management functionality.${NC}"
|
||||
|
||||
# Use the safe installation script
|
||||
if ./tools/install_validation_hook.sh; then
|
||||
echo -e "${GREEN}✓ Composite pre-commit hook installed${NC}"
|
||||
else
|
||||
echo -e "${RED}⚠ Hook installation encountered issues${NC}"
|
||||
echo " You can install manually with: ./tools/install_validation_hook.sh"
|
||||
fi
|
||||
|
||||
# Test validation tools
|
||||
echo -e "\n${YELLOW}3. Testing validation tools...${NC}"
|
||||
|
||||
# Test syntax validator
|
||||
if python3 tools/validate_syntax.py --check-only >/dev/null 2>&1; then
|
||||
echo -e "${GREEN}✓ Syntax validator working${NC}"
|
||||
else
|
||||
echo -e "${RED}⚠ Syntax validator found issues${NC}"
|
||||
echo " Run: python3 tools/validate_syntax.py --fix"
|
||||
fi
|
||||
|
||||
# Test pre-commit hook
|
||||
if ./tools/pre-commit-hook >/dev/null 2>&1; then
|
||||
echo -e "${GREEN}✓ Pre-commit hook working${NC}"
|
||||
else
|
||||
echo -e "${RED}⚠ Pre-commit hook found issues${NC}"
|
||||
echo " This is normal if there are uncommitted changes"
|
||||
fi
|
||||
|
||||
# Verify dependencies for full validation
|
||||
echo -e "\n${YELLOW}4. Checking validation dependencies...${NC}"
|
||||
missing_deps=()
|
||||
|
||||
if ! command -v python3 >/dev/null 2>&1; then
|
||||
missing_deps+=("python3")
|
||||
fi
|
||||
|
||||
if ! python3 -c "import ast" >/dev/null 2>&1; then
|
||||
missing_deps+=("python3-ast")
|
||||
fi
|
||||
|
||||
if [ ${#missing_deps[@]} -eq 0 ]; then
|
||||
echo -e "${GREEN}✓ All validation dependencies available${NC}"
|
||||
else
|
||||
echo -e "${RED}Missing dependencies: ${missing_deps[*]}${NC}"
|
||||
fi
|
||||
|
||||
# Final instructions
|
||||
echo -e "\n${GREEN}Setup complete!${NC}"
|
||||
echo ""
|
||||
echo "Development workflow:"
|
||||
echo " 1. Make your changes"
|
||||
echo " 2. python3 tools/validate_syntax.py --fix"
|
||||
echo " 3. python3 tools/validate_release.py --quick"
|
||||
echo " 4. git add . && git commit (pre-commit hook runs automatically)"
|
||||
echo ""
|
||||
echo "Before releases:"
|
||||
echo " python3 tools/validate_release.py"
|
||||
echo " cat RELEASE_CHECKLIST.md"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Tip: The pre-commit hook will now run automatically on every commit${NC}"
|
||||
echo -e "${YELLOW} and prevent syntax errors from being committed.${NC}"
|
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()
|
236
tools/validate_syntax.py
Executable file
236
tools/validate_syntax.py
Executable file
@ -0,0 +1,236 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Fenrir Syntax Validation Tool
|
||||
|
||||
Validates Python syntax across the entire Fenrir codebase without writing
|
||||
cache files. Designed to catch syntax errors before packaging or releases.
|
||||
|
||||
Usage:
|
||||
python3 tools/validate_syntax.py # Validate all Python files
|
||||
python3 tools/validate_syntax.py --fix # Fix common issues automatically
|
||||
python3 tools/validate_syntax.py --check-only # Exit with non-zero if errors found
|
||||
"""
|
||||
|
||||
import ast
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class SyntaxValidator:
|
||||
def __init__(self):
|
||||
self.errors = []
|
||||
self.warnings = []
|
||||
self.fixed = []
|
||||
|
||||
def validate_file(self, filepath):
|
||||
"""Validate syntax of a single Python file."""
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Parse with AST (catches syntax errors)
|
||||
ast.parse(content, filename=str(filepath))
|
||||
return True, content
|
||||
|
||||
except SyntaxError as e:
|
||||
error_msg = f"{filepath}:{e.lineno}: {e.msg}"
|
||||
self.errors.append((filepath, e, content))
|
||||
return False, content
|
||||
|
||||
except UnicodeDecodeError as e:
|
||||
error_msg = f"{filepath}: Unicode decode error: {e}"
|
||||
self.errors.append((filepath, e, None))
|
||||
return False, None
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"{filepath}: Unexpected error: {e}"
|
||||
self.errors.append((filepath, e, None))
|
||||
return False, None
|
||||
|
||||
def fix_common_issues(self, filepath, content):
|
||||
"""Attempt to fix common syntax issues automatically."""
|
||||
if not content:
|
||||
return False, content
|
||||
|
||||
original_content = content
|
||||
fixed_issues = []
|
||||
|
||||
# Fix unterminated f-strings (the main issue from the email)
|
||||
lines = content.split('\n')
|
||||
modified = False
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
# Look for f-strings that span multiple lines incorrectly
|
||||
if 'f"' in line and line.count('"') % 2 == 1:
|
||||
# Check if this looks like a broken multi-line f-string
|
||||
indent = len(line) - len(line.lstrip())
|
||||
|
||||
# Look ahead for continuation
|
||||
j = i + 1
|
||||
while j < len(lines) and lines[j].strip():
|
||||
next_line = lines[j]
|
||||
next_indent = len(next_line) - len(next_line.lstrip())
|
||||
|
||||
# If next line is indented more and has closing brace/quote
|
||||
if (next_indent > indent and
|
||||
('"' in next_line or '}' in next_line)):
|
||||
|
||||
# Try to fix by joining lines properly
|
||||
combined_line = line.rstrip()
|
||||
continuation = next_line.strip()
|
||||
|
||||
if continuation.startswith(('"', '}', 'str(e)', 'self.', 'fenrirVersion.')):
|
||||
# Fix common patterns
|
||||
if 'str(e)}' in continuation:
|
||||
fixed_line = line.replace('f"', 'f"').rstrip() + '{' + continuation.replace('"', '') + '}'
|
||||
elif continuation.startswith('"'):
|
||||
fixed_line = line + continuation
|
||||
else:
|
||||
fixed_line = line.rstrip() + continuation
|
||||
|
||||
lines[i] = fixed_line
|
||||
lines[j] = '' # Remove the continuation line
|
||||
modified = True
|
||||
fixed_issues.append(f"Line {i+1}: Fixed multi-line f-string")
|
||||
break
|
||||
j += 1
|
||||
|
||||
if modified:
|
||||
content = '\n'.join(lines)
|
||||
# Clean up empty lines that were created
|
||||
content = '\n'.join(line for line in content.split('\n') if line.strip() or not line)
|
||||
|
||||
# Verify the fix worked
|
||||
try:
|
||||
ast.parse(content, filename=str(filepath))
|
||||
self.fixed.append((filepath, fixed_issues))
|
||||
return True, content
|
||||
except SyntaxError:
|
||||
# Fix didn't work, return original
|
||||
return False, original_content
|
||||
|
||||
return False, content
|
||||
|
||||
def scan_directory(self, directory, fix_mode=False):
|
||||
"""Scan directory for Python files and validate them."""
|
||||
python_files = []
|
||||
|
||||
# Find all Python files
|
||||
for root, dirs, files in os.walk(directory):
|
||||
# Skip cache and build directories
|
||||
dirs[:] = [d for d in dirs if not d.startswith(('__pycache__', '.git', 'build', 'dist'))]
|
||||
|
||||
for file in files:
|
||||
if file.endswith('.py'):
|
||||
python_files.append(Path(root) / file)
|
||||
|
||||
print(f"Validating {len(python_files)} Python files...")
|
||||
|
||||
valid_count = 0
|
||||
fixed_count = 0
|
||||
|
||||
for filepath in sorted(python_files):
|
||||
is_valid, content = self.validate_file(filepath)
|
||||
|
||||
if is_valid:
|
||||
valid_count += 1
|
||||
print(f"✓ {filepath}")
|
||||
else:
|
||||
print(f"✗ {filepath}")
|
||||
|
||||
if fix_mode and content:
|
||||
# Try to fix the file
|
||||
was_fixed, fixed_content = self.fix_common_issues(filepath, content)
|
||||
if was_fixed:
|
||||
# Write the fixed content back
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(fixed_content)
|
||||
print(f" → Fixed automatically")
|
||||
fixed_count += 1
|
||||
|
||||
# Re-validate
|
||||
is_valid_now, _ = self.validate_file(filepath)
|
||||
if is_valid_now:
|
||||
valid_count += 1
|
||||
|
||||
return valid_count, len(python_files), fixed_count
|
||||
|
||||
def print_summary(self, valid_count, total_count, fixed_count=0):
|
||||
"""Print validation summary."""
|
||||
print(f"\n{'='*60}")
|
||||
print(f"SYNTAX VALIDATION SUMMARY")
|
||||
print(f"{'='*60}")
|
||||
print(f"Valid files: {valid_count}/{total_count}")
|
||||
print(f"Invalid files: {total_count - valid_count}")
|
||||
if fixed_count > 0:
|
||||
print(f"Auto-fixed: {fixed_count}")
|
||||
|
||||
if self.errors:
|
||||
print(f"\nERRORS ({len(self.errors)}):")
|
||||
for filepath, error, _ in self.errors:
|
||||
if isinstance(error, SyntaxError):
|
||||
print(f" {filepath}:{error.lineno}: {error.msg}")
|
||||
else:
|
||||
print(f" {filepath}: {error}")
|
||||
|
||||
if self.fixed:
|
||||
print(f"\nAUTO-FIXES APPLIED ({len(self.fixed)}):")
|
||||
for filepath, fixes in self.fixed:
|
||||
print(f" {filepath}:")
|
||||
for fix in fixes:
|
||||
print(f" - {fix}")
|
||||
|
||||
success_rate = (valid_count / total_count) * 100 if total_count > 0 else 0
|
||||
print(f"\nSuccess rate: {success_rate:.1f}%")
|
||||
|
||||
return len(self.errors) == 0
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Validate Python syntax in Fenrir codebase')
|
||||
parser.add_argument('--fix', action='store_true',
|
||||
help='Attempt to fix common syntax issues automatically')
|
||||
parser.add_argument('--check-only', action='store_true',
|
||||
help='Exit with non-zero code if syntax errors found')
|
||||
parser.add_argument('--directory', default='src/fenrirscreenreader',
|
||||
help='Directory to scan (default: src/fenrirscreenreader)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Find project root
|
||||
script_dir = Path(__file__).parent
|
||||
project_root = script_dir.parent
|
||||
target_dir = project_root / args.directory
|
||||
|
||||
if not target_dir.exists():
|
||||
print(f"Error: Directory {target_dir} does not exist")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Fenrir Syntax Validator")
|
||||
print(f"Target directory: {target_dir}")
|
||||
print(f"Fix mode: {'ON' if args.fix else 'OFF'}")
|
||||
print()
|
||||
|
||||
validator = SyntaxValidator()
|
||||
valid_count, total_count, fixed_count = validator.scan_directory(target_dir, fix_mode=args.fix)
|
||||
|
||||
all_valid = validator.print_summary(valid_count, total_count, fixed_count)
|
||||
|
||||
if args.check_only and not all_valid:
|
||||
print(f"\nValidation failed: {total_count - valid_count} files have syntax errors")
|
||||
sys.exit(1)
|
||||
elif not all_valid:
|
||||
print(f"\nWarning: {total_count - valid_count} files have syntax errors")
|
||||
if not args.fix:
|
||||
print("Run with --fix to attempt automatic fixes")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(f"\n✓ All {total_count} files have valid syntax")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
Reference in New Issue
Block a user