Compare commits

...

2 Commits

Author SHA1 Message Date
ad7a2575da add silent and machine readable formats to testing tool
Some checks failed
Continuous Integration / Code Formatting (push) Successful in 31s
Continuous Integration / Code Quality Check (push) Successful in 28s
Continuous Integration / Test Execution (push) Failing after 16s
Continuous Integration / CI Summary (push) Failing after 4s
2025-09-29 12:04:09 +04:00
26991ce61a add toml, yaml, json validation 2025-09-29 11:28:36 +04:00
2 changed files with 551 additions and 202 deletions

View File

@@ -84,7 +84,16 @@ jobs:
id: format id: format
run: | run: |
echo "🎨 Running GDScript formatting..." echo "🎨 Running GDScript formatting..."
python tools/run_development.py --format python tools/run_development.py --format --silent --yaml > format_results.yaml
- name: Upload formatting results
if: always()
uses: actions/upload-artifact@v3
with:
name: format-results
path: |
format_results.yaml
retention-days: 7
- name: Check for formatting changes - name: Check for formatting changes
id: check-changes id: check-changes
@@ -156,15 +165,15 @@ jobs:
id: lint id: lint
run: | run: |
echo "🔍 Running GDScript linting..." echo "🔍 Running GDScript linting..."
python tools/run_development.py --lint python tools/run_development.py --lint --silent --yaml > lint_results.yaml
- name: Upload linting results - name: Upload linting results
if: failure() if: always()
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: lint-results name: lint-results
path: | path: |
**/*.gd lint_results.yaml
retention-days: 7 retention-days: 7
test: test:
@@ -201,15 +210,15 @@ jobs:
id: test id: test
run: | run: |
echo "🧪 Running GDScript tests..." echo "🧪 Running GDScript tests..."
python tools/run_development.py --test python tools/run_development.py --test --silent --yaml > test_results.yaml
- name: Upload test results - name: Upload test results
if: failure() if: always()
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: test-results name: test-results
path: | path: |
tests/**/*.gd test_results.yaml
retention-days: 7 retention-days: 7
summary: summary:

View File

@@ -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 --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 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,8 +78,9 @@ 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."""
if not silent:
separator = Colors.colorize("=" * 48, Colors.CYAN) separator = Colors.colorize("=" * 48, Colors.CYAN)
colored_title = Colors.colorize(title, Colors.BOLD + Colors.WHITE) colored_title = Colors.colorize(title, Colors.BOLD + Colors.WHITE)
print(separator) print(separator)
@@ -64,8 +89,9 @@ def print_header(title: str) -> None:
print() 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."""
if not silent:
separator = Colors.colorize("=" * 48, Colors.CYAN) separator = Colors.colorize("=" * 48, Colors.CYAN)
colored_title = Colors.colorize(title, Colors.BOLD + Colors.WHITE) colored_title = Colors.colorize(title, Colors.BOLD + Colors.WHITE)
print(separator) print(separator)
@@ -77,6 +103,37 @@ def print_summary(title: str, stats: Dict[str, int]) -> None:
print(f"{colored_key}: {colored_value}") 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:
""" """
Execute a shell command with error handling and output filtering. Execute a shell command with error handling and output filtering.
@@ -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:
if not silent:
message = "✅ Clean" message = "✅ Clean"
colored_message = Colors.colorize(message, Colors.GREEN) 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,11 +320,13 @@ 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)
if not silent and not yaml_output:
count_msg = f"Found {len(gd_files)} GDScript files to lint." count_msg = f"Found {len(gd_files)} GDScript files to lint."
colored_count = Colors.colorize(count_msg, Colors.BLUE) colored_count = Colors.colorize(count_msg, Colors.BLUE)
print(f"{colored_count}\n") print(f"{colored_count}\n")
@@ -215,13 +336,16 @@ def run_lint(project_root: Path) -> Tuple[bool, Dict]:
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)
if not silent and not yaml_output:
file_msg = f"📄 Linting: {relative_path.name}" file_msg = f"📄 Linting: {relative_path.name}"
colored_file = Colors.colorize(file_msg, Colors.CYAN) colored_file = Colors.colorize(file_msg, Colors.CYAN)
print(colored_file) print(colored_file)
if should_skip_file(gd_file): if should_skip_file(gd_file):
if not silent and not yaml_output:
print_skip_message("gdlint") print_skip_message("gdlint")
clean_files += 1 clean_files += 1
if not silent and not yaml_output:
print() print()
continue continue
@@ -233,23 +357,29 @@ 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:
if not silent and not yaml_output:
print(" ❌ ERROR: gdlint not found") print(" ❌ ERROR: gdlint not found")
return False, {} return False, {}
except Exception as e: except Exception as e:
if not silent and not yaml_output:
print(f" ❌ ERROR: {e}") print(f" ❌ ERROR: {e}")
error_files += 1 error_files += 1
failed_paths.append(str(relative_path)) failed_paths.append(str(relative_path))
if not silent and not yaml_output:
print() print()
# Summary # Summary
@@ -259,9 +389,14 @@ 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
if yaml_output:
output_yaml_results("lint", {**stats, "failed_paths": failed_paths}, success)
else:
print_summary("Linting Summary", stats, silent)
if not silent:
print() print()
if not success: if not success:
msg = "❌ Linting FAILED - Please fix the errors above" msg = "❌ Linting FAILED - Please fix the errors above"
@@ -275,17 +410,165 @@ def run_lint(project_root: Path) -> Tuple[bool, Dict]:
msg = "✅ All GDScript files passed linting!" msg = "✅ All GDScript files passed linting!"
colored_msg = Colors.colorize(msg, Colors.GREEN + Colors.BOLD) colored_msg = Colors.colorize(msg, Colors.GREEN + Colors.BOLD)
print(colored_msg) 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)
if not silent and not yaml_output:
count_msg = f"Found {len(gd_files)} GDScript files to format." count_msg = f"Found {len(gd_files)} GDScript files to format."
colored_count = Colors.colorize(count_msg, Colors.BLUE) colored_count = Colors.colorize(count_msg, Colors.BLUE)
print(f"{colored_count}\n") print(f"{colored_count}\n")
@@ -295,13 +578,16 @@ def run_format(project_root: Path) -> Tuple[bool, Dict]:
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)
if not silent and not yaml_output:
file_msg = f"🎯 Formatting: {relative_path.name}" file_msg = f"🎯 Formatting: {relative_path.name}"
colored_file = Colors.colorize(file_msg, Colors.CYAN) colored_file = Colors.colorize(file_msg, Colors.CYAN)
print(colored_file) print(colored_file)
if should_skip_file(gd_file): if should_skip_file(gd_file):
if not silent and not yaml_output:
print_skip_message("gdformat") print_skip_message("gdformat")
formatted_files += 1 formatted_files += 1
if not silent and not yaml_output:
print() print()
continue continue
@@ -309,11 +595,13 @@ def run_format(project_root: Path) -> Tuple[bool, Dict]:
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:
if not silent and not yaml_output:
success_msg = "✅ Success" success_msg = "✅ Success"
colored_success = Colors.colorize(success_msg, Colors.GREEN) colored_success = Colors.colorize(success_msg, Colors.GREEN)
print(f" {colored_success}") print(f" {colored_success}")
formatted_files += 1 formatted_files += 1
else: else:
if not silent and not yaml_output:
fail_msg = f"❌ FAILED: {relative_path}" fail_msg = f"❌ FAILED: {relative_path}"
colored_fail = Colors.colorize(fail_msg, Colors.RED) colored_fail = Colors.colorize(fail_msg, Colors.RED)
print(f" {colored_fail}") print(f" {colored_fail}")
@@ -325,13 +613,16 @@ def run_format(project_root: Path) -> Tuple[bool, Dict]:
failed_paths.append(str(relative_path)) failed_paths.append(str(relative_path))
except FileNotFoundError: except FileNotFoundError:
if not silent and not yaml_output:
print(" ❌ ERROR: gdformat not found") print(" ❌ ERROR: gdformat not found")
return False, {} return False, {}
except Exception as e: except Exception as e:
if not silent and not yaml_output:
print(f" ❌ ERROR: {e}") print(f" ❌ ERROR: {e}")
failed_files += 1 failed_files += 1
failed_paths.append(str(relative_path)) failed_paths.append(str(relative_path))
if not silent and not yaml_output:
print() print()
# Summary # Summary
@@ -340,9 +631,14 @@ 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
if yaml_output:
output_yaml_results("format", {**stats, "failed_paths": failed_paths}, success)
else:
print_summary("Formatting Summary", stats, silent)
if not silent:
print() print()
if not success: if not success:
msg = "⚠️ WARNING: Some files failed to format" msg = "⚠️ WARNING: Some files failed to format"
@@ -352,6 +648,10 @@ def run_format(project_root: Path) -> Tuple[bool, Dict]:
msg = "✅ All GDScript files formatted successfully!" msg = "✅ All GDScript files formatted successfully!"
colored_msg = Colors.colorize(msg, Colors.GREEN + Colors.BOLD) colored_msg = Colors.colorize(msg, Colors.GREEN + Colors.BOLD)
print(colored_msg) 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,12 +674,14 @@ 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)
if not silent and not yaml_output:
scan_msg = "🔍 Scanning for test files in tests\\ directory..." scan_msg = "🔍 Scanning for test files in tests\\ directory..."
colored_scan = Colors.colorize(scan_msg, Colors.BLUE) colored_scan = Colors.colorize(scan_msg, Colors.BLUE)
print(colored_scan) print(colored_scan)
@@ -405,6 +707,7 @@ 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}"
if not silent and not yaml_output:
header_msg = f"=== {full_test_name} ===" header_msg = f"=== {full_test_name} ==="
colored_header = Colors.colorize(header_msg, Colors.CYAN + Colors.BOLD) colored_header = Colors.colorize(header_msg, Colors.CYAN + Colors.BOLD)
print(f"\n{colored_header}") print(f"\n{colored_header}")
@@ -421,14 +724,18 @@ def run_tests(project_root: Path) -> Tuple[bool, Dict]:
) )
if result.returncode == 0: if result.returncode == 0:
if not silent and not yaml_output:
pass_msg = f"✅ PASSED: {full_test_name}" pass_msg = f"✅ PASSED: {full_test_name}"
colored_pass = Colors.colorize(pass_msg, Colors.GREEN + Colors.BOLD) colored_pass = Colors.colorize(pass_msg, Colors.GREEN + Colors.BOLD)
print(colored_pass) print(colored_pass)
test_results.append((full_test_name, True, "")) test_results.append((full_test_name, True, ""))
else: else:
if not silent and not yaml_output:
fail_msg = f"❌ FAILED: {full_test_name}" fail_msg = f"❌ FAILED: {full_test_name}"
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)
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,25 +743,33 @@ def run_tests(project_root: Path) -> Tuple[bool, Dict]:
total_tests += 1 total_tests += 1
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
if not silent and not yaml_output:
timeout_msg = f"⏰ FAILED: {full_test_name} (TIMEOUT)" timeout_msg = f"⏰ FAILED: {full_test_name} (TIMEOUT)"
colored_timeout = Colors.colorize(timeout_msg, Colors.RED + Colors.BOLD) colored_timeout = Colors.colorize(timeout_msg, Colors.RED + Colors.BOLD)
print(colored_timeout) 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:
if not yaml_output and not silent:
error_msg = "❌ ERROR: Godot not found" error_msg = "❌ ERROR: Godot not found"
colored_error = Colors.colorize(error_msg, Colors.RED + Colors.BOLD) colored_error = Colors.colorize(error_msg, Colors.RED + Colors.BOLD)
print(colored_error) print(colored_error)
return False, {} return False, {}
except Exception as e: except Exception as e:
if not silent and not yaml_output:
exc_msg = f"💥 FAILED: {full_test_name} (ERROR: {e})" exc_msg = f"💥 FAILED: {full_test_name} (ERROR: {e})"
colored_exc = Colors.colorize(exc_msg, Colors.RED + Colors.BOLD) colored_exc = Colors.colorize(exc_msg, Colors.RED + Colors.BOLD)
print(colored_exc) 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
if not silent and not yaml_output:
print() print()
# Summary # Summary
@@ -464,9 +779,14 @@ 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
if yaml_output:
output_yaml_results("test", {**stats, "results": test_results}, success)
else:
print_summary("Test Execution Summary", stats, silent)
if not silent:
print() print()
if success: if success:
msg = "🎉 ALL TESTS PASSED!" msg = "🎉 ALL TESTS PASSED!"
@@ -480,7 +800,7 @@ def run_tests(project_root: Path) -> Tuple[bool, Dict]:
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,14 +813,17 @@ 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
""" """
if not silent:
print_header("🔄 Development Workflow Runner") 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))
} }
if not silent:
intro_msg = "🚀 This script will run the development workflow:" intro_msg = "🚀 This script will run the development workflow:"
colored_intro = Colors.colorize(intro_msg, Colors.BLUE + Colors.BOLD) colored_intro = Colors.colorize(intro_msg, Colors.BLUE + Colors.BOLD)
print(colored_intro) print(colored_intro)
@@ -517,6 +840,7 @@ def run_workflow(project_root: Path, steps: List[str]) -> bool:
for step in steps: for step in steps:
step_name, step_func = workflow_steps[step] step_name, step_func = workflow_steps[step]
if not silent:
separator = Colors.colorize("-" * 48, Colors.MAGENTA) separator = Colors.colorize("-" * 48, Colors.MAGENTA)
print(separator) print(separator)
@@ -528,6 +852,7 @@ def run_workflow(project_root: Path, steps: List[str]) -> bool:
success, step_results = step_func(project_root) success, step_results = step_func(project_root)
results[step] = step_results results[step] = step_results
if not silent:
if not success: if not success:
fail_msg = f"{step.upper()} FAILED - Continuing with remaining steps" fail_msg = f"{step.upper()} FAILED - Continuing with remaining steps"
colored_fail = Colors.colorize(fail_msg, Colors.RED + Colors.BOLD) colored_fail = Colors.colorize(fail_msg, Colors.RED + Colors.BOLD)
@@ -544,17 +869,20 @@ def run_workflow(project_root: Path, steps: List[str]) -> bool:
# Final summary # Final summary
elapsed_time = time.time() - start_time elapsed_time = time.time() - start_time
if not silent:
print_header("📊 Workflow Summary") 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
if not silent:
status_emoji = "" if step_success else "" status_emoji = "" if step_success else ""
status_text = "PASSED" if step_success else "FAILED" status_text = "PASSED" if step_success else "FAILED"
status_color = Colors.GREEN if step_success else Colors.RED 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}")
@@ -562,6 +890,7 @@ def run_workflow(project_root: Path, steps: List[str]) -> bool:
any_failures = True any_failures = True
all_success = False all_success = False
if not silent:
print() print()
if all_success: if all_success:
success_msg = "🎉 ALL WORKFLOW STEPS COMPLETED SUCCESSFULLY!" success_msg = "🎉 ALL WORKFLOW STEPS COMPLETED SUCCESSFULLY!"
@@ -583,17 +912,21 @@ def run_workflow(project_root: Path, steps: List[str]) -> bool:
time_msg = f"⏱️ Elapsed time: {elapsed_time:.1f} seconds" time_msg = f"⏱️ Elapsed time: {elapsed_time:.1f} seconds"
colored_time = Colors.colorize(time_msg, Colors.MAGENTA) colored_time = Colors.colorize(time_msg, Colors.MAGENTA)
print(f"\n{colored_time}") 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)