orchestrata_tools.test_generator.behavioral_tests

Reference › orchestrata_tools › test_generator › behavioral_tests

BehavioralTestGenerator derives test cases from the structure of a method's docstring. It is used by combined_generator and re-exported by the package. tests/test_orchestrata_tools.py and tests/test_orchestrata_tools_test_generator.py cover it.

_extract_raises and _extract_args parse the Raises: and Args: sections with regular expressions into (name, description) pairs; a comment records that the Raises pattern is greedy so every entry of a multi-exception docstring is captured. generate_from_docstring emits pytest.raises tests for each documented exception, and generate_parametrized emits parametrized tests from the documented arguments. A method with no docstring gives nothing. Input is a MethodInfo from ast_models.

At a glance

Source src/orchestrata_tools/test_generator/behavioral_tests.py
Lines of code 106
Classes 1
Functions 0
Imports (internal) orchestrata_tools.models.ast_models
Imported by orchestrata_tools.test_generator, orchestrata_tools.test_generator.combined_generator
Tests that import it tests/test_orchestrata_tools.py, tests/test_orchestrata_tools_test_generator.py

Class diagram

Class diagram orchestrata_tools.test_generator.behavioral_tests

Dependency diagram

Dependency diagram orchestrata_tools.test_generator.behavioral_tests

API

Classes

BehavioralTestGenerator

Line 16

Generate behavioral tests from method docstrings.

Method Signature Summary
_extract_raises _extract_raises(docstring: str | None) -> list[tuple[str, str]] Extract (exception_type, description) from Raises section.
_extract_args _extract_args(docstring: str | None) -> list[tuple[str, str]] Extract (arg_name, description) from Args section.
generate_from_docstring generate_from_docstring(method: MethodInfo) -> str Generate pytest.raises() tests from Raises clauses.
generate_parametrized generate_parametrized(method: MethodInfo) -> str Generate parametrized tests from Args clauses.

Source

Permalink: https://gitlab.com/MSamy/orchestrata-ultimate-architecture/-/blob/a4cbc2bdf/src/orchestrata_tools/test_generator/behavioral_tests.py

"""Generate behavioral tests from docstring Raises/Returns/Args clauses.

Uses docstring structure to infer expected behavior and generate
pytest test cases.
"""

from __future__ import annotations

import re
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from orchestrata_tools.models.ast_models import MethodInfo


class BehavioralTestGenerator:
    """Generate behavioral tests from method docstrings."""

    def _extract_raises(self, docstring: str | None) -> list[tuple[str, str]]:
        """Extract (exception_type, description) from Raises section."""
        if not docstring:
            return []
        result = []
        # Match "Raises:" or "Raises:" followed by bullet list. Greedy `.*`
        # (matching branch_tests.py's equivalent regex) so the repeated
        # group consumes every subsequent line under the section instead of
        # stopping after the first — a non-greedy `.*?` here silently
        # dropped every entry but the first in a multi-exception docstring.
        section = re.search(
            r"Raises?:\s*\n?((?:\s*[-*]?\s*\w+.*\n?)+)",
            docstring,
            re.IGNORECASE,
        )
        if section:
            for line in section.group(1).split("\n"):
                match = re.match(r"\s*[-*]?\s*(\w+Error|\w+Exception):?\s*(.*)", line)
                if match:
                    result.append((match.group(1), match.group(2)))
        return result

    def _extract_args(self, docstring: str | None) -> list[tuple[str, str]]:
        """Extract (arg_name, description) from Args section."""
        if not docstring:
            return []
        result = []
        # Greedy `.*` — see the matching note in _extract_raises above.
        section = re.search(
            r"Args?:\s*\n?((?:\s*[-*]?\s*\w+.*\n?)+)",
            docstring,
            re.IGNORECASE,
        )
        if section:
            for line in section.group(1).split("\n"):
                match = re.match(r"\s*[-*]?\s*(\w+)[\s:(].*", line)
                if match:
                    result.append((match.group(1), line.strip()))
        return result

    def generate_from_docstring(self, method: MethodInfo) -> str:
        """Generate pytest.raises() tests from Raises clauses."""
        raises = self._extract_raises(method.docstring)
        if not raises:
            return ""

        lines = [
            f"class Test{method.class_name or ''}{method.name.title()}Raises:",
            f'    """Exception tests for {method.class_name}.{method.name}()."""',
            "",
        ]

        for exc_type, desc in raises:
            test_name = f"test_raises_{exc_type.lower()}"
            lines.extend(
                [
                    f"    def {test_name}(self):",
                    f'        """{desc or f"Should raise {exc_type}"}."""',
                    f"        with pytest.raises({exc_type}):",
                    f"            # TODO: trigger {exc_type}",
                    "            pass",
                    "",
                ]
            )

        return "\n".join(lines)

    def generate_parametrized(self, method: MethodInfo) -> str:
        """Generate parametrized tests from Args clauses."""
        args = self._extract_args(method.docstring)
        if len(args) < 2:
            return ""

        param_names = [a[0] for a in args]
        lines = [
            f"class Test{method.class_name or ''}{method.name.title()}Params:",
            f'    """Parametrized tests for {method.class_name}.{method.name}()."""',
            "",
            f"    @pytest.mark.parametrize('{', '.join(param_names)}, expected', [",
            f'        ({", ".join(["None"] * len(param_names))}, "expected_result"),',
            "    ])",
            f"    def test_{method.name}_with_params(self, {', '.join(param_names)}, expected):",
            f'        """Test {method.name} with various inputs."""',
            f"        result = {method.class_name or ''}.{method.name}({', '.join(param_names)})",
            "        assert result == expected",
            "",
        ]
        return "\n".join(lines)