176 lines
4.3 KiB
Python
176 lines
4.3 KiB
Python
"""
|
|
Map tests to Gherkin scenarios based on metadata.
|
|
|
|
Tests can declare their Gherkin metadata via docstrings:
|
|
|
|
```python
|
|
def test_coverage_check(self):
|
|
'''
|
|
Feature: Reservar turno veterinario
|
|
Scenario: Verificar cobertura en zona disponible
|
|
Tags: @smoke @coverage
|
|
'''
|
|
```
|
|
|
|
Or via class docstrings:
|
|
|
|
```python
|
|
class TestCoverageFlow(ContractHTTPTestCase):
|
|
"""
|
|
Feature: Reservar turno veterinario
|
|
Tags: @coverage
|
|
"""
|
|
```
|
|
"""
|
|
|
|
import re
|
|
from typing import Optional
|
|
from dataclasses import dataclass
|
|
|
|
|
|
@dataclass
|
|
class TestGherkinMetadata:
|
|
"""Gherkin metadata extracted from a test."""
|
|
feature: Optional[str] = None
|
|
scenario: Optional[str] = None
|
|
tags: list[str] = None
|
|
|
|
def __post_init__(self):
|
|
if self.tags is None:
|
|
self.tags = []
|
|
|
|
|
|
def extract_gherkin_metadata(docstring: Optional[str]) -> TestGherkinMetadata:
|
|
"""
|
|
Extract Gherkin metadata from a test docstring.
|
|
|
|
Looks for:
|
|
- Feature: <name>
|
|
- Scenario: <name>
|
|
- Tags: @tag1 @tag2
|
|
|
|
Args:
|
|
docstring: Test or class docstring
|
|
|
|
Returns:
|
|
TestGherkinMetadata with extracted info
|
|
"""
|
|
if not docstring:
|
|
return TestGherkinMetadata()
|
|
|
|
# Extract Feature
|
|
feature = None
|
|
feature_match = re.search(r"Feature:\s*(.+)", docstring)
|
|
if feature_match:
|
|
feature = feature_match.group(1).strip()
|
|
|
|
# Extract Scenario (also try Spanish: Escenario)
|
|
scenario = None
|
|
scenario_match = re.search(r"(Scenario|Escenario):\s*(.+)", docstring)
|
|
if scenario_match:
|
|
scenario = scenario_match.group(2).strip()
|
|
|
|
# Extract Tags
|
|
tags = []
|
|
tags_match = re.search(r"Tags:\s*(.+)", docstring)
|
|
if tags_match:
|
|
tags_str = tags_match.group(1).strip()
|
|
tags = re.findall(r"@[\w-]+", tags_str)
|
|
|
|
return TestGherkinMetadata(
|
|
feature=feature,
|
|
scenario=scenario,
|
|
tags=tags
|
|
)
|
|
|
|
|
|
def has_gherkin_metadata(docstring: Optional[str]) -> bool:
|
|
"""Check if a docstring contains Gherkin metadata."""
|
|
if not docstring:
|
|
return False
|
|
|
|
return bool(
|
|
re.search(r"Feature:\s*", docstring) or
|
|
re.search(r"Scenario:\s*", docstring) or
|
|
re.search(r"Escenario:\s*", docstring) or
|
|
re.search(r"Tags:\s*@", docstring)
|
|
)
|
|
|
|
|
|
def match_test_to_feature(
|
|
test_metadata: TestGherkinMetadata,
|
|
feature_names: list[str]
|
|
) -> Optional[str]:
|
|
"""
|
|
Match a test's feature metadata to an actual feature name.
|
|
|
|
Uses fuzzy matching if exact match not found.
|
|
|
|
Args:
|
|
test_metadata: Extracted test metadata
|
|
feature_names: List of available feature names
|
|
|
|
Returns:
|
|
Matched feature name or None
|
|
"""
|
|
if not test_metadata.feature:
|
|
return None
|
|
|
|
# Exact match
|
|
if test_metadata.feature in feature_names:
|
|
return test_metadata.feature
|
|
|
|
# Case-insensitive match
|
|
test_feature_lower = test_metadata.feature.lower()
|
|
for feature_name in feature_names:
|
|
if feature_name.lower() == test_feature_lower:
|
|
return feature_name
|
|
|
|
# Partial match (feature name contains test feature or vice versa)
|
|
for feature_name in feature_names:
|
|
if test_feature_lower in feature_name.lower():
|
|
return feature_name
|
|
if feature_name.lower() in test_feature_lower:
|
|
return feature_name
|
|
|
|
return None
|
|
|
|
|
|
def match_test_to_scenario(
|
|
test_metadata: TestGherkinMetadata,
|
|
scenario_names: list[str]
|
|
) -> Optional[str]:
|
|
"""
|
|
Match a test's scenario metadata to an actual scenario name.
|
|
|
|
Uses fuzzy matching if exact match not found.
|
|
|
|
Args:
|
|
test_metadata: Extracted test metadata
|
|
scenario_names: List of available scenario names
|
|
|
|
Returns:
|
|
Matched scenario name or None
|
|
"""
|
|
if not test_metadata.scenario:
|
|
return None
|
|
|
|
# Exact match
|
|
if test_metadata.scenario in scenario_names:
|
|
return test_metadata.scenario
|
|
|
|
# Case-insensitive match
|
|
test_scenario_lower = test_metadata.scenario.lower()
|
|
for scenario_name in scenario_names:
|
|
if scenario_name.lower() == test_scenario_lower:
|
|
return scenario_name
|
|
|
|
# Partial match
|
|
for scenario_name in scenario_names:
|
|
if test_scenario_lower in scenario_name.lower():
|
|
return scenario_name
|
|
if scenario_name.lower() in test_scenario_lower:
|
|
return scenario_name
|
|
|
|
return None
|