orchestrata_tools.test_generator.interface_tests

Reference › orchestrata_tools › test_generator › interface_tests

InterfaceTestGenerator writes tests for Protocol and abstract base classes. A protocol or ABC cannot be instantiated, so it first emits a concrete implementation. generate_for_protocol returns a concrete class implementing a ClassInfo protocol plus tests, and generate_for_abc returns a minimal concrete subclass plus tests. _signature renders a method signature and _param_default picks a default for a parameter from its annotation, checking container types such as list and dict before scalar types, because a type like list[str] contains the text str. It is used by combined_generator and covered by tests/test_orchestrata_tools_test_generator.py.

At a glance

Source src/orchestrata_tools/test_generator/interface_tests.py
Lines of code 118
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.interface_tests

Dependency diagram

Dependency diagram orchestrata_tools.test_generator.interface_tests

API

Classes

InterfaceTestGenerator

Line 15

Generate tests for Protocol and ABC classes.

Method Signature Summary
_param_default _param_default(param: ParamInfo) -> str
_signature _signature(method: MethodInfo) -> str
generate_for_protocol generate_for_protocol(protocol: ClassInfo) -> str Generate a concrete implementation + tests for a Protocol.
generate_for_abc generate_for_abc(abstract_class: ClassInfo) -> str Generate a minimal concrete subclass + tests for an ABC.

Source

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

"""Generate tests for Protocol and ABC classes.

Creates concrete implementations + pytest test stubs for protocols
and abstract base classes.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from orchestrata_tools.models.ast_models import ClassInfo, MethodInfo, ParamInfo


class InterfaceTestGenerator:
    """Generate tests for Protocol and ABC classes."""

    def _param_default(self, param: ParamInfo) -> str:
        if param.type_annotation:
            ann = param.type_annotation
            # Container checks MUST run before the scalar substring checks:
            # a generic like "list[str]" or "dict[str, int]" contains "str"/
            # "int" as a substring of its element type, so checking scalar
            # types first produced the wrong default (e.g. a `list[str]`
            # param defaulted to '"test"' instead of "[]").
            if "list" in ann or "List" in ann:
                return "[]"
            if "dict" in ann or "Dict" in ann:
                return "{}"
            if "str" in ann:
                return '"test"'
            if "int" in ann:
                return "0"
            if "float" in ann:
                return "0.0"
            if "bool" in ann:
                return "True"
        return "None"

    def _signature(self, method: MethodInfo) -> str:
        params = []
        for p in method.parameters:
            if p.name in ("self", "cls"):
                continue
            ann = f": {p.type_annotation}" if p.type_annotation else ""
            default = f" = {p.default}" if p.default else ""
            params.append(f"{p.name}{ann}{default}")
        ret = f" -> {method.return_type}" if method.return_type else ""
        return f"def {method.name}({', '.join(params)}){ret}:"

    def generate_for_protocol(self, protocol: ClassInfo) -> str:
        """Generate a concrete implementation + tests for a Protocol."""
        impl_name = f"_Concrete{protocol.name}"
        lines = [
            f"class {impl_name}:",
            f'    """Concrete implementation of {protocol.name} for testing."""',
            "",
        ]
        for method in protocol.methods:
            if method.name.startswith("_"):
                continue
            sig = self._signature(method)
            lines.append(f"    {sig}")
            lines.append("        ...")
            lines.append("")

        lines.extend(
            [
                "",
                f"class Test{protocol.name}:",
                f'    """Tests for {protocol.name} protocol compliance."""',
                "",
                "    def test_implements_all_methods(self):",
                f"        obj = {impl_name}()",
            ]
        )
        for method in protocol.methods:
            if method.name.startswith("_"):
                continue
            params = ", ".join(
                self._param_default(p)
                for p in method.parameters
                if p.name not in ("self", "cls")
            )
            lines.append(f"        assert hasattr(obj, '{method.name}')")
            lines.append(f"        obj.{method.name}({params})")
        lines.append("")

        return "\n".join(lines)

    def generate_for_abc(self, abstract_class: ClassInfo) -> str:
        """Generate a minimal concrete subclass + tests for an ABC."""
        impl_name = f"_Concrete{abstract_class.name}"
        lines = [
            f"class {impl_name}({abstract_class.name}):",
            f'    """Minimal concrete implementation of {abstract_class.name}."""',
            "",
        ]
        for method in abstract_class.methods:
            if method.is_abstract and not method.name.startswith("_"):
                sig = self._signature(method)
                lines.append(f"    {sig}")
                lines.append("        ...")
                lines.append("")

        lines.extend(
            [
                "",
                f"class Test{abstract_class.name}:",
                f'    """Tests for {abstract_class.name} ABC compliance."""',
                "",
                "    def test_can_instantiate(self):",
                f"        obj = {impl_name}()",
                f"        assert isinstance(obj, {abstract_class.name})",
                "",
            ]
        )
        return "\n".join(lines)