]> git.openstreetmap.org Git - nominatim.git/blob - test/bdd/conftest.py
replace behave BDD API tests with pytest-bdd tests
[nominatim.git] / test / bdd / conftest.py
1 # SPDX-License-Identifier: GPL-3.0-or-later
2 #
3 # This file is part of Nominatim. (https://nominatim.org)
4 #
5 # Copyright (C) 2025 by the Nominatim developer community.
6 # For a full list of authors see the git log.
7 """
8 Fixtures for BDD test steps
9 """
10 import sys
11 import json
12 from pathlib import Path
13
14 import pytest
15 from pytest_bdd.parsers import re as step_parse
16 from pytest_bdd import when, then
17
18 from utils.api_runner import APIRunner
19 from utils.api_result import APIResult
20 from utils.checks import ResultAttr, COMPARATOR_TERMS
21
22 # always test against the source
23 SRC_DIR = (Path(__file__) / '..' / '..' / '..').resolve()
24 sys.path.insert(0, str(SRC_DIR / 'src'))
25
26
27 def _strlist(inp):
28     return [s.strip() for s in inp.split(',')]
29
30
31 def _pretty_json(inp):
32     return json.dumps(inp, indent=2)
33
34
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',
41                      default='falcon',
42                      help='Chose the API engine to use when sending requests.')
43     parser.addoption('--nominatim-tokenizer', dest='NOMINATIM_TOKENIZER',
44                      metavar='TOKENIZER',
45                      help='Use the specified tokenizer for importing data into '
46                           'a Nominatim database.')
47
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.')
54
55
56 @pytest.fixture
57 def datatable():
58     """ Default fixture for datatables, so that their presence can be optional.
59     """
60     return None
61
62
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', {})
70
71     assert api_response.status == 200
72     assert api_response.headers['content-type'] == 'application/json; charset=utf-8'
73
74     result = APIResult('json', 'reverse', api_response.body)
75     assert result.is_simple()
76
77     return result
78
79
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)
84
85     params = {'addressdetails': '1'}
86     if query:
87         params['q'] = query
88
89     api_response = runner.run_step('search', params, datatable, 'jsonv2', {})
90
91     assert api_response.status == 200
92     assert api_response.headers['content-type'] == 'application/json; charset=utf-8'
93
94     result = APIResult('json', 'search', api_response.body)
95     assert not result.is_simple()
96
97     return result
98
99
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))
105
106
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:]
111     else:
112         pairs = zip(datatable[0], datatable[1])
113
114     for k, v in pairs:
115         assert ResultAttr(nominatim_result.meta, k) == v
116
117
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)}"
124
125
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()
129
130     if datatable[0] == ['param', 'value']:
131         pairs = datatable[1:]
132     else:
133         pairs = zip(datatable[0], datatable[1])
134
135     prefix = field + '+' if field else ''
136
137     for k, v in pairs:
138         assert ResultAttr(nominatim_result.result, prefix + k) == v
139
140
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)
146
147
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)
153
154
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()
158
159     result_set = set(range(len(nominatim_result.result)))
160
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:
165                     break
166             else:
167                 # found a match
168                 result_set.remove(idx)
169                 break
170         else:
171             assert False, f"Missing data row {row}. Full response:\n{nominatim_result}"
172
173     if exact:
174         assert not [nominatim_result.result[i] for i in result_set]
175
176
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)}"
184
185
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)}"
193
194
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()
198
199     if datatable[0] == ['param', 'value']:
200         pairs = datatable[1:]
201     else:
202         pairs = zip(datatable[0], datatable[1])
203
204     prefix = field + '+' if field else ''
205
206     for k, v in pairs:
207         for r in nominatim_result.result:
208             assert ResultAttr(r, prefix + k) == v
209
210
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
216
217     if datatable[0] == ['param', 'value']:
218         pairs = datatable[1:]
219     else:
220         pairs = zip(datatable[0], datatable[1])
221
222     prefix = field + '+' if field else ''
223
224     for k, v in pairs:
225         assert ResultAttr(nominatim_result.result[num], prefix + k) == v