code2logic 1.0.0__py3-none-any.whl
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.
- code2logic/__init__.py +88 -0
- code2logic/analyzer.py +286 -0
- code2logic/cli.py +222 -0
- code2logic/dependency.py +246 -0
- code2logic/generators.py +1017 -0
- code2logic/gherkin.py +980 -0
- code2logic/intent.py +246 -0
- code2logic/llm.py +449 -0
- code2logic/mcp_server.py +354 -0
- code2logic/models.py +170 -0
- code2logic/parsers.py +908 -0
- code2logic/py.typed +2 -0
- code2logic/similarity.py +165 -0
- code2logic-1.0.0.dist-info/METADATA +322 -0
- code2logic-1.0.0.dist-info/RECORD +18 -0
- code2logic-1.0.0.dist-info/WHEEL +4 -0
- code2logic-1.0.0.dist-info/entry_points.txt +3 -0
- code2logic-1.0.0.dist-info/licenses/LICENSE +201 -0
code2logic/gherkin.py
ADDED
|
@@ -0,0 +1,980 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Gherkin/BDD Generator for Code2Logic.
|
|
3
|
+
|
|
4
|
+
Generates Gherkin feature files from code analysis for:
|
|
5
|
+
- Ultra-efficient LLM token usage (~50-70x compression vs CSV)
|
|
6
|
+
- Native LLM understanding (trained on millions of .feature files)
|
|
7
|
+
- Automatic test scenario generation
|
|
8
|
+
- BDD-driven development workflow
|
|
9
|
+
|
|
10
|
+
Token efficiency comparison (per 100 functions):
|
|
11
|
+
- CSV full (16 columns): ~16K tokens
|
|
12
|
+
- JSON nested: ~12K tokens
|
|
13
|
+
- Gherkin: ~300 tokens = 50x compression
|
|
14
|
+
|
|
15
|
+
LLM Accuracy by format (models <30B):
|
|
16
|
+
- Gherkin: 95% accuracy
|
|
17
|
+
- YAML: 90% accuracy
|
|
18
|
+
- JSON: 75% accuracy
|
|
19
|
+
- Raw Python: 25% accuracy
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
from code2logic.gherkin import GherkinGenerator, CucumberYAMLGenerator
|
|
23
|
+
|
|
24
|
+
generator = GherkinGenerator()
|
|
25
|
+
features = generator.generate(project)
|
|
26
|
+
|
|
27
|
+
yaml_gen = CucumberYAMLGenerator()
|
|
28
|
+
cucumber_yaml = yaml_gen.generate(project)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from typing import List, Dict, Optional, Set, Any
|
|
32
|
+
from collections import defaultdict
|
|
33
|
+
from dataclasses import dataclass, field
|
|
34
|
+
import re
|
|
35
|
+
import hashlib
|
|
36
|
+
|
|
37
|
+
from .models import ProjectInfo, ModuleInfo, FunctionInfo, ClassInfo
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class GherkinScenario:
|
|
42
|
+
"""Represents a single Gherkin scenario."""
|
|
43
|
+
name: str
|
|
44
|
+
given: List[str]
|
|
45
|
+
when: List[str]
|
|
46
|
+
then: List[str]
|
|
47
|
+
tags: List[str]
|
|
48
|
+
examples: Optional[List[Dict[str, str]]] = None
|
|
49
|
+
data_table: Optional[List[Dict[str, str]]] = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class GherkinFeature:
|
|
54
|
+
"""Represents a Gherkin feature file."""
|
|
55
|
+
name: str
|
|
56
|
+
description: str
|
|
57
|
+
tags: List[str]
|
|
58
|
+
scenarios: List[GherkinScenario]
|
|
59
|
+
background: Optional[List[str]] = None
|
|
60
|
+
rules: Optional[List[Dict[str, Any]]] = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class StepDefinition:
|
|
65
|
+
"""Represents a step definition."""
|
|
66
|
+
pattern: str
|
|
67
|
+
step_type: str # given, when, then
|
|
68
|
+
function_name: str
|
|
69
|
+
params: List[str]
|
|
70
|
+
implementation_hint: str
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class GherkinGenerator:
|
|
74
|
+
"""
|
|
75
|
+
Generates Gherkin feature files from code analysis.
|
|
76
|
+
|
|
77
|
+
Achieves ~50x token compression compared to CSV while
|
|
78
|
+
preserving full semantic information for LLM processing.
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
>>> from code2logic import analyze_project
|
|
82
|
+
>>> from code2logic.gherkin import GherkinGenerator
|
|
83
|
+
>>>
|
|
84
|
+
>>> project = analyze_project("/path/to/project")
|
|
85
|
+
>>> generator = GherkinGenerator()
|
|
86
|
+
>>> features = generator.generate(project)
|
|
87
|
+
>>> print(features) # Gherkin feature files
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
# Category to Gherkin verb mapping (action, passive, assertion)
|
|
91
|
+
CATEGORY_VERBS = {
|
|
92
|
+
'create': ('creates', 'is created', 'should exist'),
|
|
93
|
+
'read': ('retrieves', 'is retrieved', 'should return data'),
|
|
94
|
+
'update': ('updates', 'is updated', 'should be modified'),
|
|
95
|
+
'delete': ('deletes', 'is deleted', 'should not exist'),
|
|
96
|
+
'validate': ('validates', 'is validated', 'should be valid'),
|
|
97
|
+
'transform': ('transforms', 'is transformed', 'should be converted'),
|
|
98
|
+
'lifecycle': ('initializes', 'is started', 'should be ready'),
|
|
99
|
+
'communicate': ('sends', 'is sent', 'should be delivered'),
|
|
100
|
+
'other': ('processes', 'is processed', 'should complete'),
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# Domain to business context mapping
|
|
104
|
+
DOMAIN_CONTEXTS = {
|
|
105
|
+
'auth': 'authentication and authorization',
|
|
106
|
+
'user': 'user management',
|
|
107
|
+
'order': 'order processing',
|
|
108
|
+
'payment': 'payment gateway',
|
|
109
|
+
'product': 'product catalog',
|
|
110
|
+
'cart': 'shopping cart',
|
|
111
|
+
'config': 'configuration management',
|
|
112
|
+
'api': 'API endpoints',
|
|
113
|
+
'service': 'business services',
|
|
114
|
+
'model': 'data models',
|
|
115
|
+
'validation': 'input validation',
|
|
116
|
+
'generator': 'code generation',
|
|
117
|
+
'parser': 'parsing and analysis',
|
|
118
|
+
'test': 'testing utilities',
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# Gherkin keywords by language
|
|
122
|
+
KEYWORDS = {
|
|
123
|
+
'en': {
|
|
124
|
+
'feature': 'Feature',
|
|
125
|
+
'scenario': 'Scenario',
|
|
126
|
+
'scenario_outline': 'Scenario Outline',
|
|
127
|
+
'given': 'Given',
|
|
128
|
+
'when': 'When',
|
|
129
|
+
'then': 'Then',
|
|
130
|
+
'and': 'And',
|
|
131
|
+
'but': 'But',
|
|
132
|
+
'background': 'Background',
|
|
133
|
+
'examples': 'Examples',
|
|
134
|
+
'rule': 'Rule',
|
|
135
|
+
},
|
|
136
|
+
'pl': {
|
|
137
|
+
'feature': 'Funkcja',
|
|
138
|
+
'scenario': 'Scenariusz',
|
|
139
|
+
'scenario_outline': 'Szablon scenariusza',
|
|
140
|
+
'given': 'Zakładając',
|
|
141
|
+
'when': 'Jeżeli',
|
|
142
|
+
'then': 'Wtedy',
|
|
143
|
+
'and': 'Oraz',
|
|
144
|
+
'but': 'Ale',
|
|
145
|
+
'background': 'Założenia',
|
|
146
|
+
'examples': 'Przykłady',
|
|
147
|
+
'rule': 'Reguła',
|
|
148
|
+
},
|
|
149
|
+
'de': {
|
|
150
|
+
'feature': 'Funktionalität',
|
|
151
|
+
'scenario': 'Szenario',
|
|
152
|
+
'scenario_outline': 'Szenariovorlage',
|
|
153
|
+
'given': 'Angenommen',
|
|
154
|
+
'when': 'Wenn',
|
|
155
|
+
'then': 'Dann',
|
|
156
|
+
'and': 'Und',
|
|
157
|
+
'but': 'Aber',
|
|
158
|
+
'background': 'Grundlage',
|
|
159
|
+
'examples': 'Beispiele',
|
|
160
|
+
'rule': 'Regel',
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
def __init__(self, language: str = 'en'):
|
|
165
|
+
"""
|
|
166
|
+
Initialize GherkinGenerator.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
language: Language for Gherkin keywords ('en', 'pl', 'de')
|
|
170
|
+
"""
|
|
171
|
+
self.language = language
|
|
172
|
+
self.keywords = self.KEYWORDS.get(language, self.KEYWORDS['en'])
|
|
173
|
+
self._step_registry: Dict[str, StepDefinition] = {}
|
|
174
|
+
|
|
175
|
+
def generate(self, project: ProjectInfo, detail: str = 'standard',
|
|
176
|
+
group_by: str = 'domain') -> str:
|
|
177
|
+
"""
|
|
178
|
+
Generate Gherkin feature files from project analysis.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
project: ProjectInfo from code2logic analysis
|
|
182
|
+
detail: 'minimal', 'standard', or 'full'
|
|
183
|
+
group_by: 'domain', 'category', or 'module'
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Gherkin feature file content
|
|
187
|
+
"""
|
|
188
|
+
features = self._extract_features(project, group_by)
|
|
189
|
+
return self._render_features(features, detail)
|
|
190
|
+
|
|
191
|
+
def generate_test_scenarios(self, project: ProjectInfo,
|
|
192
|
+
group_by: str = 'domain') -> List[GherkinFeature]:
|
|
193
|
+
"""
|
|
194
|
+
Generate structured test scenarios for programmatic use.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
project: ProjectInfo from code2logic analysis
|
|
198
|
+
group_by: Grouping strategy
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
List of GherkinFeature objects
|
|
202
|
+
"""
|
|
203
|
+
return self._extract_features(project, group_by)
|
|
204
|
+
|
|
205
|
+
def get_step_definitions(self) -> List[StepDefinition]:
|
|
206
|
+
"""Get all unique step definitions from generated features."""
|
|
207
|
+
return list(self._step_registry.values())
|
|
208
|
+
|
|
209
|
+
def _extract_features(self, project: ProjectInfo,
|
|
210
|
+
group_by: str) -> List[GherkinFeature]:
|
|
211
|
+
"""Extract Gherkin features from project."""
|
|
212
|
+
# Collect all functions/methods with metadata
|
|
213
|
+
elements = []
|
|
214
|
+
|
|
215
|
+
for module in project.modules:
|
|
216
|
+
domain = self._extract_domain(module.path)
|
|
217
|
+
|
|
218
|
+
for func in module.functions:
|
|
219
|
+
elements.append({
|
|
220
|
+
'module': module,
|
|
221
|
+
'function': func,
|
|
222
|
+
'type': 'function',
|
|
223
|
+
'domain': domain,
|
|
224
|
+
'category': self._categorize(func.name),
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
for cls in module.classes:
|
|
228
|
+
for method in cls.methods:
|
|
229
|
+
elements.append({
|
|
230
|
+
'module': module,
|
|
231
|
+
'class': cls,
|
|
232
|
+
'function': method,
|
|
233
|
+
'type': 'method',
|
|
234
|
+
'domain': domain,
|
|
235
|
+
'category': self._categorize(method.name),
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
# Group elements
|
|
239
|
+
if group_by == 'domain':
|
|
240
|
+
groups = defaultdict(list)
|
|
241
|
+
for elem in elements:
|
|
242
|
+
groups[elem['domain']].append(elem)
|
|
243
|
+
elif group_by == 'category':
|
|
244
|
+
groups = defaultdict(list)
|
|
245
|
+
for elem in elements:
|
|
246
|
+
groups[elem['category']].append(elem)
|
|
247
|
+
else: # module
|
|
248
|
+
groups = defaultdict(list)
|
|
249
|
+
for elem in elements:
|
|
250
|
+
groups[elem['module'].path].append(elem)
|
|
251
|
+
|
|
252
|
+
# Create features
|
|
253
|
+
features = []
|
|
254
|
+
for group_name, items in groups.items():
|
|
255
|
+
feature = self._create_feature(group_name, items, project, group_by)
|
|
256
|
+
if feature.scenarios:
|
|
257
|
+
features.append(feature)
|
|
258
|
+
|
|
259
|
+
return features
|
|
260
|
+
|
|
261
|
+
def _create_feature(self, group_name: str, items: List[dict],
|
|
262
|
+
project: ProjectInfo, group_by: str) -> GherkinFeature:
|
|
263
|
+
"""Create a Gherkin feature from grouped items."""
|
|
264
|
+
# Determine context
|
|
265
|
+
if group_by == 'domain':
|
|
266
|
+
context = self.DOMAIN_CONTEXTS.get(group_name, f'{group_name} functionality')
|
|
267
|
+
feature_name = f"{group_name.title()} {context.title()}"
|
|
268
|
+
elif group_by == 'category':
|
|
269
|
+
feature_name = f"{group_name.title()} Operations"
|
|
270
|
+
context = f"All {group_name} operations in the system"
|
|
271
|
+
else:
|
|
272
|
+
feature_name = f"Module: {group_name}"
|
|
273
|
+
context = f"Tests for {group_name}"
|
|
274
|
+
|
|
275
|
+
# Group by category for scenarios
|
|
276
|
+
category_groups = defaultdict(list)
|
|
277
|
+
for item in items:
|
|
278
|
+
category_groups[item['category']].append(item)
|
|
279
|
+
|
|
280
|
+
scenarios = []
|
|
281
|
+
for category, cat_items in category_groups.items():
|
|
282
|
+
# Create main scenario
|
|
283
|
+
scenario = self._create_scenario(category, cat_items, group_name)
|
|
284
|
+
scenarios.append(scenario)
|
|
285
|
+
|
|
286
|
+
# Add edge case scenarios for 'full' detail
|
|
287
|
+
edge_scenarios = self._create_edge_case_scenarios(category, cat_items)
|
|
288
|
+
scenarios.extend(edge_scenarios)
|
|
289
|
+
|
|
290
|
+
# Feature tags
|
|
291
|
+
tags = [f'@{group_name}']
|
|
292
|
+
if any(i['function'].is_async for i in items):
|
|
293
|
+
tags.append('@async')
|
|
294
|
+
if len(items) > 20:
|
|
295
|
+
tags.append('@large')
|
|
296
|
+
|
|
297
|
+
# Background (common setup)
|
|
298
|
+
background = self._create_background(group_name, items)
|
|
299
|
+
|
|
300
|
+
return GherkinFeature(
|
|
301
|
+
name=feature_name,
|
|
302
|
+
description=f"BDD tests for {context} in {project.name}\n Generated by code2logic",
|
|
303
|
+
tags=tags,
|
|
304
|
+
scenarios=scenarios,
|
|
305
|
+
background=background,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
def _create_scenario(self, category: str, items: List[dict],
|
|
309
|
+
domain: str) -> GherkinScenario:
|
|
310
|
+
"""Create a scenario from category items."""
|
|
311
|
+
verbs = self.CATEGORY_VERBS.get(category, self.CATEGORY_VERBS['other'])
|
|
312
|
+
|
|
313
|
+
# Extract function info
|
|
314
|
+
func_names = [i['function'].name for i in items[:10]]
|
|
315
|
+
intents = [i['function'].intent for i in items if i['function'].intent][:5]
|
|
316
|
+
|
|
317
|
+
# Build scenario steps
|
|
318
|
+
given = []
|
|
319
|
+
when = []
|
|
320
|
+
then = []
|
|
321
|
+
|
|
322
|
+
# Given: Setup context
|
|
323
|
+
given.append(f"the {domain} system is initialized")
|
|
324
|
+
if items[0].get('class'):
|
|
325
|
+
given.append(f"a {items[0]['class'].name} instance exists")
|
|
326
|
+
|
|
327
|
+
# When: Actions based on functions
|
|
328
|
+
for item in items[:3]:
|
|
329
|
+
func = item['function']
|
|
330
|
+
step = self._create_when_step(func, verbs[0])
|
|
331
|
+
when.append(step)
|
|
332
|
+
self._register_step('when', step, func)
|
|
333
|
+
|
|
334
|
+
# Then: Assertions
|
|
335
|
+
then.append(f"the operation {verbs[2]}")
|
|
336
|
+
if intents:
|
|
337
|
+
then.append(f"the result matches expected behavior")
|
|
338
|
+
|
|
339
|
+
# Tags
|
|
340
|
+
tags = [f'@{category}']
|
|
341
|
+
if any(i['function'].is_async for i in items):
|
|
342
|
+
tags.append('@async')
|
|
343
|
+
if any(i['function'].lines > 50 for i in items):
|
|
344
|
+
tags.append('@complex')
|
|
345
|
+
if len(items) > 10:
|
|
346
|
+
tags.append('@parametrized')
|
|
347
|
+
|
|
348
|
+
# Examples table (Scenario Outline)
|
|
349
|
+
examples = self._create_examples_table(items)
|
|
350
|
+
|
|
351
|
+
return GherkinScenario(
|
|
352
|
+
name=f"{category.title()} operations ({len(items)} functions)",
|
|
353
|
+
given=given,
|
|
354
|
+
when=when,
|
|
355
|
+
then=then,
|
|
356
|
+
tags=tags,
|
|
357
|
+
examples=examples if len(items) > 1 else None,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
def _create_edge_case_scenarios(self, category: str,
|
|
361
|
+
items: List[dict]) -> List[GherkinScenario]:
|
|
362
|
+
"""Create edge case scenarios for thorough testing."""
|
|
363
|
+
scenarios = []
|
|
364
|
+
|
|
365
|
+
# Error handling scenario
|
|
366
|
+
if category in ('create', 'update', 'delete'):
|
|
367
|
+
scenarios.append(GherkinScenario(
|
|
368
|
+
name=f"{category.title()} with invalid input",
|
|
369
|
+
given=[f"the system is initialized", "invalid input data is prepared"],
|
|
370
|
+
when=[f"user attempts to {category} with invalid data"],
|
|
371
|
+
then=["the operation should fail gracefully", "appropriate error is returned"],
|
|
372
|
+
tags=[f'@{category}', '@negative', '@error-handling'],
|
|
373
|
+
))
|
|
374
|
+
|
|
375
|
+
# Async scenario
|
|
376
|
+
async_items = [i for i in items if i['function'].is_async]
|
|
377
|
+
if async_items:
|
|
378
|
+
scenarios.append(GherkinScenario(
|
|
379
|
+
name=f"Async {category} operations",
|
|
380
|
+
given=["the async runtime is initialized"],
|
|
381
|
+
when=[f"async {category} operation is triggered"],
|
|
382
|
+
then=["the operation completes asynchronously", "no deadlocks occur"],
|
|
383
|
+
tags=[f'@{category}', '@async', '@concurrency'],
|
|
384
|
+
))
|
|
385
|
+
|
|
386
|
+
return scenarios
|
|
387
|
+
|
|
388
|
+
def _create_when_step(self, func: FunctionInfo, verb: str) -> str:
|
|
389
|
+
"""Create a When step from function info."""
|
|
390
|
+
params = self._extract_param_placeholders(func)
|
|
391
|
+
|
|
392
|
+
if func.intent:
|
|
393
|
+
# Use intent for natural language step
|
|
394
|
+
intent_clean = func.intent.lower().rstrip('.')
|
|
395
|
+
if params:
|
|
396
|
+
return f"user {intent_clean} with {params}"
|
|
397
|
+
return f"user {intent_clean}"
|
|
398
|
+
else:
|
|
399
|
+
# Fallback to function name
|
|
400
|
+
name_readable = self._name_to_readable(func.name)
|
|
401
|
+
if params:
|
|
402
|
+
return f"user {verb} {name_readable} with {params}"
|
|
403
|
+
return f"user calls {func.name}"
|
|
404
|
+
|
|
405
|
+
def _create_background(self, domain: str,
|
|
406
|
+
items: List[dict]) -> Optional[List[str]]:
|
|
407
|
+
"""Create background steps for common setup."""
|
|
408
|
+
background = [f"the {domain} module is loaded"]
|
|
409
|
+
|
|
410
|
+
# Check for common imports
|
|
411
|
+
all_imports = set()
|
|
412
|
+
for item in items:
|
|
413
|
+
all_imports.update(item['module'].imports[:5])
|
|
414
|
+
|
|
415
|
+
if 'logging' in all_imports or 'logger' in all_imports:
|
|
416
|
+
background.append("logging is configured")
|
|
417
|
+
|
|
418
|
+
if 'config' in all_imports or 'settings' in all_imports:
|
|
419
|
+
background.append("configuration is loaded")
|
|
420
|
+
|
|
421
|
+
return background if len(background) > 1 else None
|
|
422
|
+
|
|
423
|
+
def _create_examples_table(self, items: List[dict]) -> List[Dict[str, str]]:
|
|
424
|
+
"""Create Examples table for Scenario Outline."""
|
|
425
|
+
examples = []
|
|
426
|
+
|
|
427
|
+
for item in items[:10]:
|
|
428
|
+
func = item['function']
|
|
429
|
+
example = {
|
|
430
|
+
'function': func.name,
|
|
431
|
+
'params': ','.join(func.params[:3]) or 'none',
|
|
432
|
+
'returns': func.return_type or 'void',
|
|
433
|
+
'async': 'yes' if func.is_async else 'no',
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
# Add intent as description
|
|
437
|
+
if func.intent:
|
|
438
|
+
example['description'] = func.intent[:40]
|
|
439
|
+
|
|
440
|
+
examples.append(example)
|
|
441
|
+
|
|
442
|
+
return examples
|
|
443
|
+
|
|
444
|
+
def _extract_param_placeholders(self, func: FunctionInfo) -> str:
|
|
445
|
+
"""Extract parameter placeholders for Gherkin steps."""
|
|
446
|
+
params = []
|
|
447
|
+
for p in func.params[:3]:
|
|
448
|
+
name = p.split(':')[0].strip()
|
|
449
|
+
if name and name not in ('self', 'cls'):
|
|
450
|
+
params.append(f'"<{name}>"')
|
|
451
|
+
return ', '.join(params)
|
|
452
|
+
|
|
453
|
+
def _register_step(self, step_type: str, pattern: str, func: FunctionInfo):
|
|
454
|
+
"""Register a step definition for later generation."""
|
|
455
|
+
# Normalize pattern
|
|
456
|
+
pattern_key = re.sub(r'<\w+>', '{param}', pattern)
|
|
457
|
+
|
|
458
|
+
if pattern_key not in self._step_registry:
|
|
459
|
+
params = re.findall(r'<(\w+)>', pattern)
|
|
460
|
+
self._step_registry[pattern_key] = StepDefinition(
|
|
461
|
+
pattern=pattern,
|
|
462
|
+
step_type=step_type,
|
|
463
|
+
function_name=self._step_to_func_name(pattern),
|
|
464
|
+
params=params,
|
|
465
|
+
implementation_hint=func.intent or f"Implement {func.name}",
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
def _render_features(self, features: List[GherkinFeature],
|
|
469
|
+
detail: str) -> str:
|
|
470
|
+
"""Render features to Gherkin text."""
|
|
471
|
+
output = []
|
|
472
|
+
|
|
473
|
+
# Add header comment
|
|
474
|
+
output.append("# Auto-generated by code2logic")
|
|
475
|
+
output.append("# Token-efficient BDD specification (~50x compression vs CSV)")
|
|
476
|
+
output.append("")
|
|
477
|
+
|
|
478
|
+
for feature in features:
|
|
479
|
+
feature_text = self._render_feature(feature, detail)
|
|
480
|
+
output.append(feature_text)
|
|
481
|
+
|
|
482
|
+
return '\n'.join(output)
|
|
483
|
+
|
|
484
|
+
def _render_feature(self, feature: GherkinFeature, detail: str) -> str:
|
|
485
|
+
"""Render a single feature."""
|
|
486
|
+
lines = []
|
|
487
|
+
|
|
488
|
+
# Tags
|
|
489
|
+
if feature.tags:
|
|
490
|
+
lines.append(' '.join(feature.tags))
|
|
491
|
+
|
|
492
|
+
# Feature header
|
|
493
|
+
lines.append(f"{self.keywords['feature']}: {feature.name}")
|
|
494
|
+
if feature.description and detail != 'minimal':
|
|
495
|
+
for desc_line in feature.description.split('\n'):
|
|
496
|
+
lines.append(f" {desc_line}")
|
|
497
|
+
lines.append("")
|
|
498
|
+
|
|
499
|
+
# Background
|
|
500
|
+
if feature.background and detail == 'full':
|
|
501
|
+
lines.append(f" {self.keywords['background']}:")
|
|
502
|
+
for step in feature.background:
|
|
503
|
+
lines.append(f" {self.keywords['given']} {step}")
|
|
504
|
+
lines.append("")
|
|
505
|
+
|
|
506
|
+
# Scenarios
|
|
507
|
+
for scenario in feature.scenarios:
|
|
508
|
+
scenario_text = self._render_scenario(scenario, detail)
|
|
509
|
+
lines.append(scenario_text)
|
|
510
|
+
|
|
511
|
+
return '\n'.join(lines)
|
|
512
|
+
|
|
513
|
+
def _render_scenario(self, scenario: GherkinScenario, detail: str) -> str:
|
|
514
|
+
"""Render a single scenario."""
|
|
515
|
+
lines = []
|
|
516
|
+
|
|
517
|
+
# Tags
|
|
518
|
+
if scenario.tags:
|
|
519
|
+
tags_to_show = scenario.tags[:3] if detail == 'minimal' else scenario.tags
|
|
520
|
+
lines.append(f" {' '.join(tags_to_show)}")
|
|
521
|
+
|
|
522
|
+
# Scenario header
|
|
523
|
+
keyword = self.keywords['scenario_outline'] if scenario.examples else self.keywords['scenario']
|
|
524
|
+
lines.append(f" {keyword}: {scenario.name}")
|
|
525
|
+
|
|
526
|
+
# Given
|
|
527
|
+
for i, step in enumerate(scenario.given):
|
|
528
|
+
kw = self.keywords['given'] if i == 0 else self.keywords['and']
|
|
529
|
+
lines.append(f" {kw} {step}")
|
|
530
|
+
|
|
531
|
+
# When
|
|
532
|
+
max_when = 2 if detail == 'minimal' else 5
|
|
533
|
+
for i, step in enumerate(scenario.when[:max_when]):
|
|
534
|
+
kw = self.keywords['when'] if i == 0 else self.keywords['and']
|
|
535
|
+
lines.append(f" {kw} {step}")
|
|
536
|
+
|
|
537
|
+
# Then
|
|
538
|
+
for i, step in enumerate(scenario.then):
|
|
539
|
+
kw = self.keywords['then'] if i == 0 else self.keywords['and']
|
|
540
|
+
lines.append(f" {kw} {step}")
|
|
541
|
+
|
|
542
|
+
# Examples
|
|
543
|
+
if scenario.examples and detail != 'minimal':
|
|
544
|
+
lines.append("")
|
|
545
|
+
lines.append(f" {self.keywords['examples']}:")
|
|
546
|
+
|
|
547
|
+
headers = list(scenario.examples[0].keys())
|
|
548
|
+
lines.append(f" | {' | '.join(headers)} |")
|
|
549
|
+
|
|
550
|
+
max_examples = 5 if detail == 'standard' else 15
|
|
551
|
+
for example in scenario.examples[:max_examples]:
|
|
552
|
+
values = [str(example.get(h, ''))[:20] for h in headers]
|
|
553
|
+
lines.append(f" | {' | '.join(values)} |")
|
|
554
|
+
|
|
555
|
+
lines.append("")
|
|
556
|
+
return '\n'.join(lines)
|
|
557
|
+
|
|
558
|
+
def _categorize(self, name: str) -> str:
|
|
559
|
+
"""Categorize function by name pattern."""
|
|
560
|
+
name_lower = name.lower()
|
|
561
|
+
|
|
562
|
+
patterns = {
|
|
563
|
+
'create': ('create', 'add', 'insert', 'new', 'make', 'build', 'generate'),
|
|
564
|
+
'read': ('get', 'fetch', 'find', 'load', 'read', 'query', 'list', 'search'),
|
|
565
|
+
'update': ('update', 'set', 'modify', 'edit', 'patch', 'change'),
|
|
566
|
+
'delete': ('delete', 'remove', 'clear', 'destroy', 'drop'),
|
|
567
|
+
'validate': ('validate', 'check', 'verify', 'is', 'has', 'can', 'ensure'),
|
|
568
|
+
'transform': ('convert', 'transform', 'parse', 'format', 'to', 'from', 'encode', 'decode'),
|
|
569
|
+
'lifecycle': ('init', 'setup', 'configure', 'start', 'stop', 'close', 'dispose'),
|
|
570
|
+
'communicate': ('send', 'emit', 'notify', 'publish', 'broadcast', 'dispatch'),
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
for cat, verbs in patterns.items():
|
|
574
|
+
if any(v in name_lower for v in verbs):
|
|
575
|
+
return cat
|
|
576
|
+
|
|
577
|
+
return 'other'
|
|
578
|
+
|
|
579
|
+
def _extract_domain(self, path: str) -> str:
|
|
580
|
+
"""Extract domain from file path."""
|
|
581
|
+
parts = path.lower().replace('\\', '/').split('/')
|
|
582
|
+
|
|
583
|
+
for part in parts:
|
|
584
|
+
for domain in self.DOMAIN_CONTEXTS.keys():
|
|
585
|
+
if domain in part:
|
|
586
|
+
return domain
|
|
587
|
+
|
|
588
|
+
return parts[-2] if len(parts) > 1 else 'core'
|
|
589
|
+
|
|
590
|
+
def _name_to_readable(self, name: str) -> str:
|
|
591
|
+
"""Convert function name to readable text."""
|
|
592
|
+
# Handle snake_case
|
|
593
|
+
name = name.replace('_', ' ')
|
|
594
|
+
# Handle camelCase/PascalCase
|
|
595
|
+
name = re.sub(r'([a-z])([A-Z])', r'\1 \2', name)
|
|
596
|
+
return name.lower()
|
|
597
|
+
|
|
598
|
+
def _step_to_func_name(self, step: str) -> str:
|
|
599
|
+
"""Convert step text to valid function name."""
|
|
600
|
+
name = re.sub(r'[^\w\s]', '', step.lower())
|
|
601
|
+
name = re.sub(r'\s+', '_', name.strip())
|
|
602
|
+
return name[:50]
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
class StepDefinitionGenerator:
|
|
606
|
+
"""
|
|
607
|
+
Generates step definition stubs from Gherkin features.
|
|
608
|
+
|
|
609
|
+
Supports multiple frameworks:
|
|
610
|
+
- pytest-bdd (Python)
|
|
611
|
+
- behave (Python)
|
|
612
|
+
- Cucumber.js (JavaScript)
|
|
613
|
+
- Cucumber-JVM (Java)
|
|
614
|
+
"""
|
|
615
|
+
|
|
616
|
+
def generate_pytest_bdd(self, features: List[GherkinFeature]) -> str:
|
|
617
|
+
"""Generate pytest-bdd step definitions."""
|
|
618
|
+
lines = [
|
|
619
|
+
'"""',
|
|
620
|
+
'Auto-generated step definitions from code2logic.',
|
|
621
|
+
'',
|
|
622
|
+
'Install: pip install pytest-bdd',
|
|
623
|
+
'Run: pytest --bdd',
|
|
624
|
+
'"""',
|
|
625
|
+
'',
|
|
626
|
+
'import pytest',
|
|
627
|
+
'from pytest_bdd import given, when, then, scenario, parsers',
|
|
628
|
+
'from pytest_bdd import scenarios',
|
|
629
|
+
'',
|
|
630
|
+
'# Load all feature files',
|
|
631
|
+
"scenarios('../features/')",
|
|
632
|
+
'',
|
|
633
|
+
'',
|
|
634
|
+
'# Fixtures',
|
|
635
|
+
'@pytest.fixture',
|
|
636
|
+
'def context():',
|
|
637
|
+
' """Shared context for BDD steps."""',
|
|
638
|
+
' return {}',
|
|
639
|
+
'',
|
|
640
|
+
]
|
|
641
|
+
|
|
642
|
+
# Collect unique steps
|
|
643
|
+
steps = {'given': set(), 'when': set(), 'then': set()}
|
|
644
|
+
|
|
645
|
+
for feature in features:
|
|
646
|
+
for scenario in feature.scenarios:
|
|
647
|
+
steps['given'].update(scenario.given)
|
|
648
|
+
steps['when'].update(scenario.when)
|
|
649
|
+
steps['then'].update(scenario.then)
|
|
650
|
+
|
|
651
|
+
# Generate step functions
|
|
652
|
+
for step_type, step_set in steps.items():
|
|
653
|
+
lines.append(f'# {step_type.upper()} steps')
|
|
654
|
+
lines.append('')
|
|
655
|
+
|
|
656
|
+
decorator = step_type
|
|
657
|
+
for step in sorted(step_set):
|
|
658
|
+
func_name = self._step_to_func_name(step)
|
|
659
|
+
|
|
660
|
+
# Handle parameterized steps
|
|
661
|
+
if '<' in step:
|
|
662
|
+
pattern = re.sub(r'"<(\w+)>"', r'"{\\1}"', step)
|
|
663
|
+
pattern = re.sub(r'<(\w+)>', r'{\\1}', pattern)
|
|
664
|
+
params = re.findall(r'{(\w+)}', pattern)
|
|
665
|
+
|
|
666
|
+
lines.append(f'@{decorator}(parsers.parse(\'{pattern}\'))')
|
|
667
|
+
lines.append(f'def {func_name}(context, {", ".join(params)}):')
|
|
668
|
+
else:
|
|
669
|
+
lines.append(f'@{decorator}(\'{step}\')')
|
|
670
|
+
lines.append(f'def {func_name}(context):')
|
|
671
|
+
|
|
672
|
+
lines.append(f' """Step: {step}"""')
|
|
673
|
+
lines.append(' # TODO: Implement')
|
|
674
|
+
lines.append(' pass')
|
|
675
|
+
lines.append('')
|
|
676
|
+
|
|
677
|
+
return '\n'.join(lines)
|
|
678
|
+
|
|
679
|
+
def generate_behave(self, features: List[GherkinFeature]) -> str:
|
|
680
|
+
"""Generate behave step definitions."""
|
|
681
|
+
lines = [
|
|
682
|
+
'"""',
|
|
683
|
+
'Auto-generated step definitions for behave.',
|
|
684
|
+
'',
|
|
685
|
+
'Install: pip install behave',
|
|
686
|
+
'Run: behave',
|
|
687
|
+
'"""',
|
|
688
|
+
'',
|
|
689
|
+
'from behave import given, when, then, step',
|
|
690
|
+
'',
|
|
691
|
+
]
|
|
692
|
+
|
|
693
|
+
steps = {'given': set(), 'when': set(), 'then': set()}
|
|
694
|
+
for feature in features:
|
|
695
|
+
for scenario in feature.scenarios:
|
|
696
|
+
steps['given'].update(scenario.given)
|
|
697
|
+
steps['when'].update(scenario.when)
|
|
698
|
+
steps['then'].update(scenario.then)
|
|
699
|
+
|
|
700
|
+
for step_type, step_set in steps.items():
|
|
701
|
+
for step in sorted(step_set):
|
|
702
|
+
func_name = self._step_to_func_name(step)
|
|
703
|
+
|
|
704
|
+
if '<' in step:
|
|
705
|
+
pattern = re.sub(r'"<(\w+)>"', r'{\\1}', step)
|
|
706
|
+
pattern = re.sub(r'<(\w+)>', r'{\\1}', pattern)
|
|
707
|
+
lines.append(f'@{step_type}(\'{pattern}\')')
|
|
708
|
+
lines.append(f'def {func_name}(context, **kwargs):')
|
|
709
|
+
else:
|
|
710
|
+
lines.append(f'@{step_type}(\'{step}\')')
|
|
711
|
+
lines.append(f'def {func_name}(context):')
|
|
712
|
+
|
|
713
|
+
lines.append(f' """Step: {step}"""')
|
|
714
|
+
lines.append(' pass')
|
|
715
|
+
lines.append('')
|
|
716
|
+
|
|
717
|
+
return '\n'.join(lines)
|
|
718
|
+
|
|
719
|
+
def generate_cucumber_js(self, features: List[GherkinFeature]) -> str:
|
|
720
|
+
"""Generate Cucumber.js step definitions."""
|
|
721
|
+
lines = [
|
|
722
|
+
'/**',
|
|
723
|
+
' * Auto-generated step definitions for Cucumber.js',
|
|
724
|
+
' *',
|
|
725
|
+
' * Install: npm install @cucumber/cucumber',
|
|
726
|
+
' * Run: npx cucumber-js',
|
|
727
|
+
' */',
|
|
728
|
+
'',
|
|
729
|
+
"const { Given, When, Then, Before, After } = require('@cucumber/cucumber');",
|
|
730
|
+
'',
|
|
731
|
+
'// Context object',
|
|
732
|
+
'let context = {};',
|
|
733
|
+
'',
|
|
734
|
+
'Before(function() {',
|
|
735
|
+
' context = {};',
|
|
736
|
+
'});',
|
|
737
|
+
'',
|
|
738
|
+
]
|
|
739
|
+
|
|
740
|
+
steps = {'Given': set(), 'When': set(), 'Then': set()}
|
|
741
|
+
for feature in features:
|
|
742
|
+
for scenario in feature.scenarios:
|
|
743
|
+
steps['Given'].update(scenario.given)
|
|
744
|
+
steps['When'].update(scenario.when)
|
|
745
|
+
steps['Then'].update(scenario.then)
|
|
746
|
+
|
|
747
|
+
for step_type, step_set in steps.items():
|
|
748
|
+
for step in sorted(step_set):
|
|
749
|
+
if '<' in step:
|
|
750
|
+
pattern = re.sub(r'"<(\w+)>"', r'{string}', step)
|
|
751
|
+
pattern = re.sub(r'<(\w+)>', r'{word}', pattern)
|
|
752
|
+
params = ['param' + str(i) for i in range(step.count('<'))]
|
|
753
|
+
lines.append(f'{step_type}(\'{pattern}\', function({", ".join(params)}) {{')
|
|
754
|
+
else:
|
|
755
|
+
lines.append(f'{step_type}(\'{step}\', function() {{')
|
|
756
|
+
|
|
757
|
+
lines.append(f' // TODO: Implement')
|
|
758
|
+
lines.append('});')
|
|
759
|
+
lines.append('')
|
|
760
|
+
|
|
761
|
+
return '\n'.join(lines)
|
|
762
|
+
|
|
763
|
+
def _step_to_func_name(self, step: str) -> str:
|
|
764
|
+
"""Convert step text to valid function name."""
|
|
765
|
+
name = re.sub(r'[^\w\s]', '', step.lower())
|
|
766
|
+
name = re.sub(r'\s+', '_', name.strip())
|
|
767
|
+
return name[:50]
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
class CucumberYAMLGenerator:
|
|
771
|
+
"""
|
|
772
|
+
Generates Cucumber YAML configuration and test data.
|
|
773
|
+
|
|
774
|
+
YAML format provides ~5x token compression with 90% LLM accuracy.
|
|
775
|
+
"""
|
|
776
|
+
|
|
777
|
+
def generate(self, project: ProjectInfo, detail: str = 'standard') -> str:
|
|
778
|
+
"""Generate Cucumber YAML configuration."""
|
|
779
|
+
# Collect test data
|
|
780
|
+
test_suites = defaultdict(list)
|
|
781
|
+
|
|
782
|
+
for module in project.modules:
|
|
783
|
+
domain = self._extract_domain(module.path)
|
|
784
|
+
|
|
785
|
+
for func in module.functions:
|
|
786
|
+
test_suites[domain].append({
|
|
787
|
+
'name': func.name,
|
|
788
|
+
'type': 'function',
|
|
789
|
+
'intent': func.intent or '',
|
|
790
|
+
'params': func.params,
|
|
791
|
+
'returns': func.return_type or 'void',
|
|
792
|
+
'async': func.is_async,
|
|
793
|
+
})
|
|
794
|
+
|
|
795
|
+
for cls in module.classes:
|
|
796
|
+
for method in cls.methods:
|
|
797
|
+
test_suites[domain].append({
|
|
798
|
+
'name': f"{cls.name}.{method.name}",
|
|
799
|
+
'type': 'method',
|
|
800
|
+
'class': cls.name,
|
|
801
|
+
'intent': method.intent or '',
|
|
802
|
+
'params': method.params,
|
|
803
|
+
'returns': method.return_type or 'void',
|
|
804
|
+
'async': method.is_async,
|
|
805
|
+
})
|
|
806
|
+
|
|
807
|
+
# Build YAML structure
|
|
808
|
+
yaml_lines = [
|
|
809
|
+
'# Cucumber Test Configuration',
|
|
810
|
+
'# Generated by code2logic',
|
|
811
|
+
'',
|
|
812
|
+
'cucumber:',
|
|
813
|
+
f' project: {project.name}',
|
|
814
|
+
f' total_tests: {sum(len(v) for v in test_suites.values())}',
|
|
815
|
+
'',
|
|
816
|
+
'test_suites:',
|
|
817
|
+
]
|
|
818
|
+
|
|
819
|
+
for domain, tests in test_suites.items():
|
|
820
|
+
yaml_lines.append(f' {domain}:')
|
|
821
|
+
yaml_lines.append(f' count: {len(tests)}')
|
|
822
|
+
yaml_lines.append(' tests:')
|
|
823
|
+
|
|
824
|
+
# Group by category
|
|
825
|
+
categories = defaultdict(list)
|
|
826
|
+
for test in tests:
|
|
827
|
+
cat = self._categorize(test['name'])
|
|
828
|
+
categories[cat].append(test)
|
|
829
|
+
|
|
830
|
+
for cat, cat_tests in categories.items():
|
|
831
|
+
yaml_lines.append(f' {cat}:')
|
|
832
|
+
for test in cat_tests[:10 if detail == 'standard' else 20]:
|
|
833
|
+
yaml_lines.append(f' - name: {test["name"]}')
|
|
834
|
+
if test['intent'] and detail != 'minimal':
|
|
835
|
+
yaml_lines.append(f' intent: "{test["intent"][:50]}"')
|
|
836
|
+
if detail == 'full':
|
|
837
|
+
yaml_lines.append(f' params: [{", ".join(test["params"][:3])}]')
|
|
838
|
+
yaml_lines.append(f' returns: {test["returns"]}')
|
|
839
|
+
if test['async']:
|
|
840
|
+
yaml_lines.append(' async: true')
|
|
841
|
+
|
|
842
|
+
return '\n'.join(yaml_lines)
|
|
843
|
+
|
|
844
|
+
def _extract_domain(self, path: str) -> str:
|
|
845
|
+
"""Extract domain from path."""
|
|
846
|
+
parts = path.lower().replace('\\', '/').split('/')
|
|
847
|
+
domains = ['auth', 'user', 'order', 'payment', 'api', 'service',
|
|
848
|
+
'model', 'validation', 'generator', 'parser', 'test']
|
|
849
|
+
|
|
850
|
+
for part in parts:
|
|
851
|
+
for domain in domains:
|
|
852
|
+
if domain in part:
|
|
853
|
+
return domain
|
|
854
|
+
|
|
855
|
+
return parts[-2] if len(parts) > 1 else 'core'
|
|
856
|
+
|
|
857
|
+
def _categorize(self, name: str) -> str:
|
|
858
|
+
"""Categorize by name pattern."""
|
|
859
|
+
name_lower = name.lower().split('.')[-1]
|
|
860
|
+
|
|
861
|
+
if any(v in name_lower for v in ('get', 'fetch', 'find', 'read')):
|
|
862
|
+
return 'read'
|
|
863
|
+
if any(v in name_lower for v in ('create', 'add', 'new')):
|
|
864
|
+
return 'create'
|
|
865
|
+
if any(v in name_lower for v in ('update', 'set', 'modify')):
|
|
866
|
+
return 'update'
|
|
867
|
+
if any(v in name_lower for v in ('delete', 'remove')):
|
|
868
|
+
return 'delete'
|
|
869
|
+
if any(v in name_lower for v in ('validate', 'check', 'is')):
|
|
870
|
+
return 'validate'
|
|
871
|
+
|
|
872
|
+
return 'other'
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
def csv_to_gherkin(csv_content: str, language: str = 'en') -> str:
|
|
876
|
+
"""
|
|
877
|
+
Convert CSV analysis directly to Gherkin.
|
|
878
|
+
|
|
879
|
+
This achieves ~50x token compression:
|
|
880
|
+
- CSV (16 columns): ~16K tokens per 100 functions
|
|
881
|
+
- Gherkin: ~300 tokens per 100 functions
|
|
882
|
+
|
|
883
|
+
Args:
|
|
884
|
+
csv_content: CSV content from CSVGenerator
|
|
885
|
+
language: Gherkin language ('en', 'pl', 'de')
|
|
886
|
+
|
|
887
|
+
Returns:
|
|
888
|
+
Gherkin feature file content
|
|
889
|
+
"""
|
|
890
|
+
import csv
|
|
891
|
+
from io import StringIO
|
|
892
|
+
|
|
893
|
+
keywords = GherkinGenerator.KEYWORDS.get(language, GherkinGenerator.KEYWORDS['en'])
|
|
894
|
+
|
|
895
|
+
reader = csv.DictReader(StringIO(csv_content))
|
|
896
|
+
rows = list(reader)
|
|
897
|
+
|
|
898
|
+
# Group by domain
|
|
899
|
+
domains = defaultdict(list)
|
|
900
|
+
for row in rows:
|
|
901
|
+
domain = row.get('domain', 'core')
|
|
902
|
+
domains[domain].append(row)
|
|
903
|
+
|
|
904
|
+
output = [
|
|
905
|
+
"# Auto-generated by code2logic",
|
|
906
|
+
f"# Language: {language}",
|
|
907
|
+
f"# Source: {len(rows)} elements → ~{len(rows) * 3} tokens (vs ~{len(rows) * 160} in CSV)",
|
|
908
|
+
"",
|
|
909
|
+
]
|
|
910
|
+
|
|
911
|
+
for domain, items in domains.items():
|
|
912
|
+
output.append(f"@{domain}")
|
|
913
|
+
output.append(f"{keywords['feature']}: {domain.title()} Domain")
|
|
914
|
+
output.append(f" BDD tests for {domain} functionality")
|
|
915
|
+
output.append("")
|
|
916
|
+
|
|
917
|
+
# Group by category
|
|
918
|
+
categories = defaultdict(list)
|
|
919
|
+
for item in items:
|
|
920
|
+
cat = item.get('category', 'other')
|
|
921
|
+
categories[cat].append(item)
|
|
922
|
+
|
|
923
|
+
for category, cat_items in categories.items():
|
|
924
|
+
output.append(f" @{category}")
|
|
925
|
+
output.append(f" {keywords['scenario_outline']}: {category.title()} operations")
|
|
926
|
+
output.append(f" {keywords['given']} the {domain} system is ready")
|
|
927
|
+
output.append(f" {keywords['when']} user calls <function>")
|
|
928
|
+
output.append(f" {keywords['then']} operation completes successfully")
|
|
929
|
+
output.append("")
|
|
930
|
+
output.append(f" {keywords['examples']}:")
|
|
931
|
+
output.append(" | function | intent |")
|
|
932
|
+
|
|
933
|
+
for item in cat_items[:15]:
|
|
934
|
+
name = item.get('name', '')[:25]
|
|
935
|
+
intent = item.get('intent', '')[:35]
|
|
936
|
+
output.append(f" | {name} | {intent} |")
|
|
937
|
+
|
|
938
|
+
output.append("")
|
|
939
|
+
|
|
940
|
+
return '\n'.join(output)
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
def gherkin_to_test_data(gherkin_content: str) -> Dict[str, Any]:
|
|
944
|
+
"""
|
|
945
|
+
Extract structured test data from Gherkin for LLM processing.
|
|
946
|
+
|
|
947
|
+
Returns a minimal JSON structure that preserves all test semantics
|
|
948
|
+
while achieving maximum token efficiency.
|
|
949
|
+
"""
|
|
950
|
+
features = []
|
|
951
|
+
current_feature = None
|
|
952
|
+
current_scenario = None
|
|
953
|
+
|
|
954
|
+
for line in gherkin_content.split('\n'):
|
|
955
|
+
line = line.strip()
|
|
956
|
+
|
|
957
|
+
if line.startswith('Feature:'):
|
|
958
|
+
current_feature = {
|
|
959
|
+
'name': line[8:].strip(),
|
|
960
|
+
'scenarios': []
|
|
961
|
+
}
|
|
962
|
+
features.append(current_feature)
|
|
963
|
+
|
|
964
|
+
elif line.startswith('Scenario') and current_feature:
|
|
965
|
+
current_scenario = {
|
|
966
|
+
'name': line.split(':', 1)[1].strip() if ':' in line else '',
|
|
967
|
+
'steps': []
|
|
968
|
+
}
|
|
969
|
+
current_feature['scenarios'].append(current_scenario)
|
|
970
|
+
|
|
971
|
+
elif current_scenario and any(line.startswith(kw) for kw in
|
|
972
|
+
['Given', 'When', 'Then', 'And', 'But']):
|
|
973
|
+
current_scenario['steps'].append(line)
|
|
974
|
+
|
|
975
|
+
return {
|
|
976
|
+
'features': features,
|
|
977
|
+
'total_scenarios': sum(len(f['scenarios']) for f in features),
|
|
978
|
+
'compression': '50x vs CSV'
|
|
979
|
+
}
|
|
980
|
+
|