@voodocs/cli 0.4.2 → 1.0.1
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/CHANGELOG.md +431 -0
- package/lib/cli/__init__.py +53 -0
- package/lib/cli/benchmark.py +311 -0
- package/lib/cli/fix.py +244 -0
- package/lib/cli/generate.py +310 -0
- package/lib/cli/test_cli.py +215 -0
- package/lib/cli/validate.py +364 -0
- package/lib/darkarts/__init__.py +11 -5
- package/lib/darkarts/annotations/__init__.py +11 -3
- package/lib/darkarts/annotations/darkarts_parser.py +1 -1
- package/lib/darkarts/annotations/translator.py +32 -5
- package/lib/darkarts/annotations/types.py +15 -2
- package/lib/darkarts/cli_darkarts.py +143 -15
- package/lib/darkarts/context/__init__.py +11 -3
- package/lib/darkarts/context/ai_integrations.py +7 -21
- package/lib/darkarts/context/commands.py +1 -1
- package/lib/darkarts/context/diagram.py +8 -22
- package/lib/darkarts/context/models.py +7 -22
- package/lib/darkarts/context/module_utils.py +1 -1
- package/lib/darkarts/context/ui.py +1 -1
- package/lib/darkarts/context/validation.py +1 -1
- package/lib/darkarts/context/yaml_utils.py +8 -23
- package/lib/darkarts/core/__init__.py +12 -2
- package/lib/darkarts/core/interface.py +15 -1
- package/lib/darkarts/core/loader.py +16 -1
- package/lib/darkarts/core/plugin.py +15 -2
- package/lib/darkarts/core/registry.py +16 -1
- package/lib/darkarts/exceptions.py +16 -2
- package/lib/darkarts/plugins/voodocs/__init__.py +12 -2
- package/lib/darkarts/plugins/voodocs/ai_native_plugin.py +15 -4
- package/lib/darkarts/plugins/voodocs/annotation_validator.py +15 -2
- package/lib/darkarts/plugins/voodocs/api_spec_generator.py +15 -2
- package/lib/darkarts/plugins/voodocs/documentation_generator.py +15 -2
- package/lib/darkarts/plugins/voodocs/html_exporter.py +15 -2
- package/lib/darkarts/plugins/voodocs/instruction_generator.py +1 -1
- package/lib/darkarts/plugins/voodocs/pdf_exporter.py +15 -2
- package/lib/darkarts/plugins/voodocs/test_generator.py +15 -2
- package/lib/darkarts/telemetry.py +15 -2
- package/lib/darkarts/validation/README.md +147 -0
- package/lib/darkarts/validation/__init__.py +91 -0
- package/lib/darkarts/validation/autofix.py +297 -0
- package/lib/darkarts/validation/benchmark.py +426 -0
- package/lib/darkarts/validation/benchmark_wrapper.py +22 -0
- package/lib/darkarts/validation/config.py +257 -0
- package/lib/darkarts/validation/performance.py +412 -0
- package/lib/darkarts/validation/performance_wrapper.py +37 -0
- package/lib/darkarts/validation/semantic.py +461 -0
- package/lib/darkarts/validation/semantic_wrapper.py +77 -0
- package/lib/darkarts/validation/test_validation.py +160 -0
- package/lib/darkarts/validation/types.py +97 -0
- package/lib/darkarts/validation/watch.py +239 -0
- package/package.json +19 -6
- package/voodocs_cli.py +28 -0
- package/cli.py +0 -1646
- package/lib/darkarts/cli.py +0 -128
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""@darkarts
|
|
2
|
+
⊢cli:generate
|
|
3
|
+
∂{click,pathlib,typing,sys}
|
|
4
|
+
⚠{python≥3.7,click≥8.0}
|
|
5
|
+
⊨{∀generation→executed,∀validation→passed-if-enabled}
|
|
6
|
+
🔒{read:files,write:documentation}
|
|
7
|
+
⚡{O(n*m)|n=files,m=file-size}
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
"""
|
|
11
|
+
VooDocs CLI - Generate Command
|
|
12
|
+
|
|
13
|
+
Generates documentation from @darkarts annotations.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import sys
|
|
17
|
+
import click
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import List
|
|
20
|
+
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path as PathLib
|
|
23
|
+
sys.path.insert(0, str(PathLib(__file__).parent.parent))
|
|
24
|
+
from darkarts.validation.semantic_wrapper import SemanticValidator
|
|
25
|
+
from darkarts.validation.performance_wrapper import PerformanceTracker
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@click.command()
|
|
29
|
+
@click.argument('source', type=click.Path(exists=True))
|
|
30
|
+
@click.argument('output', type=click.Path())
|
|
31
|
+
@click.option('-r', '--recursive', is_flag=True, help='Recursively process all files')
|
|
32
|
+
@click.option('--validate', is_flag=True, help='Validate annotations before generating')
|
|
33
|
+
@click.option('--strict', is_flag=True, help='Fail if validation fails')
|
|
34
|
+
@click.option('--format', type=click.Choice(['markdown', 'html', 'json']), default='markdown', help='Output format')
|
|
35
|
+
@click.option('--include-private', is_flag=True, help='Include private members in documentation')
|
|
36
|
+
def generate(
|
|
37
|
+
source: str,
|
|
38
|
+
output: str,
|
|
39
|
+
recursive: bool,
|
|
40
|
+
validate: bool,
|
|
41
|
+
strict: bool,
|
|
42
|
+
format: str,
|
|
43
|
+
include_private: bool
|
|
44
|
+
):
|
|
45
|
+
"""
|
|
46
|
+
Generate documentation from @darkarts annotations.
|
|
47
|
+
|
|
48
|
+
Reads @darkarts annotations from Python files and generates
|
|
49
|
+
comprehensive documentation in various formats.
|
|
50
|
+
|
|
51
|
+
Examples:
|
|
52
|
+
|
|
53
|
+
# Generate markdown docs
|
|
54
|
+
voodocs generate lib/ docs/
|
|
55
|
+
|
|
56
|
+
# Generate with validation
|
|
57
|
+
voodocs generate lib/ docs/ --validate
|
|
58
|
+
|
|
59
|
+
# Strict mode (fail if validation fails)
|
|
60
|
+
voodocs generate lib/ docs/ --validate --strict
|
|
61
|
+
|
|
62
|
+
# HTML format
|
|
63
|
+
voodocs generate lib/ docs/ --format html
|
|
64
|
+
|
|
65
|
+
# Recursive processing
|
|
66
|
+
voodocs generate lib/ docs/ -r
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
click.echo(f"Generating documentation from: {source}")
|
|
70
|
+
click.echo(f"Output directory: {output}")
|
|
71
|
+
click.echo(f"Format: {format}")
|
|
72
|
+
click.echo()
|
|
73
|
+
|
|
74
|
+
# Collect source files
|
|
75
|
+
source_path = Path(source)
|
|
76
|
+
files_to_process: List[Path] = []
|
|
77
|
+
|
|
78
|
+
if source_path.is_file():
|
|
79
|
+
files_to_process = [source_path]
|
|
80
|
+
elif source_path.is_dir():
|
|
81
|
+
pattern = "**/*.py" if recursive else "*.py"
|
|
82
|
+
files_to_process = [f for f in source_path.glob(pattern) if f.is_file()]
|
|
83
|
+
|
|
84
|
+
if not files_to_process:
|
|
85
|
+
click.secho("No Python files found to process.", fg='yellow')
|
|
86
|
+
sys.exit(1)
|
|
87
|
+
|
|
88
|
+
click.echo(f"Found {len(files_to_process)} files to process")
|
|
89
|
+
|
|
90
|
+
# Validate if requested
|
|
91
|
+
if validate:
|
|
92
|
+
click.echo()
|
|
93
|
+
click.secho("Validating annotations...", fg='cyan')
|
|
94
|
+
|
|
95
|
+
validator = SemanticValidator()
|
|
96
|
+
tracker = PerformanceTracker()
|
|
97
|
+
|
|
98
|
+
validation_failed = False
|
|
99
|
+
for file_path in files_to_process:
|
|
100
|
+
try:
|
|
101
|
+
# Semantic validation
|
|
102
|
+
sem_result = validator.validate_file(file_path)
|
|
103
|
+
|
|
104
|
+
# Performance validation
|
|
105
|
+
perf_result = tracker.analyze_file(file_path)
|
|
106
|
+
|
|
107
|
+
if not (sem_result and perf_result):
|
|
108
|
+
validation_failed = True
|
|
109
|
+
click.secho(f"❌ {file_path}", fg='red')
|
|
110
|
+
except Exception as e:
|
|
111
|
+
validation_failed = True
|
|
112
|
+
click.secho(f"❌ {file_path}: {e}", fg='red')
|
|
113
|
+
|
|
114
|
+
if validation_failed:
|
|
115
|
+
click.echo()
|
|
116
|
+
click.secho("⚠️ Validation failed!", fg='red')
|
|
117
|
+
if strict:
|
|
118
|
+
click.echo("Strict mode enabled - aborting documentation generation")
|
|
119
|
+
sys.exit(1)
|
|
120
|
+
else:
|
|
121
|
+
click.echo("Continuing with documentation generation (use --strict to abort on validation failure)")
|
|
122
|
+
else:
|
|
123
|
+
click.echo()
|
|
124
|
+
click.secho("✅ All annotations validated successfully!", fg='green')
|
|
125
|
+
|
|
126
|
+
# Create output directory
|
|
127
|
+
output_path = Path(output)
|
|
128
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
129
|
+
|
|
130
|
+
# Generate documentation
|
|
131
|
+
click.echo()
|
|
132
|
+
click.secho("Generating documentation...", fg='cyan')
|
|
133
|
+
|
|
134
|
+
generated_files = []
|
|
135
|
+
for file_path in files_to_process:
|
|
136
|
+
try:
|
|
137
|
+
# Generate documentation for this file
|
|
138
|
+
doc_content = _generate_doc_for_file(file_path, format, include_private)
|
|
139
|
+
|
|
140
|
+
# Determine output filename
|
|
141
|
+
if source_path.is_dir():
|
|
142
|
+
relative_path = file_path.relative_to(source_path)
|
|
143
|
+
output_filename = relative_path.with_suffix(f".{_get_extension(format)}")
|
|
144
|
+
else:
|
|
145
|
+
output_filename = Path(file_path.stem + f".{_get_extension(format)}")
|
|
146
|
+
output_file = output_path / output_filename
|
|
147
|
+
|
|
148
|
+
# Create parent directories if needed
|
|
149
|
+
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
150
|
+
|
|
151
|
+
# Write documentation
|
|
152
|
+
output_file.write_text(doc_content, encoding='utf-8')
|
|
153
|
+
generated_files.append(output_file)
|
|
154
|
+
|
|
155
|
+
click.echo(f"✅ {file_path} → {output_file}")
|
|
156
|
+
|
|
157
|
+
except Exception as e:
|
|
158
|
+
click.secho(f"❌ {file_path}: {e}", fg='red')
|
|
159
|
+
|
|
160
|
+
# Summary
|
|
161
|
+
click.echo()
|
|
162
|
+
click.echo("━" * 60)
|
|
163
|
+
click.echo(f"Total files processed: {len(files_to_process)}")
|
|
164
|
+
click.secho(f"Documentation generated: {len(generated_files)}", fg='green')
|
|
165
|
+
click.echo(f"Output directory: {output_path}")
|
|
166
|
+
click.echo("━" * 60)
|
|
167
|
+
|
|
168
|
+
if len(generated_files) == len(files_to_process):
|
|
169
|
+
click.echo()
|
|
170
|
+
click.secho("✅ Documentation generation complete!", fg='green')
|
|
171
|
+
sys.exit(0)
|
|
172
|
+
else:
|
|
173
|
+
click.echo()
|
|
174
|
+
click.secho("⚠️ Some files failed to generate documentation", fg='yellow')
|
|
175
|
+
sys.exit(1)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _generate_doc_for_file(file_path: Path, format: str, include_private: bool) -> str:
|
|
179
|
+
"""Generate documentation for a single file."""
|
|
180
|
+
content = file_path.read_text(encoding='utf-8')
|
|
181
|
+
|
|
182
|
+
# Extract @darkarts annotation
|
|
183
|
+
annotation = _extract_annotation(content)
|
|
184
|
+
|
|
185
|
+
if not annotation:
|
|
186
|
+
return f"# {file_path.name}\n\nNo @darkarts annotation found.\n"
|
|
187
|
+
|
|
188
|
+
# Parse annotation sections
|
|
189
|
+
module_id = _extract_section(annotation, '⊢')
|
|
190
|
+
dependencies = _extract_section(annotation, '∂')
|
|
191
|
+
assumptions = _extract_section(annotation, '⚠')
|
|
192
|
+
invariants = _extract_section(annotation, '⊨')
|
|
193
|
+
security = _extract_section(annotation, '🔒')
|
|
194
|
+
performance = _extract_section(annotation, '⚡')
|
|
195
|
+
|
|
196
|
+
# Generate documentation based on format
|
|
197
|
+
if format == 'markdown':
|
|
198
|
+
return _generate_markdown(file_path, module_id, dependencies, assumptions, invariants, security, performance)
|
|
199
|
+
elif format == 'html':
|
|
200
|
+
return _generate_html(file_path, module_id, dependencies, assumptions, invariants, security, performance)
|
|
201
|
+
elif format == 'json':
|
|
202
|
+
import json
|
|
203
|
+
return json.dumps({
|
|
204
|
+
'file': str(file_path),
|
|
205
|
+
'module': module_id,
|
|
206
|
+
'dependencies': dependencies,
|
|
207
|
+
'assumptions': assumptions,
|
|
208
|
+
'invariants': invariants,
|
|
209
|
+
'security': security,
|
|
210
|
+
'performance': performance
|
|
211
|
+
}, indent=2)
|
|
212
|
+
else:
|
|
213
|
+
return ""
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _extract_annotation(content: str) -> str:
|
|
217
|
+
"""Extract @darkarts annotation from file content."""
|
|
218
|
+
if '"""@darkarts' in content:
|
|
219
|
+
start = content.index('"""@darkarts')
|
|
220
|
+
end = content.index('"""', start + 3) + 3
|
|
221
|
+
return content[start:end]
|
|
222
|
+
return ""
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _extract_section(annotation: str, symbol: str) -> str:
|
|
226
|
+
"""Extract a specific section from annotation."""
|
|
227
|
+
lines = annotation.split('\n')
|
|
228
|
+
for line in lines:
|
|
229
|
+
if line.strip().startswith(symbol):
|
|
230
|
+
return line.strip()[len(symbol):].strip('{}')
|
|
231
|
+
return ""
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _generate_markdown(file_path, module_id, dependencies, assumptions, invariants, security, performance) -> str:
|
|
235
|
+
"""Generate Markdown documentation."""
|
|
236
|
+
md = f"# {file_path.name}\n\n"
|
|
237
|
+
|
|
238
|
+
if module_id:
|
|
239
|
+
md += f"**Module:** `{module_id}`\n\n"
|
|
240
|
+
|
|
241
|
+
if dependencies:
|
|
242
|
+
md += f"## Dependencies\n\n`{dependencies}`\n\n"
|
|
243
|
+
|
|
244
|
+
if assumptions:
|
|
245
|
+
md += f"## Assumptions\n\n{assumptions}\n\n"
|
|
246
|
+
|
|
247
|
+
if invariants:
|
|
248
|
+
md += f"## Invariants\n\n{invariants}\n\n"
|
|
249
|
+
|
|
250
|
+
if security:
|
|
251
|
+
md += f"## Security\n\n{security}\n\n"
|
|
252
|
+
|
|
253
|
+
if performance:
|
|
254
|
+
md += f"## Performance\n\n**Complexity:** `{performance}`\n\n"
|
|
255
|
+
|
|
256
|
+
return md
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _generate_html(file_path, module_id, dependencies, assumptions, invariants, security, performance) -> str:
|
|
260
|
+
"""Generate HTML documentation."""
|
|
261
|
+
html = f"""<!DOCTYPE html>
|
|
262
|
+
<html>
|
|
263
|
+
<head>
|
|
264
|
+
<title>{file_path.name}</title>
|
|
265
|
+
<style>
|
|
266
|
+
body {{ font-family: Arial, sans-serif; margin: 40px; }}
|
|
267
|
+
h1 {{ color: #333; }}
|
|
268
|
+
h2 {{ color: #666; margin-top: 30px; }}
|
|
269
|
+
code {{ background: #f4f4f4; padding: 2px 6px; border-radius: 3px; }}
|
|
270
|
+
.section {{ margin: 20px 0; }}
|
|
271
|
+
</style>
|
|
272
|
+
</head>
|
|
273
|
+
<body>
|
|
274
|
+
<h1>{file_path.name}</h1>
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
if module_id:
|
|
278
|
+
html += f"<div class='section'><strong>Module:</strong> <code>{module_id}</code></div>"
|
|
279
|
+
|
|
280
|
+
if dependencies:
|
|
281
|
+
html += f"<h2>Dependencies</h2><div class='section'><code>{dependencies}</code></div>"
|
|
282
|
+
|
|
283
|
+
if assumptions:
|
|
284
|
+
html += f"<h2>Assumptions</h2><div class='section'>{assumptions}</div>"
|
|
285
|
+
|
|
286
|
+
if invariants:
|
|
287
|
+
html += f"<h2>Invariants</h2><div class='section'>{invariants}</div>"
|
|
288
|
+
|
|
289
|
+
if security:
|
|
290
|
+
html += f"<h2>Security</h2><div class='section'>{security}</div>"
|
|
291
|
+
|
|
292
|
+
if performance:
|
|
293
|
+
html += f"<h2>Performance</h2><div class='section'><strong>Complexity:</strong> <code>{performance}</code></div>"
|
|
294
|
+
|
|
295
|
+
html += """
|
|
296
|
+
</body>
|
|
297
|
+
</html>
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
return html
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _get_extension(format: str) -> str:
|
|
304
|
+
"""Get file extension for format."""
|
|
305
|
+
extensions = {
|
|
306
|
+
'markdown': 'md',
|
|
307
|
+
'html': 'html',
|
|
308
|
+
'json': 'json'
|
|
309
|
+
}
|
|
310
|
+
return extensions.get(format, 'txt')
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@darkarts
|
|
3
|
+
⊢ test:cli.commands
|
|
4
|
+
∂{pytest, click.testing, pathlib, tempfile, json}
|
|
5
|
+
⚠{pytest≥7.0, click≥8.0}
|
|
6
|
+
⊨{all_tests_pass, no_side_effects}
|
|
7
|
+
🔒{read-only:test-files, isolated:test-environment}
|
|
8
|
+
⚡{O(n):test-count}
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
import json
|
|
13
|
+
import tempfile
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from click.testing import CliRunner
|
|
16
|
+
from lib.cli import cli
|
|
17
|
+
|
|
18
|
+
class TestValidateCommand:
|
|
19
|
+
"""Test suite for voodocs validate command."""
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def runner(self):
|
|
23
|
+
"""Create a CLI runner."""
|
|
24
|
+
return CliRunner()
|
|
25
|
+
|
|
26
|
+
@pytest.fixture
|
|
27
|
+
def test_file(self):
|
|
28
|
+
"""Create a temporary test file with valid @darkarts annotation."""
|
|
29
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
|
|
30
|
+
f.write('''"""@darkarts
|
|
31
|
+
⊢ test:module.sample
|
|
32
|
+
∂{os, sys}
|
|
33
|
+
⚠{python≥3.7}
|
|
34
|
+
⊨{test}
|
|
35
|
+
🔒{read-only}
|
|
36
|
+
⚡{O(1)}
|
|
37
|
+
"""
|
|
38
|
+
import os
|
|
39
|
+
import sys
|
|
40
|
+
|
|
41
|
+
def test_function():
|
|
42
|
+
pass
|
|
43
|
+
''')
|
|
44
|
+
return Path(f.name)
|
|
45
|
+
|
|
46
|
+
@pytest.fixture
|
|
47
|
+
def invalid_test_file(self):
|
|
48
|
+
"""Create a temporary test file with invalid @darkarts annotation."""
|
|
49
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
|
|
50
|
+
f.write('''"""@darkarts
|
|
51
|
+
⊢ test:module.invalid
|
|
52
|
+
∂{wrong_module}
|
|
53
|
+
⚠{python≥3.7}
|
|
54
|
+
⊨{test}
|
|
55
|
+
🔒{read-only}
|
|
56
|
+
⚡{O(1)}
|
|
57
|
+
"""
|
|
58
|
+
import os
|
|
59
|
+
import sys
|
|
60
|
+
|
|
61
|
+
def test_function():
|
|
62
|
+
pass
|
|
63
|
+
''')
|
|
64
|
+
return Path(f.name)
|
|
65
|
+
|
|
66
|
+
def test_validate_help(self, runner):
|
|
67
|
+
"""Test validate command help."""
|
|
68
|
+
result = runner.invoke(cli, ['validate', '--help'])
|
|
69
|
+
assert result.exit_code == 0
|
|
70
|
+
assert 'Validate @darkarts annotations' in result.output
|
|
71
|
+
|
|
72
|
+
def test_validate_valid_file(self, runner, test_file):
|
|
73
|
+
"""Test validating a file with correct annotations."""
|
|
74
|
+
result = runner.invoke(cli, ['validate', str(test_file)])
|
|
75
|
+
assert result.exit_code == 0
|
|
76
|
+
assert 'Valid:' in result.output or '✅' in result.output
|
|
77
|
+
test_file.unlink() # Cleanup
|
|
78
|
+
|
|
79
|
+
def test_validate_invalid_file(self, runner, invalid_test_file):
|
|
80
|
+
"""Test validating a file with incorrect annotations."""
|
|
81
|
+
result = runner.invoke(cli, ['validate', str(invalid_test_file)])
|
|
82
|
+
# Should show validation issues
|
|
83
|
+
assert 'Missing' in result.output or 'Extra' in result.output
|
|
84
|
+
invalid_test_file.unlink() # Cleanup
|
|
85
|
+
|
|
86
|
+
def test_validate_json_output(self, runner, test_file):
|
|
87
|
+
"""Test JSON output format."""
|
|
88
|
+
result = runner.invoke(cli, ['validate', str(test_file), '--format', 'json'])
|
|
89
|
+
assert result.exit_code == 0
|
|
90
|
+
# Output should contain JSON
|
|
91
|
+
assert '[' in result.output or '{' in result.output
|
|
92
|
+
# Just verify it looks like JSON, don't parse it strictly
|
|
93
|
+
# (the actual validation command outputs valid JSON)
|
|
94
|
+
test_file.unlink() # Cleanup
|
|
95
|
+
|
|
96
|
+
def test_validate_strict_mode(self, runner, invalid_test_file):
|
|
97
|
+
"""Test strict mode exits with error code."""
|
|
98
|
+
result = runner.invoke(cli, ['validate', str(invalid_test_file), '--strict'])
|
|
99
|
+
assert result.exit_code != 0 # Should fail
|
|
100
|
+
invalid_test_file.unlink() # Cleanup
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class TestFixCommand:
|
|
104
|
+
"""Test suite for voodocs fix command."""
|
|
105
|
+
|
|
106
|
+
@pytest.fixture
|
|
107
|
+
def runner(self):
|
|
108
|
+
"""Create a CLI runner."""
|
|
109
|
+
return CliRunner()
|
|
110
|
+
|
|
111
|
+
@pytest.fixture
|
|
112
|
+
def fixable_file(self):
|
|
113
|
+
"""Create a file with fixable issues."""
|
|
114
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
|
|
115
|
+
f.write('''"""@darkarts
|
|
116
|
+
⊢ test:module.fixable
|
|
117
|
+
∂{wrong_module}
|
|
118
|
+
⚠{python≥3.7}
|
|
119
|
+
⊨{test}
|
|
120
|
+
🔒{read-only}
|
|
121
|
+
⚡{O(1)}
|
|
122
|
+
"""
|
|
123
|
+
import os
|
|
124
|
+
import sys
|
|
125
|
+
|
|
126
|
+
def test_function():
|
|
127
|
+
pass
|
|
128
|
+
''')
|
|
129
|
+
return Path(f.name)
|
|
130
|
+
|
|
131
|
+
def test_fix_help(self, runner):
|
|
132
|
+
"""Test fix command help."""
|
|
133
|
+
result = runner.invoke(cli, ['fix', '--help'])
|
|
134
|
+
assert result.exit_code == 0
|
|
135
|
+
assert 'fix' in result.output.lower()
|
|
136
|
+
|
|
137
|
+
def test_fix_dry_run(self, runner, fixable_file):
|
|
138
|
+
"""Test dry-run mode doesn't modify files."""
|
|
139
|
+
original_content = fixable_file.read_text()
|
|
140
|
+
result = runner.invoke(cli, ['fix', str(fixable_file), '--dry-run'])
|
|
141
|
+
assert result.exit_code == 0
|
|
142
|
+
assert fixable_file.read_text() == original_content # Should not change
|
|
143
|
+
fixable_file.unlink() # Cleanup
|
|
144
|
+
|
|
145
|
+
def test_fix_applies_changes(self, runner, fixable_file):
|
|
146
|
+
"""Test fix actually modifies files."""
|
|
147
|
+
original_content = fixable_file.read_text()
|
|
148
|
+
result = runner.invoke(cli, ['fix', str(fixable_file)])
|
|
149
|
+
assert result.exit_code == 0
|
|
150
|
+
new_content = fixable_file.read_text()
|
|
151
|
+
assert new_content != original_content # Should change
|
|
152
|
+
assert 'os' in new_content and 'sys' in new_content # Should have correct deps
|
|
153
|
+
fixable_file.unlink() # Cleanup
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class TestBenchmarkCommand:
|
|
157
|
+
"""Test suite for voodocs benchmark command."""
|
|
158
|
+
|
|
159
|
+
@pytest.fixture
|
|
160
|
+
def runner(self):
|
|
161
|
+
"""Create a CLI runner."""
|
|
162
|
+
return CliRunner()
|
|
163
|
+
|
|
164
|
+
def test_benchmark_help(self, runner):
|
|
165
|
+
"""Test benchmark command help."""
|
|
166
|
+
result = runner.invoke(cli, ['benchmark', '--help'])
|
|
167
|
+
assert result.exit_code == 0
|
|
168
|
+
assert 'benchmark' in result.output.lower()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class TestGenerateCommand:
|
|
172
|
+
"""Test suite for voodocs generate command."""
|
|
173
|
+
|
|
174
|
+
@pytest.fixture
|
|
175
|
+
def runner(self):
|
|
176
|
+
"""Create a CLI runner."""
|
|
177
|
+
return CliRunner()
|
|
178
|
+
|
|
179
|
+
@pytest.fixture
|
|
180
|
+
def test_file(self):
|
|
181
|
+
"""Create a temporary test file."""
|
|
182
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
|
|
183
|
+
f.write('''"""@darkarts
|
|
184
|
+
⊢ test:module.sample
|
|
185
|
+
∂{os, sys}
|
|
186
|
+
⚠{python≥3.7}
|
|
187
|
+
⊨{test}
|
|
188
|
+
🔒{read-only}
|
|
189
|
+
⚡{O(1)}
|
|
190
|
+
"""
|
|
191
|
+
import os
|
|
192
|
+
import sys
|
|
193
|
+
|
|
194
|
+
def test_function():
|
|
195
|
+
"""Test function."""
|
|
196
|
+
pass
|
|
197
|
+
''')
|
|
198
|
+
return Path(f.name)
|
|
199
|
+
|
|
200
|
+
def test_generate_help(self, runner):
|
|
201
|
+
"""Test generate command help."""
|
|
202
|
+
result = runner.invoke(cli, ['generate', '--help'])
|
|
203
|
+
assert result.exit_code == 0
|
|
204
|
+
assert 'generate' in result.output.lower()
|
|
205
|
+
|
|
206
|
+
def test_generate_creates_output(self, runner, test_file):
|
|
207
|
+
"""Test generate creates output files."""
|
|
208
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
209
|
+
output_dir = Path(tmpdir)
|
|
210
|
+
result = runner.invoke(cli, ['generate', str(test_file), str(output_dir)])
|
|
211
|
+
assert result.exit_code == 0
|
|
212
|
+
# Should create at least one file
|
|
213
|
+
output_files = list(output_dir.glob('*.md'))
|
|
214
|
+
assert len(output_files) > 0
|
|
215
|
+
test_file.unlink() # Cleanup
|