@voodocs/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +37 -0
  2. package/README.md +153 -0
  3. package/USAGE.md +314 -0
  4. package/cli.py +1340 -0
  5. package/examples/.cursorrules +437 -0
  6. package/examples/instructions/.claude/instructions.md +372 -0
  7. package/examples/instructions/.cursorrules +437 -0
  8. package/examples/instructions/.windsurfrules +437 -0
  9. package/examples/instructions/VOODOCS_INSTRUCTIONS.md +437 -0
  10. package/examples/math_example.py +41 -0
  11. package/examples/phase2_test.py +24 -0
  12. package/examples/test_compound_conditions.py +40 -0
  13. package/examples/test_math_example.py +186 -0
  14. package/lib/darkarts/README.md +115 -0
  15. package/lib/darkarts/__init__.py +16 -0
  16. package/lib/darkarts/annotations/__init__.py +34 -0
  17. package/lib/darkarts/annotations/parser.py +618 -0
  18. package/lib/darkarts/annotations/types.py +181 -0
  19. package/lib/darkarts/cli.py +128 -0
  20. package/lib/darkarts/core/__init__.py +32 -0
  21. package/lib/darkarts/core/interface.py +256 -0
  22. package/lib/darkarts/core/loader.py +231 -0
  23. package/lib/darkarts/core/plugin.py +215 -0
  24. package/lib/darkarts/core/registry.py +146 -0
  25. package/lib/darkarts/exceptions.py +51 -0
  26. package/lib/darkarts/parsers/typescript/dist/cli.d.ts +9 -0
  27. package/lib/darkarts/parsers/typescript/dist/cli.d.ts.map +1 -0
  28. package/lib/darkarts/parsers/typescript/dist/cli.js +69 -0
  29. package/lib/darkarts/parsers/typescript/dist/cli.js.map +1 -0
  30. package/lib/darkarts/parsers/typescript/dist/parser.d.ts +111 -0
  31. package/lib/darkarts/parsers/typescript/dist/parser.d.ts.map +1 -0
  32. package/lib/darkarts/parsers/typescript/dist/parser.js +365 -0
  33. package/lib/darkarts/parsers/typescript/dist/parser.js.map +1 -0
  34. package/lib/darkarts/parsers/typescript/package-lock.json +51 -0
  35. package/lib/darkarts/parsers/typescript/package.json +19 -0
  36. package/lib/darkarts/parsers/typescript/src/cli.ts +41 -0
  37. package/lib/darkarts/parsers/typescript/src/parser.ts +408 -0
  38. package/lib/darkarts/parsers/typescript/tsconfig.json +19 -0
  39. package/lib/darkarts/plugins/voodocs/__init__.py +379 -0
  40. package/lib/darkarts/plugins/voodocs/ai_native_plugin.py +151 -0
  41. package/lib/darkarts/plugins/voodocs/annotation_validator.py +280 -0
  42. package/lib/darkarts/plugins/voodocs/api_spec_generator.py +486 -0
  43. package/lib/darkarts/plugins/voodocs/documentation_generator.py +610 -0
  44. package/lib/darkarts/plugins/voodocs/html_exporter.py +260 -0
  45. package/lib/darkarts/plugins/voodocs/instruction_generator.py +706 -0
  46. package/lib/darkarts/plugins/voodocs/pdf_exporter.py +66 -0
  47. package/lib/darkarts/plugins/voodocs/test_generator.py +636 -0
  48. package/package.json +70 -0
  49. package/requirements.txt +13 -0
  50. package/templates/ci/github-actions.yml +73 -0
  51. package/templates/ci/gitlab-ci.yml +35 -0
  52. package/templates/ci/pre-commit-hook.sh +26 -0
@@ -0,0 +1,636 @@
1
+ """
2
+ VooDocs Test Generator
3
+
4
+ Generates automated test cases from @voodocs annotations, including property-based tests.
5
+ """
6
+
7
+ from typing import List, Optional, Dict, Any, Tuple
8
+ from pathlib import Path
9
+ import re
10
+
11
+ from darkarts.annotations.types import (
12
+ ParsedAnnotations,
13
+ FunctionAnnotation,
14
+ ClassAnnotation,
15
+ )
16
+
17
+
18
+ class TestGenerator:
19
+ """Generates test cases from @voodocs annotations."""
20
+
21
+ def __init__(self, framework: str = "pytest"):
22
+ """
23
+ Initialize test generator.
24
+
25
+ Args:
26
+ framework: Test framework to use ("pytest", "unittest", "jest", "junit")
27
+ """
28
+ self.framework = framework
29
+
30
+ def generate(self, parsed: ParsedAnnotations, output_file: Optional[str] = None) -> str:
31
+ """Generate test cases from parsed annotations."""
32
+ if not parsed.has_annotations():
33
+ return self._generate_empty_test(parsed)
34
+
35
+ if self.framework == "pytest":
36
+ return self._generate_pytest(parsed, output_file)
37
+ elif self.framework == "unittest":
38
+ return self._generate_unittest(parsed, output_file)
39
+ elif self.framework == "jest":
40
+ return self._generate_jest(parsed, output_file)
41
+ else:
42
+ raise ValueError(f"Unsupported test framework: {self.framework}")
43
+
44
+ def _generate_empty_test(self, parsed: ParsedAnnotations) -> str:
45
+ """Generate empty test file for code without annotations."""
46
+ return f"""# No @voodocs annotations found in {parsed.module.name}
47
+ # Cannot generate automated tests without annotations
48
+ """
49
+
50
+ def _generate_pytest(self, parsed: ParsedAnnotations, output_file: Optional[str] = None) -> str:
51
+ """Generate pytest test cases with property-based testing."""
52
+ lines = []
53
+
54
+ # Header
55
+ lines.append('"""')
56
+ lines.append(f"Automated tests for {parsed.module.name}")
57
+ lines.append("Generated from @voodocs annotations")
58
+ lines.append('"""')
59
+ lines.append("")
60
+ lines.append("import pytest")
61
+ lines.append("from hypothesis import given, strategies as st, assume")
62
+ lines.append("from hypothesis import settings, HealthCheck")
63
+ lines.append(f"from {parsed.module.name} import *")
64
+ lines.append("")
65
+ lines.append("")
66
+
67
+ # Generate tests for each function
68
+ all_functions = parsed.get_all_functions()
69
+ for func in all_functions:
70
+ lines.extend(self._generate_pytest_function_tests(func))
71
+ lines.append("")
72
+
73
+ # Generate tests for each class
74
+ for cls in parsed.module.classes:
75
+ lines.extend(self._generate_pytest_class_tests(cls))
76
+ lines.append("")
77
+
78
+ test_code = "\n".join(lines)
79
+
80
+ if output_file:
81
+ output_path = Path(output_file)
82
+ output_path.parent.mkdir(parents=True, exist_ok=True)
83
+ output_path.write_text(test_code, encoding='utf-8')
84
+
85
+ return test_code
86
+
87
+ def _generate_pytest_function_tests(self, func: FunctionAnnotation) -> List[str]:
88
+ """Generate pytest tests for a function with property-based testing."""
89
+ lines = []
90
+
91
+ # Test class for the function
92
+ test_class_name = f"Test{func.name.replace('_', ' ').title().replace(' ', '')}"
93
+ lines.append(f"class {test_class_name}:")
94
+ lines.append(f' """Tests for {func.name}()"""')
95
+ lines.append("")
96
+
97
+ # Property-based test for preconditions and postconditions
98
+ if func.preconditions or func.postconditions:
99
+ lines.extend(self._generate_property_based_test(func))
100
+ lines.append("")
101
+
102
+ # Test preconditions enforcement
103
+ if func.preconditions:
104
+ lines.extend(self._generate_precondition_tests(func))
105
+ lines.append("")
106
+
107
+ # Test postconditions
108
+ if func.postconditions:
109
+ lines.extend(self._generate_postcondition_tests(func))
110
+ lines.append("")
111
+
112
+ # Test invariants
113
+ if func.invariants:
114
+ lines.extend(self._generate_invariant_tests(func))
115
+ lines.append("")
116
+
117
+ # Test error cases
118
+ if func.error_cases:
119
+ lines.extend(self._generate_error_case_tests(func))
120
+ lines.append("")
121
+
122
+ # Test edge cases
123
+ lines.extend(self._generate_edge_case_tests(func))
124
+ lines.append("")
125
+
126
+ # Test complexity/performance
127
+ if func.complexity:
128
+ lines.extend(self._generate_performance_test(func))
129
+ lines.append("")
130
+
131
+ # Happy path test
132
+ lines.extend(self._generate_happy_path_test(func))
133
+
134
+ return lines
135
+
136
+ def _generate_property_based_test(self, func: FunctionAnnotation) -> List[str]:
137
+ """Generate property-based test using Hypothesis."""
138
+ lines = []
139
+
140
+ # Infer input strategies from preconditions
141
+ strategies = self._infer_strategies_from_preconditions(func.preconditions)
142
+
143
+ # Build @given decorator
144
+ if strategies:
145
+ strategy_params = ", ".join([f"{name}={strat}" for name, strat in strategies.items()])
146
+ lines.append(f" @given({strategy_params})")
147
+ lines.append(" @settings(suppress_health_check=[HealthCheck.function_scoped_fixture])")
148
+ else:
149
+ # Generic strategy if we can't infer
150
+ lines.append(" @given(st.data())")
151
+
152
+ lines.append(f" def test_property_based_{func.name}(self{', ' + ', '.join(strategies.keys()) if strategies else ', data'}):")
153
+ lines.append(f' """Property-based test for {func.name}()"""')
154
+
155
+ # Add precondition assumptions
156
+ if func.preconditions:
157
+ lines.append(" # Assume preconditions hold")
158
+ for pre in func.preconditions:
159
+ assumption = self._convert_precondition_to_assumption(pre)
160
+ if assumption:
161
+ lines.append(f" {assumption}")
162
+
163
+ # Call the function
164
+ param_names = list(strategies.keys()) if strategies else []
165
+ if param_names:
166
+ lines.append(f" result = {func.name}({', '.join(param_names)})")
167
+ else:
168
+ lines.append(f" # result = {func.name}(...) # TODO: Provide appropriate arguments")
169
+ lines.append(" pass # Remove this when implementing")
170
+ return lines
171
+
172
+ # Assert postconditions
173
+ if func.postconditions:
174
+ lines.append(" # Assert postconditions")
175
+ for post in func.postconditions:
176
+ assertion = self._convert_postcondition_to_assertion(post, "result")
177
+ if assertion:
178
+ lines.append(f" {assertion}")
179
+
180
+ return lines
181
+
182
+ def _generate_precondition_tests(self, func: FunctionAnnotation) -> List[str]:
183
+ """Generate tests that verify preconditions are enforced."""
184
+ lines = []
185
+
186
+ lines.append(" def test_precondition_enforcement(self):")
187
+ lines.append(f' """Test that {func.name}() enforces its preconditions"""')
188
+
189
+ for i, pre in enumerate(func.preconditions):
190
+ lines.append(f" # Precondition {i+1}: {pre}")
191
+
192
+ # Try to generate a violation test
193
+ violation = self._generate_precondition_violation(pre, func.name)
194
+ if violation:
195
+ lines.append(f" # Test violation: {violation}")
196
+
197
+ lines.append(" # TODO: Implement specific precondition violation tests")
198
+ lines.append(" pass")
199
+
200
+ return lines
201
+
202
+ def _generate_postcondition_tests(self, func: FunctionAnnotation) -> List[str]:
203
+ """Generate tests that verify postconditions hold."""
204
+ lines = []
205
+
206
+ lines.append(" def test_postconditions_hold(self):")
207
+ lines.append(f' """Test that postconditions hold after {func.name}() executes"""')
208
+
209
+ for i, post in enumerate(func.postconditions):
210
+ lines.append(f" # Postcondition {i+1}: {post}")
211
+ assertion = self._convert_postcondition_to_assertion(post, "result")
212
+ if assertion:
213
+ lines.append(f" # {assertion}")
214
+
215
+ lines.append(" # TODO: Call function with valid inputs and verify postconditions")
216
+ lines.append(" pass")
217
+
218
+ return lines
219
+
220
+ def _generate_invariant_tests(self, func: FunctionAnnotation) -> List[str]:
221
+ """Generate tests for function invariants."""
222
+ lines = []
223
+
224
+ lines.append(" def test_invariants(self):")
225
+ lines.append(f' """Test that invariants hold throughout {func.name}() execution"""')
226
+
227
+ for i, inv in enumerate(func.invariants):
228
+ lines.append(f" # Invariant {i+1}: {inv}")
229
+
230
+ lines.append(" # TODO: Verify invariants before, during, and after execution")
231
+ lines.append(" pass")
232
+
233
+ return lines
234
+
235
+ def _generate_error_case_tests(self, func: FunctionAnnotation) -> List[str]:
236
+ """Generate tests for error cases."""
237
+ lines = []
238
+
239
+ for i, error in enumerate(func.error_cases):
240
+ error_str = str(error)
241
+
242
+ # Parse error case
243
+ if hasattr(error, 'condition') and hasattr(error, 'error_type'):
244
+ condition = error.condition
245
+ error_type = error.error_type
246
+ elif '→' in error_str:
247
+ parts = error_str.split('→')
248
+ condition = parts[0].strip()
249
+ error_type = parts[1].strip() if len(parts) > 1 else "Exception"
250
+ else:
251
+ condition = error_str
252
+ error_type = "Exception"
253
+
254
+ test_name = f"test_error_case_{i+1}_{self._sanitize_name(condition)}"
255
+ lines.append(f" def {test_name}(self):")
256
+ lines.append(f' """Test error case: {condition}"""')
257
+ lines.append(f" with pytest.raises({error_type}):")
258
+ lines.append(f" # Create condition: {condition}")
259
+ lines.append(f" {func.name}(...) # TODO: Provide arguments that trigger {condition}")
260
+ lines.append("")
261
+
262
+ return lines
263
+
264
+ def _generate_edge_case_tests(self, func: FunctionAnnotation) -> List[str]:
265
+ """Generate edge case tests based on common patterns."""
266
+ lines = []
267
+
268
+ lines.append(" def test_edge_cases(self):")
269
+ lines.append(f' """Test edge cases for {func.name}()"""')
270
+ lines.append(" # Common edge cases to consider:")
271
+ lines.append(" # - Empty inputs (empty strings, empty lists, None)")
272
+ lines.append(" # - Boundary values (0, -1, max int, min int)")
273
+ lines.append(" # - Special values (NaN, Infinity for numbers)")
274
+ lines.append(" # TODO: Implement edge case tests based on function signature")
275
+ lines.append(" pass")
276
+
277
+ return lines
278
+
279
+ def _generate_performance_test(self, func: FunctionAnnotation) -> List[str]:
280
+ """Generate performance/complexity test."""
281
+ lines = []
282
+
283
+ lines.append(" def test_performance(self):")
284
+ lines.append(f' """Test performance characteristics of {func.name}()"""')
285
+ lines.append(f" # Expected complexity: Time={func.complexity.time}, Space={func.complexity.space}")
286
+ lines.append(" import time")
287
+ lines.append(" ")
288
+ lines.append(" # TODO: Implement performance test")
289
+ lines.append(" # Measure execution time for various input sizes")
290
+ lines.append(" # Verify it matches the expected complexity")
291
+ lines.append(" pass")
292
+
293
+ return lines
294
+
295
+ def _generate_happy_path_test(self, func: FunctionAnnotation) -> List[str]:
296
+ """Generate happy path test."""
297
+ lines = []
298
+
299
+ lines.append(" def test_happy_path(self):")
300
+ lines.append(f' """Test normal execution of {func.name}()"""')
301
+
302
+ if func.preconditions:
303
+ lines.append(" # Ensure all preconditions are met:")
304
+ for pre in func.preconditions:
305
+ lines.append(f" # - {pre}")
306
+
307
+ lines.append(f" # result = {func.name}(...) # TODO: Provide valid arguments")
308
+
309
+ if func.postconditions:
310
+ lines.append(" # Verify all postconditions hold:")
311
+ for post in func.postconditions:
312
+ lines.append(f" # - {post}")
313
+
314
+ lines.append(" pass")
315
+
316
+ return lines
317
+
318
+ def _infer_strategies_from_preconditions(self, preconditions: List[str]) -> Dict[str, str]:
319
+ """Infer Hypothesis strategies from preconditions."""
320
+ strategies = {}
321
+
322
+ for pre in preconditions:
323
+ # Pattern: "x > 0 and x < 100" or "0 < x < 100" → x: st.integers(min_value=1, max_value=99)
324
+ if match := re.match(r'(\w+)\s*>\s*(\d+)\s+and\s+\1\s*<\s*(\d+)', pre):
325
+ var_name = match.group(1)
326
+ min_val = int(match.group(2)) + 1
327
+ max_val = int(match.group(3)) - 1
328
+ strategies[var_name] = f"st.integers(min_value={min_val}, max_value={max_val})"
329
+
330
+ # Pattern: "0 < x < 100" (mathematical notation)
331
+ elif match := re.match(r'(\d+)\s*<\s*(\w+)\s*<\s*(\d+)', pre):
332
+ var_name = match.group(2)
333
+ min_val = int(match.group(1)) + 1
334
+ max_val = int(match.group(3)) - 1
335
+ strategies[var_name] = f"st.integers(min_value={min_val}, max_value={max_val})"
336
+
337
+ # Pattern: "x >= 0 and x <= 100"
338
+ elif match := re.match(r'(\w+)\s*>=\s*(\d+)\s+and\s+\1\s*<=\s*(\d+)', pre):
339
+ var_name = match.group(1)
340
+ min_val = int(match.group(2))
341
+ max_val = int(match.group(3))
342
+ strategies[var_name] = f"st.integers(min_value={min_val}, max_value={max_val})"
343
+
344
+ # Pattern: "x in [1, 2, 3]" → x: st.sampled_from([1, 2, 3])
345
+ elif match := re.match(r'(\w+)\s+in\s+\[(.+?)\]', pre):
346
+ var_name = match.group(1)
347
+ values = match.group(2)
348
+ strategies[var_name] = f"st.sampled_from([{values}])"
349
+
350
+ # Pattern: "x > 0" → x: st.integers(min_value=1)
351
+ elif match := re.match(r'(\w+)\s*>\s*(\d+)', pre):
352
+ var_name = match.group(1)
353
+ min_val = int(match.group(2)) + 1
354
+ if var_name not in strategies: # Don't override compound conditions
355
+ strategies[var_name] = f"st.integers(min_value={min_val})"
356
+
357
+ # Pattern: "x >= 0" → x: st.integers(min_value=0)
358
+ elif match := re.match(r'(\w+)\s*>=\s*(\d+)', pre):
359
+ var_name = match.group(1)
360
+ min_val = int(match.group(2))
361
+ if var_name not in strategies:
362
+ strategies[var_name] = f"st.integers(min_value={min_val})"
363
+
364
+ # Pattern: "x < 100" → x: st.integers(max_value=99)
365
+ elif match := re.match(r'(\w+)\s*<\s*(\d+)', pre):
366
+ var_name = match.group(1)
367
+ max_val = int(match.group(2)) - 1
368
+ if var_name not in strategies:
369
+ strategies[var_name] = f"st.integers(max_value={max_val})"
370
+
371
+ # Pattern: "x <= 100" → x: st.integers(max_value=100)
372
+ elif match := re.match(r'(\w+)\s*<=\s*(\d+)', pre):
373
+ var_name = match.group(1)
374
+ max_val = int(match.group(2))
375
+ if var_name not in strategies:
376
+ strategies[var_name] = f"st.integers(max_value={max_val})"
377
+
378
+ # Pattern: "x is string" or "x must be string"
379
+ elif match := re.search(r'(\w+).*(?:is|must be).*string', pre, re.IGNORECASE):
380
+ var_name = match.group(1)
381
+ if var_name not in strategies:
382
+ strategies[var_name] = "st.text()"
383
+
384
+ # Pattern: "x is list" or "x must be list"
385
+ elif match := re.search(r'(\w+).*(?:is|must be).*list', pre, re.IGNORECASE):
386
+ var_name = match.group(1)
387
+ if var_name not in strategies:
388
+ strategies[var_name] = "st.lists(st.integers())"
389
+
390
+ # Pattern: "x is boolean" or "x must be boolean"
391
+ elif match := re.search(r'(\w+).*(?:is|must be).*(?:bool|boolean)', pre, re.IGNORECASE):
392
+ var_name = match.group(1)
393
+ if var_name not in strategies:
394
+ strategies[var_name] = "st.booleans()"
395
+
396
+ # Pattern: "x is float" or "x must be float"
397
+ elif match := re.search(r'(\w+).*(?:is|must be).*(?:float|decimal|number)', pre, re.IGNORECASE):
398
+ var_name = match.group(1)
399
+ if var_name not in strategies:
400
+ strategies[var_name] = "st.floats(allow_nan=False, allow_infinity=False)"
401
+
402
+ return strategies
403
+
404
+ def _convert_precondition_to_assumption(self, precondition: str) -> Optional[str]:
405
+ """Convert a precondition to a Hypothesis assume() statement."""
406
+ # Pattern: "x > 0"
407
+ if match := re.match(r'(\w+)\s*>\s*(\d+)', precondition):
408
+ var_name = match.group(1)
409
+ value = match.group(2)
410
+ return f"assume({var_name} > {value})"
411
+
412
+ # Pattern: "x >= 0"
413
+ elif match := re.match(r'(\w+)\s*>=\s*(\d+)', precondition):
414
+ var_name = match.group(1)
415
+ value = match.group(2)
416
+ return f"assume({var_name} >= {value})"
417
+
418
+ # Pattern: "x is not None" or "x must not be None"
419
+ elif match := re.search(r'(\w+).*(?:is not|must not be).*None', precondition, re.IGNORECASE):
420
+ var_name = match.group(1)
421
+ return f"assume({var_name} is not None)"
422
+
423
+ return None
424
+
425
+ def _convert_postcondition_to_assertion(self, postcondition: str, result_var: str = "result") -> Optional[str]:
426
+ """Convert a postcondition to an assertion."""
427
+ # Pattern: "result > 0"
428
+ if match := re.match(r'result\s*>\s*(\d+)', postcondition):
429
+ value = match.group(1)
430
+ return f"assert {result_var} > {value}"
431
+
432
+ # Pattern: "result >= 0"
433
+ elif match := re.match(r'result\s*>=\s*(\d+)', postcondition):
434
+ value = match.group(1)
435
+ return f"assert {result_var} >= {value}"
436
+
437
+ # Pattern: "returns true" or "result is true"
438
+ elif re.search(r'(?:returns|result is)\s+true', postcondition, re.IGNORECASE):
439
+ return f"assert {result_var} is True"
440
+
441
+ # Pattern: "returns false" or "result is false"
442
+ elif re.search(r'(?:returns|result is)\s+false', postcondition, re.IGNORECASE):
443
+ return f"assert {result_var} is False"
444
+
445
+ # Pattern: "result is not None"
446
+ elif re.search(r'result.*is not None', postcondition, re.IGNORECASE):
447
+ return f"assert {result_var} is not None"
448
+
449
+ # Pattern: "result ⇔ condition" (if and only if)
450
+ elif '⇔' in postcondition or 'if and only if' in postcondition:
451
+ # This is a complex logical condition, generate a comment
452
+ return f"# TODO: Verify: {postcondition}"
453
+
454
+ return None
455
+
456
+ def _generate_precondition_violation(self, precondition: str, func_name: str) -> Optional[str]:
457
+ """Generate code that violates a precondition."""
458
+ # Pattern: "x > 0" → call with x = 0 or x = -1
459
+ if match := re.match(r'(\w+)\s*>\s*(\d+)', precondition):
460
+ var_name = match.group(1)
461
+ value = match.group(2)
462
+ return f"{func_name}({var_name}={value})"
463
+
464
+ # Pattern: "x must not be None" → call with x = None
465
+ elif match := re.search(r'(\w+).*must not be None', precondition, re.IGNORECASE):
466
+ var_name = match.group(1)
467
+ return f"{func_name}({var_name}=None)"
468
+
469
+ return None
470
+
471
+ def _sanitize_name(self, name: str) -> str:
472
+ """Sanitize a string to be a valid Python identifier."""
473
+ # Remove special characters and replace spaces with underscores
474
+ sanitized = re.sub(r'[^a-zA-Z0-9_]', '_', name)
475
+ sanitized = re.sub(r'_+', '_', sanitized)
476
+ return sanitized.strip('_').lower()[:50] # Limit length
477
+
478
+ def _generate_pytest_class_tests(self, cls: ClassAnnotation) -> List[str]:
479
+ """Generate pytest tests for a class."""
480
+ lines = []
481
+
482
+ test_class_name = f"Test{cls.name}"
483
+ lines.append(f"class {test_class_name}:")
484
+ lines.append(f' """Tests for {cls.name}"""')
485
+ lines.append("")
486
+
487
+ # Fixture for class instance
488
+ lines.append(" @pytest.fixture")
489
+ lines.append(" def instance(self):")
490
+ lines.append(f' """Create a {cls.name} instance for testing"""')
491
+ lines.append(f" return {cls.name}() # TODO: Provide appropriate constructor arguments")
492
+ lines.append("")
493
+
494
+ # Test class invariants
495
+ if cls.class_invariants:
496
+ lines.append(" def test_class_invariants(self, instance):")
497
+ lines.append(f' """Test that class invariants always hold"""')
498
+ for i, inv in enumerate(cls.class_invariants):
499
+ lines.append(f" # Invariant {i+1}: {inv}")
500
+ lines.append(" # TODO: Verify invariants hold after construction")
501
+ lines.append(" pass")
502
+ lines.append("")
503
+
504
+ # Test state transitions
505
+ if cls.state_transitions:
506
+ lines.append(" def test_state_transitions(self, instance):")
507
+ lines.append(f' """Test valid state transitions"""')
508
+ for st in cls.state_transitions:
509
+ lines.append(f" # Transition: {st}")
510
+ lines.append(" # TODO: Test each valid state transition")
511
+ lines.append(" pass")
512
+ lines.append("")
513
+
514
+ # Test each method
515
+ for method in cls.methods:
516
+ lines.extend(self._generate_pytest_method_tests(cls, method))
517
+ lines.append("")
518
+
519
+ return lines
520
+
521
+ def _generate_pytest_method_tests(self, cls: ClassAnnotation, method: FunctionAnnotation) -> List[str]:
522
+ """Generate pytest tests for a class method."""
523
+ lines = []
524
+
525
+ # Method test
526
+ test_name = f"test_{method.name}"
527
+ lines.append(f" def {test_name}(self, instance):")
528
+ lines.append(f' """Test {cls.name}.{method.name}()"""')
529
+
530
+ if method.preconditions:
531
+ lines.append(" # Preconditions:")
532
+ for pre in method.preconditions:
533
+ lines.append(f" # - {pre}")
534
+
535
+ lines.append(f" # result = instance.{method.name}(...) # TODO: Provide arguments")
536
+
537
+ if method.postconditions:
538
+ lines.append(" # Postconditions:")
539
+ for post in method.postconditions:
540
+ lines.append(f" # - {post}")
541
+
542
+ lines.append(" pass")
543
+
544
+ return lines
545
+
546
+ def _generate_unittest(self, parsed: ParsedAnnotations, output_file: Optional[str] = None) -> str:
547
+ """Generate unittest test cases."""
548
+ # Keep the existing unittest implementation
549
+ lines = []
550
+
551
+ # Header
552
+ lines.append('"""')
553
+ lines.append(f"Automated tests for {parsed.module.name}")
554
+ lines.append("Generated from @voodocs annotations")
555
+ lines.append('"""')
556
+ lines.append("")
557
+ lines.append("import unittest")
558
+ lines.append(f"from {parsed.module.name} import *")
559
+ lines.append("")
560
+ lines.append("")
561
+
562
+ # Generate test classes
563
+ all_functions = parsed.get_all_functions()
564
+ for func in all_functions:
565
+ test_class_name = f"Test{func.name.replace('_', ' ').title().replace(' ', '')}"
566
+ lines.append(f"class {test_class_name}(unittest.TestCase):")
567
+ lines.append(f' """Tests for {func.name}()"""')
568
+ lines.append("")
569
+ lines.append(" def test_basic(self):")
570
+ lines.append(" # TODO: Implement test")
571
+ lines.append(" pass")
572
+ lines.append("")
573
+
574
+ # Main
575
+ lines.append("")
576
+ lines.append('if __name__ == "__main__":')
577
+ lines.append(" unittest.main()")
578
+
579
+ test_code = "\n".join(lines)
580
+
581
+ if output_file:
582
+ output_path = Path(output_file)
583
+ output_path.parent.mkdir(parents=True, exist_ok=True)
584
+ output_path.write_text(test_code, encoding='utf-8')
585
+
586
+ return test_code
587
+
588
+ def _generate_jest(self, parsed: ParsedAnnotations, output_file: Optional[str] = None) -> str:
589
+ """Generate Jest test cases for JavaScript/TypeScript."""
590
+ # Keep the existing Jest implementation
591
+ lines = []
592
+
593
+ # Header
594
+ lines.append("/**")
595
+ lines.append(f" * Automated tests for {parsed.module.name}")
596
+ lines.append(" * Generated from @voodocs annotations")
597
+ lines.append(" */")
598
+ lines.append("")
599
+ lines.append(f"import * as module from './{parsed.module.name}';")
600
+ lines.append("")
601
+
602
+ # Generate test suites
603
+ all_functions = parsed.get_all_functions()
604
+ for func in all_functions:
605
+ lines.append(f"describe('{func.name}', () => {{")
606
+
607
+ if func.preconditions:
608
+ lines.append(" test('should enforce preconditions', () => {")
609
+ for pre in func.preconditions:
610
+ lines.append(f" // Precondition: {pre}")
611
+ lines.append(" // TODO: Implement test")
612
+ lines.append(" });")
613
+ lines.append("")
614
+
615
+ if func.postconditions:
616
+ lines.append(" test('should satisfy postconditions', () => {")
617
+ for post in func.postconditions:
618
+ lines.append(f" // Postcondition: {post}")
619
+ lines.append(" // TODO: Implement test")
620
+ lines.append(" });")
621
+ lines.append("")
622
+
623
+ lines.append(" test('should work in happy path', () => {")
624
+ lines.append(" // TODO: Implement test")
625
+ lines.append(" });")
626
+ lines.append("});")
627
+ lines.append("")
628
+
629
+ test_code = "\n".join(lines)
630
+
631
+ if output_file:
632
+ output_path = Path(output_file)
633
+ output_path.parent.mkdir(parents=True, exist_ok=True)
634
+ output_path.write_text(test_code, encoding='utf-8')
635
+
636
+ return test_code