enable flake for bdd test code

This commit is contained in:
Sarah Hoffmann
2025-03-09 17:34:04 +01:00
parent c70dfccaca
commit 78f839fbd3
15 changed files with 396 additions and 393 deletions

View File

@@ -8,3 +8,4 @@ per-file-ignores =
__init__.py: F401 __init__.py: F401
test/python/utils/test_json_writer.py: E131 test/python/utils/test_json_writer.py: E131
test/python/conftest.py: E402 test/python/conftest.py: E402
test/bdd/*: F821

View File

@@ -100,7 +100,7 @@ jobs:
run: ./venv/bin/pip install -U flake8 run: ./venv/bin/pip install -U flake8
- name: Python linting - name: Python linting
run: ../venv/bin/python -m flake8 src test/python run: ../venv/bin/python -m flake8 src test/python test/bdd
working-directory: Nominatim working-directory: Nominatim
- name: Install mypy and typechecking info - name: Install mypy and typechecking info

View File

@@ -24,7 +24,7 @@ pytest:
pytest test/python pytest test/python
lint: lint:
flake8 src test/python flake8 src test/python test/bdd
bdd: bdd:
cd test/bdd; behave -DREMOVE_TEMPLATE=1 cd test/bdd; behave -DREMOVE_TEMPLATE=1

View File

@@ -2,43 +2,45 @@
# #
# This file is part of Nominatim. (https://nominatim.org) # This file is part of Nominatim. (https://nominatim.org)
# #
# Copyright (C) 2024 by the Nominatim developer community. # Copyright (C) 2025 by the Nominatim developer community.
# For a full list of authors see the git log. # For a full list of authors see the git log.
from pathlib import Path from pathlib import Path
import sys import sys
from behave import * from behave import * # noqa
sys.path.insert(1, str(Path(__file__, '..', '..', '..', 'src').resolve())) sys.path.insert(1, str(Path(__file__, '..', '..', '..', 'src').resolve()))
from steps.geometry_factory import GeometryFactory from steps.geometry_factory import GeometryFactory # noqa: E402
from steps.nominatim_environment import NominatimEnvironment from steps.nominatim_environment import NominatimEnvironment # noqa: E402
TEST_BASE_DIR = Path(__file__, '..', '..').resolve() TEST_BASE_DIR = Path(__file__, '..', '..').resolve()
userconfig = { userconfig = {
'REMOVE_TEMPLATE' : False, 'REMOVE_TEMPLATE': False,
'KEEP_TEST_DB' : False, 'KEEP_TEST_DB': False,
'DB_HOST' : None, 'DB_HOST': None,
'DB_PORT' : None, 'DB_PORT': None,
'DB_USER' : None, 'DB_USER': None,
'DB_PASS' : None, 'DB_PASS': None,
'TEMPLATE_DB' : 'test_template_nominatim', 'TEMPLATE_DB': 'test_template_nominatim',
'TEST_DB' : 'test_nominatim', 'TEST_DB': 'test_nominatim',
'API_TEST_DB' : 'test_api_nominatim', 'API_TEST_DB': 'test_api_nominatim',
'API_TEST_FILE' : TEST_BASE_DIR / 'testdb' / 'apidb-test-data.pbf', 'API_TEST_FILE': TEST_BASE_DIR / 'testdb' / 'apidb-test-data.pbf',
'TOKENIZER' : None, # Test with a custom tokenizer 'TOKENIZER': None, # Test with a custom tokenizer
'STYLE' : 'extratags', 'STYLE': 'extratags',
'API_ENGINE': 'falcon' 'API_ENGINE': 'falcon'
} }
use_step_matcher("re")
use_step_matcher("re") # noqa: F405
def before_all(context): def before_all(context):
# logging setup # logging setup
context.config.setup_logging() context.config.setup_logging()
# set up -D options # set up -D options
for k,v in userconfig.items(): for k, v in userconfig.items():
context.config.userdata.setdefault(k, v) context.config.userdata.setdefault(k, v)
# Nominatim test setup # Nominatim test setup
context.nominatim = NominatimEnvironment(context.config.userdata) context.nominatim = NominatimEnvironment(context.config.userdata)
@@ -46,7 +48,7 @@ def before_all(context):
def before_scenario(context, scenario): def before_scenario(context, scenario):
if not 'SQLITE' in context.tags \ if 'SQLITE' not in context.tags \
and context.config.userdata['API_TEST_DB'].startswith('sqlite:'): and context.config.userdata['API_TEST_DB'].startswith('sqlite:'):
context.scenario.skip("Not usable with Sqlite database.") context.scenario.skip("Not usable with Sqlite database.")
elif 'DB' in context.tags: elif 'DB' in context.tags:
@@ -56,6 +58,7 @@ def before_scenario(context, scenario):
elif 'UNKNOWNDB' in context.tags: elif 'UNKNOWNDB' in context.tags:
context.nominatim.setup_unknown_db() context.nominatim.setup_unknown_db()
def after_scenario(context, scenario): def after_scenario(context, scenario):
if 'DB' in context.tags: if 'DB' in context.tags:
context.nominatim.teardown_db(context) context.nominatim.teardown_db(context)

View File

@@ -2,7 +2,7 @@
# #
# This file is part of Nominatim. (https://nominatim.org) # This file is part of Nominatim. (https://nominatim.org)
# #
# Copyright (C) 2023 by the Nominatim developer community. # Copyright (C) 2025 by the Nominatim developer community.
# For a full list of authors see the git log. # For a full list of authors see the git log.
""" """
Collection of assertion functions used for the steps. Collection of assertion functions used for the steps.
@@ -11,9 +11,10 @@ import json
import math import math
import re import re
OSM_TYPE = {'N' : 'node', 'W' : 'way', 'R' : 'relation',
'n' : 'node', 'w' : 'way', 'r' : 'relation', OSM_TYPE = {'N': 'node', 'W': 'way', 'R': 'relation',
'node' : 'n', 'way' : 'w', 'relation' : 'r'} 'n': 'node', 'w': 'way', 'r': 'relation',
'node': 'n', 'way': 'w', 'relation': 'r'}
class OsmType: class OsmType:
@@ -23,11 +24,9 @@ class OsmType:
def __init__(self, value): def __init__(self, value):
self.value = value self.value = value
def __eq__(self, other): def __eq__(self, other):
return other == self.value or other == OSM_TYPE[self.value] return other == self.value or other == OSM_TYPE[self.value]
def __str__(self): def __str__(self):
return f"{self.value} or {OSM_TYPE[self.value]}" return f"{self.value} or {OSM_TYPE[self.value]}"
@@ -81,7 +80,6 @@ class Bbox:
return str(self.coord) return str(self.coord)
def check_for_attributes(obj, attrs, presence='present'): def check_for_attributes(obj, attrs, presence='present'):
""" Check that the object has the given attributes. 'attrs' is a """ Check that the object has the given attributes. 'attrs' is a
string with a comma-separated list of attributes. If 'presence' string with a comma-separated list of attributes. If 'presence'
@@ -99,4 +97,3 @@ def check_for_attributes(obj, attrs, presence='present'):
else: else:
assert attr in obj, \ assert attr in obj, \
f"No attribute '{attr}'. Full response:\n{_dump_json()}" f"No attribute '{attr}'. Full response:\n{_dump_json()}"

View File

@@ -2,261 +2,261 @@
# #
# This file is part of Nominatim. (https://nominatim.org) # This file is part of Nominatim. (https://nominatim.org)
# #
# Copyright (C) 2022 by the Nominatim developer community. # Copyright (C) 2025 by the Nominatim developer community.
# For a full list of authors see the git log. # For a full list of authors see the git log.
""" """
Collection of aliases for various world coordinates. Collection of aliases for various world coordinates.
""" """
ALIASES = { ALIASES = {
# Country aliases # Country aliases
'AD': (1.58972, 42.54241), 'AD': (1.58972, 42.54241),
'AE': (54.61589, 24.82431), 'AE': (54.61589, 24.82431),
'AF': (65.90264, 34.84708), 'AF': (65.90264, 34.84708),
'AG': (-61.72430, 17.069), 'AG': (-61.72430, 17.069),
'AI': (-63.10571, 18.25461), 'AI': (-63.10571, 18.25461),
'AL': (19.84941, 40.21232), 'AL': (19.84941, 40.21232),
'AM': (44.64229, 40.37821), 'AM': (44.64229, 40.37821),
'AO': (16.21924, -12.77014), 'AO': (16.21924, -12.77014),
'AQ': (44.99999, -75.65695), 'AQ': (44.99999, -75.65695),
'AR': (-61.10759, -34.37615), 'AR': (-61.10759, -34.37615),
'AS': (-170.68470, -14.29307), 'AS': (-170.68470, -14.29307),
'AT': (14.25747, 47.36542), 'AT': (14.25747, 47.36542),
'AU': (138.23155, -23.72068), 'AU': (138.23155, -23.72068),
'AW': (-69.98255, 12.555), 'AW': (-69.98255, 12.555),
'AX': (19.91839, 59.81682), 'AX': (19.91839, 59.81682),
'AZ': (48.38555, 40.61639), 'AZ': (48.38555, 40.61639),
'BA': (17.18514, 44.25582), 'BA': (17.18514, 44.25582),
'BB': (-59.53342, 13.19), 'BB': (-59.53342, 13.19),
'BD': (89.75989, 24.34205), 'BD': (89.75989, 24.34205),
'BE': (4.90078, 50.34682), 'BE': (4.90078, 50.34682),
'BF': (-0.56743, 11.90471), 'BF': (-0.56743, 11.90471),
'BG': (24.80616, 43.09859), 'BG': (24.80616, 43.09859),
'BH': (50.52032, 25.94685), 'BH': (50.52032, 25.94685),
'BI': (29.54561, -2.99057), 'BI': (29.54561, -2.99057),
'BJ': (2.70062, 10.02792), 'BJ': (2.70062, 10.02792),
'BL': (-62.79349, 17.907), 'BL': (-62.79349, 17.907),
'BM': (-64.77406, 32.30199), 'BM': (-64.77406, 32.30199),
'BN': (114.52196, 4.28638), 'BN': (114.52196, 4.28638),
'BO': (-62.02473, -17.77723), 'BO': (-62.02473, -17.77723),
'BQ': (-63.14322, 17.566), 'BQ': (-63.14322, 17.566),
'BR': (-45.77065, -9.58685), 'BR': (-45.77065, -9.58685),
'BS': (-77.60916, 23.8745), 'BS': (-77.60916, 23.8745),
'BT': (90.01350, 27.28137), 'BT': (90.01350, 27.28137),
'BV': (3.35744, -54.4215), 'BV': (3.35744, -54.4215),
'BW': (23.51505, -23.48391), 'BW': (23.51505, -23.48391),
'BY': (26.77259, 53.15885), 'BY': (26.77259, 53.15885),
'BZ': (-88.63489, 16.33951), 'BZ': (-88.63489, 16.33951),
'CA': (-107.74817, 67.12612), 'CA': (-107.74817, 67.12612),
'CC': (96.84420, -12.01734), 'CC': (96.84420, -12.01734),
'CD': (24.09544, -1.67713), 'CD': (24.09544, -1.67713),
'CF': (22.58701, 5.98438), 'CF': (22.58701, 5.98438),
'CG': (15.78875, 0.40388), 'CG': (15.78875, 0.40388),
'CH': (7.65705, 46.57446), 'CH': (7.65705, 46.57446),
'CI': (-6.31190, 6.62783), 'CI': (-6.31190, 6.62783),
'CK': (-159.77835, -21.23349), 'CK': (-159.77835, -21.23349),
'CL': (-70.41790, -53.77189), 'CL': (-70.41790, -53.77189),
'CM': (13.26022, 5.94519), 'CM': (13.26022, 5.94519),
'CN': (96.44285, 38.04260), 'CN': (96.44285, 38.04260),
'CO': (-72.52951, 2.45174), 'CO': (-72.52951, 2.45174),
'CR': (-83.83314, 9.93514), 'CR': (-83.83314, 9.93514),
'CU': (-80.81673, 21.88852), 'CU': (-80.81673, 21.88852),
'CV': (-24.50810, 14.929), 'CV': (-24.50810, 14.929),
'CW': (-68.96409, 12.1845), 'CW': (-68.96409, 12.1845),
'CX': (105.62411, -10.48417), 'CX': (105.62411, -10.48417),
'CY': (32.95922, 35.37010), 'CY': (32.95922, 35.37010),
'CZ': (16.32098, 49.50692), 'CZ': (16.32098, 49.50692),
'DE': (9.30716, 50.21289), 'DE': (9.30716, 50.21289),
'DJ': (42.96904, 11.41542), 'DJ': (42.96904, 11.41542),
'DK': (9.18490, 55.98916), 'DK': (9.18490, 55.98916),
'DM': (-61.00358, 15.65470), 'DM': (-61.00358, 15.65470),
'DO': (-69.62855, 18.58841), 'DO': (-69.62855, 18.58841),
'DZ': (4.24749, 25.79721), 'DZ': (4.24749, 25.79721),
'EC': (-77.45831, -0.98284), 'EC': (-77.45831, -0.98284),
'EE': (23.94288, 58.43952), 'EE': (23.94288, 58.43952),
'EG': (28.95293, 28.17718), 'EG': (28.95293, 28.17718),
'EH': (-13.69031, 25.01241), 'EH': (-13.69031, 25.01241),
'ER': (39.01223, 14.96033), 'ER': (39.01223, 14.96033),
'ES': (-2.59110, 38.79354), 'ES': (-2.59110, 38.79354),
'ET': (38.61697, 7.71399), 'ET': (38.61697, 7.71399),
'FI': (26.89798, 63.56194), 'FI': (26.89798, 63.56194),
'FJ': (177.91853, -17.74237), 'FJ': (177.91853, -17.74237),
'FK': (-58.99044, -51.34509), 'FK': (-58.99044, -51.34509),
'FM': (151.95358, 8.5045), 'FM': (151.95358, 8.5045),
'FO': (-6.60483, 62.10000), 'FO': (-6.60483, 62.10000),
'FR': (0.28410, 47.51045), 'FR': (0.28410, 47.51045),
'GA': (10.81070, -0.07429), 'GA': (10.81070, -0.07429),
'GB': (-0.92823, 52.01618), 'GB': (-0.92823, 52.01618),
'GD': (-61.64524, 12.191), 'GD': (-61.64524, 12.191),
'GE': (44.16664, 42.00385), 'GE': (44.16664, 42.00385),
'GF': (-53.46524, 3.56188), 'GF': (-53.46524, 3.56188),
'GG': (-2.50580, 49.58543), 'GG': (-2.50580, 49.58543),
'GH': (-0.46348, 7.16051), 'GH': (-0.46348, 7.16051),
'GI': (-5.32053, 36.11066), 'GI': (-5.32053, 36.11066),
'GL': (-33.85511, 74.66355), 'GL': (-33.85511, 74.66355),
'GM': (-16.40960, 13.25), 'GM': (-16.40960, 13.25),
'GN': (-13.83940, 10.96291), 'GN': (-13.83940, 10.96291),
'GP': (-61.68712, 16.23049), 'GP': (-61.68712, 16.23049),
'GQ': (10.23973, 1.43119), 'GQ': (10.23973, 1.43119),
'GR': (23.17850, 39.06206), 'GR': (23.17850, 39.06206),
'GS': (-36.49430, -54.43067), 'GS': (-36.49430, -54.43067),
'GT': (-90.74368, 15.20428), 'GT': (-90.74368, 15.20428),
'GU': (144.73362, 13.44413), 'GU': (144.73362, 13.44413),
'GW': (-14.83525, 11.92486), 'GW': (-14.83525, 11.92486),
'GY': (-58.45167, 5.73698), 'GY': (-58.45167, 5.73698),
'HK': (114.18577, 22.34923), 'HK': (114.18577, 22.34923),
'HM': (73.68230, -53.22105), 'HM': (73.68230, -53.22105),
'HN': (-86.95414, 15.23820), 'HN': (-86.95414, 15.23820),
'HR': (17.49966, 45.52689), 'HR': (17.49966, 45.52689),
'HT': (-73.51925, 18.32492), 'HT': (-73.51925, 18.32492),
'HU': (20.35362, 47.51721), 'HU': (20.35362, 47.51721),
'ID': (123.34505, -0.83791), 'ID': (123.34505, -0.83791),
'IE': (-9.00520, 52.87725), 'IE': (-9.00520, 52.87725),
'IL': (35.46314, 32.86165), 'IL': (35.46314, 32.86165),
'IM': (-4.86740, 54.023), 'IM': (-4.86740, 54.023),
'IN': (88.67620, 27.86155), 'IN': (88.67620, 27.86155),
'IO': (71.42743, -6.14349), 'IO': (71.42743, -6.14349),
'IQ': (42.58109, 34.26103), 'IQ': (42.58109, 34.26103),
'IR': (56.09355, 30.46751), 'IR': (56.09355, 30.46751),
'IS': (-17.51785, 64.71687), 'IS': (-17.51785, 64.71687),
'IT': (10.42639, 44.87904), 'IT': (10.42639, 44.87904),
'JE': (-2.19261, 49.12458), 'JE': (-2.19261, 49.12458),
'JM': (-76.84020, 18.3935), 'JM': (-76.84020, 18.3935),
'JO': (36.55552, 30.75741), 'JO': (36.55552, 30.75741),
'JP': (138.72531, 35.92099), 'JP': (138.72531, 35.92099),
'KE': (36.90602, 1.08512), 'KE': (36.90602, 1.08512),
'KG': (76.15571, 41.66497), 'KG': (76.15571, 41.66497),
'KH': (104.31901, 12.95555), 'KH': (104.31901, 12.95555),
'KI': (173.63353, 0.139), 'KI': (173.63353, 0.139),
'KM': (44.31474, -12.241), 'KM': (44.31474, -12.241),
'KN': (-62.69379, 17.2555), 'KN': (-62.69379, 17.2555),
'KP': (126.65575, 39.64575), 'KP': (126.65575, 39.64575),
'KR': (127.27740, 36.41388), 'KR': (127.27740, 36.41388),
'KW': (47.30684, 29.69180), 'KW': (47.30684, 29.69180),
'KY': (-81.07455, 19.29949), 'KY': (-81.07455, 19.29949),
'KZ': (72.00811, 49.88855), 'KZ': (72.00811, 49.88855),
'LA': (102.44391, 19.81609), 'LA': (102.44391, 19.81609),
'LB': (35.48464, 33.41766), 'LB': (35.48464, 33.41766),
'LC': (-60.97894, 13.891), 'LC': (-60.97894, 13.891),
'LI': (9.54693, 47.15934), 'LI': (9.54693, 47.15934),
'LK': (80.38520, 8.41649), 'LK': (80.38520, 8.41649),
'LR': (-11.16960, 4.04122), 'LR': (-11.16960, 4.04122),
'LS': (28.66984, -29.94538), 'LS': (28.66984, -29.94538),
'LT': (24.51735, 55.49293), 'LT': (24.51735, 55.49293),
'LU': (6.08649, 49.81533), 'LU': (6.08649, 49.81533),
'LV': (23.51033, 56.67144), 'LV': (23.51033, 56.67144),
'LY': (15.36841, 28.12177), 'LY': (15.36841, 28.12177),
'MA': (-4.03061, 33.21696), 'MA': (-4.03061, 33.21696),
'MC': (7.47743, 43.62917), 'MC': (7.47743, 43.62917),
'MD': (29.61725, 46.66517), 'MD': (29.61725, 46.66517),
'ME': (19.72291, 43.02441), 'ME': (19.72291, 43.02441),
'MF': (-63.06666, 18.08102), 'MF': (-63.06666, 18.08102),
'MG': (45.86378, -20.50245), 'MG': (45.86378, -20.50245),
'MH': (171.94982, 5.983), 'MH': (171.94982, 5.983),
'MK': (21.42108, 41.08980), 'MK': (21.42108, 41.08980),
'ML': (-1.93310, 16.46993), 'ML': (-1.93310, 16.46993),
'MM': (95.54624, 21.09620), 'MM': (95.54624, 21.09620),
'MN': (99.81138, 48.18615), 'MN': (99.81138, 48.18615),
'MO': (113.56441, 22.16209), 'MO': (113.56441, 22.16209),
'MP': (145.21345, 14.14902), 'MP': (145.21345, 14.14902),
'MQ': (-60.81128, 14.43706), 'MQ': (-60.81128, 14.43706),
'MR': (-9.42324, 22.59251), 'MR': (-9.42324, 22.59251),
'MS': (-62.19455, 16.745), 'MS': (-62.19455, 16.745),
'MT': (14.38363, 35.94467), 'MT': (14.38363, 35.94467),
'MU': (57.55121, -20.41), 'MU': (57.55121, -20.41),
'MV': (73.39292, 4.19375), 'MV': (73.39292, 4.19375),
'MW': (33.95722, -12.28218), 'MW': (33.95722, -12.28218),
'MX': (-105.89221, 25.86826), 'MX': (-105.89221, 25.86826),
'MY': (112.71154, 2.10098), 'MY': (112.71154, 2.10098),
'MZ': (37.58689, -13.72682), 'MZ': (37.58689, -13.72682),
'NA': (16.68569, -21.46572), 'NA': (16.68569, -21.46572),
'NC': (164.95322, -20.38889), 'NC': (164.95322, -20.38889),
'NE': (10.06041, 19.08273), 'NE': (10.06041, 19.08273),
'NF': (167.95718, -29.0645), 'NF': (167.95718, -29.0645),
'NG': (10.17781, 10.17804), 'NG': (10.17781, 10.17804),
'NI': (-85.87974, 13.21715), 'NI': (-85.87974, 13.21715),
'NL': (-68.57062, 12.041), 'NL': (-68.57062, 12.041),
'NO': (23.11556, 70.09934), 'NO': (23.11556, 70.09934),
'NP': (83.36259, 28.13107), 'NP': (83.36259, 28.13107),
'NR': (166.93479, -0.5275), 'NR': (166.93479, -0.5275),
'NU': (-169.84873, -19.05305), 'NU': (-169.84873, -19.05305),
'NZ': (167.97209, -45.13056), 'NZ': (167.97209, -45.13056),
'OM': (56.86055, 20.47413), 'OM': (56.86055, 20.47413),
'PA': (-79.40160, 8.80656), 'PA': (-79.40160, 8.80656),
'PE': (-78.66540, -7.54711), 'PE': (-78.66540, -7.54711),
'PF': (-145.05719, -16.70862), 'PF': (-145.05719, -16.70862),
'PG': (146.64600, -7.37427), 'PG': (146.64600, -7.37427),
'PH': (121.48359, 15.09965), 'PH': (121.48359, 15.09965),
'PK': (72.11347, 31.14629), 'PK': (72.11347, 31.14629),
'PL': (17.88136, 52.77182), 'PL': (17.88136, 52.77182),
'PM': (-56.19515, 46.78324), 'PM': (-56.19515, 46.78324),
'PN': (-130.10642, -25.06955), 'PN': (-130.10642, -25.06955),
'PR': (-65.88755, 18.37169), 'PR': (-65.88755, 18.37169),
'PS': (35.39801, 32.24773), 'PS': (35.39801, 32.24773),
'PT': (-8.45743, 40.11154), 'PT': (-8.45743, 40.11154),
'PW': (134.49645, 7.3245), 'PW': (134.49645, 7.3245),
'PY': (-59.51787, -22.41281), 'PY': (-59.51787, -22.41281),
'QA': (51.49903, 24.99816), 'QA': (51.49903, 24.99816),
'RE': (55.77345, -21.36388), 'RE': (55.77345, -21.36388),
'RO': (26.37632, 45.36120), 'RO': (26.37632, 45.36120),
'RS': (20.40371, 44.56413), 'RS': (20.40371, 44.56413),
'RU': (116.44060, 59.06780), 'RU': (116.44060, 59.06780),
'RW': (29.57882, -1.62404), 'RW': (29.57882, -1.62404),
'SA': (47.73169, 22.43790), 'SA': (47.73169, 22.43790),
'SB': (164.63894, -10.23606), 'SB': (164.63894, -10.23606),
'SC': (46.36566, -9.454), 'SC': (46.36566, -9.454),
'SD': (28.14720, 14.56423), 'SD': (28.14720, 14.56423),
'SE': (15.68667, 60.35568), 'SE': (15.68667, 60.35568),
'SG': (103.84187, 1.304), 'SG': (103.84187, 1.304),
'SH': (-12.28155, -37.11546), 'SH': (-12.28155, -37.11546),
'SI': (14.04738, 46.39085), 'SI': (14.04738, 46.39085),
'SJ': (15.27552, 79.23365), 'SJ': (15.27552, 79.23365),
'SK': (20.41603, 48.86970), 'SK': (20.41603, 48.86970),
'SL': (-11.47773, 8.78156), 'SL': (-11.47773, 8.78156),
'SM': (12.46062, 43.94279), 'SM': (12.46062, 43.94279),
'SN': (-15.37111, 14.99477), 'SN': (-15.37111, 14.99477),
'SO': (46.93383, 9.34094), 'SO': (46.93383, 9.34094),
'SR': (-55.42864, 4.56985), 'SR': (-55.42864, 4.56985),
'SS': (28.13573, 8.50933), 'SS': (28.13573, 8.50933),
'ST': (6.61025, 0.2215), 'ST': (6.61025, 0.2215),
'SV': (-89.36665, 13.43072), 'SV': (-89.36665, 13.43072),
'SX': (-63.15393, 17.9345), 'SX': (-63.15393, 17.9345),
'SY': (38.15513, 35.34221), 'SY': (38.15513, 35.34221),
'SZ': (31.78263, -26.14244), 'SZ': (31.78263, -26.14244),
'TC': (-71.32554, 21.35), 'TC': (-71.32554, 21.35),
'TD': (17.42092, 13.46223), 'TD': (17.42092, 13.46223),
'TF': (137.5, -67.5), 'TF': (137.5, -67.5),
'TG': (1.06983, 7.87677), 'TG': (1.06983, 7.87677),
'TH': (102.00877, 16.42310), 'TH': (102.00877, 16.42310),
'TJ': (71.91349, 39.01527), 'TJ': (71.91349, 39.01527),
'TK': (-171.82603, -9.20990), 'TK': (-171.82603, -9.20990),
'TL': (126.22520, -8.72636), 'TL': (126.22520, -8.72636),
'TM': (57.71603, 39.92534), 'TM': (57.71603, 39.92534),
'TN': (9.04958, 34.84199), 'TN': (9.04958, 34.84199),
'TO': (-176.99320, -23.11104), 'TO': (-176.99320, -23.11104),
'TR': (32.82002, 39.86350), 'TR': (32.82002, 39.86350),
'TT': (-60.70793, 11.1385), 'TT': (-60.70793, 11.1385),
'TV': (178.77499, -9.41685), 'TV': (178.77499, -9.41685),
'TW': (120.30074, 23.17002), 'TW': (120.30074, 23.17002),
'TZ': (33.53892, -5.01840), 'TZ': (33.53892, -5.01840),
'UA': (33.44335, 49.30619), 'UA': (33.44335, 49.30619),
'UG': (32.96523, 2.08584), 'UG': (32.96523, 2.08584),
'UM': (-169.50993, 16.74605), 'UM': (-169.50993, 16.74605),
'US': (-116.39535, 40.71379), 'US': (-116.39535, 40.71379),
'UY': (-56.46505, -33.62658), 'UY': (-56.46505, -33.62658),
'UZ': (61.35529, 42.96107), 'UZ': (61.35529, 42.96107),
'VA': (12.33197, 42.04931), 'VA': (12.33197, 42.04931),
'VC': (-61.09905, 13.316), 'VC': (-61.09905, 13.316),
'VE': (-64.88323, 7.69849), 'VE': (-64.88323, 7.69849),
'VG': (-64.62479, 18.419), 'VG': (-64.62479, 18.419),
'VI': (-64.88950, 18.32263), 'VI': (-64.88950, 18.32263),
'VN': (104.20179, 10.27644), 'VN': (104.20179, 10.27644),
'VU': (167.31919, -15.88687), 'VU': (167.31919, -15.88687),
'WF': (-176.20781, -13.28535), 'WF': (-176.20781, -13.28535),
'WS': (-172.10966, -13.85093), 'WS': (-172.10966, -13.85093),
'YE': (45.94562, 16.16338), 'YE': (45.94562, 16.16338),
'YT': (44.93774, -12.60882), 'YT': (44.93774, -12.60882),
'ZA': (23.19488, -30.43276), 'ZA': (23.19488, -30.43276),
'ZM': (26.38618, -14.39966), 'ZM': (26.38618, -14.39966),
'ZW': (30.12419, -19.86907) 'ZW': (30.12419, -19.86907)
} }

View File

@@ -2,13 +2,11 @@
# #
# This file is part of Nominatim. (https://nominatim.org) # This file is part of Nominatim. (https://nominatim.org)
# #
# Copyright (C) 2022 by the Nominatim developer community. # Copyright (C) 2025 by the Nominatim developer community.
# For a full list of authors see the git log. # For a full list of authors see the git log.
from pathlib import Path
import os
from steps.geometry_alias import ALIASES from steps.geometry_alias import ALIASES
class GeometryFactory: class GeometryFactory:
""" Provides functions to create geometries from coordinates and data grids. """ Provides functions to create geometries from coordinates and data grids.
""" """
@@ -47,7 +45,6 @@ class GeometryFactory:
return "ST_SetSRID('{}'::geometry, 4326)".format(out) return "ST_SetSRID('{}'::geometry, 4326)".format(out)
def mk_wkt_point(self, point): def mk_wkt_point(self, point):
""" Parse a point description. """ Parse a point description.
The point may either consist of 'x y' coordinates or a number The point may either consist of 'x y' coordinates or a number
@@ -65,7 +62,6 @@ class GeometryFactory:
assert pt is not None, "Scenario error: Point '{}' not found in grid".format(geom) assert pt is not None, "Scenario error: Point '{}' not found in grid".format(geom)
return "{} {}".format(*pt) return "{} {}".format(*pt)
def mk_wkt_points(self, geom): def mk_wkt_points(self, geom):
""" Parse a list of points. """ Parse a list of points.
The list must be a comma-separated list of points. Points The list must be a comma-separated list of points. Points
@@ -73,7 +69,6 @@ class GeometryFactory:
""" """
return ','.join([self.mk_wkt_point(x) for x in geom.split(',')]) return ','.join([self.mk_wkt_point(x) for x in geom.split(',')])
def set_grid(self, lines, grid_step, origin=(0.0, 0.0)): def set_grid(self, lines, grid_step, origin=(0.0, 0.0)):
""" Replace the grid with one from the given lines. """ Replace the grid with one from the given lines.
""" """
@@ -87,7 +82,6 @@ class GeometryFactory:
x += grid_step x += grid_step
y += grid_step y += grid_step
def grid_node(self, nodeid): def grid_node(self, nodeid):
""" Get the coordinates for the given grid node. """ Get the coordinates for the given grid node.
""" """

View File

@@ -2,7 +2,7 @@
# #
# This file is part of Nominatim. (https://nominatim.org) # This file is part of Nominatim. (https://nominatim.org)
# #
# Copyright (C) 2023 by the Nominatim developer community. # Copyright (C) 2025 by the Nominatim developer community.
# For a full list of authors see the git log. # For a full list of authors see the git log.
""" """
Classes wrapping HTTP responses from the Nominatim API. Classes wrapping HTTP responses from the Nominatim API.
@@ -45,7 +45,6 @@ class GenericResponse:
else: else:
self.result = [self.result] self.result = [self.result]
def _parse_geojson(self): def _parse_geojson(self):
self._parse_json() self._parse_json()
if self.result: if self.result:
@@ -76,7 +75,6 @@ class GenericResponse:
new['__' + k] = v new['__' + k] = v
self.result.append(new) self.result.append(new)
def _parse_geocodejson(self): def _parse_geocodejson(self):
self._parse_geojson() self._parse_geojson()
if self.result: if self.result:
@@ -87,7 +85,6 @@ class GenericResponse:
inner = r.pop('geocoding') inner = r.pop('geocoding')
r.update(inner) r.update(inner)
def assert_address_field(self, idx, field, value): def assert_address_field(self, idx, field, value):
""" Check that result rows`idx` has a field `field` with value `value` """ Check that result rows`idx` has a field `field` with value `value`
in its address. If idx is None, then all results are checked. in its address. If idx is None, then all results are checked.
@@ -103,7 +100,6 @@ class GenericResponse:
address = self.result[idx]['address'] address = self.result[idx]['address']
self.check_row_field(idx, field, value, base=address) self.check_row_field(idx, field, value, base=address)
def match_row(self, row, context=None, field=None): def match_row(self, row, context=None, field=None):
""" Match the result fields against the given behave table row. """ Match the result fields against the given behave table row.
""" """
@@ -139,7 +135,6 @@ class GenericResponse:
else: else:
self.check_row_field(i, name, Field(value), base=subdict) self.check_row_field(i, name, Field(value), base=subdict)
def check_row(self, idx, check, msg): def check_row(self, idx, check, msg):
""" Assert for the condition 'check' and print 'msg' on fail together """ Assert for the condition 'check' and print 'msg' on fail together
with the contents of the failing result. with the contents of the failing result.
@@ -154,7 +149,6 @@ class GenericResponse:
assert check, _RowError(self.result[idx]) assert check, _RowError(self.result[idx])
def check_row_field(self, idx, field, expected, base=None): def check_row_field(self, idx, field, expected, base=None):
""" Check field 'field' of result 'idx' for the expected value """ Check field 'field' of result 'idx' for the expected value
and print a meaningful error if the condition fails. and print a meaningful error if the condition fails.
@@ -172,7 +166,6 @@ class GenericResponse:
f"\nBad value for field '{field}'. Expected: {expected}, got: {value}") f"\nBad value for field '{field}'. Expected: {expected}, got: {value}")
class SearchResponse(GenericResponse): class SearchResponse(GenericResponse):
""" Specialised class for search and lookup responses. """ Specialised class for search and lookup responses.
Transforms the xml response in a format similar to json. Transforms the xml response in a format similar to json.
@@ -240,7 +233,8 @@ class ReverseResponse(GenericResponse):
assert 'namedetails' not in self.result[0], "More than one namedetails in result" assert 'namedetails' not in self.result[0], "More than one namedetails in result"
self.result[0]['namedetails'] = {} self.result[0]['namedetails'] = {}
for tag in child: for tag in child:
assert len(tag) == 0, f"Namedetails element '{tag.attrib['desc']}' has subelements" assert len(tag) == 0, \
f"Namedetails element '{tag.attrib['desc']}' has subelements"
self.result[0]['namedetails'][tag.attrib['desc']] = tag.text self.result[0]['namedetails'][tag.attrib['desc']] = tag.text
elif child.tag == 'geokml': elif child.tag == 'geokml':
assert 'geokml' not in self.result[0], "More than one geokml in result" assert 'geokml' not in self.result[0], "More than one geokml in result"

View File

@@ -2,10 +2,9 @@
# #
# This file is part of Nominatim. (https://nominatim.org) # This file is part of Nominatim. (https://nominatim.org)
# #
# Copyright (C) 2024 by the Nominatim developer community. # Copyright (C) 2025 by the Nominatim developer community.
# For a full list of authors see the git log. # For a full list of authors see the git log.
from pathlib import Path from pathlib import Path
import importlib
import tempfile import tempfile
import psycopg import psycopg
@@ -13,10 +12,9 @@ from psycopg import sql as pysql
from nominatim_db import cli from nominatim_db import cli
from nominatim_db.config import Configuration from nominatim_db.config import Configuration
from nominatim_db.db.connection import Connection, register_hstore, execute_scalar from nominatim_db.db.connection import register_hstore, execute_scalar
from nominatim_db.tools import refresh
from nominatim_db.tokenizer import factory as tokenizer_factory from nominatim_db.tokenizer import factory as tokenizer_factory
from steps.utils import run_script
class NominatimEnvironment: class NominatimEnvironment:
""" Collects all functions for the execution of Nominatim functions. """ Collects all functions for the execution of Nominatim functions.
@@ -62,7 +60,6 @@ class NominatimEnvironment:
dbargs['password'] = self.db_pass dbargs['password'] = self.db_pass
return psycopg.connect(**dbargs) return psycopg.connect(**dbargs)
def write_nominatim_config(self, dbname): def write_nominatim_config(self, dbname):
""" Set up a custom test configuration that connects to the given """ Set up a custom test configuration that connects to the given
database. This sets up the environment variables so that they can database. This sets up the environment variables so that they can
@@ -101,7 +98,6 @@ class NominatimEnvironment:
self.website_dir = tempfile.TemporaryDirectory() self.website_dir = tempfile.TemporaryDirectory()
def get_test_config(self): def get_test_config(self):
cfg = Configuration(Path(self.website_dir.name), environ=self.test_env) cfg = Configuration(Path(self.website_dir.name), environ=self.test_env)
return cfg return cfg
@@ -122,14 +118,13 @@ class NominatimEnvironment:
return dsn return dsn
def db_drop_database(self, name): def db_drop_database(self, name):
""" Drop the database with the given name. """ Drop the database with the given name.
""" """
with self.connect_database('postgres') as conn: with self.connect_database('postgres') as conn:
conn.autocommit = True conn.autocommit = True
conn.execute(pysql.SQL('DROP DATABASE IF EXISTS') conn.execute(pysql.SQL('DROP DATABASE IF EXISTS')
+ pysql.Identifier(name)) + pysql.Identifier(name))
def setup_template_db(self): def setup_template_db(self):
""" Setup a template database that already contains common test data. """ Setup a template database that already contains common test data.
@@ -153,13 +148,12 @@ class NominatimEnvironment:
'--osm2pgsql-cache', '1', '--osm2pgsql-cache', '1',
'--ignore-errors', '--ignore-errors',
'--offline', '--index-noanalyse') '--offline', '--index-noanalyse')
except: except: # noqa: E722
self.db_drop_database(self.template_db) self.db_drop_database(self.template_db)
raise raise
self.run_nominatim('refresh', '--functions') self.run_nominatim('refresh', '--functions')
def setup_api_db(self): def setup_api_db(self):
""" Setup a test against the API test database. """ Setup a test against the API test database.
""" """
@@ -184,13 +178,12 @@ class NominatimEnvironment:
csv_path = str(testdata / 'full_en_phrases_test.csv') csv_path = str(testdata / 'full_en_phrases_test.csv')
self.run_nominatim('special-phrases', '--import-from-csv', csv_path) self.run_nominatim('special-phrases', '--import-from-csv', csv_path)
except: except: # noqa: E722
self.db_drop_database(self.api_test_db) self.db_drop_database(self.api_test_db)
raise raise
tokenizer_factory.get_tokenizer_for_db(self.get_test_config()) tokenizer_factory.get_tokenizer_for_db(self.get_test_config())
def setup_unknown_db(self): def setup_unknown_db(self):
""" Setup a test against a non-existing database. """ Setup a test against a non-existing database.
""" """
@@ -213,7 +206,7 @@ class NominatimEnvironment:
with self.connect_database(self.template_db) as conn: with self.connect_database(self.template_db) as conn:
conn.autocommit = True conn.autocommit = True
conn.execute(pysql.SQL('DROP DATABASE IF EXISTS') conn.execute(pysql.SQL('DROP DATABASE IF EXISTS')
+ pysql.Identifier(self.test_db)) + pysql.Identifier(self.test_db))
conn.execute(pysql.SQL('CREATE DATABASE {} TEMPLATE = {}').format( conn.execute(pysql.SQL('CREATE DATABASE {} TEMPLATE = {}').format(
pysql.Identifier(self.test_db), pysql.Identifier(self.test_db),
pysql.Identifier(self.template_db))) pysql.Identifier(self.template_db)))
@@ -250,7 +243,6 @@ class NominatimEnvironment:
return False return False
def reindex_placex(self, db): def reindex_placex(self, db):
""" Run the indexing step until all data in the placex has """ Run the indexing step until all data in the placex has
been processed. Indexing during updates can produce more data been processed. Indexing during updates can produce more data
@@ -259,7 +251,6 @@ class NominatimEnvironment:
""" """
self.run_nominatim('index') self.run_nominatim('index')
def run_nominatim(self, *cmdline): def run_nominatim(self, *cmdline):
""" Run the nominatim command-line tool via the library. """ Run the nominatim command-line tool via the library.
""" """
@@ -270,7 +261,6 @@ class NominatimEnvironment:
cli_args=cmdline, cli_args=cmdline,
environ=self.test_env) environ=self.test_env)
def copy_from_place(self, db): def copy_from_place(self, db):
""" Copy data from place to the placex and location_property_osmline """ Copy data from place to the placex and location_property_osmline
tables invoking the appropriate triggers. tables invoking the appropriate triggers.
@@ -293,7 +283,6 @@ class NominatimEnvironment:
and osm_type='W' and osm_type='W'
and ST_GeometryType(geometry) = 'ST_LineString'""") and ST_GeometryType(geometry) = 'ST_LineString'""")
def create_api_request_func_starlette(self): def create_api_request_func_starlette(self):
import nominatim_api.server.starlette.server import nominatim_api.server.starlette.server
from asgi_lifespan import LifespanManager from asgi_lifespan import LifespanManager
@@ -311,7 +300,6 @@ class NominatimEnvironment:
return _request return _request
def create_api_request_func_falcon(self): def create_api_request_func_falcon(self):
import nominatim_api.server.falcon.server import nominatim_api.server.falcon.server
import falcon.testing import falcon.testing
@@ -326,6 +314,3 @@ class NominatimEnvironment:
return response.text, response.status_code return response.text, response.status_code
return _request return _request

View File

@@ -2,7 +2,7 @@
# #
# This file is part of Nominatim. (https://nominatim.org) # This file is part of Nominatim. (https://nominatim.org)
# #
# Copyright (C) 2022 by the Nominatim developer community. # Copyright (C) 2025 by the Nominatim developer community.
# For a full list of authors see the git log. # For a full list of authors see the git log.
""" """
Helper classes for filling the place table. Helper classes for filling the place table.
@@ -10,12 +10,13 @@ Helper classes for filling the place table.
import random import random
import string import string
class PlaceColumn: class PlaceColumn:
""" Helper class to collect contents from a behave table row and """ Helper class to collect contents from a behave table row and
insert it into the place table. insert it into the place table.
""" """
def __init__(self, context): def __init__(self, context):
self.columns = {'admin_level' : 15} self.columns = {'admin_level': 15}
self.context = context self.context = context
self.geometry = None self.geometry = None
@@ -98,7 +99,7 @@ class PlaceColumn:
""" Issue a delete for the given OSM object. """ Issue a delete for the given OSM object.
""" """
cursor.execute('DELETE FROM place WHERE osm_type = %s and osm_id = %s', cursor.execute('DELETE FROM place WHERE osm_type = %s and osm_id = %s',
(self.columns['osm_type'] , self.columns['osm_id'])) (self.columns['osm_type'], self.columns['osm_id']))
def db_insert(self, cursor): def db_insert(self, cursor):
""" Insert the collected data into the database. """ Insert the collected data into the database.

View File

@@ -2,20 +2,16 @@
# #
# This file is part of Nominatim. (https://nominatim.org) # This file is part of Nominatim. (https://nominatim.org)
# #
# Copyright (C) 2024 by the Nominatim developer community. # Copyright (C) 2025 by the Nominatim developer community.
# For a full list of authors see the git log. # For a full list of authors see the git log.
""" Steps that run queries against the API. """ Steps that run queries against the API.
""" """
from pathlib import Path from pathlib import Path
import json
import os
import re import re
import logging import logging
import asyncio import asyncio
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from urllib.parse import urlencode
from utils import run_script
from http_responses import GenericResponse, SearchResponse, ReverseResponse, StatusResponse from http_responses import GenericResponse, SearchResponse, ReverseResponse, StatusResponse
from check_functions import Bbox, check_for_attributes from check_functions import Bbox, check_for_attributes
from table_compare import NominatimID from table_compare import NominatimID
@@ -68,7 +64,7 @@ def send_api_query(endpoint, params, fmt, context):
getattr(context, 'http_headers', {}))) getattr(context, 'http_headers', {})))
@given(u'the HTTP header') @given('the HTTP header')
def add_http_header(context): def add_http_header(context):
if not hasattr(context, 'http_headers'): if not hasattr(context, 'http_headers'):
context.http_headers = {} context.http_headers = {}
@@ -77,7 +73,7 @@ def add_http_header(context):
context.http_headers[h] = context.table[0][h] context.http_headers[h] = context.table[0][h]
@when(u'sending (?P<fmt>\S+ )?search query "(?P<query>.*)"(?P<addr> with address)?') @when(r'sending (?P<fmt>\S+ )?search query "(?P<query>.*)"(?P<addr> with address)?')
def website_search_request(context, fmt, query, addr): def website_search_request(context, fmt, query, addr):
params = {} params = {}
if query: if query:
@@ -90,7 +86,7 @@ def website_search_request(context, fmt, query, addr):
context.response = SearchResponse(outp, fmt or 'json', status) context.response = SearchResponse(outp, fmt or 'json', status)
@when('sending v1/reverse at (?P<lat>[\d.-]*),(?P<lon>[\d.-]*)(?: with format (?P<fmt>.+))?') @when(r'sending v1/reverse at (?P<lat>[\d.-]*),(?P<lon>[\d.-]*)(?: with format (?P<fmt>.+))?')
def api_endpoint_v1_reverse(context, lat, lon, fmt): def api_endpoint_v1_reverse(context, lat, lon, fmt):
params = {} params = {}
if lat is not None: if lat is not None:
@@ -106,7 +102,7 @@ def api_endpoint_v1_reverse(context, lat, lon, fmt):
context.response = ReverseResponse(outp, fmt or 'xml', status) context.response = ReverseResponse(outp, fmt or 'xml', status)
@when('sending v1/reverse N(?P<nodeid>\d+)(?: with format (?P<fmt>.+))?') @when(r'sending v1/reverse N(?P<nodeid>\d+)(?: with format (?P<fmt>.+))?')
def api_endpoint_v1_reverse_from_node(context, nodeid, fmt): def api_endpoint_v1_reverse_from_node(context, nodeid, fmt):
params = {} params = {}
params['lon'], params['lat'] = (f'{c:f}' for c in context.osm.grid_node(int(nodeid))) params['lon'], params['lat'] = (f'{c:f}' for c in context.osm.grid_node(int(nodeid)))
@@ -115,7 +111,7 @@ def api_endpoint_v1_reverse_from_node(context, nodeid, fmt):
context.response = ReverseResponse(outp, fmt or 'xml', status) context.response = ReverseResponse(outp, fmt or 'xml', status)
@when(u'sending (?P<fmt>\S+ )?details query for (?P<query>.*)') @when(r'sending (?P<fmt>\S+ )?details query for (?P<query>.*)')
def website_details_request(context, fmt, query): def website_details_request(context, fmt, query):
params = {} params = {}
if query[0] in 'NWR': if query[0] in 'NWR':
@@ -130,38 +126,45 @@ def website_details_request(context, fmt, query):
context.response = GenericResponse(outp, fmt or 'json', status) context.response = GenericResponse(outp, fmt or 'json', status)
@when(u'sending (?P<fmt>\S+ )?lookup query for (?P<query>.*)')
@when(r'sending (?P<fmt>\S+ )?lookup query for (?P<query>.*)')
def website_lookup_request(context, fmt, query): def website_lookup_request(context, fmt, query):
params = { 'osm_ids' : query } params = {'osm_ids': query}
outp, status = send_api_query('lookup', params, fmt, context) outp, status = send_api_query('lookup', params, fmt, context)
context.response = SearchResponse(outp, fmt or 'xml', status) context.response = SearchResponse(outp, fmt or 'xml', status)
@when(u'sending (?P<fmt>\S+ )?status query')
@when(r'sending (?P<fmt>\S+ )?status query')
def website_status_request(context, fmt): def website_status_request(context, fmt):
params = {} params = {}
outp, status = send_api_query('status', params, fmt, context) outp, status = send_api_query('status', params, fmt, context)
context.response = StatusResponse(outp, fmt or 'text', status) context.response = StatusResponse(outp, fmt or 'text', status)
@step(u'(?P<operator>less than|more than|exactly|at least|at most) (?P<number>\d+) results? (?:is|are) returned')
@step(r'(?P<operator>less than|more than|exactly|at least|at most) '
r'(?P<number>\d+) results? (?:is|are) returned')
def validate_result_number(context, operator, number): def validate_result_number(context, operator, number):
context.execute_steps("Then a HTTP 200 is returned") context.execute_steps("Then a HTTP 200 is returned")
numres = len(context.response.result) numres = len(context.response.result)
assert compare(operator, numres, int(number)), \ assert compare(operator, numres, int(number)), \
f"Bad number of results: expected {operator} {number}, got {numres}." f"Bad number of results: expected {operator} {number}, got {numres}."
@then(u'a HTTP (?P<status>\d+) is returned')
@then(r'a HTTP (?P<status>\d+) is returned')
def check_http_return_status(context, status): def check_http_return_status(context, status):
assert context.response.errorcode == int(status), \ assert context.response.errorcode == int(status), \
f"Return HTTP status is {context.response.errorcode}."\ f"Return HTTP status is {context.response.errorcode}."\
f" Full response:\n{context.response.page}" f" Full response:\n{context.response.page}"
@then(u'the page contents equals "(?P<text>.+)"')
@then(r'the page contents equals "(?P<text>.+)"')
def check_page_content_equals(context, text): def check_page_content_equals(context, text):
assert context.response.page == text assert context.response.page == text
@then(u'the result is valid (?P<fmt>\w+)')
@then(r'the result is valid (?P<fmt>\w+)')
def step_impl(context, fmt): def step_impl(context, fmt):
context.execute_steps("Then a HTTP 200 is returned") context.execute_steps("Then a HTTP 200 is returned")
if fmt.strip() == 'html': if fmt.strip() == 'html':
@@ -178,7 +181,7 @@ def step_impl(context, fmt):
assert context.response.format == fmt assert context.response.format == fmt
@then(u'a (?P<fmt>\w+) user error is returned') @then(r'a (?P<fmt>\w+) user error is returned')
def check_page_error(context, fmt): def check_page_error(context, fmt):
context.execute_steps("Then a HTTP 400 is returned") context.execute_steps("Then a HTTP 400 is returned")
assert context.response.format == fmt assert context.response.format == fmt
@@ -188,32 +191,34 @@ def check_page_error(context, fmt):
else: else:
assert re.search(r'({"error":)', context.response.page, re.DOTALL) is not None assert re.search(r'({"error":)', context.response.page, re.DOTALL) is not None
@then(u'result header contains')
@then('result header contains')
def check_header_attr(context): def check_header_attr(context):
context.execute_steps("Then a HTTP 200 is returned") context.execute_steps("Then a HTTP 200 is returned")
for line in context.table: for line in context.table:
assert line['attr'] in context.response.header, \ assert line['attr'] in context.response.header, \
f"Field '{line['attr']}' missing in header. Full header:\n{context.response.header}" f"Field '{line['attr']}' missing in header. " \
f"Full header:\n{context.response.header}"
value = context.response.header[line['attr']] value = context.response.header[line['attr']]
assert re.fullmatch(line['value'], value) is not None, \ assert re.fullmatch(line['value'], value) is not None, \
f"Attribute '{line['attr']}': expected: '{line['value']}', got '{value}'" f"Attribute '{line['attr']}': expected: '{line['value']}', got '{value}'"
@then(u'result header has (?P<neg>not )?attributes (?P<attrs>.*)') @then('result header has (?P<neg>not )?attributes (?P<attrs>.*)')
def check_header_no_attr(context, neg, attrs): def check_header_no_attr(context, neg, attrs):
check_for_attributes(context.response.header, attrs, check_for_attributes(context.response.header, attrs,
'absent' if neg else 'present') 'absent' if neg else 'present')
@then(u'results contain(?: in field (?P<field>.*))?') @then(r'results contain(?: in field (?P<field>.*))?')
def step_impl(context, field): def results_contain_in_field(context, field):
context.execute_steps("then at least 1 result is returned") context.execute_steps("then at least 1 result is returned")
for line in context.table: for line in context.table:
context.response.match_row(line, context=context, field=field) context.response.match_row(line, context=context, field=field)
@then(u'result (?P<lid>\d+ )?has (?P<neg>not )?attributes (?P<attrs>.*)') @then(r'result (?P<lid>\d+ )?has (?P<neg>not )?attributes (?P<attrs>.*)')
def validate_attributes(context, lid, neg, attrs): def validate_attributes(context, lid, neg, attrs):
for i in make_todo_list(context, lid): for i in make_todo_list(context, lid):
check_for_attributes(context.response.result[i], attrs, check_for_attributes(context.response.result[i], attrs,
@@ -221,7 +226,7 @@ def validate_attributes(context, lid, neg, attrs):
@then(u'result addresses contain') @then(u'result addresses contain')
def step_impl(context): def result_addresses_contain(context):
context.execute_steps("then at least 1 result is returned") context.execute_steps("then at least 1 result is returned")
for line in context.table: for line in context.table:
@@ -231,8 +236,9 @@ def step_impl(context):
if name != 'ID': if name != 'ID':
context.response.assert_address_field(idx, name, value) context.response.assert_address_field(idx, name, value)
@then(u'address of result (?P<lid>\d+) has(?P<neg> no)? types (?P<attrs>.*)')
def check_address(context, lid, neg, attrs): @then(r'address of result (?P<lid>\d+) has(?P<neg> no)? types (?P<attrs>.*)')
def check_address_has_types(context, lid, neg, attrs):
context.execute_steps(f"then more than {lid} results are returned") context.execute_steps(f"then more than {lid} results are returned")
addr_parts = context.response.result[int(lid)]['address'] addr_parts = context.response.result[int(lid)]['address']
@@ -243,7 +249,8 @@ def check_address(context, lid, neg, attrs):
else: else:
assert attr in addr_parts assert attr in addr_parts
@then(u'address of result (?P<lid>\d+) (?P<complete>is|contains)')
@then(r'address of result (?P<lid>\d+) (?P<complete>is|contains)')
def check_address(context, lid, complete): def check_address(context, lid, complete):
context.execute_steps(f"then more than {lid} results are returned") context.execute_steps(f"then more than {lid} results are returned")
@@ -258,7 +265,7 @@ def check_address(context, lid, complete):
assert len(addr_parts) == 0, f"Additional address parts found: {addr_parts!s}" assert len(addr_parts) == 0, f"Additional address parts found: {addr_parts!s}"
@then(u'result (?P<lid>\d+ )?has bounding box in (?P<coords>[\d,.-]+)') @then(r'result (?P<lid>\d+ )?has bounding box in (?P<coords>[\d,.-]+)')
def check_bounding_box_in_area(context, lid, coords): def check_bounding_box_in_area(context, lid, coords):
expected = Bbox(coords) expected = Bbox(coords)
@@ -269,7 +276,7 @@ def check_bounding_box_in_area(context, lid, coords):
f"Bbox is not contained in {expected}") f"Bbox is not contained in {expected}")
@then(u'result (?P<lid>\d+ )?has centroid in (?P<coords>[\d,.-]+)') @then(r'result (?P<lid>\d+ )?has centroid in (?P<coords>[\d,.-]+)')
def check_centroid_in_area(context, lid, coords): def check_centroid_in_area(context, lid, coords):
expected = Bbox(coords) expected = Bbox(coords)
@@ -280,7 +287,7 @@ def check_centroid_in_area(context, lid, coords):
f"Centroid is not inside {expected}") f"Centroid is not inside {expected}")
@then(u'there are(?P<neg> no)? duplicates') @then('there are(?P<neg> no)? duplicates')
def check_for_duplicates(context, neg): def check_for_duplicates(context, neg):
context.execute_steps("then at least 1 result is returned") context.execute_steps("then at least 1 result is returned")
@@ -298,4 +305,3 @@ def check_for_duplicates(context, neg):
assert not has_dupe, f"Found duplicate for {dup}" assert not has_dupe, f"Found duplicate for {dup}"
else: else:
assert has_dupe, "No duplicates found" assert has_dupe, "No duplicates found"

View File

@@ -2,9 +2,8 @@
# #
# This file is part of Nominatim. (https://nominatim.org) # This file is part of Nominatim. (https://nominatim.org)
# #
# Copyright (C) 2024 by the Nominatim developer community. # Copyright (C) 2025 by the Nominatim developer community.
# For a full list of authors see the git log. # For a full list of authors see the git log.
import logging
from itertools import chain from itertools import chain
import psycopg import psycopg
@@ -13,9 +12,9 @@ from psycopg import sql as pysql
from place_inserter import PlaceColumn from place_inserter import PlaceColumn
from table_compare import NominatimID, DBRow from table_compare import NominatimID, DBRow
from nominatim_db.indexer import indexer
from nominatim_db.tokenizer import factory as tokenizer_factory from nominatim_db.tokenizer import factory as tokenizer_factory
def check_database_integrity(context): def check_database_integrity(context):
""" Check some generic constraints on the tables. """ Check some generic constraints on the tables.
""" """
@@ -31,10 +30,9 @@ def check_database_integrity(context):
cur.execute("SELECT count(*) FROM word WHERE word_token = ''") cur.execute("SELECT count(*) FROM word WHERE word_token = ''")
assert cur.fetchone()[0] == 0, "Empty word tokens found in word table" assert cur.fetchone()[0] == 0, "Empty word tokens found in word table"
# GIVEN ##################################
################################ GIVEN ##################################
@given("the (?P<named>named )?places") @given("the (?P<named>named )?places")
def add_data_to_place_table(context, named): def add_data_to_place_table(context, named):
""" Add entries into the place table. 'named places' makes sure that """ Add entries into the place table. 'named places' makes sure that
@@ -46,6 +44,7 @@ def add_data_to_place_table(context, named):
PlaceColumn(context).add_row(row, named is not None).db_insert(cur) PlaceColumn(context).add_row(row, named is not None).db_insert(cur)
cur.execute('ALTER TABLE place ENABLE TRIGGER place_before_insert') cur.execute('ALTER TABLE place ENABLE TRIGGER place_before_insert')
@given("the relations") @given("the relations")
def add_data_to_planet_relations(context): def add_data_to_planet_relations(context):
""" Add entries into the osm2pgsql relation middle table. This is needed """ Add entries into the osm2pgsql relation middle table. This is needed
@@ -77,9 +76,11 @@ def add_data_to_planet_relations(context):
else: else:
members = None members = None
tags = chain.from_iterable([(h[5:], r[h]) for h in r.headings if h.startswith("tags+")]) tags = chain.from_iterable([(h[5:], r[h]) for h in r.headings
if h.startswith("tags+")])
cur.execute("""INSERT INTO planet_osm_rels (id, way_off, rel_off, parts, members, tags) cur.execute("""INSERT INTO planet_osm_rels (id, way_off, rel_off,
parts, members, tags)
VALUES (%s, %s, %s, %s, %s, %s)""", VALUES (%s, %s, %s, %s, %s, %s)""",
(r['id'], last_node, last_way, parts, members, list(tags))) (r['id'], last_node, last_way, parts, members, list(tags)))
else: else:
@@ -99,6 +100,7 @@ def add_data_to_planet_relations(context):
(r['id'], psycopg.types.json.Json(tags), (r['id'], psycopg.types.json.Json(tags),
psycopg.types.json.Json(members))) psycopg.types.json.Json(members)))
@given("the ways") @given("the ways")
def add_data_to_planet_ways(context): def add_data_to_planet_ways(context):
""" Add entries into the osm2pgsql way middle table. This is necessary for """ Add entries into the osm2pgsql way middle table. This is necessary for
@@ -110,16 +112,18 @@ def add_data_to_planet_ways(context):
json_tags = row is not None and row['value'] != '1' json_tags = row is not None and row['value'] != '1'
for r in context.table: for r in context.table:
if json_tags: if json_tags:
tags = psycopg.types.json.Json({h[5:]: r[h] for h in r.headings if h.startswith("tags+")}) tags = psycopg.types.json.Json({h[5:]: r[h] for h in r.headings
if h.startswith("tags+")})
else: else:
tags = list(chain.from_iterable([(h[5:], r[h]) tags = list(chain.from_iterable([(h[5:], r[h])
for h in r.headings if h.startswith("tags+")])) for h in r.headings if h.startswith("tags+")]))
nodes = [ int(x.strip()) for x in r['nodes'].split(',') ] nodes = [int(x.strip()) for x in r['nodes'].split(',')]
cur.execute("INSERT INTO planet_osm_ways (id, nodes, tags) VALUES (%s, %s, %s)", cur.execute("INSERT INTO planet_osm_ways (id, nodes, tags) VALUES (%s, %s, %s)",
(r['id'], nodes, tags)) (r['id'], nodes, tags))
################################ WHEN ################################## # WHEN ##################################
@when("importing") @when("importing")
def import_and_index_data_from_place_table(context): def import_and_index_data_from_place_table(context):
@@ -136,6 +140,7 @@ def import_and_index_data_from_place_table(context):
# itself. # itself.
context.log_capture.buffer.clear() context.log_capture.buffer.clear()
@when("updating places") @when("updating places")
def update_place_table(context): def update_place_table(context):
""" Update the place table with the given data. Also runs all triggers """ Update the place table with the given data. Also runs all triggers
@@ -164,6 +169,7 @@ def update_postcodes(context):
""" """
context.nominatim.run_nominatim('refresh', '--postcodes') context.nominatim.run_nominatim('refresh', '--postcodes')
@when("marking for delete (?P<oids>.*)") @when("marking for delete (?P<oids>.*)")
def delete_places(context, oids): def delete_places(context, oids):
""" Remove entries from the place table. Multiple ids may be given """ Remove entries from the place table. Multiple ids may be given
@@ -184,7 +190,8 @@ def delete_places(context, oids):
# itself. # itself.
context.log_capture.buffer.clear() context.log_capture.buffer.clear()
################################ THEN ################################## # THEN ##################################
@then("(?P<table>placex|place) contains(?P<exact> exactly)?") @then("(?P<table>placex|place) contains(?P<exact> exactly)?")
def check_place_contents(context, table, exact): def check_place_contents(context, table, exact):
@@ -201,7 +208,8 @@ def check_place_contents(context, table, exact):
expected_content = set() expected_content = set()
for row in context.table: for row in context.table:
nid = NominatimID(row['object']) nid = NominatimID(row['object'])
query = 'SELECT *, ST_AsText(geometry) as geomtxt, ST_GeometryType(geometry) as geometrytype' query = """SELECT *, ST_AsText(geometry) as geomtxt,
ST_GeometryType(geometry) as geometrytype """
if table == 'placex': if table == 'placex':
query += ' ,ST_X(centroid) as cx, ST_Y(centroid) as cy' query += ' ,ST_X(centroid) as cx, ST_Y(centroid) as cy'
query += " FROM %s WHERE {}" % (table, ) query += " FROM %s WHERE {}" % (table, )
@@ -261,17 +269,18 @@ def check_search_name_contents(context, exclude):
if not exclude: if not exclude:
assert len(tokens) >= len(items), \ assert len(tokens) >= len(items), \
"No word entry found for {}. Entries found: {!s}".format(value, len(tokens)) f"No word entry found for {value}. Entries found: {len(tokens)}"
for word, token, wid in tokens: for word, token, wid in tokens:
if exclude: if exclude:
assert wid not in res[name], \ assert wid not in res[name], \
"Found term for {}/{}: {}".format(nid, name, wid) "Found term for {}/{}: {}".format(nid, name, wid)
else: else:
assert wid in res[name], \ assert wid in res[name], \
"Missing term for {}/{}: {}".format(nid, name, wid) "Missing term for {}/{}: {}".format(nid, name, wid)
elif name != 'object': elif name != 'object':
assert db_row.contains(name, value), db_row.assert_msg(name, value) assert db_row.contains(name, value), db_row.assert_msg(name, value)
@then("search_name has no entry for (?P<oid>.*)") @then("search_name has no entry for (?P<oid>.*)")
def check_search_name_has_entry(context, oid): def check_search_name_has_entry(context, oid):
""" Check that there is noentry in the search_name table for the given """ Check that there is noentry in the search_name table for the given
@@ -283,6 +292,7 @@ def check_search_name_has_entry(context, oid):
assert cur.rowcount == 0, \ assert cur.rowcount == 0, \
"Found {} entries for ID {}".format(cur.rowcount, oid) "Found {} entries for ID {}".format(cur.rowcount, oid)
@then("location_postcode contains exactly") @then("location_postcode contains exactly")
def check_location_postcode(context): def check_location_postcode(context):
""" Check full contents for location_postcode table. Each row represents a table row """ Check full contents for location_postcode table. Each row represents a table row
@@ -294,21 +304,22 @@ def check_location_postcode(context):
with context.db.cursor() as cur: with context.db.cursor() as cur:
cur.execute("SELECT *, ST_AsText(geometry) as geomtxt FROM location_postcode") cur.execute("SELECT *, ST_AsText(geometry) as geomtxt FROM location_postcode")
assert cur.rowcount == len(list(context.table)), \ assert cur.rowcount == len(list(context.table)), \
"Postcode table has {} rows, expected {}.".format(cur.rowcount, len(list(context.table))) "Postcode table has {cur.rowcount} rows, expected {len(list(context.table))}."
results = {} results = {}
for row in cur: for row in cur:
key = (row['country_code'], row['postcode']) key = (row['country_code'], row['postcode'])
assert key not in results, "Postcode table has duplicate entry: {}".format(row) assert key not in results, "Postcode table has duplicate entry: {}".format(row)
results[key] = DBRow((row['country_code'],row['postcode']), row, context) results[key] = DBRow((row['country_code'], row['postcode']), row, context)
for row in context.table: for row in context.table:
db_row = results.get((row['country'],row['postcode'])) db_row = results.get((row['country'], row['postcode']))
assert db_row is not None, \ assert db_row is not None, \
f"Missing row for country '{row['country']}' postcode '{row['postcode']}'." f"Missing row for country '{row['country']}' postcode '{row['postcode']}'."
db_row.assert_row(row, ('country', 'postcode')) db_row.assert_row(row, ('country', 'postcode'))
@then("there are(?P<exclude> no)? word tokens for postcodes (?P<postcodes>.*)") @then("there are(?P<exclude> no)? word tokens for postcodes (?P<postcodes>.*)")
def check_word_table_for_postcodes(context, exclude, postcodes): def check_word_table_for_postcodes(context, exclude, postcodes):
""" Check that the tokenizer produces postcode tokens for the given """ Check that the tokenizer produces postcode tokens for the given
@@ -333,7 +344,8 @@ def check_word_table_for_postcodes(context, exclude, postcodes):
assert len(found) == 0, f"Unexpected postcodes: {found}" assert len(found) == 0, f"Unexpected postcodes: {found}"
else: else:
assert set(found) == set(plist), \ assert set(found) == set(plist), \
f"Missing postcodes {set(plist) - set(found)}. Found: {found}" f"Missing postcodes {set(plist) - set(found)}. Found: {found}"
@then("place_addressline contains") @then("place_addressline contains")
def check_place_addressline(context): def check_place_addressline(context):
@@ -352,11 +364,12 @@ def check_place_addressline(context):
WHERE place_id = %s AND address_place_id = %s""", WHERE place_id = %s AND address_place_id = %s""",
(pid, apid)) (pid, apid))
assert cur.rowcount > 0, \ assert cur.rowcount > 0, \
"No rows found for place %s and address %s" % (row['object'], row['address']) f"No rows found for place {row['object']} and address {row['address']}."
for res in cur: for res in cur:
DBRow(nid, res, context).assert_row(row, ('address', 'object')) DBRow(nid, res, context).assert_row(row, ('address', 'object'))
@then("place_addressline doesn't contain") @then("place_addressline doesn't contain")
def check_place_addressline_exclude(context): def check_place_addressline_exclude(context):
""" Check that the place_addressline doesn't contain any entries for the """ Check that the place_addressline doesn't contain any entries for the
@@ -371,9 +384,10 @@ def check_place_addressline_exclude(context):
WHERE place_id = %s AND address_place_id = %s""", WHERE place_id = %s AND address_place_id = %s""",
(pid, apid)) (pid, apid))
assert cur.rowcount == 0, \ assert cur.rowcount == 0, \
"Row found for place %s and address %s" % (row['object'], row['address']) f"Row found for place {row['object']} and address {row['address']}."
@then("W(?P<oid>\d+) expands to(?P<neg> no)? interpolation")
@then(r"W(?P<oid>\d+) expands to(?P<neg> no)? interpolation")
def check_location_property_osmline(context, oid, neg): def check_location_property_osmline(context, oid, neg):
""" Check that the given way is present in the interpolation table. """ Check that the given way is present in the interpolation table.
""" """
@@ -392,7 +406,7 @@ def check_location_property_osmline(context, oid, neg):
for i in todo: for i in todo:
row = context.table[i] row = context.table[i]
if (int(row['start']) == res['startnumber'] if (int(row['start']) == res['startnumber']
and int(row['end']) == res['endnumber']): and int(row['end']) == res['endnumber']):
todo.remove(i) todo.remove(i)
break break
else: else:
@@ -402,8 +416,9 @@ def check_location_property_osmline(context, oid, neg):
assert not todo, f"Unmatched lines in table: {list(context.table[i] for i in todo)}" assert not todo, f"Unmatched lines in table: {list(context.table[i] for i in todo)}"
@then("location_property_osmline contains(?P<exact> exactly)?") @then("location_property_osmline contains(?P<exact> exactly)?")
def check_place_contents(context, exact): def check_osmline_contents(context, exact):
""" Check contents of the interpolation table. Each row represents a table row """ Check contents of the interpolation table. Each row represents a table row
and all data must match. Data not present in the expected table, may and all data must match. Data not present in the expected table, may
be arbitrary. The rows are identified via the 'object' column which must be arbitrary. The rows are identified via the 'object' column which must
@@ -447,4 +462,3 @@ def check_place_contents(context, exact):
assert expected_content == actual, \ assert expected_content == actual, \
f"Missing entries: {expected_content - actual}\n" \ f"Missing entries: {expected_content - actual}\n" \
f"Not expected in table: {actual - expected_content}" f"Not expected in table: {actual - expected_content}"

View File

@@ -14,6 +14,7 @@ from nominatim_db.tools.replication import run_osm2pgsql_updates
from geometry_alias import ALIASES from geometry_alias import ALIASES
def get_osm2pgsql_options(nominatim_env, fname, append): def get_osm2pgsql_options(nominatim_env, fname, append):
return dict(import_file=fname, return dict(import_file=fname,
osm2pgsql='osm2pgsql', osm2pgsql='osm2pgsql',
@@ -25,8 +26,7 @@ def get_osm2pgsql_options(nominatim_env, fname, append):
flatnode_file='', flatnode_file='',
tablespaces=dict(slim_data='', slim_index='', tablespaces=dict(slim_data='', slim_index='',
main_data='', main_index=''), main_data='', main_index=''),
append=append append=append)
)
def write_opl_file(opl, grid): def write_opl_file(opl, grid):
@@ -48,6 +48,7 @@ def write_opl_file(opl, grid):
return fd.name return fd.name
@given('the lua style file') @given('the lua style file')
def lua_style_file(context): def lua_style_file(context):
""" Define a custom style file to use for the import. """ Define a custom style file to use for the import.
@@ -90,7 +91,7 @@ def define_node_grid(context, grid_step, origin):
@when(u'loading osm data') @when(u'loading osm data')
def load_osm_file(context): def load_osm_file(context):
""" """
Load the given data into a freshly created test data using osm2pgsql. Load the given data into a freshly created test database using osm2pgsql.
No further indexing is done. No further indexing is done.
The data is expected as attached text in OPL format. The data is expected as attached text in OPL format.
@@ -102,13 +103,14 @@ def load_osm_file(context):
finally: finally:
os.remove(fname) os.remove(fname)
### reintroduce the triggers/indexes we've lost by having osm2pgsql set up place again # reintroduce the triggers/indexes we've lost by having osm2pgsql set up place again
cur = context.db.cursor() cur = context.db.cursor()
cur.execute("""CREATE TRIGGER place_before_delete BEFORE DELETE ON place cur.execute("""CREATE TRIGGER place_before_delete BEFORE DELETE ON place
FOR EACH ROW EXECUTE PROCEDURE place_delete()""") FOR EACH ROW EXECUTE PROCEDURE place_delete()""")
cur.execute("""CREATE TRIGGER place_before_insert BEFORE INSERT ON place cur.execute("""CREATE TRIGGER place_before_insert BEFORE INSERT ON place
FOR EACH ROW EXECUTE PROCEDURE place_insert()""") FOR EACH ROW EXECUTE PROCEDURE place_insert()""")
cur.execute("""CREATE UNIQUE INDEX idx_place_osm_unique on place using btree(osm_id,osm_type,class,type)""") cur.execute("""CREATE UNIQUE INDEX idx_place_osm_unique ON place
USING btree(osm_id,osm_type,class,type)""")
context.db.commit() context.db.commit()
@@ -132,6 +134,7 @@ def update_from_osm_file(context):
finally: finally:
os.remove(fname) os.remove(fname)
@when('indexing') @when('indexing')
def index_database(context): def index_database(context):
""" """

View File

@@ -2,7 +2,7 @@
# #
# This file is part of Nominatim. (https://nominatim.org) # This file is part of Nominatim. (https://nominatim.org)
# #
# Copyright (C) 2022 by the Nominatim developer community. # Copyright (C) 2025 by the Nominatim developer community.
# For a full list of authors see the git log. # For a full list of authors see the git log.
""" """
Functions to facilitate accessing and comparing the content of DB tables. Functions to facilitate accessing and comparing the content of DB tables.
@@ -16,6 +16,7 @@ from psycopg import sql as pysql
ID_REGEX = re.compile(r"(?P<typ>[NRW])(?P<oid>\d+)(:(?P<cls>\w+))?") ID_REGEX = re.compile(r"(?P<typ>[NRW])(?P<oid>\d+)(:(?P<cls>\w+))?")
class NominatimID: class NominatimID:
""" Splits a unique identifier for places into its components. """ Splits a unique identifier for places into its components.
As place_ids cannot be used for testing, we use a unique As place_ids cannot be used for testing, we use a unique
@@ -146,10 +147,10 @@ class DBRow:
return str(actual) == expected return str(actual) == expected
def _compare_place_id(self, actual, expected): def _compare_place_id(self, actual, expected):
if expected == '0': if expected == '0':
return actual == 0 return actual == 0
with self.context.db.cursor() as cur: with self.context.db.cursor() as cur:
return NominatimID(expected).get_place_id(cur) == actual return NominatimID(expected).get_place_id(cur) == actual
def _has_centroid(self, expected): def _has_centroid(self, expected):
@@ -165,13 +166,15 @@ class DBRow:
else: else:
x, y = self.context.osm.grid_node(int(expected)) x, y = self.context.osm.grid_node(int(expected))
return math.isclose(float(x), self.db_row['cx']) and math.isclose(float(y), self.db_row['cy']) return math.isclose(float(x), self.db_row['cx']) \
and math.isclose(float(y), self.db_row['cy'])
def _has_geometry(self, expected): def _has_geometry(self, expected):
geom = self.context.osm.parse_geometry(expected) geom = self.context.osm.parse_geometry(expected)
with self.context.db.cursor(row_factory=psycopg.rows.tuple_row) as cur: with self.context.db.cursor(row_factory=psycopg.rows.tuple_row) as cur:
cur.execute(pysql.SQL("""SELECT ST_Equals(ST_SnapToGrid({}, 0.00001, 0.00001), cur.execute(pysql.SQL("""
ST_SnapToGrid(ST_SetSRID({}::geometry, 4326), 0.00001, 0.00001))""") SELECT ST_Equals(ST_SnapToGrid({}, 0.00001, 0.00001),
ST_SnapToGrid(ST_SetSRID({}::geometry, 4326), 0.00001, 0.00001))""")
.format(pysql.SQL(geom), .format(pysql.SQL(geom),
pysql.Literal(self.db_row['geomtxt']))) pysql.Literal(self.db_row['geomtxt'])))
return cur.fetchone()[0] return cur.fetchone()[0]
@@ -186,7 +189,8 @@ class DBRow:
else: else:
msg += " No such column." msg += " No such column."
return msg + "\nFull DB row: {}".format(json.dumps(dict(self.db_row), indent=4, default=str)) return msg + "\nFull DB row: {}".format(json.dumps(dict(self.db_row),
indent=4, default=str))
def _get_actual(self, name): def _get_actual(self, name):
if '+' in name: if '+' in name:

View File

@@ -2,7 +2,7 @@
# #
# This file is part of Nominatim. (https://nominatim.org) # This file is part of Nominatim. (https://nominatim.org)
# #
# Copyright (C) 2022 by the Nominatim developer community. # Copyright (C) 2025 by the Nominatim developer community.
# For a full list of authors see the git log. # For a full list of authors see the git log.
""" """
Various smaller helps for step execution. Various smaller helps for step execution.
@@ -12,6 +12,7 @@ import subprocess
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
def run_script(cmd, **kwargs): def run_script(cmd, **kwargs):
""" Run the given command, check that it is successful and output """ Run the given command, check that it is successful and output
when necessary. when necessary.