-
Notifications
You must be signed in to change notification settings - Fork 7
docs: generate additional docs #1166
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e9ab576
2ac0e8d
9700fa1
5fade23
3344cbb
768089e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,156 @@ | ||
| #!/usr/bin/env python3 | ||
| """BATS test file to SBDL converter. | ||
|
|
||
| Parses BATS test files and extracts @test definitions along with their | ||
| optional `# bats test_tags=` annotations, producing SBDL `test` elements | ||
| with traceability to requirements via tag-based mapping. | ||
| """ | ||
|
|
||
| import re | ||
| from dataclasses import dataclass, field | ||
| from typing import Dict, List, Optional | ||
|
|
||
| from gherkin_sbdl_converter import to_slug | ||
|
|
||
| try: | ||
| import sbdl | ||
|
|
||
| def sanitize_description(text: str) -> str: | ||
| return sbdl.SBDL_Parser.sanitize(text) | ||
|
|
||
| except ImportError: | ||
| def sanitize_description(text: str) -> str: | ||
| return text.replace('"', '\\"') | ||
|
|
||
|
|
||
| @dataclass | ||
| class BatsTest: | ||
| """Represents a single BATS test case.""" | ||
|
|
||
| name: str | ||
| identifier: str | ||
| tags: List[str] = field(default_factory=list) | ||
| file_path: str = "" | ||
| line_number: int = 0 | ||
|
|
||
| def __post_init__(self): | ||
| if not self.identifier: | ||
| self.identifier = to_slug(self.name) | ||
|
|
||
|
|
||
| class BatsConverter: | ||
| """Converts BATS test files to SBDL test elements.""" | ||
|
|
||
| # Regex patterns for BATS file parsing | ||
| _TAG_PATTERN = re.compile(r"^#\s*bats\s+test_tags\s*=\s*(.+)$", re.IGNORECASE) | ||
| _TEST_PATTERN = re.compile(r'^@test\s+"(.+?)"\s*\{', re.MULTILINE) | ||
|
|
||
| def __init__(self, requirement_tag_map: Optional[Dict[str, str]] = None): | ||
| """Initialize the converter. | ||
|
|
||
| Args: | ||
| requirement_tag_map: Optional mapping from tag names to requirement | ||
| identifiers (SBDL element IDs). When provided, tests with | ||
| matching tags will get a `requirement` relation in SBDL output. | ||
| """ | ||
| self.requirement_tag_map = requirement_tag_map or {} | ||
|
|
||
| def extract_from_bats_file(self, file_path: str, flavor: str = "") -> List[BatsTest]: | ||
| """Extract all test definitions from a BATS file. | ||
|
|
||
| Parses `@test "..."` blocks and any preceding `# bats test_tags=...` | ||
| comment lines. | ||
|
|
||
| Args: | ||
| file_path: Path to the .bats file. | ||
| flavor: Optional identifier prefix for disambiguation | ||
| (e.g. 'cpp', 'rust', 'base'). | ||
|
|
||
| Returns: | ||
| List of BatsTest objects. | ||
| """ | ||
| try: | ||
| with open(file_path, "r", encoding="utf-8") as f: | ||
| lines = f.readlines() | ||
| except (IOError, OSError) as e: | ||
| print(f"Error reading {file_path}: {e}") | ||
| return [] | ||
|
|
||
| tests: List[BatsTest] = [] | ||
| pending_tags: List[str] = [] | ||
|
|
||
| for line_number, line in enumerate(lines, start=1): | ||
| stripped = line.strip() | ||
|
|
||
| # Check for tag annotation | ||
| tag_match = self._TAG_PATTERN.match(stripped) | ||
| if tag_match: | ||
| tags_str = tag_match.group(1) | ||
| pending_tags = [t.strip() for t in tags_str.split(",") if t.strip()] | ||
| continue | ||
|
|
||
| # Check for test definition | ||
| test_match = self._TEST_PATTERN.match(stripped) | ||
| if test_match: | ||
| test_name = test_match.group(1) | ||
| base_id = to_slug(test_name) | ||
| identifier = f"{flavor}-{base_id}" if flavor else base_id | ||
| test = BatsTest( | ||
| name=test_name, | ||
| identifier=identifier, | ||
| tags=list(pending_tags), | ||
| file_path=file_path, | ||
| line_number=line_number, | ||
| ) | ||
| tests.append(test) | ||
| pending_tags = [] | ||
| print(f" Extracted BATS test: {test.identifier}") | ||
| continue | ||
|
|
||
| # Reset pending tags if we hit a non-comment, non-empty line | ||
| # that isn't a test (so tags don't bleed across unrelated lines) | ||
| if stripped and not stripped.startswith("#"): | ||
| pending_tags = [] | ||
|
|
||
| return tests | ||
|
|
||
| _REQ_TAG_PATTERN = re.compile(r'^REQ-', re.IGNORECASE) | ||
|
|
||
| def _add_relation(self, relations: Dict[str, List[str]], elem_type: str, identifier: str): | ||
| """Add a relation entry, creating the type list if needed.""" | ||
| if elem_type not in relations: | ||
| relations[elem_type] = [] | ||
| if identifier not in relations[elem_type]: | ||
| relations[elem_type].append(identifier) | ||
|
|
||
| def _resolve_typed_relations(self, test: BatsTest) -> Dict[str, List[str]]: | ||
| """Resolve typed relations from test tags using the tag map. | ||
|
|
||
| Tags matching the REQ-* pattern are treated as direct requirement | ||
| references. Other tags are resolved through the tag map, which | ||
| groups resolved identifiers by their SBDL element type so the | ||
| correct relation keyword (e.g. 'aspect', 'requirement') is used | ||
| in the SBDL output. | ||
|
|
||
| Args: | ||
| test: A BatsTest with tags. | ||
|
|
||
| Returns: | ||
| Dict mapping SBDL type names to lists of identifiers. | ||
| """ | ||
| relations: Dict[str, List[str]] = {} | ||
| for tag in test.tags: | ||
| # Direct REQ-* tag: use as requirement identifier directly | ||
| if self._REQ_TAG_PATTERN.match(tag): | ||
| self._add_relation(relations, "requirement", to_slug(tag)) | ||
| continue | ||
|
|
||
| tag_lower = tag.lower() | ||
| if tag_lower in self.requirement_tag_map: | ||
| entry = self.requirement_tag_map[tag_lower] | ||
| if isinstance(entry, tuple): | ||
| identifier, elem_type = entry | ||
| else: | ||
| identifier, elem_type = entry, "requirement" | ||
| self._add_relation(relations, elem_type, identifier) | ||
| return relations | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| #!/usr/bin/env python3 | ||
| """Unified converter for generating SBDL from Gherkin feature files and BATS test files. | ||
|
|
||
| Supports multiple output configurations: | ||
| - requirements: Feature/Rule hierarchy as requirements (for SRS) | ||
| - test-specification: Feature→aspect, Rule→requirement, Scenario→test + BATS→test (for test spec & traceability) | ||
|
|
||
| Usage examples: | ||
| # Generate requirements SBDL (existing behavior) | ||
| python generate-sbdl.py --config requirements --gherkin test/cpp/features/*.feature | ||
|
|
||
| # Generate test specification SBDL with both Gherkin scenarios and BATS tests | ||
| python generate-sbdl.py --config test-specification \\ | ||
| --gherkin test/cpp/features/*.feature \\ | ||
| --bats test/cpp/integration-tests.bats test/base/integration-tests.bats | ||
| """ | ||
|
|
||
| import argparse | ||
| import os | ||
| import sys | ||
|
|
||
| from gherkin_mapping_config import FEATURE_RULE_CONFIG, TEST_SPECIFICATION_CONFIG | ||
| from gherkin_sbdl_converter import GherkinConverter, to_slug, write_gherkin_sbdl_elements | ||
| from bats_sbdl_converter import BatsConverter | ||
|
|
||
|
|
||
| def main(): | ||
| configs = { | ||
| "requirements": FEATURE_RULE_CONFIG, | ||
| "test-specification": TEST_SPECIFICATION_CONFIG, | ||
| } | ||
|
|
||
| parser = argparse.ArgumentParser( | ||
| description="Unified Gherkin + BATS to SBDL converter" | ||
| ) | ||
| parser.add_argument( | ||
| "--gherkin", | ||
| nargs="*", | ||
| default=[], | ||
| help="Paths to Gherkin feature files", | ||
| ) | ||
| parser.add_argument( | ||
| "--bats", | ||
| nargs="*", | ||
| default=[], | ||
| help="Paths to BATS test files", | ||
| ) | ||
| parser.add_argument( | ||
| "--output", | ||
| "-o", | ||
| default="output.sbdl", | ||
| help="Output SBDL file", | ||
| ) | ||
| parser.add_argument( | ||
| "--config", | ||
| choices=configs.keys(), | ||
| default="requirements", | ||
| help="Conversion configuration preset", | ||
| ) | ||
| parser.add_argument( | ||
| "--flavor", | ||
| default="", | ||
| help="Container flavor name for per-flavor document generation", | ||
| ) | ||
|
|
||
| args = parser.parse_args() | ||
|
|
||
| if not args.gherkin and not args.bats: | ||
| parser.error("At least one --gherkin or --bats file must be specified") | ||
|
|
||
| config = configs[args.config] | ||
| gherkin_converter = GherkinConverter(config) | ||
| gherkin_elements = [] | ||
|
|
||
| # Process Gherkin feature files | ||
| for feature_path in args.gherkin: | ||
| if os.path.isfile(feature_path): | ||
| print(f"Processing Gherkin: {feature_path}") | ||
| elements = gherkin_converter.extract_from_feature_file(feature_path) | ||
| gherkin_elements.extend(elements) | ||
| else: | ||
| print(f"File not found: {feature_path}", file=sys.stderr) | ||
|
|
||
|
Comment on lines
+75
to
+83
|
||
| # Build requirement tag map from Gherkin elements for BATS traceability. | ||
| # Maps lowercased sanitized identifiers and feature names to (identifier, element_type) | ||
| # tuples, enabling BATS tests tagged with e.g. "Compatibility" to trace to the | ||
| # corresponding Gherkin element with the correct SBDL relation type. | ||
| requirement_tag_map = {} | ||
| for elem in gherkin_elements: | ||
| if elem.metadata and elem.metadata.get("gherkin_type") in ("feature", "rule"): | ||
| entry = (elem.identifier, elem.element_type.value) | ||
| original = elem.metadata.get("original_name", "") | ||
| # Map the original feature/rule name (lowercased) to the SBDL identifier + type | ||
| if original: | ||
| requirement_tag_map[original.lower()] = entry | ||
| # Also map the slugified version for BATS tag matching | ||
| slug_original = to_slug(original) | ||
| if slug_original: | ||
| requirement_tag_map[slug_original] = entry | ||
| # Also map the SBDL identifier itself | ||
| requirement_tag_map[elem.identifier.lower()] = entry | ||
|
|
||
| # Process BATS test files | ||
| bats_converter = BatsConverter(requirement_tag_map=requirement_tag_map) | ||
| bats_tests = [] | ||
|
|
||
| for bats_path in args.bats: | ||
| if os.path.isfile(bats_path): | ||
| print(f"Processing BATS: {bats_path}") | ||
| flavor_prefix = to_slug(os.path.basename(os.path.dirname(os.path.abspath(bats_path)))) | ||
| tests = bats_converter.extract_from_bats_file(bats_path, flavor=flavor_prefix) | ||
| bats_tests.extend(tests) | ||
| else: | ||
| print(f"File not found: {bats_path}", file=sys.stderr) | ||
|
|
||
| # Write combined SBDL output | ||
| _write_combined_sbdl(gherkin_elements, bats_converter, bats_tests, args.output, args.flavor) | ||
|
|
||
| total = len(gherkin_elements) + len(bats_tests) | ||
| print(f"Extracted {total} elements ({len(gherkin_elements)} from Gherkin, {len(bats_tests)} from BATS) to {args.output}") | ||
|
|
||
|
|
||
| def _write_combined_sbdl(gherkin_elements, bats_converter, bats_tests, output_file, flavor=""): | ||
| """Write a single combined SBDL file from both Gherkin and BATS sources.""" | ||
| import sbdl as sbdl_lib | ||
|
|
||
| with open(output_file, "w", encoding="utf-8") as f: | ||
| f.write("#!sbdl\n\n") | ||
|
|
||
| # Write flavor marker element for per-flavor document generation | ||
| if flavor: | ||
| escaped_flavor = sbdl_lib.SBDL_Parser.sanitize(flavor) | ||
| f.write(f'document-flavor is definition {{ description is "{escaped_flavor}" custom:title is "{escaped_flavor}" }}\n\n') | ||
|
|
||
| f.write("# Elements extracted from Gherkin feature files\n") | ||
| write_gherkin_sbdl_elements(f, gherkin_elements) | ||
|
|
||
| if bats_tests: | ||
| f.write("\n# Elements extracted from BATS integration test files\n") | ||
|
|
||
| for test in bats_tests: | ||
| escaped_desc = sbdl_lib.SBDL_Parser.sanitize(test.name) | ||
| identifier = test.identifier | ||
|
|
||
| f.write(f'{identifier} is test {{ description is "{escaped_desc}" custom:title is "{escaped_desc}"') | ||
|
|
||
| if test.tags: | ||
| tag_str = ",".join(test.tags) | ||
| f.write(f" tag is {tag_str}") | ||
|
|
||
| resolved = bats_converter._resolve_typed_relations(test) | ||
| for rel_type, rel_ids in resolved.items(): | ||
| f.write(f" {rel_type} is {','.join(rel_ids)}") | ||
|
|
||
| f.write(" }\n") | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
requirement_tag_mapis annotated asOptional[Dict[str, str]], but the converter actually supports tuple entries(identifier, type)(see_resolve_requirement_relations/_resolve_typed_relations). The current type hint and docstring are inconsistent with runtime behavior; update the annotation (e.g. to aUnion[str, tuple[str, str]]value type) and adjust the docstring accordingly.