返回 ppt-master
batch_validate.py
根目录 / skills / ppt-master / scripts / batch_validate.py
1 #!/usr/bin/env python3
2 """
3 PPT Master - Batch Project Validation Tool
4
5 Checks the structural integrity and compliance of multiple projects at once.
6
7 Usage:
8 python3 scripts/batch_validate.py examples
9 python3 scripts/batch_validate.py projects
10 python3 scripts/batch_validate.py --all
11 python3 scripts/batch_validate.py examples projects
12 """
13
14 import argparse
15 import sys
16 from collections import defaultdict
17 from pathlib import Path
18
19 from console_encoding import configure_utf8_stdio
20
21 configure_utf8_stdio()
22
23 try:
24 from project_utils import (
25 find_all_projects,
26 get_project_info,
27 validate_project_structure,
28 validate_svg_viewbox,
29 CANVAS_FORMATS
30 )
31 except ImportError:
32 print("Error: Unable to import project_utils module")
33 print("Please ensure project_utils.py is in the same directory")
34 sys.exit(1)
35
36
37 class BatchValidator:
38 """Batch validator"""
39
40 def __init__(self):
41 self.results: list[dict[str, object]] = []
42 self.summary = {
43 'total': 0,
44 'valid': 0,
45 'has_errors': 0,
46 'has_warnings': 0,
47 'missing_readme': 0,
48 'missing_spec': 0,
49 'svg_issues': 0
50 }
51
52 def validate_directory(self, directory: str, recursive: bool = False) -> list[dict[str, object]]:
53 """
54 Validate all projects in a directory
55
56 Args:
57 directory: Directory path
58 recursive: Whether to recursively search subdirectories
59
60 Returns:
61 List of validation results
62 """
63 dir_path = Path(directory)
64 if not dir_path.exists():
65 print(f"[ERROR] Directory does not exist: {directory}")
66 return []
67
68 print(f"\n[SCAN] Scanning directory: {directory}")
69 print("=" * 80)
70
71 projects = find_all_projects(directory)
72
73 if not projects:
74 print(f"[WARN] No projects found")
75 return []
76
77 print(f"Found {len(projects)} project(s)\n")
78
79 for project_path in projects:
80 self.validate_project(str(project_path))
81
82 return self.results
83
84 def validate_project(self, project_path: str) -> dict[str, object]:
85 """
86 Validate a single project
87
88 Args:
89 project_path: Project path
90
91 Returns:
92 Validation result dictionary
93 """
94 self.summary['total'] += 1
95
96 # Get project info
97 info = get_project_info(project_path)
98
99 # Validate project structure
100 is_valid, errors, warnings = validate_project_structure(project_path)
101
102 # Validate SVG viewBox
103 svg_warnings = []
104 if info['svg_files']:
105 project_path_obj = Path(project_path)
106 svg_files = [project_path_obj / 'svg_output' /
107 f for f in info['svg_files']]
108 svg_warnings = validate_svg_viewbox(svg_files, info['format'])
109
110 # Aggregate results
111 result = {
112 'path': project_path,
113 'name': info['name'],
114 'format': info['format_name'],
115 'date': info['date_formatted'],
116 'svg_count': info['svg_count'],
117 'is_valid': is_valid,
118 'errors': errors,
119 'warnings': warnings + svg_warnings,
120 'has_readme': info['has_readme'],
121 'has_spec': info['has_spec']
122 }
123
124 self.results.append(result)
125
126 # Update statistics
127 if is_valid and not warnings and not svg_warnings:
128 self.summary['valid'] += 1
129 status = "[OK]"
130 elif errors:
131 self.summary['has_errors'] += 1
132 status = "[ERROR]"
133 else:
134 self.summary['has_warnings'] += 1
135 status = "[WARN]"
136
137 if not info['has_readme']:
138 self.summary['missing_readme'] += 1
139 if not info['has_spec']:
140 self.summary['missing_spec'] += 1
141 if svg_warnings:
142 self.summary['svg_issues'] += 1
143
144 # Print result
145 print(f"{status} {info['name']}")
146 print(f" Path: {project_path}")
147 print(
148 f" Format: {info['format_name']} | SVG: {info['svg_count']} file(s) | Date: {info['date_formatted']}")
149
150 if errors:
151 print(f" [ERROR] Errors ({len(errors)}):")
152 for error in errors:
153 print(f" - {error}")
154
155 if warnings or svg_warnings:
156 all_warnings = warnings + svg_warnings
157 print(f" [WARN] Warnings ({len(all_warnings)}):")
158 for warning in all_warnings[:3]: # Only show first 3 warnings
159 print(f" - {warning}")
160 if len(all_warnings) > 3:
161 print(f" ... and {len(all_warnings) - 3} more warning(s)")
162
163 print()
164
165 return result
166
167 def print_summary(self) -> None:
168 """Print a summary of validation results."""
169 print("\n" + "=" * 80)
170 print("[Summary] Validation Summary")
171 print("=" * 80)
172
173 print(f"\nTotal projects: {self.summary['total']}")
174 print(
175 f" [OK] Fully valid: {self.summary['valid']} ({self._percentage(self.summary['valid'])}%)")
176 print(
177 f" [WARN] With warnings: {self.summary['has_warnings']} ({self._percentage(self.summary['has_warnings'])}%)")
178 print(
179 f" [ERROR] With errors: {self.summary['has_errors']} ({self._percentage(self.summary['has_errors'])}%)")
180
181 print(f"\nCommon issues:")
182 print(f" Missing README.md: {self.summary['missing_readme']} project(s)")
183 print(f" Missing design spec: {self.summary['missing_spec']} project(s)")
184 print(f" SVG format issues: {self.summary['svg_issues']} project(s)")
185
186 # Group statistics by format
187 format_stats = defaultdict(int)
188 for result in self.results:
189 format_stats[result['format']] += 1
190
191 if format_stats:
192 print(f"\nCanvas format distribution:")
193 for fmt, count in sorted(format_stats.items(), key=lambda x: x[1], reverse=True):
194 print(f" {fmt}: {count} project(s)")
195
196 # Provide fix suggestions
197 if self.summary['has_errors'] > 0 or self.summary['has_warnings'] > 0:
198 print(f"\n[TIP] Fix suggestions:")
199
200 if self.summary['missing_readme'] > 0:
201 print(f" 1. Create documentation for projects missing README")
202 print(
203 f" Include the project goal, sources, canvas, artifacts, and export path")
204
205 if self.summary['svg_issues'] > 0:
206 print(f" 2. Check SVG root viewBox settings")
207 print(f" The SVG root viewBox is the export canvas authority")
208
209 if self.summary['missing_spec'] > 0:
210 print(f" 3. Add design specification files")
211
212 def _percentage(self, count: int) -> int:
213 """Calculate percentage"""
214 if self.summary['total'] == 0:
215 return 0
216 return int(count / self.summary['total'] * 100)
217
218 def export_report(self, output_file: str = 'validation_report.txt') -> None:
219 """
220 Export validation report to file
221
222 Args:
223 output_file: Output file path
224 """
225 with open(output_file, 'w', encoding='utf-8') as f:
226 f.write("PPT Master Project Validation Report\n")
227 f.write("=" * 80 + "\n\n")
228
229 for result in self.results:
230 status = "[OK] Valid" if result['is_valid'] and not result['warnings'] else \
231 "[ERROR] Error" if result['errors'] else "[WARN] Warning"
232
233 f.write(f"{status} - {result['name']}\n")
234 f.write(f"Path: {result['path']}\n")
235 f.write(
236 f"Format: {result['format']} | SVG: {result['svg_count']} file(s)\n")
237
238 if result['errors']:
239 f.write(f"\nErrors:\n")
240 for error in result['errors']:
241 f.write(f" - {error}\n")
242
243 if result['warnings']:
244 f.write(f"\nWarnings:\n")
245 for warning in result['warnings']:
246 f.write(f" - {warning}\n")
247
248 f.write("\n" + "-" * 80 + "\n\n")
249
250 # Write summary
251 f.write("\n" + "=" * 80 + "\n")
252 f.write("Validation Summary\n")
253 f.write("=" * 80 + "\n\n")
254 f.write(f"Total projects: {self.summary['total']}\n")
255 f.write(f"Fully valid: {self.summary['valid']}\n")
256 f.write(f"With warnings: {self.summary['has_warnings']}\n")
257 f.write(f"With errors: {self.summary['has_errors']}\n")
258
259 print(f"\n[REPORT] Validation report exported: {output_file}")
260
261
262 def build_parser() -> argparse.ArgumentParser:
263 """Build the command-line parser."""
264 parser = argparse.ArgumentParser(
265 description="Validate one or more PPT Master project directories.",
266 formatter_class=argparse.RawDescriptionHelpFormatter,
267 epilog="""Examples:
268 python3 scripts/batch_validate.py examples
269 python3 scripts/batch_validate.py projects
270 python3 scripts/batch_validate.py examples projects
271 python3 scripts/batch_validate.py --all
272 """,
273 )
274 parser.add_argument("directories", nargs="*", help="Directories to scan")
275 parser.add_argument("--all", action="store_true", help="Validate examples and projects")
276 parser.add_argument("--export", action="store_true", help="Write a validation report")
277 parser.add_argument(
278 "--output",
279 default="validation_report.txt",
280 help="Report path when --export is used",
281 )
282 return parser
283
284
285 def main(argv: list[str] | None = None) -> int:
286 """Run the CLI entry point."""
287 parser = build_parser()
288 args = parser.parse_args(argv)
289
290 validator = BatchValidator()
291
292 if args.all:
293 directories = ['examples', 'projects']
294 else:
295 directories = args.directories
296
297 if not directories:
298 parser.print_help()
299 print(
300 "\n[ERROR] Provide at least one directory or pass --all.",
301 file=sys.stderr,
302 )
303 return 1
304
305 # Validate each directory
306 for directory in directories:
307 if Path(directory).exists():
308 validator.validate_directory(directory)
309 else:
310 print(f"[WARN] Skipping non-existent directory: {directory}\n")
311
312 if validator.summary['total'] == 0:
313 print(
314 "[ERROR] No projects were found in the requested directories.",
315 file=sys.stderr,
316 )
317 return 1
318
319 # Print summary
320 validator.print_summary()
321
322 # Export report (if specified)
323 if args.export:
324 validator.export_report(args.output)
325
326 # Return exit code
327 if validator.summary['has_errors'] > 0:
328 return 1
329 elif validator.summary['has_warnings'] > 0:
330 return 2
331 return 0
332
333
334 if __name__ == '__main__':
335 raise SystemExit(main())
336
336 lines PYTHON