From 55547723bfecec07e47bbecd8cfa73b4a3b77a8f Mon Sep 17 00:00:00 2001 From: Sarah Hoffmann Date: Thu, 30 Oct 2025 17:44:17 +0100 Subject: [PATCH] add custom pytest collector for BDD feature files --- docs/develop/Testing.md | 9 ++++++ test/bdd/conftest.py | 57 +++++++++++++++++++++++++++++++++++++- test/bdd/test_api.py | 4 +-- test/bdd/test_db.py | 4 +-- test/bdd/test_osm2pgsql.py | 4 +-- 5 files changed, 71 insertions(+), 7 deletions(-) diff --git a/docs/develop/Testing.md b/docs/develop/Testing.md index 738fa4b8..d282db5d 100644 --- a/docs/develop/Testing.md +++ b/docs/develop/Testing.md @@ -52,6 +52,15 @@ To run the functional tests, do pytest test/bdd +You can run a single feature file using expression matching: + + pytest test/bdd -k osm2pgsql/import/entrances.feature + +This even works for running single tests by adding the line number of the +scenario header like that: + + pytest test/bdd -k 'osm2pgsql/import/entrances.feature and L4' + The BDD tests create databases for the tests. You can set name of the databases through configuration variables in your `pytest.ini`: diff --git a/test/bdd/conftest.py b/test/bdd/conftest.py index 2135083f..0cf45835 100644 --- a/test/bdd/conftest.py +++ b/test/bdd/conftest.py @@ -9,6 +9,7 @@ Fixtures for BDD test steps """ import sys import json +import re from pathlib import Path import psycopg @@ -20,7 +21,8 @@ sys.path.insert(0, str(SRC_DIR / 'src')) import pytest from pytest_bdd.parsers import re as step_parse -from pytest_bdd import given, when, then +from pytest_bdd import given, when, then, scenario +from pytest_bdd.feature import get_features pytest.register_assert_rewrite('utils') @@ -373,3 +375,56 @@ def check_place_missing_lines(db_conn, table, osm_type, osm_id, osm_class): with db_conn.cursor() as cur: assert cur.execute(sql, params).fetchone()[0] == 0 + + +def pytest_pycollect_makemodule(module_path, parent): + return BddTestCollector.from_parent(parent, path=module_path) + + +class BddTestCollector(pytest.Module): + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def collect(self): + for item in super().collect(): + yield item + + if hasattr(self.obj, 'PYTEST_BDD_SCENARIOS'): + for path in self.obj.PYTEST_BDD_SCENARIOS: + for feature in get_features([str(Path(self.path.parent, path).resolve())]): + yield FeatureFile.from_parent(self, + name=str(Path(path, feature.rel_filename)), + path=Path(feature.filename), + feature=feature) + + +# borrowed from pytest-bdd: src/pytest_bdd/scenario.py +def make_python_name(string: str) -> str: + """Make python attribute name out of a given string.""" + string = re.sub(r"\W", "", string.replace(" ", "_")) + return re.sub(r"^\d+_*", "", string).lower() + + +class FeatureFile(pytest.File): + class obj: + pass + + def __init__(self, feature, **kwargs): + self.feature = feature + super().__init__(**kwargs) + + def collect(self): + for sname, sobject in self.feature.scenarios.items(): + class_name = f"L{sobject.line_number}" + test_name = "test_" + make_python_name(sname) + + @scenario(self.feature.filename, sname) + def _test(): + pass + + tclass = type(class_name, (), + {test_name: staticmethod(_test)}) + setattr(self.obj, class_name, tclass) + + yield pytest.Class.from_parent(self, name=class_name, obj=tclass) diff --git a/test/bdd/test_api.py b/test/bdd/test_api.py index 5ace7b94..6f0c2fab 100644 --- a/test/bdd/test_api.py +++ b/test/bdd/test_api.py @@ -15,7 +15,7 @@ import xml.etree.ElementTree as ET import pytest from pytest_bdd.parsers import re as step_parse -from pytest_bdd import scenarios, when, given, then +from pytest_bdd import when, given, then from nominatim_db import cli from nominatim_db.config import Configuration @@ -150,4 +150,4 @@ def parse_api_json_response(api_response, fmt, num): return result -scenarios('features/api') +PYTEST_BDD_SCENARIOS = ['features/api'] diff --git a/test/bdd/test_db.py b/test/bdd/test_db.py index 339618f3..954d9df3 100644 --- a/test/bdd/test_db.py +++ b/test/bdd/test_db.py @@ -15,7 +15,7 @@ import re import psycopg import pytest -from pytest_bdd import scenarios, when, then, given +from pytest_bdd import when, then, given from pytest_bdd.parsers import re as step_parse from utils.place_inserter import PlaceColumn @@ -276,4 +276,4 @@ def then_check_interpolation_table_negative(db_conn, oid): assert cur.fetchone()[0] == 0 -scenarios('features/db') +PYTEST_BDD_SCENARIOS = ['features/db'] diff --git a/test/bdd/test_osm2pgsql.py b/test/bdd/test_osm2pgsql.py index a2214b08..a0637634 100644 --- a/test/bdd/test_osm2pgsql.py +++ b/test/bdd/test_osm2pgsql.py @@ -11,7 +11,7 @@ import asyncio import random import pytest -from pytest_bdd import scenarios, when, then, given +from pytest_bdd import when, then, given from pytest_bdd.parsers import re as step_parse from nominatim_db import cli @@ -106,4 +106,4 @@ def check_place_content(db_conn, datatable, node_grid, table, exact): check_table_content(db_conn, table, datatable, grid=node_grid, exact=bool(exact)) -scenarios('features/osm2pgsql') +PYTEST_BDD_SCENARIOS = ['features/osm2pgsql'] -- 2.39.5