325 lines
10 KiB
Python
325 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
PPT Master - Batch Project Validation Tool
|
|
|
|
Checks the structural integrity and compliance of multiple projects at once.
|
|
|
|
Usage:
|
|
python3 scripts/batch_validate.py examples
|
|
python3 scripts/batch_validate.py projects
|
|
python3 scripts/batch_validate.py --all
|
|
python3 scripts/batch_validate.py examples projects
|
|
"""
|
|
|
|
import argparse
|
|
import sys
|
|
from collections import defaultdict
|
|
from pathlib import Path
|
|
|
|
from console_encoding import configure_utf8_stdio
|
|
|
|
configure_utf8_stdio()
|
|
|
|
try:
|
|
from project_utils import (
|
|
find_all_projects,
|
|
get_project_info,
|
|
validate_project_structure,
|
|
validate_svg_viewbox,
|
|
CANVAS_FORMATS
|
|
)
|
|
except ImportError:
|
|
print("Error: Unable to import project_utils module")
|
|
print("Please ensure project_utils.py is in the same directory")
|
|
sys.exit(1)
|
|
|
|
|
|
class BatchValidator:
|
|
"""Batch validator"""
|
|
|
|
def __init__(self):
|
|
self.results: list[dict[str, object]] = []
|
|
self.summary = {
|
|
'total': 0,
|
|
'valid': 0,
|
|
'has_errors': 0,
|
|
'has_warnings': 0,
|
|
'missing_readme': 0,
|
|
'missing_spec': 0,
|
|
'svg_issues': 0
|
|
}
|
|
|
|
def validate_directory(self, directory: str, recursive: bool = False) -> list[dict[str, object]]:
|
|
"""
|
|
Validate all projects in a directory
|
|
|
|
Args:
|
|
directory: Directory path
|
|
recursive: Whether to recursively search subdirectories
|
|
|
|
Returns:
|
|
List of validation results
|
|
"""
|
|
dir_path = Path(directory)
|
|
if not dir_path.exists():
|
|
print(f"[ERROR] Directory does not exist: {directory}")
|
|
return []
|
|
|
|
print(f"\n[SCAN] Scanning directory: {directory}")
|
|
print("=" * 80)
|
|
|
|
projects = find_all_projects(directory)
|
|
|
|
if not projects:
|
|
print(f"[WARN] No projects found")
|
|
return []
|
|
|
|
print(f"Found {len(projects)} project(s)\n")
|
|
|
|
for project_path in projects:
|
|
self.validate_project(str(project_path))
|
|
|
|
return self.results
|
|
|
|
def validate_project(self, project_path: str) -> dict[str, object]:
|
|
"""
|
|
Validate a single project
|
|
|
|
Args:
|
|
project_path: Project path
|
|
|
|
Returns:
|
|
Validation result dictionary
|
|
"""
|
|
self.summary['total'] += 1
|
|
|
|
# Get project info
|
|
info = get_project_info(project_path)
|
|
|
|
# Validate project structure
|
|
is_valid, errors, warnings = validate_project_structure(project_path)
|
|
|
|
# Validate SVG viewBox
|
|
svg_warnings = []
|
|
if info['svg_files']:
|
|
project_path_obj = Path(project_path)
|
|
svg_files = [project_path_obj / 'svg_output' /
|
|
f for f in info['svg_files']]
|
|
svg_warnings = validate_svg_viewbox(svg_files, info['format'])
|
|
|
|
# Aggregate results
|
|
result = {
|
|
'path': project_path,
|
|
'name': info['name'],
|
|
'format': info['format_name'],
|
|
'date': info['date_formatted'],
|
|
'svg_count': info['svg_count'],
|
|
'is_valid': is_valid,
|
|
'errors': errors,
|
|
'warnings': warnings + svg_warnings,
|
|
'has_readme': info['has_readme'],
|
|
'has_spec': info['has_spec']
|
|
}
|
|
|
|
self.results.append(result)
|
|
|
|
# Update statistics
|
|
if is_valid and not warnings and not svg_warnings:
|
|
self.summary['valid'] += 1
|
|
status = "[OK]"
|
|
elif errors:
|
|
self.summary['has_errors'] += 1
|
|
status = "[ERROR]"
|
|
else:
|
|
self.summary['has_warnings'] += 1
|
|
status = "[WARN]"
|
|
|
|
if not info['has_readme']:
|
|
self.summary['missing_readme'] += 1
|
|
if not info['has_spec']:
|
|
self.summary['missing_spec'] += 1
|
|
if svg_warnings:
|
|
self.summary['svg_issues'] += 1
|
|
|
|
# Print result
|
|
print(f"{status} {info['name']}")
|
|
print(f" Path: {project_path}")
|
|
print(
|
|
f" Format: {info['format_name']} | SVG: {info['svg_count']} file(s) | Date: {info['date_formatted']}")
|
|
|
|
if errors:
|
|
print(f" [ERROR] Errors ({len(errors)}):")
|
|
for error in errors:
|
|
print(f" - {error}")
|
|
|
|
if warnings or svg_warnings:
|
|
all_warnings = warnings + svg_warnings
|
|
print(f" [WARN] Warnings ({len(all_warnings)}):")
|
|
for warning in all_warnings[:3]: # Only show first 3 warnings
|
|
print(f" - {warning}")
|
|
if len(all_warnings) > 3:
|
|
print(f" ... and {len(all_warnings) - 3} more warning(s)")
|
|
|
|
print()
|
|
|
|
return result
|
|
|
|
def print_summary(self) -> None:
|
|
"""Print a summary of validation results."""
|
|
print("\n" + "=" * 80)
|
|
print("[Summary] Validation Summary")
|
|
print("=" * 80)
|
|
|
|
print(f"\nTotal projects: {self.summary['total']}")
|
|
print(
|
|
f" [OK] Fully valid: {self.summary['valid']} ({self._percentage(self.summary['valid'])}%)")
|
|
print(
|
|
f" [WARN] With warnings: {self.summary['has_warnings']} ({self._percentage(self.summary['has_warnings'])}%)")
|
|
print(
|
|
f" [ERROR] With errors: {self.summary['has_errors']} ({self._percentage(self.summary['has_errors'])}%)")
|
|
|
|
print(f"\nCommon issues:")
|
|
print(f" Missing README.md: {self.summary['missing_readme']} project(s)")
|
|
print(f" Missing design spec: {self.summary['missing_spec']} project(s)")
|
|
print(f" SVG format issues: {self.summary['svg_issues']} project(s)")
|
|
|
|
# Group statistics by format
|
|
format_stats = defaultdict(int)
|
|
for result in self.results:
|
|
format_stats[result['format']] += 1
|
|
|
|
if format_stats:
|
|
print(f"\nCanvas format distribution:")
|
|
for fmt, count in sorted(format_stats.items(), key=lambda x: x[1], reverse=True):
|
|
print(f" {fmt}: {count} project(s)")
|
|
|
|
# Provide fix suggestions
|
|
if self.summary['has_errors'] > 0 or self.summary['has_warnings'] > 0:
|
|
print(f"\n[TIP] Fix suggestions:")
|
|
|
|
if self.summary['missing_readme'] > 0:
|
|
print(f" 1. Create documentation for projects missing README")
|
|
print(
|
|
f" Reference: examples/google_annual_report_ppt169_20251116/README.md")
|
|
|
|
if self.summary['svg_issues'] > 0:
|
|
print(f" 2. Check SVG root viewBox settings")
|
|
print(f" The SVG root viewBox is the export canvas authority")
|
|
|
|
if self.summary['missing_spec'] > 0:
|
|
print(f" 3. Add design specification files")
|
|
|
|
def _percentage(self, count: int) -> int:
|
|
"""Calculate percentage"""
|
|
if self.summary['total'] == 0:
|
|
return 0
|
|
return int(count / self.summary['total'] * 100)
|
|
|
|
def export_report(self, output_file: str = 'validation_report.txt') -> None:
|
|
"""
|
|
Export validation report to file
|
|
|
|
Args:
|
|
output_file: Output file path
|
|
"""
|
|
with open(output_file, 'w', encoding='utf-8') as f:
|
|
f.write("PPT Master Project Validation Report\n")
|
|
f.write("=" * 80 + "\n\n")
|
|
|
|
for result in self.results:
|
|
status = "[OK] Valid" if result['is_valid'] and not result['warnings'] else \
|
|
"[ERROR] Error" if result['errors'] else "[WARN] Warning"
|
|
|
|
f.write(f"{status} - {result['name']}\n")
|
|
f.write(f"Path: {result['path']}\n")
|
|
f.write(
|
|
f"Format: {result['format']} | SVG: {result['svg_count']} file(s)\n")
|
|
|
|
if result['errors']:
|
|
f.write(f"\nErrors:\n")
|
|
for error in result['errors']:
|
|
f.write(f" - {error}\n")
|
|
|
|
if result['warnings']:
|
|
f.write(f"\nWarnings:\n")
|
|
for warning in result['warnings']:
|
|
f.write(f" - {warning}\n")
|
|
|
|
f.write("\n" + "-" * 80 + "\n\n")
|
|
|
|
# Write summary
|
|
f.write("\n" + "=" * 80 + "\n")
|
|
f.write("Validation Summary\n")
|
|
f.write("=" * 80 + "\n\n")
|
|
f.write(f"Total projects: {self.summary['total']}\n")
|
|
f.write(f"Fully valid: {self.summary['valid']}\n")
|
|
f.write(f"With warnings: {self.summary['has_warnings']}\n")
|
|
f.write(f"With errors: {self.summary['has_errors']}\n")
|
|
|
|
print(f"\n[REPORT] Validation report exported: {output_file}")
|
|
|
|
|
|
def build_parser() -> argparse.ArgumentParser:
|
|
"""Build the command-line parser."""
|
|
parser = argparse.ArgumentParser(
|
|
description="Validate one or more PPT Master project directories.",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""Examples:
|
|
python3 scripts/batch_validate.py examples
|
|
python3 scripts/batch_validate.py projects
|
|
python3 scripts/batch_validate.py examples projects
|
|
python3 scripts/batch_validate.py --all
|
|
""",
|
|
)
|
|
parser.add_argument("directories", nargs="*", help="Directories to scan")
|
|
parser.add_argument("--all", action="store_true", help="Validate examples and projects")
|
|
parser.add_argument("--export", action="store_true", help="Write a validation report")
|
|
parser.add_argument(
|
|
"--output",
|
|
default="validation_report.txt",
|
|
help="Report path when --export is used",
|
|
)
|
|
return parser
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
"""Run the CLI entry point."""
|
|
parser = build_parser()
|
|
args = parser.parse_args(argv)
|
|
|
|
validator = BatchValidator()
|
|
|
|
if args.all:
|
|
directories = ['examples', 'projects']
|
|
else:
|
|
directories = args.directories
|
|
|
|
if not directories:
|
|
parser.print_help()
|
|
return 0
|
|
|
|
# Validate each directory
|
|
for directory in directories:
|
|
if Path(directory).exists():
|
|
validator.validate_directory(directory)
|
|
else:
|
|
print(f"[WARN] Skipping non-existent directory: {directory}\n")
|
|
|
|
# Print summary
|
|
validator.print_summary()
|
|
|
|
# Export report (if specified)
|
|
if args.export:
|
|
validator.export_report(args.output)
|
|
|
|
# Return exit code
|
|
if validator.summary['has_errors'] > 0:
|
|
return 1
|
|
elif validator.summary['has_warnings'] > 0:
|
|
return 2
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
raise SystemExit(main())
|