lint fixes
Some checks failed
Continuous Integration / Code Formatting (pull_request) Successful in 27s
Continuous Integration / Code Quality Check (pull_request) Successful in 29s
Continuous Integration / Test Execution (pull_request) Failing after 33s
Continuous Integration / CI Summary (pull_request) Failing after 5s

This commit is contained in:
2025-09-28 19:16:20 +04:00
parent c1f3f9f708
commit eb99c6a18e
46 changed files with 2608 additions and 1304 deletions

622
tools/run_development.py Normal file
View File

@@ -0,0 +1,622 @@
#!/usr/bin/env python3
"""
Development workflow runner for the Skelly Godot project.
Runs code quality checks (linting, formatting, testing) 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
NOTE: Handles "successful but noisy" linter output such as
"Success: no problems found" - treats these as clean instead of warnings.
"""
import argparse
import os
import re
import subprocess
import sys
import time
import warnings
from pathlib import Path
from typing import Dict, List, Tuple
# Suppress pkg_resources deprecation warning from gdtoolkit
warnings.filterwarnings("ignore", message="pkg_resources is deprecated", category=UserWarning)
class Colors:
"""ANSI color codes for terminal output."""
# Basic colors
RED = '\033[91m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
BLUE = '\033[94m'
MAGENTA = '\033[95m'
CYAN = '\033[96m'
WHITE = '\033[97m'
# Styles
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
# Reset
RESET = '\033[0m'
@staticmethod
def colorize(text: str, color: str) -> str:
"""Add color to text if terminal supports it."""
if os.getenv('NO_COLOR') or not sys.stdout.isatty():
return text
return f"{color}{text}{Colors.RESET}"
def print_header(title: str) -> None:
"""Print a formatted header."""
separator = Colors.colorize("=" * 48, Colors.CYAN)
colored_title = Colors.colorize(title, Colors.BOLD + Colors.WHITE)
print(separator)
print(colored_title)
print(separator)
print()
def print_summary(title: str, stats: Dict[str, int]) -> None:
"""Print a formatted summary."""
separator = Colors.colorize("=" * 48, Colors.CYAN)
colored_title = Colors.colorize(title, Colors.BOLD + Colors.WHITE)
print(separator)
print(colored_title)
print(separator)
for key, value in stats.items():
colored_key = Colors.colorize(key, Colors.BLUE)
colored_value = Colors.colorize(str(value), Colors.BOLD + Colors.WHITE)
print(f"{colored_key}: {colored_value}")
def run_command(cmd: List[str], cwd: Path, timeout: int = 30) -> subprocess.CompletedProcess:
"""
Execute a shell command with error handling and output filtering.
Filters out gdtoolkit's pkg_resources deprecation warnings.
Args:
cmd: Command and arguments to execute
cwd: Working directory for command execution
timeout: Maximum execution time in seconds (default: 30s)
Returns:
CompletedProcess with filtered stdout/stderr
"""
# Suppress pkg_resources deprecation warnings in subprocesses
env = os.environ.copy()
env['PYTHONWARNINGS'] = 'ignore::UserWarning:pkg_resources'
result = subprocess.run(cmd, capture_output=True, text=True, cwd=cwd, timeout=timeout, env=env)
# Filter out pkg_resources deprecation warnings from the output
def filter_warnings(text: str) -> str:
if not text:
return text
lines = text.split('\n')
filtered_lines = []
skip_next = False
for line in lines:
if skip_next:
skip_next = False
continue
if 'pkg_resources is deprecated' in line:
skip_next = True # Skip the next line which contains "import pkg_resources"
continue
if 'import pkg_resources' in line:
continue
filtered_lines.append(line)
return '\n'.join(filtered_lines)
# Create a new result with filtered output
result.stdout = filter_warnings(result.stdout)
result.stderr = filter_warnings(result.stderr)
return result
def should_skip_file(file_path: Path) -> bool:
"""Check if file should be skipped."""
return file_path.name == "TestHelper.gd"
def print_skip_message(tool: str) -> None:
"""Print skip message for TestHelper.gd."""
message = f"⚠️ Skipped (static var syntax not supported by {tool})"
colored_message = Colors.colorize(message, Colors.YELLOW)
print(f" {colored_message}")
def print_result(success: bool, output: str = "") -> None:
"""Print command result."""
if success:
if not output:
message = "✅ Clean"
colored_message = Colors.colorize(message, Colors.GREEN)
else:
message = "⚠️ WARNINGS found:"
colored_message = Colors.colorize(message, Colors.YELLOW)
print(f" {colored_message}")
if output:
# Indent and color the output
for line in output.split('\n'):
if line.strip():
colored_line = Colors.colorize(line, Colors.YELLOW)
print(f" {colored_line}")
else:
message = "❌ ERRORS found:"
colored_message = Colors.colorize(message, Colors.RED)
print(f" {colored_message}")
if output:
# Indent and color the output
for line in output.split('\n'):
if line.strip():
colored_line = Colors.colorize(line, Colors.RED)
print(f" {colored_line}")
def get_gd_files(project_root: Path) -> List[Path]:
"""Get all .gd files in the project."""
return list(project_root.rglob("*.gd"))
def format_test_name(filename: str) -> str:
"""Convert test_filename to readable test name."""
return filename.replace("test_", "").replace("_", " ")
def _is_successful_linter_output(output: str) -> bool:
"""Interpret noisy but-successful linter output as clean.
Some linters print friendly messages even when they exit with 0. Treat
those as clean rather than "warnings". This function centralizes the
heuristics.
"""
if not output or not output.strip():
return True
# Common success patterns to treat as clean. Case-insensitive.
success_patterns = [
r"no problems found",
r"0 problems",
r"no issues found",
r"success: no problems found",
r"all good",
r"ok$",
]
text = output.lower()
for pat in success_patterns:
if re.search(pat, text):
return True
return False
def run_lint(project_root: Path) -> Tuple[bool, Dict]:
"""Run gdlint on all GDScript files."""
print_header("🔍 GDScript Linter")
gd_files = get_gd_files(project_root)
count_msg = f"Found {len(gd_files)} GDScript files to lint."
colored_count = Colors.colorize(count_msg, Colors.BLUE)
print(f"{colored_count}\n")
clean_files = warning_files = error_files = 0
failed_paths = []
for gd_file in gd_files:
relative_path = gd_file.relative_to(project_root)
file_msg = f"📄 Linting: {relative_path.name}"
colored_file = Colors.colorize(file_msg, Colors.CYAN)
print(colored_file)
if should_skip_file(gd_file):
print_skip_message("gdlint")
clean_files += 1
print()
continue
try:
result = run_command(["gdlint", str(gd_file)], project_root)
output = (result.stdout + result.stderr).strip()
if result.returncode == 0:
# If output is "no problems" (or similar), treat as clean.
if _is_successful_linter_output(output):
clean_files += 1
print_result(True, "")
else:
warning_files += 1
print_result(True, output)
else:
error_files += 1
failed_paths.append(str(relative_path))
print_result(False, output)
except FileNotFoundError:
print(" ❌ ERROR: gdlint not found")
return False, {}
except Exception as e:
print(f" ❌ ERROR: {e}")
error_files += 1
failed_paths.append(str(relative_path))
print()
# Summary
stats = {
"Total files": len(gd_files),
"Clean files": clean_files,
"Files with warnings": warning_files,
"Files with errors": error_files
}
print_summary("Linting Summary", stats)
success = error_files == 0
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)
return success, {**stats, "failed_paths": failed_paths}
# rest of file unchanged (format, tests, workflow runner) -- copied verbatim
def run_format(project_root: Path) -> Tuple[bool, Dict]:
"""Run gdformat on all GDScript files."""
print_header("🎨 GDScript Formatter")
gd_files = get_gd_files(project_root)
count_msg = f"Found {len(gd_files)} GDScript files to format."
colored_count = Colors.colorize(count_msg, Colors.BLUE)
print(f"{colored_count}\n")
formatted_files = failed_files = 0
failed_paths = []
for gd_file in gd_files:
relative_path = gd_file.relative_to(project_root)
file_msg = f"🎯 Formatting: {relative_path.name}"
colored_file = Colors.colorize(file_msg, Colors.CYAN)
print(colored_file)
if should_skip_file(gd_file):
print_skip_message("gdformat")
formatted_files += 1
print()
continue
try:
result = run_command(["gdformat", str(gd_file)], project_root)
if result.returncode == 0:
success_msg = "✅ Success"
colored_success = Colors.colorize(success_msg, Colors.GREEN)
print(f" {colored_success}")
formatted_files += 1
else:
fail_msg = f"❌ FAILED: {relative_path}"
colored_fail = Colors.colorize(fail_msg, Colors.RED)
print(f" {colored_fail}")
output = (result.stdout + result.stderr).strip()
if output:
colored_output = Colors.colorize(output, Colors.RED)
print(f" {colored_output}")
failed_files += 1
failed_paths.append(str(relative_path))
except FileNotFoundError:
print(" ❌ ERROR: gdformat not found")
return False, {}
except Exception as e:
print(f" ❌ ERROR: {e}")
failed_files += 1
failed_paths.append(str(relative_path))
print()
# Summary
stats = {
"Total files": len(gd_files),
"Successfully formatted": formatted_files,
"Failed": failed_files
}
print_summary("Formatting Summary", stats)
success = failed_files == 0
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)
return success, {**stats, "failed_paths": failed_paths}
def discover_test_files(project_root: Path) -> List[Tuple[Path, str]]:
"""Discover all test files with their prefixes."""
test_dirs = [
("tests", ""),
("tests/unit", "Unit: "),
("tests/integration", "Integration: ")
]
test_files = []
for test_dir, prefix in test_dirs:
test_path = project_root / test_dir
if test_path.exists():
for test_file in test_path.glob("test_*.gd"):
test_files.append((test_file, prefix))
return test_files
def run_tests(project_root: Path) -> Tuple[bool, Dict]:
"""Run Godot tests."""
print_header("🧪 GDScript Test Runner")
test_files = discover_test_files(project_root)
scan_msg = "🔍 Scanning for test files in tests\\ directory..."
colored_scan = Colors.colorize(scan_msg, Colors.BLUE)
print(colored_scan)
discover_msg = "\n📋 Discovered test files:"
colored_discover = Colors.colorize(discover_msg, Colors.CYAN)
print(colored_discover)
for test_file, prefix in test_files:
test_name = format_test_name(test_file.stem)
file_info = f" {prefix}{test_name}: {test_file}"
colored_file_info = Colors.colorize(file_info, Colors.MAGENTA)
print(colored_file_info)
start_msg = "\n🚀 Starting test execution...\n"
colored_start = Colors.colorize(start_msg, Colors.BLUE + Colors.BOLD)
print(colored_start)
total_tests = failed_tests = 0
test_results = []
for test_file, prefix in test_files:
test_name = format_test_name(test_file.stem)
full_test_name = f"{prefix}{test_name}"
header_msg = f"=== {full_test_name} ==="
colored_header = Colors.colorize(header_msg, Colors.CYAN + Colors.BOLD)
print(f"\n{colored_header}")
running_msg = f"🎯 Running: {test_file}"
colored_running = Colors.colorize(running_msg, Colors.BLUE)
print(colored_running)
try:
result = run_command(
["godot", "--headless", "--script", str(test_file)],
project_root,
timeout=60
)
if result.returncode == 0:
pass_msg = f"✅ PASSED: {full_test_name}"
colored_pass = Colors.colorize(pass_msg, Colors.GREEN + Colors.BOLD)
print(colored_pass)
test_results.append((full_test_name, True, ""))
else:
fail_msg = f"❌ FAILED: {full_test_name}"
colored_fail = Colors.colorize(fail_msg, Colors.RED + Colors.BOLD)
print(colored_fail)
failed_tests += 1
error_msg = (result.stderr + result.stdout).strip() or "Unknown error"
test_results.append((full_test_name, False, error_msg))
total_tests += 1
except subprocess.TimeoutExpired:
timeout_msg = f"⏰ FAILED: {full_test_name} (TIMEOUT)"
colored_timeout = Colors.colorize(timeout_msg, Colors.RED + Colors.BOLD)
print(colored_timeout)
failed_tests += 1
test_results.append((full_test_name, False, "Test timed out"))
total_tests += 1
except FileNotFoundError:
error_msg = "❌ ERROR: Godot not found"
colored_error = Colors.colorize(error_msg, Colors.RED + Colors.BOLD)
print(colored_error)
return False, {}
except Exception as e:
exc_msg = f"💥 FAILED: {full_test_name} (ERROR: {e})"
colored_exc = Colors.colorize(exc_msg, Colors.RED + Colors.BOLD)
print(colored_exc)
failed_tests += 1
test_results.append((full_test_name, False, str(e)))
total_tests += 1
print()
# Summary
passed_tests = total_tests - failed_tests
stats = {
"Total Tests Run": total_tests,
"Tests Passed": passed_tests,
"Tests Failed": failed_tests
}
print_summary("Test Execution Summary", stats)
success = failed_tests == 0
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}
def run_workflow(project_root: Path, steps: List[str]) -> bool:
"""
Execute development workflow steps in sequence.
Runs format, lint, and test steps. Continues executing all steps even if some fail.
Args:
project_root: Path to the project root directory
steps: List of workflow steps to execute ('format', 'lint', 'test')
Returns:
bool: True if all steps completed successfully, False if any failed
"""
print_header("🔄 Development Workflow Runner")
workflow_steps = {
"lint": ("🔍 Code linting (gdlint)", run_lint),
"format": ("🎨 Code formatting (gdformat)", run_format),
"test": ("🧪 Test execution (godot tests)", run_tests)
}
intro_msg = "🚀 This script will run the development workflow:"
colored_intro = Colors.colorize(intro_msg, Colors.BLUE + Colors.BOLD)
print(colored_intro)
for i, step in enumerate(steps, 1):
step_name = workflow_steps[step][0]
step_msg = f"{i}. {step_name}"
colored_step = Colors.colorize(step_msg, Colors.CYAN)
print(colored_step)
print()
start_time = time.time()
results = {}
for step in steps:
step_name, step_func = workflow_steps[step]
separator = Colors.colorize("-" * 48, Colors.MAGENTA)
print(separator)
running_msg = f"⚡ Running {step_name}"
colored_running = Colors.colorize(running_msg, Colors.BLUE + Colors.BOLD)
print(colored_running)
print(separator)
success, step_results = step_func(project_root)
results[step] = step_results
if not success:
fail_msg = f"{step.upper()} FAILED - Continuing with remaining steps"
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"
colored_warning = Colors.colorize(warning_msg, Colors.YELLOW)
print(colored_warning)
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)
print(colored_status)
print()
# Final summary
elapsed_time = time.time() - start_time
print_header("📊 Workflow Summary")
all_success = True
any_failures = False
for step in steps:
step_success = results[step].get("Tests Failed", results[step].get("Failed", 0)) == 0
status_emoji = "" if step_success else ""
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, "📋")
colored_status = Colors.colorize(f"{status_text}", status_color + Colors.BOLD)
print(f"{step_emoji} {step.capitalize()}: {status_emoji} {colored_status}")
if not step_success:
any_failures = True
all_success = False
print()
if all_success:
success_msg = "🎉 ALL WORKFLOW STEPS COMPLETED SUCCESSFULLY!"
colored_success = Colors.colorize(success_msg, Colors.GREEN + Colors.BOLD)
print(colored_success)
commit_msg = "🚀 Your code is ready for commit."
colored_commit = Colors.colorize(commit_msg, Colors.CYAN)
print(colored_commit)
else:
fail_msg = "❌ WORKFLOW COMPLETED WITH FAILURES"
colored_fail = Colors.colorize(fail_msg, Colors.RED + Colors.BOLD)
print(colored_fail)
review_msg = "🔧 Please fix the issues above before committing."
colored_review = Colors.colorize(review_msg, Colors.RED)
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}")
return all_success
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("--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")
args = parser.parse_args()
project_root = Path(__file__).parent.parent
# Determine steps to run
if args.lint:
steps = ["lint"]
elif args.format:
steps = ["format"]
elif args.test:
steps = ["test"]
else:
steps = args.steps
# Run workflow or individual step
if len(steps) == 1:
step_funcs = {"lint": run_lint, "format": run_format, "test": run_tests}
success, _ = step_funcs[steps[0]](project_root)
else:
success = run_workflow(project_root, steps)
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()