@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.
- package/LICENSE +37 -0
- package/README.md +153 -0
- package/USAGE.md +314 -0
- package/cli.py +1340 -0
- package/examples/.cursorrules +437 -0
- package/examples/instructions/.claude/instructions.md +372 -0
- package/examples/instructions/.cursorrules +437 -0
- package/examples/instructions/.windsurfrules +437 -0
- package/examples/instructions/VOODOCS_INSTRUCTIONS.md +437 -0
- package/examples/math_example.py +41 -0
- package/examples/phase2_test.py +24 -0
- package/examples/test_compound_conditions.py +40 -0
- package/examples/test_math_example.py +186 -0
- package/lib/darkarts/README.md +115 -0
- package/lib/darkarts/__init__.py +16 -0
- package/lib/darkarts/annotations/__init__.py +34 -0
- package/lib/darkarts/annotations/parser.py +618 -0
- package/lib/darkarts/annotations/types.py +181 -0
- package/lib/darkarts/cli.py +128 -0
- package/lib/darkarts/core/__init__.py +32 -0
- package/lib/darkarts/core/interface.py +256 -0
- package/lib/darkarts/core/loader.py +231 -0
- package/lib/darkarts/core/plugin.py +215 -0
- package/lib/darkarts/core/registry.py +146 -0
- package/lib/darkarts/exceptions.py +51 -0
- package/lib/darkarts/parsers/typescript/dist/cli.d.ts +9 -0
- package/lib/darkarts/parsers/typescript/dist/cli.d.ts.map +1 -0
- package/lib/darkarts/parsers/typescript/dist/cli.js +69 -0
- package/lib/darkarts/parsers/typescript/dist/cli.js.map +1 -0
- package/lib/darkarts/parsers/typescript/dist/parser.d.ts +111 -0
- package/lib/darkarts/parsers/typescript/dist/parser.d.ts.map +1 -0
- package/lib/darkarts/parsers/typescript/dist/parser.js +365 -0
- package/lib/darkarts/parsers/typescript/dist/parser.js.map +1 -0
- package/lib/darkarts/parsers/typescript/package-lock.json +51 -0
- package/lib/darkarts/parsers/typescript/package.json +19 -0
- package/lib/darkarts/parsers/typescript/src/cli.ts +41 -0
- package/lib/darkarts/parsers/typescript/src/parser.ts +408 -0
- package/lib/darkarts/parsers/typescript/tsconfig.json +19 -0
- package/lib/darkarts/plugins/voodocs/__init__.py +379 -0
- package/lib/darkarts/plugins/voodocs/ai_native_plugin.py +151 -0
- package/lib/darkarts/plugins/voodocs/annotation_validator.py +280 -0
- package/lib/darkarts/plugins/voodocs/api_spec_generator.py +486 -0
- package/lib/darkarts/plugins/voodocs/documentation_generator.py +610 -0
- package/lib/darkarts/plugins/voodocs/html_exporter.py +260 -0
- package/lib/darkarts/plugins/voodocs/instruction_generator.py +706 -0
- package/lib/darkarts/plugins/voodocs/pdf_exporter.py +66 -0
- package/lib/darkarts/plugins/voodocs/test_generator.py +636 -0
- package/package.json +70 -0
- package/requirements.txt +13 -0
- package/templates/ci/github-actions.yml +73 -0
- package/templates/ci/gitlab-ci.yml +35 -0
- 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
|