Source code for ats_linter.description

"""Copyright (c) 2023 Aydin Abdi.

This module encapsulates the logic of parsing test case test descriptions.
"""

import re
from dataclasses import asdict, dataclass, field
from typing import Any

from loguru import logger

# Disable logger for this module
logger.disable(__name__)

SECTION_VERIFY = "Verify"
SECTION_OBJECTIVE = "Objective"
SECTION_APPROVALS = "Approvals"
SECTION_PRECONDITIONS = "Preconditions"
SECTION_DATA_DRIVEN_TEST = "Data-driven-test"
SECTION_TEST_STEPS = "Test steps"


[docs] @dataclass(frozen=True) class TestDescription: """Represents a test case test description. Parameters ---------- objective: The objective of the test. approvals: The approval criteria of the test. preconditions: The preconditions of the test. None if not provided. data_driven_test: The data-driven test descriptions. None if not provided. test_steps: The steps to execute the test. verify_steps: The steps that verifies test. """ __test__ = False docstring: str objective: str | None = field(default=None) approvals: list[str] = field(default_factory=list) preconditions: dict[int, str] | None = field(default_factory=dict) data_driven_test: list[str] | None = field(default_factory=list) test_steps: dict[int, str] = field(default_factory=dict) verify_steps: dict[int, str] = field(default_factory=dict) def __post_init__(self): """Post init method (no-op, parsing handled by factory).""" def __dict__(self) -> dict[str, Any]: # type: ignore """Return the test description as a dict. Returns: The test description as a dict. """ return asdict(self)
[docs] class TestDescriptionFactory: """Create a TestDescription instance from various sources."""
[docs] @staticmethod def from_docstring(docstring: str) -> TestDescription: """Create a TestDescription from a docstring (compatibility alias).""" return TestDescriptionFactory.dataclass_test_docstring_factory(docstring)
"""Factory class to create a TestDescription instance."""
[docs] @staticmethod def parse_dash_list_section(section: str) -> list[str]: """Parse dash(start with '-') list sections from the docstring. Args: section: The section string to parse. Returns: A list of items parsed from the section. """ return [ item.partition("-")[2].strip() for item in section.split("\n") if item.partition("-")[2].strip() ]
[docs] @staticmethod def parse_numbered_list_section(section: str) -> dict[int, str]: """Parse numbered(1. 2. 3.) list sections from the docstring. Args: section: The section string to parse. Returns: A dictionary with list order as key and item as value. """ return { i + 1: item.partition(".")[2].strip() for i, item in enumerate(section.split("\n")) if item.partition(".")[2].strip() }
[docs] @staticmethod def parse_verify_steps(test_steps: dict[int, str]) -> dict[int, str]: """Parse verify steps from the test steps. Args: test_steps: The test steps to parse. Returns: A dictionary with key of test steps as key and verify step value as value. """ return { key: value for key, value in test_steps.items() if SECTION_VERIFY in value }
[docs] @staticmethod def dataclass_test_docstring_factory(docstring: str) -> TestDescription: """Create a TestDescription instance from a docstring. Args: docstring: The docstring to parse. Returns: :class: `TestDescription` instance. """ if not docstring: docstring = "" section_headers = [ SECTION_OBJECTIVE, SECTION_APPROVALS, SECTION_PRECONDITIONS, SECTION_DATA_DRIVEN_TEST, SECTION_TEST_STEPS, SECTION_VERIFY, ] header_regex = r"^([A-Za-z\- ]+):\s*$" lines = docstring.splitlines() sections = {} current_section = None current_content = [] for line in lines: header_match = re.match(header_regex, line.strip()) if header_match and header_match.group(1) in section_headers: if current_section: sections[current_section] = "\n".join(current_content).rstrip() current_section = header_match.group(1) current_content = [] elif current_section: current_content.append(line) if current_section: sections[current_section] = "\n".join(current_content).rstrip() logger.debug(f"Docstring sections: {sections}") objective = sections.get(SECTION_OBJECTIVE) if objective is not None: objective = objective.strip() approvals = TestDescriptionFactory.parse_dash_list_section( sections.get(SECTION_APPROVALS, ""), ) preconditions = ( TestDescriptionFactory.parse_numbered_list_section( sections.get(SECTION_PRECONDITIONS, ""), ) if SECTION_PRECONDITIONS in sections and sections.get(SECTION_PRECONDITIONS, "") else None ) data_driven_test = ( TestDescriptionFactory.parse_dash_list_section( sections.get(SECTION_DATA_DRIVEN_TEST, ""), ) if SECTION_DATA_DRIVEN_TEST in sections and sections.get(SECTION_DATA_DRIVEN_TEST, "") else None ) test_steps = TestDescriptionFactory.parse_numbered_list_section( sections.get(SECTION_TEST_STEPS, ""), ) # Extract verify steps: prefer dedicated Verify: section, # else extract from Test steps lines containing 'Verify that' if SECTION_VERIFY in sections and sections.get(SECTION_VERIFY, "").strip(): verify_section = sections.get(SECTION_VERIFY, "") verify_steps = TestDescriptionFactory.parse_numbered_list_section( verify_section, ) else: # Extract from test_steps values containing 'Verify that' verify_steps = { k: v for k, v in test_steps.items() if v.strip().startswith("Verify that") } logger.debug(f"Test steps: {test_steps}") logger.debug(f"Verify steps: {verify_steps}") return TestDescription( docstring=docstring, objective=objective, approvals=approvals, preconditions=preconditions, data_driven_test=data_driven_test, test_steps=test_steps, verify_steps=verify_steps, )