diff --git a/tools/run_development.py b/tools/run_development.py index 0011590..5ff5f74 100644 --- a/tools/run_development.py +++ b/tools/run_development.py @@ -2,19 +2,27 @@ """ Development workflow runner for the Skelly Godot project. -Runs code quality checks (linting, formatting, testing) individually or together. +Runs code quality checks (linting, formatting, testing, validation) individually or together. Provides colored output and error reporting. Usage examples: - python tools/run_development.py # Run all steps - python tools/run_development.py --lint # Only linting - python tools/run_development.py --steps lint test # Custom workflow + python tools/run_development.py # Run all steps + python tools/run_development.py --lint # Only linting + python tools/run_development.py --validate # Only file validation + python tools/run_development.py --steps lint test # Custom workflow + +Features: +- GDScript formatting and linting +- Test execution +- YAML, TOML, and JSON file validation (respects .gitignore) +- Colored output and comprehensive error reporting NOTE: Handles "successful but noisy" linter output such as "Success: no problems found" - treats these as clean instead of warnings. """ import argparse +import json import os import re import subprocess @@ -24,6 +32,19 @@ import warnings from pathlib import Path from typing import Dict, List, Tuple +try: + import yaml +except ImportError: + yaml = None + +try: + import tomllib +except ImportError: + try: + import tomli as tomllib + except ImportError: + tomllib = None + # Suppress pkg_resources deprecation warning from gdtoolkit warnings.filterwarnings("ignore", message="pkg_resources is deprecated", category=UserWarning) @@ -169,6 +190,67 @@ def get_gd_files(project_root: Path) -> List[Path]: return list(project_root.rglob("*.gd")) +def read_gitignore(project_root: Path) -> List[str]: + """Read gitignore patterns from .gitignore file.""" + gitignore_path = project_root / ".gitignore" + patterns = [] + + if gitignore_path.exists(): + try: + with open(gitignore_path, 'r', encoding='utf-8') as f: + for line in f: + line = line.strip() + if line and not line.startswith('#'): + patterns.append(line) + except Exception as e: + print(f"Warning: Could not read .gitignore: {e}") + + return patterns + + +def is_ignored_by_gitignore(file_path: Path, project_root: Path, patterns: List[str]) -> bool: + """Check if a file should be ignored based on gitignore patterns.""" + relative_path = file_path.relative_to(project_root) + path_str = str(relative_path).replace('\\', '/') + + for pattern in patterns: + # Handle directory patterns + if pattern.endswith('/'): + if any(part == pattern[:-1] for part in relative_path.parts): + return True + # Handle file patterns + elif pattern in path_str or relative_path.name == pattern: + return True + # Handle glob patterns (simple implementation) + elif '*' in pattern: + import fnmatch + if fnmatch.fnmatch(path_str, pattern) or fnmatch.fnmatch(relative_path.name, pattern): + return True + + return False + + +def get_validation_files(project_root: Path) -> Dict[str, List[Path]]: + """Get all YAML, TOML, and JSON files in the project, respecting gitignore.""" + gitignore_patterns = read_gitignore(project_root) + + file_types = { + 'yaml': ['*.yaml', '*.yml'], + 'toml': ['*.toml'], + 'json': ['*.json'] + } + + files = {file_type: [] for file_type in file_types} + + for file_type, patterns in file_types.items(): + for pattern in patterns: + for file_path in project_root.rglob(pattern): + if file_path.is_file() and not is_ignored_by_gitignore(file_path, project_root, gitignore_patterns): + files[file_type].append(file_path) + + return files + + def format_test_name(filename: str) -> str: """Convert test_filename to readable test name.""" return filename.replace("test_", "").replace("_", " ") @@ -279,7 +361,132 @@ def run_lint(project_root: Path) -> Tuple[bool, Dict]: return success, {**stats, "failed_paths": failed_paths} -# rest of file unchanged (format, tests, workflow runner) -- copied verbatim +def validate_yaml_file(file_path: Path) -> Tuple[bool, str]: + """Validate a YAML file.""" + if yaml is None: + return False, "PyYAML not installed. Install with: pip install PyYAML" + + try: + with open(file_path, 'r', encoding='utf-8') as f: + yaml.safe_load(f) + return True, "" + except yaml.YAMLError as e: + return False, f"YAML syntax error: {e}" + except Exception as e: + return False, f"Error reading file: {e}" + + +def validate_toml_file(file_path: Path) -> Tuple[bool, str]: + """Validate a TOML file.""" + if tomllib is None: + return False, "tomllib/tomli not available. For Python 3.11+, it's built-in. For older versions: pip install tomli" + + try: + with open(file_path, 'rb') as f: + tomllib.load(f) + return True, "" + except tomllib.TOMLDecodeError as e: + return False, f"TOML syntax error: {e}" + except Exception as e: + return False, f"Error reading file: {e}" + + +def validate_json_file(file_path: Path) -> Tuple[bool, str]: + """Validate a JSON file.""" + try: + with open(file_path, 'r', encoding='utf-8') as f: + json.load(f) + return True, "" + except json.JSONDecodeError as e: + return False, f"JSON syntax error: {e}" + except Exception as e: + return False, f"Error reading file: {e}" + + +def run_validate(project_root: Path) -> Tuple[bool, Dict]: + """Run validation on YAML, TOML, and JSON files.""" + print_header("๐Ÿ“‹ File Format Validation") + + # Get all validation files + validation_files = get_validation_files(project_root) + total_files = sum(len(files) for files in validation_files.values()) + + if total_files == 0: + msg = "No YAML, TOML, or JSON files found to validate." + colored_msg = Colors.colorize(msg, Colors.YELLOW) + print(colored_msg) + return True, {"Total files": 0, "Valid files": 0, "Invalid files": 0} + + count_msg = f"Found {total_files} files to validate:" + colored_count = Colors.colorize(count_msg, Colors.BLUE) + print(colored_count) + + for file_type, files in validation_files.items(): + if files: + type_msg = f" {file_type.upper()}: {len(files)} files" + colored_type = Colors.colorize(type_msg, Colors.CYAN) + print(colored_type) + + print() + + valid_files = invalid_files = 0 + failed_paths = [] + + # Validation functions mapping + validators = { + 'yaml': validate_yaml_file, + 'toml': validate_toml_file, + 'json': validate_json_file + } + + # Validate each file type + for file_type, files in validation_files.items(): + if not files: + continue + + validator = validators[file_type] + type_header = f"๐Ÿ” Validating {file_type.upper()} files" + colored_header = Colors.colorize(type_header, Colors.MAGENTA + Colors.BOLD) + print(colored_header) + + for file_path in files: + relative_path = file_path.relative_to(project_root) + file_msg = f"๐Ÿ“„ Validating: {relative_path}" + colored_file = Colors.colorize(file_msg, Colors.CYAN) + print(colored_file) + + is_valid, error_msg = validator(file_path) + + if is_valid: + valid_files += 1 + print_result(True, "") + else: + invalid_files += 1 + failed_paths.append(str(relative_path)) + print_result(False, error_msg) + + print() + + # Summary + stats = { + "Total files": total_files, + "Valid files": valid_files, + "Invalid files": invalid_files + } + print_summary("Validation Summary", stats) + + success = invalid_files == 0 + print() + if not success: + msg = "โŒ File validation FAILED - Please fix the syntax errors above" + colored_msg = Colors.colorize(msg, Colors.RED + Colors.BOLD) + print(colored_msg) + else: + msg = "โœ… All files passed validation!" + colored_msg = Colors.colorize(msg, Colors.GREEN + Colors.BOLD) + print(colored_msg) + + return success, {**stats, "failed_paths": failed_paths} def run_format(project_root: Path) -> Tuple[bool, Dict]: """Run gdformat on all GDScript files.""" @@ -498,7 +705,8 @@ def run_workflow(project_root: Path, steps: List[str]) -> bool: workflow_steps = { "lint": ("๐Ÿ” Code linting (gdlint)", run_lint), "format": ("๐ŸŽจ Code formatting (gdformat)", run_format), - "test": ("๐Ÿงช Test execution (godot tests)", run_tests) + "test": ("๐Ÿงช Test execution (godot tests)", run_tests), + "validate": ("๐Ÿ“‹ File format validation (yaml/toml/json)", run_validate) } intro_msg = "๐Ÿš€ This script will run the development workflow:" @@ -554,7 +762,7 @@ def run_workflow(project_root: Path, steps: List[str]) -> bool: status_text = "PASSED" if step_success else "FAILED" status_color = Colors.GREEN if step_success else Colors.RED - step_emoji = {"lint": "๐Ÿ”", "format": "๐ŸŽจ", "test": "๐Ÿงช"}.get(step, "๐Ÿ“‹") + step_emoji = {"lint": "๐Ÿ”", "format": "๐ŸŽจ", "test": "๐Ÿงช", "validate": "๐Ÿ“‹"}.get(step, "๐Ÿ“‹") colored_status = Colors.colorize(f"{status_text}", status_color + Colors.BOLD) print(f"{step_emoji} {step.capitalize()}: {status_emoji} {colored_status}") @@ -589,11 +797,12 @@ def run_workflow(project_root: Path, steps: List[str]) -> bool: def main(): """Main entry point.""" parser = argparse.ArgumentParser(description="Run development workflow for Skelly Godot project") - parser.add_argument("--steps", nargs="+", choices=["lint", "format", "test"], - default=["format", "lint", "test"], help="Workflow steps to run") + parser.add_argument("--steps", nargs="+", choices=["lint", "format", "test", "validate"], + default=["format", "lint", "test", "validate"], help="Workflow steps to run") parser.add_argument("--lint", action="store_true", help="Run linting") parser.add_argument("--format", action="store_true", help="Run formatting") parser.add_argument("--test", action="store_true", help="Run tests") + parser.add_argument("--validate", action="store_true", help="Run file format validation") args = parser.parse_args() project_root = Path(__file__).parent.parent @@ -605,12 +814,14 @@ def main(): steps = ["format"] elif args.test: steps = ["test"] + elif args.validate: + steps = ["validate"] else: steps = args.steps # Run workflow or individual step if len(steps) == 1: - step_funcs = {"lint": run_lint, "format": run_format, "test": run_tests} + step_funcs = {"lint": run_lint, "format": run_format, "test": run_tests, "validate": run_validate} success, _ = step_funcs[steps[0]](project_root) else: success = run_workflow(project_root, steps)