Files
gh-vanman2024-cli-builder-p…/skills/fire-patterns/scripts/test-fire-cli.py
2025-11-30 09:04:14 +08:00

209 lines
6.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""Test Fire CLI commands programmatically"""
import sys
import importlib.util
import inspect
from pathlib import Path
from typing import Any, List, Dict
import json
class FireCLITester:
"""Test Fire CLI commands without running them"""
def __init__(self, filepath: Path):
self.filepath = filepath
self.module = None
self.cli_class = None
def load_cli(self) -> bool:
"""Load CLI module dynamically"""
try:
spec = importlib.util.spec_from_file_location("cli_module", self.filepath)
self.module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(self.module)
# Find main CLI class (first class in module)
for name, obj in inspect.getmembers(self.module):
if inspect.isclass(obj) and obj.__module__ == self.module.__name__:
self.cli_class = obj
break
if not self.cli_class:
print("Error: No CLI class found in module", file=sys.stderr)
return False
return True
except Exception as e:
print(f"Error loading CLI module: {e}", file=sys.stderr)
return False
def get_commands(self) -> Dict[str, Any]:
"""Get all available commands"""
if not self.cli_class:
return {}
commands = {}
instance = self.cli_class()
# Get methods from main class
for name, method in inspect.getmembers(instance, predicate=inspect.ismethod):
if not name.startswith('_'):
commands[name] = {
'type': 'method',
'signature': str(inspect.signature(method)),
'doc': inspect.getdoc(method) or 'No documentation'
}
# Get nested classes (command groups)
for name, obj in inspect.getmembers(self.cli_class):
if inspect.isclass(obj) and not name.startswith('_'):
commands[name] = {
'type': 'command_group',
'doc': inspect.getdoc(obj) or 'No documentation',
'methods': {}
}
# Get methods from nested class
for method_name, method in inspect.getmembers(obj, predicate=inspect.isfunction):
if not method_name.startswith('_'):
commands[name]['methods'][method_name] = {
'signature': str(inspect.signature(method)),
'doc': inspect.getdoc(method) or 'No documentation'
}
return commands
def test_instantiation(self) -> bool:
"""Test if CLI class can be instantiated"""
try:
instance = self.cli_class()
print("✅ CLI class instantiation: PASSED")
return True
except Exception as e:
print(f"❌ CLI class instantiation: FAILED - {e}")
return False
def test_method_signatures(self) -> bool:
"""Test if all methods have valid signatures"""
try:
instance = self.cli_class()
errors = []
for name, method in inspect.getmembers(instance, predicate=inspect.ismethod):
if name.startswith('_'):
continue
try:
sig = inspect.signature(method)
# Check for invalid parameter types
for param_name, param in sig.parameters.items():
if param.kind == inspect.Parameter.VAR_KEYWORD:
errors.append(f"Method '{name}' uses **kwargs (works but not recommended)")
except Exception as e:
errors.append(f"Method '{name}' signature error: {e}")
if errors:
print("⚠️ Method signatures: WARNINGS")
for error in errors:
print(f"{error}")
return True # Warnings, not failures
else:
print("✅ Method signatures: PASSED")
return True
except Exception as e:
print(f"❌ Method signatures: FAILED - {e}")
return False
def test_docstrings(self) -> bool:
"""Test if all public methods have docstrings"""
try:
instance = self.cli_class()
missing = []
for name, method in inspect.getmembers(instance, predicate=inspect.ismethod):
if name.startswith('_'):
continue
doc = inspect.getdoc(method)
if not doc:
missing.append(name)
if missing:
print("⚠️ Docstrings: WARNINGS")
print(f" Missing docstrings for: {', '.join(missing)}")
return True # Warnings, not failures
else:
print("✅ Docstrings: PASSED")
return True
except Exception as e:
print(f"❌ Docstrings: FAILED - {e}")
return False
def print_summary(self):
"""Print CLI summary"""
commands = self.get_commands()
print(f"\n{'='*60}")
print(f"Fire CLI Test Report: {self.filepath.name}")
print(f"{'='*60}\n")
print(f"CLI Class: {self.cli_class.__name__}")
print(f"Total Commands: {len(commands)}\n")
print("Available Commands:")
for cmd_name, cmd_info in commands.items():
if cmd_info['type'] == 'method':
print(f"{cmd_name}{cmd_info['signature']}")
elif cmd_info['type'] == 'command_group':
print(f"{cmd_name}/ (command group)")
for method_name, method_info in cmd_info['methods'].items():
print(f"{method_name}{method_info['signature']}")
print()
def run_tests(self) -> bool:
"""Run all tests"""
print(f"\nTesting Fire CLI: {self.filepath.name}\n")
results = []
results.append(self.test_instantiation())
results.append(self.test_method_signatures())
results.append(self.test_docstrings())
print()
return all(results)
def main():
if len(sys.argv) < 2:
print("Usage: test-fire-cli.py <fire-cli-file.py> [--summary]")
sys.exit(1)
filepath = Path(sys.argv[1])
show_summary = '--summary' in sys.argv
if not filepath.exists():
print(f"Error: File not found: {filepath}", file=sys.stderr)
sys.exit(1)
tester = FireCLITester(filepath)
if not tester.load_cli():
sys.exit(1)
if show_summary:
tester.print_summary()
else:
passed = tester.run_tests()
tester.print_summary()
sys.exit(0 if passed else 1)
if __name__ == '__main__':
main()