""" 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: - Scenario: - 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