|
|
|
@@ -2,19 +2,30 @@
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
Development workflow runner for the Skelly Godot project.
|
|
|
|
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.
|
|
|
|
Provides colored output and error reporting.
|
|
|
|
|
|
|
|
|
|
|
|
Usage examples:
|
|
|
|
Usage examples:
|
|
|
|
python tools/run_development.py # Run all steps
|
|
|
|
python tools/run_development.py # Run all steps
|
|
|
|
python tools/run_development.py --lint # Only linting
|
|
|
|
python tools/run_development.py --lint # Only linting
|
|
|
|
python tools/run_development.py --steps lint test # Custom workflow
|
|
|
|
python tools/run_development.py --validate # Only file validation
|
|
|
|
|
|
|
|
python tools/run_development.py --validate --silent # Silent validation (only errors)
|
|
|
|
|
|
|
|
python tools/run_development.py --test --yaml # Test results in YAML format
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
- Machine-readable YAML output for CI/CD integration
|
|
|
|
|
|
|
|
|
|
|
|
NOTE: Handles "successful but noisy" linter output such as
|
|
|
|
NOTE: Handles "successful but noisy" linter output such as
|
|
|
|
"Success: no problems found" - treats these as clean instead of warnings.
|
|
|
|
"Success: no problems found" - treats these as clean instead of warnings.
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
import argparse
|
|
|
|
|
|
|
|
import json
|
|
|
|
import os
|
|
|
|
import os
|
|
|
|
import re
|
|
|
|
import re
|
|
|
|
import subprocess
|
|
|
|
import subprocess
|
|
|
|
@@ -24,6 +35,19 @@ import warnings
|
|
|
|
from pathlib import Path
|
|
|
|
from pathlib import Path
|
|
|
|
from typing import Dict, List, Tuple
|
|
|
|
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
|
|
|
|
# Suppress pkg_resources deprecation warning from gdtoolkit
|
|
|
|
warnings.filterwarnings("ignore", message="pkg_resources is deprecated", category=UserWarning)
|
|
|
|
warnings.filterwarnings("ignore", message="pkg_resources is deprecated", category=UserWarning)
|
|
|
|
|
|
|
|
|
|
|
|
@@ -54,27 +78,60 @@ class Colors:
|
|
|
|
return f"{color}{text}{Colors.RESET}"
|
|
|
|
return f"{color}{text}{Colors.RESET}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def print_header(title: str) -> None:
|
|
|
|
def print_header(title: str, silent: bool = False) -> None:
|
|
|
|
"""Print a formatted header."""
|
|
|
|
"""Print a formatted header."""
|
|
|
|
separator = Colors.colorize("=" * 48, Colors.CYAN)
|
|
|
|
if not silent:
|
|
|
|
colored_title = Colors.colorize(title, Colors.BOLD + Colors.WHITE)
|
|
|
|
separator = Colors.colorize("=" * 48, Colors.CYAN)
|
|
|
|
print(separator)
|
|
|
|
colored_title = Colors.colorize(title, Colors.BOLD + Colors.WHITE)
|
|
|
|
print(colored_title)
|
|
|
|
print(separator)
|
|
|
|
print(separator)
|
|
|
|
print(colored_title)
|
|
|
|
print()
|
|
|
|
print(separator)
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def print_summary(title: str, stats: Dict[str, int]) -> None:
|
|
|
|
def print_summary(title: str, stats: Dict[str, int], silent: bool = False) -> None:
|
|
|
|
"""Print a formatted summary."""
|
|
|
|
"""Print a formatted summary."""
|
|
|
|
separator = Colors.colorize("=" * 48, Colors.CYAN)
|
|
|
|
if not silent:
|
|
|
|
colored_title = Colors.colorize(title, Colors.BOLD + Colors.WHITE)
|
|
|
|
separator = Colors.colorize("=" * 48, Colors.CYAN)
|
|
|
|
print(separator)
|
|
|
|
colored_title = Colors.colorize(title, Colors.BOLD + Colors.WHITE)
|
|
|
|
print(colored_title)
|
|
|
|
print(separator)
|
|
|
|
print(separator)
|
|
|
|
print(colored_title)
|
|
|
|
for key, value in stats.items():
|
|
|
|
print(separator)
|
|
|
|
colored_key = Colors.colorize(key, Colors.BLUE)
|
|
|
|
for key, value in stats.items():
|
|
|
|
colored_value = Colors.colorize(str(value), Colors.BOLD + Colors.WHITE)
|
|
|
|
colored_key = Colors.colorize(key, Colors.BLUE)
|
|
|
|
print(f"{colored_key}: {colored_value}")
|
|
|
|
colored_value = Colors.colorize(str(value), Colors.BOLD + Colors.WHITE)
|
|
|
|
|
|
|
|
print(f"{colored_key}: {colored_value}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def output_yaml_results(step_name: str, results: Dict, success: bool) -> None:
|
|
|
|
|
|
|
|
"""Output results in YAML format."""
|
|
|
|
|
|
|
|
if yaml is None:
|
|
|
|
|
|
|
|
print("# YAML output requires PyYAML. Install with: pip install PyYAML")
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Convert results to YAML-friendly format
|
|
|
|
|
|
|
|
yaml_data = {
|
|
|
|
|
|
|
|
"step": step_name,
|
|
|
|
|
|
|
|
"success": success,
|
|
|
|
|
|
|
|
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
|
|
|
|
|
|
"statistics": {},
|
|
|
|
|
|
|
|
"failed_items": []
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Extract statistics
|
|
|
|
|
|
|
|
for key, value in results.items():
|
|
|
|
|
|
|
|
if key != "failed_paths" and key != "results":
|
|
|
|
|
|
|
|
yaml_data["statistics"][key.lower().replace(" ", "_")] = value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Extract failed items
|
|
|
|
|
|
|
|
if "failed_paths" in results:
|
|
|
|
|
|
|
|
yaml_data["failed_items"] = results["failed_paths"]
|
|
|
|
|
|
|
|
elif "results" in results:
|
|
|
|
|
|
|
|
# For test results, extract failed tests
|
|
|
|
|
|
|
|
failed_tests = [result[0] for result in results["results"] if not result[1]]
|
|
|
|
|
|
|
|
yaml_data["failed_items"] = failed_tests
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
print(yaml.dump(yaml_data, default_flow_style=False, sort_keys=False))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_command(cmd: List[str], cwd: Path, timeout: int = 30) -> subprocess.CompletedProcess:
|
|
|
|
def run_command(cmd: List[str], cwd: Path, timeout: int = 30) -> subprocess.CompletedProcess:
|
|
|
|
@@ -136,17 +193,18 @@ def print_skip_message(tool: str) -> None:
|
|
|
|
print(f" {colored_message}")
|
|
|
|
print(f" {colored_message}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def print_result(success: bool, output: str = "") -> None:
|
|
|
|
def print_result(success: bool, output: str = "", silent: bool = False) -> None:
|
|
|
|
"""Print command result."""
|
|
|
|
"""Print command result."""
|
|
|
|
if success:
|
|
|
|
if success:
|
|
|
|
if not output:
|
|
|
|
if not output:
|
|
|
|
message = "✅ Clean"
|
|
|
|
if not silent:
|
|
|
|
colored_message = Colors.colorize(message, Colors.GREEN)
|
|
|
|
message = "✅ Clean"
|
|
|
|
|
|
|
|
colored_message = Colors.colorize(message, Colors.GREEN)
|
|
|
|
|
|
|
|
print(f" {colored_message}")
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
message = "⚠️ WARNINGS found:"
|
|
|
|
message = "⚠️ WARNINGS found:"
|
|
|
|
colored_message = Colors.colorize(message, Colors.YELLOW)
|
|
|
|
colored_message = Colors.colorize(message, Colors.YELLOW)
|
|
|
|
print(f" {colored_message}")
|
|
|
|
print(f" {colored_message}")
|
|
|
|
if output:
|
|
|
|
|
|
|
|
# Indent and color the output
|
|
|
|
# Indent and color the output
|
|
|
|
for line in output.split('\n'):
|
|
|
|
for line in output.split('\n'):
|
|
|
|
if line.strip():
|
|
|
|
if line.strip():
|
|
|
|
@@ -169,6 +227,67 @@ def get_gd_files(project_root: Path) -> List[Path]:
|
|
|
|
return list(project_root.rglob("*.gd"))
|
|
|
|
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:
|
|
|
|
def format_test_name(filename: str) -> str:
|
|
|
|
"""Convert test_filename to readable test name."""
|
|
|
|
"""Convert test_filename to readable test name."""
|
|
|
|
return filename.replace("test_", "").replace("_", " ")
|
|
|
|
return filename.replace("test_", "").replace("_", " ")
|
|
|
|
@@ -201,28 +320,33 @@ def _is_successful_linter_output(output: str) -> bool:
|
|
|
|
return False
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_lint(project_root: Path) -> Tuple[bool, Dict]:
|
|
|
|
def run_lint(project_root: Path, silent: bool = False, yaml_output: bool = False) -> Tuple[bool, Dict]:
|
|
|
|
"""Run gdlint on all GDScript files."""
|
|
|
|
"""Run gdlint on all GDScript files."""
|
|
|
|
print_header("🔍 GDScript Linter")
|
|
|
|
if not yaml_output:
|
|
|
|
|
|
|
|
print_header("🔍 GDScript Linter", silent)
|
|
|
|
|
|
|
|
|
|
|
|
gd_files = get_gd_files(project_root)
|
|
|
|
gd_files = get_gd_files(project_root)
|
|
|
|
count_msg = f"Found {len(gd_files)} GDScript files to lint."
|
|
|
|
if not silent and not yaml_output:
|
|
|
|
colored_count = Colors.colorize(count_msg, Colors.BLUE)
|
|
|
|
count_msg = f"Found {len(gd_files)} GDScript files to lint."
|
|
|
|
print(f"{colored_count}\n")
|
|
|
|
colored_count = Colors.colorize(count_msg, Colors.BLUE)
|
|
|
|
|
|
|
|
print(f"{colored_count}\n")
|
|
|
|
|
|
|
|
|
|
|
|
clean_files = warning_files = error_files = 0
|
|
|
|
clean_files = warning_files = error_files = 0
|
|
|
|
failed_paths = []
|
|
|
|
failed_paths = []
|
|
|
|
|
|
|
|
|
|
|
|
for gd_file in gd_files:
|
|
|
|
for gd_file in gd_files:
|
|
|
|
relative_path = gd_file.relative_to(project_root)
|
|
|
|
relative_path = gd_file.relative_to(project_root)
|
|
|
|
file_msg = f"📄 Linting: {relative_path.name}"
|
|
|
|
if not silent and not yaml_output:
|
|
|
|
colored_file = Colors.colorize(file_msg, Colors.CYAN)
|
|
|
|
file_msg = f"📄 Linting: {relative_path.name}"
|
|
|
|
print(colored_file)
|
|
|
|
colored_file = Colors.colorize(file_msg, Colors.CYAN)
|
|
|
|
|
|
|
|
print(colored_file)
|
|
|
|
|
|
|
|
|
|
|
|
if should_skip_file(gd_file):
|
|
|
|
if should_skip_file(gd_file):
|
|
|
|
print_skip_message("gdlint")
|
|
|
|
if not silent and not yaml_output:
|
|
|
|
|
|
|
|
print_skip_message("gdlint")
|
|
|
|
clean_files += 1
|
|
|
|
clean_files += 1
|
|
|
|
print()
|
|
|
|
if not silent and not yaml_output:
|
|
|
|
|
|
|
|
print()
|
|
|
|
continue
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
@@ -233,24 +357,30 @@ def run_lint(project_root: Path) -> Tuple[bool, Dict]:
|
|
|
|
# If output is "no problems" (or similar), treat as clean.
|
|
|
|
# If output is "no problems" (or similar), treat as clean.
|
|
|
|
if _is_successful_linter_output(output):
|
|
|
|
if _is_successful_linter_output(output):
|
|
|
|
clean_files += 1
|
|
|
|
clean_files += 1
|
|
|
|
print_result(True, "")
|
|
|
|
if not yaml_output:
|
|
|
|
|
|
|
|
print_result(True, "", silent)
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
warning_files += 1
|
|
|
|
warning_files += 1
|
|
|
|
print_result(True, output)
|
|
|
|
if not yaml_output:
|
|
|
|
|
|
|
|
print_result(True, output, silent)
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
error_files += 1
|
|
|
|
error_files += 1
|
|
|
|
failed_paths.append(str(relative_path))
|
|
|
|
failed_paths.append(str(relative_path))
|
|
|
|
print_result(False, output)
|
|
|
|
if not yaml_output:
|
|
|
|
|
|
|
|
print_result(False, output, silent)
|
|
|
|
|
|
|
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
except FileNotFoundError:
|
|
|
|
print(" ❌ ERROR: gdlint not found")
|
|
|
|
if not silent and not yaml_output:
|
|
|
|
|
|
|
|
print(" ❌ ERROR: gdlint not found")
|
|
|
|
return False, {}
|
|
|
|
return False, {}
|
|
|
|
except Exception as e:
|
|
|
|
except Exception as e:
|
|
|
|
print(f" ❌ ERROR: {e}")
|
|
|
|
if not silent and not yaml_output:
|
|
|
|
|
|
|
|
print(f" ❌ ERROR: {e}")
|
|
|
|
error_files += 1
|
|
|
|
error_files += 1
|
|
|
|
failed_paths.append(str(relative_path))
|
|
|
|
failed_paths.append(str(relative_path))
|
|
|
|
|
|
|
|
|
|
|
|
print()
|
|
|
|
if not silent and not yaml_output:
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
|
|
# Summary
|
|
|
|
# Summary
|
|
|
|
stats = {
|
|
|
|
stats = {
|
|
|
|
@@ -259,80 +389,241 @@ def run_lint(project_root: Path) -> Tuple[bool, Dict]:
|
|
|
|
"Files with warnings": warning_files,
|
|
|
|
"Files with warnings": warning_files,
|
|
|
|
"Files with errors": error_files
|
|
|
|
"Files with errors": error_files
|
|
|
|
}
|
|
|
|
}
|
|
|
|
print_summary("Linting Summary", stats)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
success = error_files == 0
|
|
|
|
success = error_files == 0
|
|
|
|
print()
|
|
|
|
|
|
|
|
if not success:
|
|
|
|
if yaml_output:
|
|
|
|
msg = "❌ Linting FAILED - Please fix the errors above"
|
|
|
|
output_yaml_results("lint", {**stats, "failed_paths": failed_paths}, success)
|
|
|
|
colored_msg = Colors.colorize(msg, Colors.RED + Colors.BOLD)
|
|
|
|
|
|
|
|
print(colored_msg)
|
|
|
|
|
|
|
|
elif warning_files > 0:
|
|
|
|
|
|
|
|
msg = "⚠️ Linting PASSED with warnings - Consider fixing them"
|
|
|
|
|
|
|
|
colored_msg = Colors.colorize(msg, Colors.YELLOW + Colors.BOLD)
|
|
|
|
|
|
|
|
print(colored_msg)
|
|
|
|
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
msg = "✅ All GDScript files passed linting!"
|
|
|
|
print_summary("Linting Summary", stats, silent)
|
|
|
|
colored_msg = Colors.colorize(msg, Colors.GREEN + Colors.BOLD)
|
|
|
|
if not silent:
|
|
|
|
print(colored_msg)
|
|
|
|
print()
|
|
|
|
|
|
|
|
if not success:
|
|
|
|
|
|
|
|
msg = "❌ Linting FAILED - Please fix the errors above"
|
|
|
|
|
|
|
|
colored_msg = Colors.colorize(msg, Colors.RED + Colors.BOLD)
|
|
|
|
|
|
|
|
print(colored_msg)
|
|
|
|
|
|
|
|
elif warning_files > 0:
|
|
|
|
|
|
|
|
msg = "⚠️ Linting PASSED with warnings - Consider fixing them"
|
|
|
|
|
|
|
|
colored_msg = Colors.colorize(msg, Colors.YELLOW + Colors.BOLD)
|
|
|
|
|
|
|
|
print(colored_msg)
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
msg = "✅ All GDScript files passed linting!"
|
|
|
|
|
|
|
|
colored_msg = Colors.colorize(msg, Colors.GREEN + Colors.BOLD)
|
|
|
|
|
|
|
|
print(colored_msg)
|
|
|
|
|
|
|
|
elif not success:
|
|
|
|
|
|
|
|
# In silent mode, still show failed files
|
|
|
|
|
|
|
|
for failed_path in failed_paths:
|
|
|
|
|
|
|
|
print(f"❌ {failed_path}")
|
|
|
|
|
|
|
|
|
|
|
|
return success, {**stats, "failed_paths": failed_paths}
|
|
|
|
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"
|
|
|
|
|
|
|
|
|
|
|
|
def run_format(project_root: Path) -> Tuple[bool, Dict]:
|
|
|
|
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, silent: bool = False, yaml_output: bool = False) -> Tuple[bool, Dict]:
|
|
|
|
|
|
|
|
"""Run validation on YAML, TOML, and JSON files."""
|
|
|
|
|
|
|
|
if not silent and not yaml_output:
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
if not silent:
|
|
|
|
|
|
|
|
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}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not silent and not yaml_output:
|
|
|
|
|
|
|
|
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]
|
|
|
|
|
|
|
|
if not silent and not yaml_output:
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
if not silent and not yaml_output:
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
if not yaml_output:
|
|
|
|
|
|
|
|
print_result(True, "", silent)
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
invalid_files += 1
|
|
|
|
|
|
|
|
failed_paths.append(str(relative_path))
|
|
|
|
|
|
|
|
if not yaml_output:
|
|
|
|
|
|
|
|
print_result(False, error_msg, silent)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not silent and not yaml_output:
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Summary
|
|
|
|
|
|
|
|
stats = {
|
|
|
|
|
|
|
|
"Total files": total_files,
|
|
|
|
|
|
|
|
"Valid files": valid_files,
|
|
|
|
|
|
|
|
"Invalid files": invalid_files
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
success = invalid_files == 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if yaml_output:
|
|
|
|
|
|
|
|
output_yaml_results("validate", {**stats, "failed_paths": failed_paths}, success)
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
if not silent:
|
|
|
|
|
|
|
|
print_summary("Validation Summary", stats)
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
elif not success:
|
|
|
|
|
|
|
|
# In silent mode, still show errors
|
|
|
|
|
|
|
|
for failed_path in failed_paths:
|
|
|
|
|
|
|
|
print(f"❌ {failed_path}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return success, {**stats, "failed_paths": failed_paths}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_format(project_root: Path, silent: bool = False, yaml_output: bool = False) -> Tuple[bool, Dict]:
|
|
|
|
"""Run gdformat on all GDScript files."""
|
|
|
|
"""Run gdformat on all GDScript files."""
|
|
|
|
print_header("🎨 GDScript Formatter")
|
|
|
|
if not yaml_output:
|
|
|
|
|
|
|
|
print_header("🎨 GDScript Formatter", silent)
|
|
|
|
|
|
|
|
|
|
|
|
gd_files = get_gd_files(project_root)
|
|
|
|
gd_files = get_gd_files(project_root)
|
|
|
|
count_msg = f"Found {len(gd_files)} GDScript files to format."
|
|
|
|
if not silent and not yaml_output:
|
|
|
|
colored_count = Colors.colorize(count_msg, Colors.BLUE)
|
|
|
|
count_msg = f"Found {len(gd_files)} GDScript files to format."
|
|
|
|
print(f"{colored_count}\n")
|
|
|
|
colored_count = Colors.colorize(count_msg, Colors.BLUE)
|
|
|
|
|
|
|
|
print(f"{colored_count}\n")
|
|
|
|
|
|
|
|
|
|
|
|
formatted_files = failed_files = 0
|
|
|
|
formatted_files = failed_files = 0
|
|
|
|
failed_paths = []
|
|
|
|
failed_paths = []
|
|
|
|
|
|
|
|
|
|
|
|
for gd_file in gd_files:
|
|
|
|
for gd_file in gd_files:
|
|
|
|
relative_path = gd_file.relative_to(project_root)
|
|
|
|
relative_path = gd_file.relative_to(project_root)
|
|
|
|
file_msg = f"🎯 Formatting: {relative_path.name}"
|
|
|
|
if not silent and not yaml_output:
|
|
|
|
colored_file = Colors.colorize(file_msg, Colors.CYAN)
|
|
|
|
file_msg = f"🎯 Formatting: {relative_path.name}"
|
|
|
|
print(colored_file)
|
|
|
|
colored_file = Colors.colorize(file_msg, Colors.CYAN)
|
|
|
|
|
|
|
|
print(colored_file)
|
|
|
|
|
|
|
|
|
|
|
|
if should_skip_file(gd_file):
|
|
|
|
if should_skip_file(gd_file):
|
|
|
|
print_skip_message("gdformat")
|
|
|
|
if not silent and not yaml_output:
|
|
|
|
|
|
|
|
print_skip_message("gdformat")
|
|
|
|
formatted_files += 1
|
|
|
|
formatted_files += 1
|
|
|
|
print()
|
|
|
|
if not silent and not yaml_output:
|
|
|
|
|
|
|
|
print()
|
|
|
|
continue
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
result = run_command(["gdformat", str(gd_file)], project_root)
|
|
|
|
result = run_command(["gdformat", str(gd_file)], project_root)
|
|
|
|
|
|
|
|
|
|
|
|
if result.returncode == 0:
|
|
|
|
if result.returncode == 0:
|
|
|
|
success_msg = "✅ Success"
|
|
|
|
if not silent and not yaml_output:
|
|
|
|
colored_success = Colors.colorize(success_msg, Colors.GREEN)
|
|
|
|
success_msg = "✅ Success"
|
|
|
|
print(f" {colored_success}")
|
|
|
|
colored_success = Colors.colorize(success_msg, Colors.GREEN)
|
|
|
|
|
|
|
|
print(f" {colored_success}")
|
|
|
|
formatted_files += 1
|
|
|
|
formatted_files += 1
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
fail_msg = f"❌ FAILED: {relative_path}"
|
|
|
|
if not silent and not yaml_output:
|
|
|
|
colored_fail = Colors.colorize(fail_msg, Colors.RED)
|
|
|
|
fail_msg = f"❌ FAILED: {relative_path}"
|
|
|
|
print(f" {colored_fail}")
|
|
|
|
colored_fail = Colors.colorize(fail_msg, Colors.RED)
|
|
|
|
output = (result.stdout + result.stderr).strip()
|
|
|
|
print(f" {colored_fail}")
|
|
|
|
if output:
|
|
|
|
output = (result.stdout + result.stderr).strip()
|
|
|
|
colored_output = Colors.colorize(output, Colors.RED)
|
|
|
|
if output:
|
|
|
|
print(f" {colored_output}")
|
|
|
|
colored_output = Colors.colorize(output, Colors.RED)
|
|
|
|
|
|
|
|
print(f" {colored_output}")
|
|
|
|
failed_files += 1
|
|
|
|
failed_files += 1
|
|
|
|
failed_paths.append(str(relative_path))
|
|
|
|
failed_paths.append(str(relative_path))
|
|
|
|
|
|
|
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
except FileNotFoundError:
|
|
|
|
print(" ❌ ERROR: gdformat not found")
|
|
|
|
if not silent and not yaml_output:
|
|
|
|
|
|
|
|
print(" ❌ ERROR: gdformat not found")
|
|
|
|
return False, {}
|
|
|
|
return False, {}
|
|
|
|
except Exception as e:
|
|
|
|
except Exception as e:
|
|
|
|
print(f" ❌ ERROR: {e}")
|
|
|
|
if not silent and not yaml_output:
|
|
|
|
|
|
|
|
print(f" ❌ ERROR: {e}")
|
|
|
|
failed_files += 1
|
|
|
|
failed_files += 1
|
|
|
|
failed_paths.append(str(relative_path))
|
|
|
|
failed_paths.append(str(relative_path))
|
|
|
|
|
|
|
|
|
|
|
|
print()
|
|
|
|
if not silent and not yaml_output:
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
|
|
# Summary
|
|
|
|
# Summary
|
|
|
|
stats = {
|
|
|
|
stats = {
|
|
|
|
@@ -340,18 +631,27 @@ def run_format(project_root: Path) -> Tuple[bool, Dict]:
|
|
|
|
"Successfully formatted": formatted_files,
|
|
|
|
"Successfully formatted": formatted_files,
|
|
|
|
"Failed": failed_files
|
|
|
|
"Failed": failed_files
|
|
|
|
}
|
|
|
|
}
|
|
|
|
print_summary("Formatting Summary", stats)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
success = failed_files == 0
|
|
|
|
success = failed_files == 0
|
|
|
|
print()
|
|
|
|
|
|
|
|
if not success:
|
|
|
|
if yaml_output:
|
|
|
|
msg = "⚠️ WARNING: Some files failed to format"
|
|
|
|
output_yaml_results("format", {**stats, "failed_paths": failed_paths}, success)
|
|
|
|
colored_msg = Colors.colorize(msg, Colors.YELLOW + Colors.BOLD)
|
|
|
|
|
|
|
|
print(colored_msg)
|
|
|
|
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
msg = "✅ All GDScript files formatted successfully!"
|
|
|
|
print_summary("Formatting Summary", stats, silent)
|
|
|
|
colored_msg = Colors.colorize(msg, Colors.GREEN + Colors.BOLD)
|
|
|
|
if not silent:
|
|
|
|
print(colored_msg)
|
|
|
|
print()
|
|
|
|
|
|
|
|
if not success:
|
|
|
|
|
|
|
|
msg = "⚠️ WARNING: Some files failed to format"
|
|
|
|
|
|
|
|
colored_msg = Colors.colorize(msg, Colors.YELLOW + Colors.BOLD)
|
|
|
|
|
|
|
|
print(colored_msg)
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
msg = "✅ All GDScript files formatted successfully!"
|
|
|
|
|
|
|
|
colored_msg = Colors.colorize(msg, Colors.GREEN + Colors.BOLD)
|
|
|
|
|
|
|
|
print(colored_msg)
|
|
|
|
|
|
|
|
elif not success:
|
|
|
|
|
|
|
|
# In silent mode, still show failed files
|
|
|
|
|
|
|
|
for failed_path in failed_paths:
|
|
|
|
|
|
|
|
print(f"❌ {failed_path}")
|
|
|
|
|
|
|
|
|
|
|
|
return success, {**stats, "failed_paths": failed_paths}
|
|
|
|
return success, {**stats, "failed_paths": failed_paths}
|
|
|
|
|
|
|
|
|
|
|
|
@@ -374,29 +674,31 @@ def discover_test_files(project_root: Path) -> List[Tuple[Path, str]]:
|
|
|
|
return test_files
|
|
|
|
return test_files
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_tests(project_root: Path) -> Tuple[bool, Dict]:
|
|
|
|
def run_tests(project_root: Path, silent: bool = False, yaml_output: bool = False) -> Tuple[bool, Dict]:
|
|
|
|
"""Run Godot tests."""
|
|
|
|
"""Run Godot tests."""
|
|
|
|
print_header("🧪 GDScript Test Runner")
|
|
|
|
if not yaml_output:
|
|
|
|
|
|
|
|
print_header("🧪 GDScript Test Runner", silent)
|
|
|
|
|
|
|
|
|
|
|
|
test_files = discover_test_files(project_root)
|
|
|
|
test_files = discover_test_files(project_root)
|
|
|
|
|
|
|
|
|
|
|
|
scan_msg = "🔍 Scanning for test files in tests\\ directory..."
|
|
|
|
if not silent and not yaml_output:
|
|
|
|
colored_scan = Colors.colorize(scan_msg, Colors.BLUE)
|
|
|
|
scan_msg = "🔍 Scanning for test files in tests\\ directory..."
|
|
|
|
print(colored_scan)
|
|
|
|
colored_scan = Colors.colorize(scan_msg, Colors.BLUE)
|
|
|
|
|
|
|
|
print(colored_scan)
|
|
|
|
|
|
|
|
|
|
|
|
discover_msg = "\n📋 Discovered test files:"
|
|
|
|
discover_msg = "\n📋 Discovered test files:"
|
|
|
|
colored_discover = Colors.colorize(discover_msg, Colors.CYAN)
|
|
|
|
colored_discover = Colors.colorize(discover_msg, Colors.CYAN)
|
|
|
|
print(colored_discover)
|
|
|
|
print(colored_discover)
|
|
|
|
|
|
|
|
|
|
|
|
for test_file, prefix in test_files:
|
|
|
|
for test_file, prefix in test_files:
|
|
|
|
test_name = format_test_name(test_file.stem)
|
|
|
|
test_name = format_test_name(test_file.stem)
|
|
|
|
file_info = f" {prefix}{test_name}: {test_file}"
|
|
|
|
file_info = f" {prefix}{test_name}: {test_file}"
|
|
|
|
colored_file_info = Colors.colorize(file_info, Colors.MAGENTA)
|
|
|
|
colored_file_info = Colors.colorize(file_info, Colors.MAGENTA)
|
|
|
|
print(colored_file_info)
|
|
|
|
print(colored_file_info)
|
|
|
|
|
|
|
|
|
|
|
|
start_msg = "\n🚀 Starting test execution...\n"
|
|
|
|
start_msg = "\n🚀 Starting test execution...\n"
|
|
|
|
colored_start = Colors.colorize(start_msg, Colors.BLUE + Colors.BOLD)
|
|
|
|
colored_start = Colors.colorize(start_msg, Colors.BLUE + Colors.BOLD)
|
|
|
|
print(colored_start)
|
|
|
|
print(colored_start)
|
|
|
|
|
|
|
|
|
|
|
|
total_tests = failed_tests = 0
|
|
|
|
total_tests = failed_tests = 0
|
|
|
|
test_results = []
|
|
|
|
test_results = []
|
|
|
|
@@ -405,13 +707,14 @@ def run_tests(project_root: Path) -> Tuple[bool, Dict]:
|
|
|
|
test_name = format_test_name(test_file.stem)
|
|
|
|
test_name = format_test_name(test_file.stem)
|
|
|
|
full_test_name = f"{prefix}{test_name}"
|
|
|
|
full_test_name = f"{prefix}{test_name}"
|
|
|
|
|
|
|
|
|
|
|
|
header_msg = f"=== {full_test_name} ==="
|
|
|
|
if not silent and not yaml_output:
|
|
|
|
colored_header = Colors.colorize(header_msg, Colors.CYAN + Colors.BOLD)
|
|
|
|
header_msg = f"=== {full_test_name} ==="
|
|
|
|
print(f"\n{colored_header}")
|
|
|
|
colored_header = Colors.colorize(header_msg, Colors.CYAN + Colors.BOLD)
|
|
|
|
|
|
|
|
print(f"\n{colored_header}")
|
|
|
|
|
|
|
|
|
|
|
|
running_msg = f"🎯 Running: {test_file}"
|
|
|
|
running_msg = f"🎯 Running: {test_file}"
|
|
|
|
colored_running = Colors.colorize(running_msg, Colors.BLUE)
|
|
|
|
colored_running = Colors.colorize(running_msg, Colors.BLUE)
|
|
|
|
print(colored_running)
|
|
|
|
print(colored_running)
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
result = run_command(
|
|
|
|
result = run_command(
|
|
|
|
@@ -421,14 +724,18 @@ def run_tests(project_root: Path) -> Tuple[bool, Dict]:
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if result.returncode == 0:
|
|
|
|
if result.returncode == 0:
|
|
|
|
pass_msg = f"✅ PASSED: {full_test_name}"
|
|
|
|
if not silent and not yaml_output:
|
|
|
|
colored_pass = Colors.colorize(pass_msg, Colors.GREEN + Colors.BOLD)
|
|
|
|
pass_msg = f"✅ PASSED: {full_test_name}"
|
|
|
|
print(colored_pass)
|
|
|
|
colored_pass = Colors.colorize(pass_msg, Colors.GREEN + Colors.BOLD)
|
|
|
|
|
|
|
|
print(colored_pass)
|
|
|
|
test_results.append((full_test_name, True, ""))
|
|
|
|
test_results.append((full_test_name, True, ""))
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
fail_msg = f"❌ FAILED: {full_test_name}"
|
|
|
|
if not silent and not yaml_output:
|
|
|
|
colored_fail = Colors.colorize(fail_msg, Colors.RED + Colors.BOLD)
|
|
|
|
fail_msg = f"❌ FAILED: {full_test_name}"
|
|
|
|
print(colored_fail)
|
|
|
|
colored_fail = Colors.colorize(fail_msg, Colors.RED + Colors.BOLD)
|
|
|
|
|
|
|
|
print(colored_fail)
|
|
|
|
|
|
|
|
elif silent and not yaml_output:
|
|
|
|
|
|
|
|
print(f"❌ {full_test_name}")
|
|
|
|
failed_tests += 1
|
|
|
|
failed_tests += 1
|
|
|
|
error_msg = (result.stderr + result.stdout).strip() or "Unknown error"
|
|
|
|
error_msg = (result.stderr + result.stdout).strip() or "Unknown error"
|
|
|
|
test_results.append((full_test_name, False, error_msg))
|
|
|
|
test_results.append((full_test_name, False, error_msg))
|
|
|
|
@@ -436,26 +743,34 @@ def run_tests(project_root: Path) -> Tuple[bool, Dict]:
|
|
|
|
total_tests += 1
|
|
|
|
total_tests += 1
|
|
|
|
|
|
|
|
|
|
|
|
except subprocess.TimeoutExpired:
|
|
|
|
except subprocess.TimeoutExpired:
|
|
|
|
timeout_msg = f"⏰ FAILED: {full_test_name} (TIMEOUT)"
|
|
|
|
if not silent and not yaml_output:
|
|
|
|
colored_timeout = Colors.colorize(timeout_msg, Colors.RED + Colors.BOLD)
|
|
|
|
timeout_msg = f"⏰ FAILED: {full_test_name} (TIMEOUT)"
|
|
|
|
print(colored_timeout)
|
|
|
|
colored_timeout = Colors.colorize(timeout_msg, Colors.RED + Colors.BOLD)
|
|
|
|
|
|
|
|
print(colored_timeout)
|
|
|
|
|
|
|
|
elif silent and not yaml_output:
|
|
|
|
|
|
|
|
print(f"⏰ {full_test_name} (TIMEOUT)")
|
|
|
|
failed_tests += 1
|
|
|
|
failed_tests += 1
|
|
|
|
test_results.append((full_test_name, False, "Test timed out"))
|
|
|
|
test_results.append((full_test_name, False, "Test timed out"))
|
|
|
|
total_tests += 1
|
|
|
|
total_tests += 1
|
|
|
|
except FileNotFoundError:
|
|
|
|
except FileNotFoundError:
|
|
|
|
error_msg = "❌ ERROR: Godot not found"
|
|
|
|
if not yaml_output and not silent:
|
|
|
|
colored_error = Colors.colorize(error_msg, Colors.RED + Colors.BOLD)
|
|
|
|
error_msg = "❌ ERROR: Godot not found"
|
|
|
|
print(colored_error)
|
|
|
|
colored_error = Colors.colorize(error_msg, Colors.RED + Colors.BOLD)
|
|
|
|
|
|
|
|
print(colored_error)
|
|
|
|
return False, {}
|
|
|
|
return False, {}
|
|
|
|
except Exception as e:
|
|
|
|
except Exception as e:
|
|
|
|
exc_msg = f"💥 FAILED: {full_test_name} (ERROR: {e})"
|
|
|
|
if not silent and not yaml_output:
|
|
|
|
colored_exc = Colors.colorize(exc_msg, Colors.RED + Colors.BOLD)
|
|
|
|
exc_msg = f"💥 FAILED: {full_test_name} (ERROR: {e})"
|
|
|
|
print(colored_exc)
|
|
|
|
colored_exc = Colors.colorize(exc_msg, Colors.RED + Colors.BOLD)
|
|
|
|
|
|
|
|
print(colored_exc)
|
|
|
|
|
|
|
|
elif silent and not yaml_output:
|
|
|
|
|
|
|
|
print(f"💥 {full_test_name} (ERROR: {e})")
|
|
|
|
failed_tests += 1
|
|
|
|
failed_tests += 1
|
|
|
|
test_results.append((full_test_name, False, str(e)))
|
|
|
|
test_results.append((full_test_name, False, str(e)))
|
|
|
|
total_tests += 1
|
|
|
|
total_tests += 1
|
|
|
|
|
|
|
|
|
|
|
|
print()
|
|
|
|
if not silent and not yaml_output:
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
|
|
# Summary
|
|
|
|
# Summary
|
|
|
|
passed_tests = total_tests - failed_tests
|
|
|
|
passed_tests = total_tests - failed_tests
|
|
|
|
@@ -464,23 +779,28 @@ def run_tests(project_root: Path) -> Tuple[bool, Dict]:
|
|
|
|
"Tests Passed": passed_tests,
|
|
|
|
"Tests Passed": passed_tests,
|
|
|
|
"Tests Failed": failed_tests
|
|
|
|
"Tests Failed": failed_tests
|
|
|
|
}
|
|
|
|
}
|
|
|
|
print_summary("Test Execution Summary", stats)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
success = failed_tests == 0
|
|
|
|
success = failed_tests == 0
|
|
|
|
print()
|
|
|
|
|
|
|
|
if success:
|
|
|
|
if yaml_output:
|
|
|
|
msg = "🎉 ALL TESTS PASSED!"
|
|
|
|
output_yaml_results("test", {**stats, "results": test_results}, success)
|
|
|
|
colored_msg = Colors.colorize(msg, Colors.GREEN + Colors.BOLD)
|
|
|
|
|
|
|
|
print(colored_msg)
|
|
|
|
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
msg = f"💥 {failed_tests} TEST(S) FAILED"
|
|
|
|
print_summary("Test Execution Summary", stats, silent)
|
|
|
|
colored_msg = Colors.colorize(msg, Colors.RED + Colors.BOLD)
|
|
|
|
if not silent:
|
|
|
|
print(colored_msg)
|
|
|
|
print()
|
|
|
|
|
|
|
|
if success:
|
|
|
|
|
|
|
|
msg = "🎉 ALL TESTS PASSED!"
|
|
|
|
|
|
|
|
colored_msg = Colors.colorize(msg, Colors.GREEN + Colors.BOLD)
|
|
|
|
|
|
|
|
print(colored_msg)
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
msg = f"💥 {failed_tests} TEST(S) FAILED"
|
|
|
|
|
|
|
|
colored_msg = Colors.colorize(msg, Colors.RED + Colors.BOLD)
|
|
|
|
|
|
|
|
print(colored_msg)
|
|
|
|
|
|
|
|
|
|
|
|
return success, {**stats, "results": test_results}
|
|
|
|
return success, {**stats, "results": test_results}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_workflow(project_root: Path, steps: List[str]) -> bool:
|
|
|
|
def run_workflow(project_root: Path, steps: List[str], silent: bool = False, yaml_output: bool = False) -> bool:
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
Execute development workflow steps in sequence.
|
|
|
|
Execute development workflow steps in sequence.
|
|
|
|
|
|
|
|
|
|
|
|
@@ -493,107 +813,120 @@ def run_workflow(project_root: Path, steps: List[str]) -> bool:
|
|
|
|
Returns:
|
|
|
|
Returns:
|
|
|
|
bool: True if all steps completed successfully, False if any failed
|
|
|
|
bool: True if all steps completed successfully, False if any failed
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
print_header("🔄 Development Workflow Runner")
|
|
|
|
if not silent:
|
|
|
|
|
|
|
|
print_header("🔄 Development Workflow Runner")
|
|
|
|
|
|
|
|
|
|
|
|
workflow_steps = {
|
|
|
|
workflow_steps = {
|
|
|
|
"lint": ("🔍 Code linting (gdlint)", run_lint),
|
|
|
|
"lint": ("🔍 Code linting (gdlint)", lambda root: run_lint(root, silent, yaml_output)),
|
|
|
|
"format": ("🎨 Code formatting (gdformat)", run_format),
|
|
|
|
"format": ("🎨 Code formatting (gdformat)", lambda root: run_format(root, silent, yaml_output)),
|
|
|
|
"test": ("🧪 Test execution (godot tests)", run_tests)
|
|
|
|
"test": ("🧪 Test execution (godot tests)", lambda root: run_tests(root, silent, yaml_output)),
|
|
|
|
|
|
|
|
"validate": ("📋 File format validation (yaml/toml/json)", lambda root: run_validate(root, silent, yaml_output))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
intro_msg = "🚀 This script will run the development workflow:"
|
|
|
|
if not silent:
|
|
|
|
colored_intro = Colors.colorize(intro_msg, Colors.BLUE + Colors.BOLD)
|
|
|
|
intro_msg = "🚀 This script will run the development workflow:"
|
|
|
|
print(colored_intro)
|
|
|
|
colored_intro = Colors.colorize(intro_msg, Colors.BLUE + Colors.BOLD)
|
|
|
|
|
|
|
|
print(colored_intro)
|
|
|
|
|
|
|
|
|
|
|
|
for i, step in enumerate(steps, 1):
|
|
|
|
for i, step in enumerate(steps, 1):
|
|
|
|
step_name = workflow_steps[step][0]
|
|
|
|
step_name = workflow_steps[step][0]
|
|
|
|
step_msg = f"{i}. {step_name}"
|
|
|
|
step_msg = f"{i}. {step_name}"
|
|
|
|
colored_step = Colors.colorize(step_msg, Colors.CYAN)
|
|
|
|
colored_step = Colors.colorize(step_msg, Colors.CYAN)
|
|
|
|
print(colored_step)
|
|
|
|
print(colored_step)
|
|
|
|
print()
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
|
|
start_time = time.time()
|
|
|
|
start_time = time.time()
|
|
|
|
results = {}
|
|
|
|
results = {}
|
|
|
|
|
|
|
|
|
|
|
|
for step in steps:
|
|
|
|
for step in steps:
|
|
|
|
step_name, step_func = workflow_steps[step]
|
|
|
|
step_name, step_func = workflow_steps[step]
|
|
|
|
separator = Colors.colorize("-" * 48, Colors.MAGENTA)
|
|
|
|
if not silent:
|
|
|
|
print(separator)
|
|
|
|
separator = Colors.colorize("-" * 48, Colors.MAGENTA)
|
|
|
|
|
|
|
|
print(separator)
|
|
|
|
|
|
|
|
|
|
|
|
running_msg = f"⚡ Running {step_name}"
|
|
|
|
running_msg = f"⚡ Running {step_name}"
|
|
|
|
colored_running = Colors.colorize(running_msg, Colors.BLUE + Colors.BOLD)
|
|
|
|
colored_running = Colors.colorize(running_msg, Colors.BLUE + Colors.BOLD)
|
|
|
|
print(colored_running)
|
|
|
|
print(colored_running)
|
|
|
|
print(separator)
|
|
|
|
print(separator)
|
|
|
|
|
|
|
|
|
|
|
|
success, step_results = step_func(project_root)
|
|
|
|
success, step_results = step_func(project_root)
|
|
|
|
results[step] = step_results
|
|
|
|
results[step] = step_results
|
|
|
|
|
|
|
|
|
|
|
|
if not success:
|
|
|
|
if not silent:
|
|
|
|
fail_msg = f"❌ {step.upper()} FAILED - Continuing with remaining steps"
|
|
|
|
if not success:
|
|
|
|
colored_fail = Colors.colorize(fail_msg, Colors.RED + Colors.BOLD)
|
|
|
|
fail_msg = f"❌ {step.upper()} FAILED - Continuing with remaining steps"
|
|
|
|
print(f"\n{colored_fail}")
|
|
|
|
colored_fail = Colors.colorize(fail_msg, Colors.RED + Colors.BOLD)
|
|
|
|
|
|
|
|
print(f"\n{colored_fail}")
|
|
|
|
|
|
|
|
|
|
|
|
warning_msg = "⚠️ Issues found, but continuing to provide complete feedback"
|
|
|
|
warning_msg = "⚠️ Issues found, but continuing to provide complete feedback"
|
|
|
|
colored_warning = Colors.colorize(warning_msg, Colors.YELLOW)
|
|
|
|
colored_warning = Colors.colorize(warning_msg, Colors.YELLOW)
|
|
|
|
print(colored_warning)
|
|
|
|
print(colored_warning)
|
|
|
|
|
|
|
|
|
|
|
|
status_msg = f"✅ {step_name} completed {'successfully' if success else 'with issues'}"
|
|
|
|
status_msg = f"✅ {step_name} completed {'successfully' if success else 'with issues'}"
|
|
|
|
colored_status = Colors.colorize(status_msg, Colors.GREEN if success else Colors.YELLOW)
|
|
|
|
colored_status = Colors.colorize(status_msg, Colors.GREEN if success else Colors.YELLOW)
|
|
|
|
print(colored_status)
|
|
|
|
print(colored_status)
|
|
|
|
print()
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
|
|
# Final summary
|
|
|
|
# Final summary
|
|
|
|
elapsed_time = time.time() - start_time
|
|
|
|
elapsed_time = time.time() - start_time
|
|
|
|
print_header("📊 Workflow Summary")
|
|
|
|
|
|
|
|
|
|
|
|
if not silent:
|
|
|
|
|
|
|
|
print_header("📊 Workflow Summary")
|
|
|
|
|
|
|
|
|
|
|
|
all_success = True
|
|
|
|
all_success = True
|
|
|
|
any_failures = False
|
|
|
|
any_failures = False
|
|
|
|
for step in steps:
|
|
|
|
for step in steps:
|
|
|
|
step_success = results[step].get("Tests Failed", results[step].get("Failed", 0)) == 0
|
|
|
|
step_success = results[step].get("Tests Failed", results[step].get("Failed", 0)) == 0
|
|
|
|
status_emoji = "✅" if step_success else "❌"
|
|
|
|
if not silent:
|
|
|
|
status_text = "PASSED" if step_success else "FAILED"
|
|
|
|
status_emoji = "✅" if step_success else "❌"
|
|
|
|
status_color = Colors.GREEN if step_success else Colors.RED
|
|
|
|
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)
|
|
|
|
colored_status = Colors.colorize(f"{status_text}", status_color + Colors.BOLD)
|
|
|
|
print(f"{step_emoji} {step.capitalize()}: {status_emoji} {colored_status}")
|
|
|
|
print(f"{step_emoji} {step.capitalize()}: {status_emoji} {colored_status}")
|
|
|
|
|
|
|
|
|
|
|
|
if not step_success:
|
|
|
|
if not step_success:
|
|
|
|
any_failures = True
|
|
|
|
any_failures = True
|
|
|
|
all_success = False
|
|
|
|
all_success = False
|
|
|
|
|
|
|
|
|
|
|
|
print()
|
|
|
|
if not silent:
|
|
|
|
if all_success:
|
|
|
|
print()
|
|
|
|
success_msg = "🎉 ALL WORKFLOW STEPS COMPLETED SUCCESSFULLY!"
|
|
|
|
if all_success:
|
|
|
|
colored_success = Colors.colorize(success_msg, Colors.GREEN + Colors.BOLD)
|
|
|
|
success_msg = "🎉 ALL WORKFLOW STEPS COMPLETED SUCCESSFULLY!"
|
|
|
|
print(colored_success)
|
|
|
|
colored_success = Colors.colorize(success_msg, Colors.GREEN + Colors.BOLD)
|
|
|
|
|
|
|
|
print(colored_success)
|
|
|
|
|
|
|
|
|
|
|
|
commit_msg = "🚀 Your code is ready for commit."
|
|
|
|
commit_msg = "🚀 Your code is ready for commit."
|
|
|
|
colored_commit = Colors.colorize(commit_msg, Colors.CYAN)
|
|
|
|
colored_commit = Colors.colorize(commit_msg, Colors.CYAN)
|
|
|
|
print(colored_commit)
|
|
|
|
print(colored_commit)
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
fail_msg = "❌ WORKFLOW COMPLETED WITH FAILURES"
|
|
|
|
fail_msg = "❌ WORKFLOW COMPLETED WITH FAILURES"
|
|
|
|
colored_fail = Colors.colorize(fail_msg, Colors.RED + Colors.BOLD)
|
|
|
|
colored_fail = Colors.colorize(fail_msg, Colors.RED + Colors.BOLD)
|
|
|
|
print(colored_fail)
|
|
|
|
print(colored_fail)
|
|
|
|
|
|
|
|
|
|
|
|
review_msg = "🔧 Please fix the issues above before committing."
|
|
|
|
review_msg = "🔧 Please fix the issues above before committing."
|
|
|
|
colored_review = Colors.colorize(review_msg, Colors.RED)
|
|
|
|
colored_review = Colors.colorize(review_msg, Colors.RED)
|
|
|
|
print(colored_review)
|
|
|
|
print(colored_review)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
time_msg = f"⏱️ Elapsed time: {elapsed_time:.1f} seconds"
|
|
|
|
|
|
|
|
colored_time = Colors.colorize(time_msg, Colors.MAGENTA)
|
|
|
|
|
|
|
|
print(f"\n{colored_time}")
|
|
|
|
|
|
|
|
|
|
|
|
time_msg = f"⏱️ Elapsed time: {elapsed_time:.1f} seconds"
|
|
|
|
|
|
|
|
colored_time = Colors.colorize(time_msg, Colors.MAGENTA)
|
|
|
|
|
|
|
|
print(f"\n{colored_time}")
|
|
|
|
|
|
|
|
return all_success
|
|
|
|
return all_success
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
def main():
|
|
|
|
"""Main entry point."""
|
|
|
|
"""Main entry point."""
|
|
|
|
parser = argparse.ArgumentParser(description="Run development workflow for Skelly Godot project")
|
|
|
|
parser = argparse.ArgumentParser(description="Run development workflow for Skelly Godot project")
|
|
|
|
parser.add_argument("--steps", nargs="+", choices=["lint", "format", "test"],
|
|
|
|
parser.add_argument("--steps", nargs="+", choices=["lint", "format", "test", "validate"],
|
|
|
|
default=["format", "lint", "test"], help="Workflow steps to run")
|
|
|
|
default=["format", "lint", "test", "validate"], help="Workflow steps to run")
|
|
|
|
parser.add_argument("--lint", action="store_true", help="Run linting")
|
|
|
|
parser.add_argument("--lint", action="store_true", help="Run linting")
|
|
|
|
parser.add_argument("--format", action="store_true", help="Run formatting")
|
|
|
|
parser.add_argument("--format", action="store_true", help="Run formatting")
|
|
|
|
parser.add_argument("--test", action="store_true", help="Run tests")
|
|
|
|
parser.add_argument("--test", action="store_true", help="Run tests")
|
|
|
|
|
|
|
|
parser.add_argument("--validate", action="store_true", help="Run file format validation")
|
|
|
|
|
|
|
|
parser.add_argument("--silent", "-s", action="store_true", help="Silent mode - hide success messages, only show errors")
|
|
|
|
|
|
|
|
parser.add_argument("--yaml", action="store_true", help="Output results in machine-readable YAML format")
|
|
|
|
|
|
|
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
args = parser.parse_args()
|
|
|
|
project_root = Path(__file__).parent.parent
|
|
|
|
project_root = Path(__file__).parent.parent
|
|
|
|
@@ -605,15 +938,22 @@ def main():
|
|
|
|
steps = ["format"]
|
|
|
|
steps = ["format"]
|
|
|
|
elif args.test:
|
|
|
|
elif args.test:
|
|
|
|
steps = ["test"]
|
|
|
|
steps = ["test"]
|
|
|
|
|
|
|
|
elif args.validate:
|
|
|
|
|
|
|
|
steps = ["validate"]
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
steps = args.steps
|
|
|
|
steps = args.steps
|
|
|
|
|
|
|
|
|
|
|
|
# Run workflow or individual step
|
|
|
|
# Run workflow or individual step
|
|
|
|
if len(steps) == 1:
|
|
|
|
if len(steps) == 1:
|
|
|
|
step_funcs = {"lint": run_lint, "format": run_format, "test": run_tests}
|
|
|
|
step_funcs = {
|
|
|
|
|
|
|
|
"lint": lambda root: run_lint(root, args.silent, args.yaml),
|
|
|
|
|
|
|
|
"format": lambda root: run_format(root, args.silent, args.yaml),
|
|
|
|
|
|
|
|
"test": lambda root: run_tests(root, args.silent, args.yaml),
|
|
|
|
|
|
|
|
"validate": lambda root: run_validate(root, args.silent, args.yaml)
|
|
|
|
|
|
|
|
}
|
|
|
|
success, _ = step_funcs[steps[0]](project_root)
|
|
|
|
success, _ = step_funcs[steps[0]](project_root)
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
success = run_workflow(project_root, steps)
|
|
|
|
success = run_workflow(project_root, steps, args.silent, args.yaml)
|
|
|
|
|
|
|
|
|
|
|
|
sys.exit(0 if success else 1)
|
|
|
|
sys.exit(0 if success else 1)
|
|
|
|
|
|
|
|
|
|
|
|
|