1 # SPDX-License-Identifier: GPL-3.0-or-later
3 # This file is part of Nominatim. (https://nominatim.org)
5 # Copyright (C) 2025 by the Nominatim developer community.
6 # For a full list of authors see the git log.
8 Fixtures for BDD test steps
12 from pathlib import Path
15 from pytest_bdd.parsers import re as step_parse
16 from pytest_bdd import when, then
18 from utils.api_runner import APIRunner
19 from utils.api_result import APIResult
20 from utils.checks import ResultAttr, COMPARATOR_TERMS
22 # always test against the source
23 SRC_DIR = (Path(__file__) / '..' / '..' / '..').resolve()
24 sys.path.insert(0, str(SRC_DIR / 'src'))
28 return [s.strip() for s in inp.split(',')]
31 def _pretty_json(inp):
32 return json.dumps(inp, indent=2)
35 def pytest_addoption(parser, pluginmanager):
36 parser.addoption('--nominatim-purge', dest='NOMINATIM_PURGE', action='store_true',
37 help='Force recreation of test databases from scratch.')
38 parser.addoption('--nominatim-keep-db', dest='NOMINATIM_KEEP_DB', action='store_true',
39 help='Do not drop the database after tests are finished.')
40 parser.addoption('--nominatim-api-engine', dest='NOMINATIM_API_ENGINE',
42 help='Chose the API engine to use when sending requests.')
43 parser.addoption('--nominatim-tokenizer', dest='NOMINATIM_TOKENIZER',
45 help='Use the specified tokenizer for importing data into '
46 'a Nominatim database.')
48 parser.addini('nominatim_test_db', default='test_nominatim',
49 help='Name of the database used for running a single test.')
50 parser.addini('nominatim_api_test_db', default='test_api_nominatim',
51 help='Name of the database for storing API test data.')
52 parser.addini('nominatim_template_db', default='test_template_nominatim',
53 help='Name of database used as a template for test databases.')
58 """ Default fixture for datatables, so that their presence can be optional.
63 @when(step_parse(r'reverse geocoding (?P<lat>[\d.-]*),(?P<lon>[\d.-]*)'),
64 target_fixture='nominatim_result')
65 def reverse_geocode_via_api(test_config_env, pytestconfig, datatable, lat, lon):
66 runner = APIRunner(test_config_env, pytestconfig.option.NOMINATIM_API_ENGINE)
67 api_response = runner.run_step('reverse',
68 {'lat': float(lat), 'lon': float(lon)},
69 datatable, 'jsonv2', {})
71 assert api_response.status == 200
72 assert api_response.headers['content-type'] == 'application/json; charset=utf-8'
74 result = APIResult('json', 'reverse', api_response.body)
75 assert result.is_simple()
80 @when(step_parse(r'geocoding(?: "(?P<query>.*)")?'),
81 target_fixture='nominatim_result')
82 def forward_geocode_via_api(test_config_env, pytestconfig, datatable, query):
83 runner = APIRunner(test_config_env, pytestconfig.option.NOMINATIM_API_ENGINE)
85 params = {'addressdetails': '1'}
89 api_response = runner.run_step('search', params, datatable, 'jsonv2', {})
91 assert api_response.status == 200
92 assert api_response.headers['content-type'] == 'application/json; charset=utf-8'
94 result = APIResult('json', 'search', api_response.body)
95 assert not result.is_simple()
100 @then(step_parse(r'(?P<op>[a-z ]+) (?P<num>\d+) results? (?:are|is) returned'),
101 converters={'num': int})
102 def check_number_of_results(nominatim_result, op, num):
103 assert not nominatim_result.is_simple()
104 assert COMPARATOR_TERMS[op](num, len(nominatim_result))
107 @then(step_parse('the result metadata contains'))
108 def check_metadata_for_fields(nominatim_result, datatable):
109 if datatable[0] == ['param', 'value']:
110 pairs = datatable[1:]
112 pairs = zip(datatable[0], datatable[1])
115 assert ResultAttr(nominatim_result.meta, k) == v
118 @then(step_parse('the result metadata has no attributes (?P<attributes>.*)'),
119 converters={'attributes': _strlist})
120 def check_metadata_for_field_presence(nominatim_result, attributes):
121 assert all(a not in nominatim_result.meta for a in attributes), \
122 f"Unexpectedly have one of the attributes '{attributes}' in\n" \
123 f"{_pretty_json(nominatim_result.meta)}"
126 @then(step_parse(r'the result contains(?: in field (?P<field>\S+))?'))
127 def check_result_for_fields(nominatim_result, datatable, field):
128 assert nominatim_result.is_simple()
130 if datatable[0] == ['param', 'value']:
131 pairs = datatable[1:]
133 pairs = zip(datatable[0], datatable[1])
135 prefix = field + '+' if field else ''
138 assert ResultAttr(nominatim_result.result, prefix + k) == v
141 @then(step_parse('the result has attributes (?P<attributes>.*)'),
142 converters={'attributes': _strlist})
143 def check_result_for_field_presence(nominatim_result, attributes):
144 assert nominatim_result.is_simple()
145 assert all(a in nominatim_result.result for a in attributes)
148 @then(step_parse('the result has no attributes (?P<attributes>.*)'),
149 converters={'attributes': _strlist})
150 def check_result_for_field_absence(nominatim_result, attributes):
151 assert nominatim_result.is_simple()
152 assert all(a not in nominatim_result.result for a in attributes)
155 @then(step_parse('the result set contains(?P<exact> exactly)?'))
156 def check_result_list_match(nominatim_result, datatable, exact):
157 assert not nominatim_result.is_simple()
159 result_set = set(range(len(nominatim_result.result)))
161 for row in datatable[1:]:
162 for idx in result_set:
163 for key, value in zip(datatable[0], row):
164 if ResultAttr(nominatim_result.result[idx], key) != value:
168 result_set.remove(idx)
171 assert False, f"Missing data row {row}. Full response:\n{nominatim_result}"
174 assert not [nominatim_result.result[i] for i in result_set]
177 @then(step_parse('all results have attributes (?P<attributes>.*)'),
178 converters={'attributes': _strlist})
179 def check_all_results_for_field_presence(nominatim_result, attributes):
180 assert not nominatim_result.is_simple()
181 for res in nominatim_result.result:
182 assert all(a in res for a in attributes), \
183 f"Missing one of the attributes '{attributes}' in\n{_pretty_json(res)}"
186 @then(step_parse('all results have no attributes (?P<attributes>.*)'),
187 converters={'attributes': _strlist})
188 def check_all_result_for_field_absence(nominatim_result, attributes):
189 assert not nominatim_result.is_simple()
190 for res in nominatim_result.result:
191 assert all(a not in res for a in attributes), \
192 f"Unexpectedly have one of the attributes '{attributes}' in\n{_pretty_json(res)}"
195 @then(step_parse(r'all results contain(?: in field (?P<field>\S+))?'))
196 def check_all_results_contain(nominatim_result, datatable, field):
197 assert not nominatim_result.is_simple()
199 if datatable[0] == ['param', 'value']:
200 pairs = datatable[1:]
202 pairs = zip(datatable[0], datatable[1])
204 prefix = field + '+' if field else ''
207 for r in nominatim_result.result:
208 assert ResultAttr(r, prefix + k) == v
211 @then(step_parse(r'result (?P<num>\d+) contains(?: in field (?P<field>\S+))?'),
212 converters={'num': int})
213 def check_specific_result_for_fields(nominatim_result, datatable, num, field):
214 assert not nominatim_result.is_simple()
215 assert len(nominatim_result) >= num + 1
217 if datatable[0] == ['param', 'value']:
218 pairs = datatable[1:]
220 pairs = zip(datatable[0], datatable[1])
222 prefix = field + '+' if field else ''
225 assert ResultAttr(nominatim_result.result[num], prefix + k) == v