pytest_beehave.plugin

pytest plugin entry point for pytest-beehave.

  1"""pytest plugin entry point for pytest-beehave."""
  2
  3from __future__ import annotations
  4
  5import importlib.util
  6import sys
  7from pathlib import Path
  8
  9import pytest
 10
 11from pytest_beehave.bootstrap import bootstrap_features_directory
 12from pytest_beehave.config import (
 13    is_explicitly_configured,
 14    read_stub_format,
 15    resolve_features_path,
 16    show_steps_in_html,
 17    show_steps_in_terminal,
 18)
 19from pytest_beehave.hatch import run_hatch
 20from pytest_beehave.html_steps_plugin import HtmlStepsPlugin
 21from pytest_beehave.id_generator import assign_ids
 22from pytest_beehave.reporter import (
 23    report_bootstrap,
 24    report_id_write_back,
 25    report_sync_actions,
 26)
 27from pytest_beehave.steps_reporter import StepsReporter
 28from pytest_beehave.sync_engine import run_sync
 29
 30features_path_key: pytest.StashKey[Path] = pytest.StashKey()
 31
 32
 33class _PytestTerminalWriter:
 34    """Adapter wrapping pytest's terminal writer to match TerminalWriterProtocol."""
 35
 36    def __init__(self, config: pytest.Config) -> None:
 37        """Initialise the adapter.
 38
 39        Args:
 40            config: The pytest Config object.
 41        """
 42        self._config = config
 43
 44    def line(self, text: str = "") -> None:
 45        """Write a line to the terminal.
 46
 47        Args:
 48            text: The line to write.
 49        """
 50        try:
 51            config = self._config
 52            writer = config.get_terminal_writer()
 53            writer.line(text)
 54        except (AssertionError, AttributeError):
 55            sys.stdout.write(text + "\n")
 56            sys.stdout.flush()
 57
 58
 59def _exit_if_missing_configured_path(rootdir: Path, path: Path) -> None:
 60    """Exit pytest if features_path is explicitly configured but missing.
 61
 62    Args:
 63        rootdir: Project root directory.
 64        path: Resolved features path.
 65    """
 66    if not path.exists() and is_explicitly_configured(rootdir):
 67        message = f"[beehave] features_path not found: {path}"
 68        sys.stderr.write(message + "\n")
 69        sys.stderr.flush()
 70        pytest.exit(message, returncode=1)
 71
 72
 73def _run_beehave_sync(config: pytest.Config, path: Path) -> None:
 74    """Bootstrap, assign IDs, and sync stubs for the features directory.
 75
 76    Args:
 77        config: The pytest Config object.
 78        path: The resolved features directory path.
 79    """
 80    writer = _PytestTerminalWriter(config)
 81    report_bootstrap(writer, bootstrap_features_directory(path))
 82    errors = assign_ids(path)
 83    report_id_write_back(writer, errors)
 84    if errors:
 85        pytest.exit("[beehave] untagged Examples in read-only files", returncode=1)
 86    try:
 87        stub_format = read_stub_format(config.rootpath)
 88    except SystemExit as exc:
 89        pytest.exit(str(exc), returncode=1)
 90    report_sync_actions(
 91        writer,
 92        run_sync(path, config.rootpath / "tests" / "features", stub_format=stub_format),
 93    )
 94
 95
 96def _html_available() -> bool:
 97    """Return True if pytest-html is importable.
 98
 99    Returns:
100        True when pytest-html is installed.
101    """
102    return importlib.util.find_spec("pytest_html") is not None
103
104
105@pytest.hookimpl(hookwrapper=True)
106def pytest_runtest_makereport(item: pytest.Item, call: pytest.CallInfo[None]) -> object:
107    """Attach the test docstring to the report for steps display.
108
109    Args:
110        item: The test item being reported.
111        call: The call info (unused).
112    """
113    outcome = yield
114    report = outcome.get_result()
115    test_object = getattr(item, "obj", None)
116    report._beehave_docstring = (
117        getattr(test_object, "__doc__", None) or "" if test_object is not None else ""
118    )
119
120
121def _register_output_plugins(config: pytest.Config, rootdir: Path) -> None:
122    """Register terminal and HTML output plugins based on configuration.
123
124    Args:
125        config: The pytest Config object.
126        rootdir: Project root directory.
127    """
128    pm = config.pluginmanager
129    if show_steps_in_terminal(rootdir):
130        pm.register(StepsReporter(config), "beehave-steps-reporter")
131    if show_steps_in_html(rootdir) and _html_available():
132        pm.register(HtmlStepsPlugin(), "beehave-html-steps")
133
134
135def pytest_addoption(parser: pytest.Parser) -> None:
136    """Register --beehave-hatch and --beehave-hatch-force CLI options.
137
138    Args:
139        parser: The pytest argument parser.
140    """
141    group = parser.getgroup("beehave")
142    group.addoption(
143        "--beehave-hatch",
144        action="store_true",
145        default=False,
146        help="Generate bee-themed example features directory and exit.",
147    )
148    group.addoption(
149        "--beehave-hatch-force",
150        action="store_true",
151        default=False,
152        help="Overwrite existing content when using --beehave-hatch.",
153    )
154
155
156def pytest_configure(config: pytest.Config) -> None:
157    """Read beehave configuration, bootstrap directory, sync stubs.
158
159    Args:
160        config: The pytest Config object (provides rootdir and stash).
161    """
162    rootdir = config.rootpath
163    path = resolve_features_path(rootdir)
164    if config.getoption("--beehave-hatch", default=False):
165        force = bool(config.getoption("--beehave-hatch-force", default=False))
166        try:
167            written = run_hatch(path, force)
168        except SystemExit as exc:
169            pytest.exit(str(exc), returncode=1)
170        writer = _PytestTerminalWriter(config)
171        for entry in written:
172            writer.line(f"[beehave] HATCH {entry}")
173        pytest.exit("[beehave] hatch complete", returncode=0)
174    _exit_if_missing_configured_path(rootdir, path)
175    config.stash[features_path_key] = path
176    if path.exists():
177        _run_beehave_sync(config, path)
178    _register_output_plugins(config, rootdir)
features_path_key: _pytest.stash.StashKey[pathlib._local.Path] = <_pytest.stash.StashKey object>
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport( item: _pytest.nodes.Item, call: _pytest.runner.CallInfo[NoneType]) -> object:
106@pytest.hookimpl(hookwrapper=True)
107def pytest_runtest_makereport(item: pytest.Item, call: pytest.CallInfo[None]) -> object:
108    """Attach the test docstring to the report for steps display.
109
110    Args:
111        item: The test item being reported.
112        call: The call info (unused).
113    """
114    outcome = yield
115    report = outcome.get_result()
116    test_object = getattr(item, "obj", None)
117    report._beehave_docstring = (
118        getattr(test_object, "__doc__", None) or "" if test_object is not None else ""
119    )

Attach the test docstring to the report for steps display.

Args: item: The test item being reported. call: The call info (unused).

def pytest_addoption(parser: _pytest.config.argparsing.Parser) -> None:
136def pytest_addoption(parser: pytest.Parser) -> None:
137    """Register --beehave-hatch and --beehave-hatch-force CLI options.
138
139    Args:
140        parser: The pytest argument parser.
141    """
142    group = parser.getgroup("beehave")
143    group.addoption(
144        "--beehave-hatch",
145        action="store_true",
146        default=False,
147        help="Generate bee-themed example features directory and exit.",
148    )
149    group.addoption(
150        "--beehave-hatch-force",
151        action="store_true",
152        default=False,
153        help="Overwrite existing content when using --beehave-hatch.",
154    )

Register --beehave-hatch and --beehave-hatch-force CLI options.

Args: parser: The pytest argument parser.

def pytest_configure(config: _pytest.config.Config) -> None:
157def pytest_configure(config: pytest.Config) -> None:
158    """Read beehave configuration, bootstrap directory, sync stubs.
159
160    Args:
161        config: The pytest Config object (provides rootdir and stash).
162    """
163    rootdir = config.rootpath
164    path = resolve_features_path(rootdir)
165    if config.getoption("--beehave-hatch", default=False):
166        force = bool(config.getoption("--beehave-hatch-force", default=False))
167        try:
168            written = run_hatch(path, force)
169        except SystemExit as exc:
170            pytest.exit(str(exc), returncode=1)
171        writer = _PytestTerminalWriter(config)
172        for entry in written:
173            writer.line(f"[beehave] HATCH {entry}")
174        pytest.exit("[beehave] hatch complete", returncode=0)
175    _exit_if_missing_configured_path(rootdir, path)
176    config.stash[features_path_key] = path
177    if path.exists():
178        _run_beehave_sync(config, path)
179    _register_output_plugins(config, rootdir)

Read beehave configuration, bootstrap directory, sync stubs.

Args: config: The pytest Config object (provides rootdir and stash).