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
Dependency diagram
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
"""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)