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).