generate-tests.py
1 #!/usr/bin/env python3 2 """ 3 Test stub generator for Alpha-Delta frontend repos. 4 5 Parses component cspec files and generates test stubs for: 6 - TypeScript/React (vitest/jest) 7 - Rust (cargo test) 8 9 Usage: 10 python scripts/generate-tests.py F002 # Generate for specific component 11 python scripts/generate-tests.py --all # Generate for all components 12 python scripts/generate-tests.py F001 --output /path/to/repo # Custom output 13 """ 14 15 import argparse 16 import os 17 import re 18 import sys 19 from pathlib import Path 20 from typing import Optional 21 22 # Component cspec directory 23 CSPEC_DIR = Path(__file__).parent.parent / "components" / "frontend" 24 25 # Repo mapping 26 REPO_MAP = { 27 "F001": {"name": "wallet-core", "lang": "rust"}, 28 "F002": {"name": "acdc-wallet", "lang": "typescript"}, 29 "F003": {"name": "acdc-governor", "lang": "typescript"}, 30 "F004": {"name": "acdc-messenger", "lang": "typescript"}, 31 "F005": {"name": "acdc-cli", "lang": "rust"}, 32 "F006": {"name": "acdc-scanner", "lang": "typescript"}, 33 "F007": {"name": "acdc-docs", "lang": "typescript"}, 34 "F008": {"name": "acdc-design", "lang": "typescript"}, 35 "F009": {"name": "acdc-i18n", "lang": "typescript"}, 36 "F010": {"name": "acdc-contracts", "lang": "leo"}, 37 } 38 39 40 def parse_cspec(cspec_path: Path) -> dict: 41 """Parse a cspec file and extract interface definitions.""" 42 content = cspec_path.read_text() 43 lines = content.split('\n') 44 45 result = { 46 "id": "", 47 "name": "", 48 "functions": {}, 49 "events": [], 50 "types": {}, 51 } 52 53 # Extract metadata 54 id_match = re.search(r'id:\s*(\w+)', content) 55 name_match = re.search(r'name:\s*(\w+)', content) 56 if id_match: 57 result["id"] = id_match.group(1) 58 if name_match: 59 result["name"] = name_match.group(1) 60 61 # State machine for parsing 62 in_interface = False 63 in_functions = False 64 current_func = None 65 current_func_data = {"inputs": [], "outputs": []} 66 67 for i, line in enumerate(lines): 68 stripped = line.strip() 69 70 # Track sections 71 if line.startswith('interface:'): 72 in_interface = True 73 continue 74 elif line.startswith('spec:') or line.startswith('dependencies:'): 75 in_interface = False 76 in_functions = False 77 if current_func: 78 result["functions"][current_func] = current_func_data 79 current_func = None 80 continue 81 82 if not in_interface: 83 continue 84 85 # Inside interface section 86 if stripped.startswith('functions:'): 87 in_functions = True 88 continue 89 elif stripped.startswith('events:'): 90 in_functions = False 91 if current_func: 92 result["functions"][current_func] = current_func_data 93 current_func = None 94 continue 95 elif stripped.startswith('types:') or stripped.startswith('types_ref:'): 96 in_functions = False 97 if current_func: 98 result["functions"][current_func] = current_func_data 99 current_func = None 100 continue 101 102 if in_functions and stripped: 103 # Single-line format: func_name: {inputs: [...], outputs: [...]} 104 single_line_match = re.match(r'(\w+):\s*\{inputs:\s*\[([^\]]*)\],\s*outputs:\s*\[([^\]]*)\]\}', stripped) 105 if single_line_match: 106 func_name = single_line_match.group(1) 107 inputs = [i.strip() for i in single_line_match.group(2).split(',') if i.strip()] 108 outputs = [o.strip() for o in single_line_match.group(3).split(',') if o.strip()] 109 result["functions"][func_name] = {"inputs": inputs, "outputs": outputs} 110 continue 111 112 # Multi-line format detection 113 # Function name line: " func_name:" 114 func_name_match = re.match(r'^(\s{4})(\w+):$', line) 115 if func_name_match: 116 # Save previous function 117 if current_func: 118 result["functions"][current_func] = current_func_data 119 current_func = func_name_match.group(2) 120 current_func_data = {"inputs": [], "outputs": []} 121 continue 122 123 # Inputs line: " inputs: [...]" 124 if current_func and 'inputs:' in stripped: 125 inputs_match = re.search(r'inputs:\s*\[([^\]]*)\]', stripped) 126 if inputs_match: 127 inputs_str = inputs_match.group(1) 128 current_func_data["inputs"] = [i.split(':')[0].strip() for i in inputs_str.split(',') if i.strip()] 129 continue 130 131 # Outputs line: " outputs: [...]" 132 if current_func and 'outputs:' in stripped: 133 outputs_match = re.search(r'outputs:\s*\[([^\]]*)\]', stripped) 134 if outputs_match: 135 outputs_str = outputs_match.group(1) 136 current_func_data["outputs"] = [o.split(':')[0].strip() for o in outputs_str.split(',') if o.strip()] 137 continue 138 139 # Save last function if any 140 if current_func: 141 result["functions"][current_func] = current_func_data 142 143 # Extract events 144 events_match = re.search(r'events:\s*\n((?:\s+-\s+\w+\n?)+)', content) 145 if events_match: 146 events_block = events_match.group(1) 147 result["events"] = [e.strip()[2:].strip() for e in events_block.strip().split('\n') if e.strip().startswith('-')] 148 149 return result 150 151 152 def generate_typescript_tests(spec: dict, output_dir: Path) -> list[Path]: 153 """Generate TypeScript/React test stubs.""" 154 generated = [] 155 156 # Create test directory 157 test_dir = output_dir / "src" / "__tests__" 158 test_dir.mkdir(parents=True, exist_ok=True) 159 160 # Generate function tests 161 if spec["functions"]: 162 func_test_path = test_dir / f"{spec['name']}.test.ts" 163 164 test_content = f'''/** 165 * Auto-generated test stubs for {spec['name']} ({spec['id']}) 166 * Generated from: components/frontend/{spec['id']}-{spec['name']}.component.cspec 167 * 168 * TODO: Implement these test cases 169 */ 170 171 import {{ describe, it, expect, vi, beforeEach }} from 'vitest'; 172 173 ''' 174 175 for func_name, func_def in spec["functions"].items(): 176 inputs = func_def["inputs"] 177 outputs = func_def["outputs"] 178 179 input_params = ", ".join([f"{i}: unknown" for i in inputs]) if inputs else "" 180 181 test_content += f'''describe('{func_name}', () => {{ 182 beforeEach(() => {{ 183 // Setup mocks 184 }}); 185 186 it('should handle valid input', async () => {{ 187 // TODO: Implement test 188 // Inputs: {inputs} 189 // Expected outputs: {outputs} 190 expect(true).toBe(true); 191 }}); 192 193 it('should reject invalid input', async () => {{ 194 // TODO: Test error cases 195 expect(true).toBe(true); 196 }}); 197 198 it('should handle edge cases', async () => {{ 199 // TODO: Test boundary conditions 200 expect(true).toBe(true); 201 }}); 202 }}); 203 204 ''' 205 206 func_test_path.write_text(test_content) 207 generated.append(func_test_path) 208 209 # Generate event tests 210 if spec["events"]: 211 event_test_path = test_dir / f"{spec['name']}.events.test.ts" 212 213 event_content = f'''/** 214 * Auto-generated event test stubs for {spec['name']} ({spec['id']}) 215 * Generated from: components/frontend/{spec['id']}-{spec['name']}.component.cspec 216 */ 217 218 import {{ describe, it, expect, vi }} from 'vitest'; 219 220 ''' 221 222 for event in spec["events"]: 223 event_content += f'''describe('Event: {event}', () => {{ 224 it('should emit {event} when triggered', () => {{ 225 // TODO: Implement event emission test 226 expect(true).toBe(true); 227 }}); 228 229 it('should include correct payload in {event}', () => {{ 230 // TODO: Verify event payload structure 231 expect(true).toBe(true); 232 }}); 233 }}); 234 235 ''' 236 237 event_test_path.write_text(event_content) 238 generated.append(event_test_path) 239 240 return generated 241 242 243 def generate_rust_tests(spec: dict, output_dir: Path) -> list[Path]: 244 """Generate Rust test stubs.""" 245 generated = [] 246 247 # Create test directory 248 test_dir = output_dir / "tests" 249 test_dir.mkdir(parents=True, exist_ok=True) 250 251 if spec["functions"]: 252 func_test_path = test_dir / f"{spec['name']}_test.rs" 253 254 test_content = f'''//! Auto-generated test stubs for {spec['name']} ({spec['id']}) 255 //! Generated from: components/frontend/{spec['id']}-{spec['name']}.component.cspec 256 //! 257 //! TODO: Implement these test cases 258 259 use {spec['name'].replace('-', '_')}::*; 260 261 ''' 262 263 for func_name, func_def in spec["functions"].items(): 264 inputs = func_def["inputs"] 265 outputs = func_def["outputs"] 266 267 test_content += f'''#[cfg(test)] 268 mod {func_name}_tests {{ 269 use super::*; 270 271 #[test] 272 fn test_{func_name}_valid_input() {{ 273 // TODO: Implement test 274 // Inputs: {inputs} 275 // Expected outputs: {outputs} 276 assert!(true); 277 }} 278 279 #[test] 280 fn test_{func_name}_invalid_input() {{ 281 // TODO: Test error cases 282 assert!(true); 283 }} 284 285 #[test] 286 fn test_{func_name}_edge_cases() {{ 287 // TODO: Test boundary conditions 288 assert!(true); 289 }} 290 }} 291 292 ''' 293 294 func_test_path.write_text(test_content) 295 generated.append(func_test_path) 296 297 return generated 298 299 300 def generate_tests(component_id: str, output_dir: Optional[Path] = None) -> list[Path]: 301 """Generate test stubs for a component.""" 302 303 if component_id not in REPO_MAP: 304 print(f"Error: Unknown component {component_id}") 305 return [] 306 307 repo_info = REPO_MAP[component_id] 308 309 # Find cspec file 310 cspec_pattern = f"{component_id}-*.cspec" 311 cspec_files = list(CSPEC_DIR.glob(cspec_pattern)) 312 313 if not cspec_files: 314 print(f"Error: No cspec found for {component_id}") 315 return [] 316 317 cspec_path = cspec_files[0] 318 319 # Parse cspec 320 spec = parse_cspec(cspec_path) 321 322 if not spec["functions"] and not spec["events"]: 323 print(f"Warning: No functions or events found in {cspec_path}") 324 return [] 325 326 # Determine output directory 327 if output_dir is None: 328 # Default to sibling repo directory 329 output_dir = CSPEC_DIR.parent.parent.parent / repo_info["name"] 330 331 if not output_dir.exists(): 332 print(f"Warning: Output directory {output_dir} does not exist, creating...") 333 output_dir.mkdir(parents=True, exist_ok=True) 334 335 # Generate tests based on language 336 lang = repo_info["lang"] 337 338 if lang == "typescript": 339 return generate_typescript_tests(spec, output_dir) 340 elif lang == "rust": 341 return generate_rust_tests(spec, output_dir) 342 elif lang == "leo": 343 print(f"Leo test generation not yet implemented for {component_id}") 344 return [] 345 else: 346 print(f"Unknown language {lang} for {component_id}") 347 return [] 348 349 350 def main(): 351 parser = argparse.ArgumentParser(description="Generate test stubs from cspec files") 352 parser.add_argument("component", nargs="?", help="Component ID (e.g., F002)") 353 parser.add_argument("--all", action="store_true", help="Generate for all components") 354 parser.add_argument("--output", "-o", type=Path, help="Output directory") 355 parser.add_argument("--list", action="store_true", help="List available components") 356 357 args = parser.parse_args() 358 359 if args.list: 360 print("Available components:") 361 for cid, info in REPO_MAP.items(): 362 print(f" {cid}: {info['name']} ({info['lang']})") 363 return 364 365 if args.all: 366 components = list(REPO_MAP.keys()) 367 elif args.component: 368 components = [args.component.upper()] 369 else: 370 parser.print_help() 371 return 372 373 total_generated = [] 374 375 for component_id in components: 376 print(f"\nGenerating tests for {component_id}...") 377 generated = generate_tests(component_id, args.output) 378 total_generated.extend(generated) 379 380 for path in generated: 381 print(f" Created: {path}") 382 383 print(f"\n✓ Generated {len(total_generated)} test files") 384 385 386 if __name__ == "__main__": 387 main()