Merge pull request #3670 from lonvia/flake-for-tests

Extend linting with flake to tests
This commit is contained in:
Sarah Hoffmann
2025-03-10 09:35:24 +01:00
committed by GitHub
106 changed files with 1342 additions and 1610 deletions

View File

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

View File

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

View File

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

View File

@@ -2,43 +2,45 @@
#
# 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.
from pathlib import Path
import sys
from behave import *
from behave import * # noqa
sys.path.insert(1, str(Path(__file__, '..', '..', '..', 'src').resolve()))
from steps.geometry_factory import GeometryFactory
from steps.nominatim_environment import NominatimEnvironment
from steps.geometry_factory import GeometryFactory # noqa: E402
from steps.nominatim_environment import NominatimEnvironment # noqa: E402
TEST_BASE_DIR = Path(__file__, '..', '..').resolve()
userconfig = {
'REMOVE_TEMPLATE' : False,
'KEEP_TEST_DB' : False,
'DB_HOST' : None,
'DB_PORT' : None,
'DB_USER' : None,
'DB_PASS' : None,
'TEMPLATE_DB' : 'test_template_nominatim',
'TEST_DB' : 'test_nominatim',
'API_TEST_DB' : 'test_api_nominatim',
'API_TEST_FILE' : TEST_BASE_DIR / 'testdb' / 'apidb-test-data.pbf',
'TOKENIZER' : None, # Test with a custom tokenizer
'STYLE' : 'extratags',
'REMOVE_TEMPLATE': False,
'KEEP_TEST_DB': False,
'DB_HOST': None,
'DB_PORT': None,
'DB_USER': None,
'DB_PASS': None,
'TEMPLATE_DB': 'test_template_nominatim',
'TEST_DB': 'test_nominatim',
'API_TEST_DB': 'test_api_nominatim',
'API_TEST_FILE': TEST_BASE_DIR / 'testdb' / 'apidb-test-data.pbf',
'TOKENIZER': None, # Test with a custom tokenizer
'STYLE': 'extratags',
'API_ENGINE': 'falcon'
}
use_step_matcher("re")
use_step_matcher("re") # noqa: F405
def before_all(context):
# logging setup
context.config.setup_logging()
# set up -D options
for k,v in userconfig.items():
for k, v in userconfig.items():
context.config.userdata.setdefault(k, v)
# Nominatim test setup
context.nominatim = NominatimEnvironment(context.config.userdata)
@@ -46,7 +48,7 @@ def before_all(context):
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:'):
context.scenario.skip("Not usable with Sqlite database.")
elif 'DB' in context.tags:
@@ -56,6 +58,7 @@ def before_scenario(context, scenario):
elif 'UNKNOWNDB' in context.tags:
context.nominatim.setup_unknown_db()
def after_scenario(context, scenario):
if 'DB' in context.tags:
context.nominatim.teardown_db(context)

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Collection of assertion functions used for the steps.
@@ -11,9 +11,10 @@ import json
import math
import re
OSM_TYPE = {'N' : 'node', 'W' : 'way', 'R' : 'relation',
'n' : 'node', 'w' : 'way', 'r' : 'relation',
'node' : 'n', 'way' : 'w', 'relation' : 'r'}
OSM_TYPE = {'N': 'node', 'W': 'way', 'R': 'relation',
'n': 'node', 'w': 'way', 'r': 'relation',
'node': 'n', 'way': 'w', 'relation': 'r'}
class OsmType:
@@ -23,11 +24,9 @@ class OsmType:
def __init__(self, value):
self.value = value
def __eq__(self, other):
return other == self.value or other == OSM_TYPE[self.value]
def __str__(self):
return f"{self.value} or {OSM_TYPE[self.value]}"
@@ -81,7 +80,6 @@ class Bbox:
return str(self.coord)
def check_for_attributes(obj, attrs, presence='present'):
""" Check that the object has the given attributes. 'attrs' is a
string with a comma-separated list of attributes. If 'presence'
@@ -99,4 +97,3 @@ def check_for_attributes(obj, attrs, presence='present'):
else:
assert attr in obj, \
f"No attribute '{attr}'. Full response:\n{_dump_json()}"

View File

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

View File

@@ -2,13 +2,11 @@
#
# 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.
from pathlib import Path
import os
from steps.geometry_alias import ALIASES
class GeometryFactory:
""" Provides functions to create geometries from coordinates and data grids.
"""
@@ -47,7 +45,6 @@ class GeometryFactory:
return "ST_SetSRID('{}'::geometry, 4326)".format(out)
def mk_wkt_point(self, point):
""" Parse a point description.
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)
return "{} {}".format(*pt)
def mk_wkt_points(self, geom):
""" Parse a list of 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(',')])
def set_grid(self, lines, grid_step, origin=(0.0, 0.0)):
""" Replace the grid with one from the given lines.
"""
@@ -87,7 +82,6 @@ class GeometryFactory:
x += grid_step
y += grid_step
def grid_node(self, nodeid):
""" Get the coordinates for the given grid node.
"""

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Classes wrapping HTTP responses from the Nominatim API.
@@ -45,7 +45,6 @@ class GenericResponse:
else:
self.result = [self.result]
def _parse_geojson(self):
self._parse_json()
if self.result:
@@ -76,7 +75,6 @@ class GenericResponse:
new['__' + k] = v
self.result.append(new)
def _parse_geocodejson(self):
self._parse_geojson()
if self.result:
@@ -87,7 +85,6 @@ class GenericResponse:
inner = r.pop('geocoding')
r.update(inner)
def assert_address_field(self, idx, field, 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.
@@ -103,7 +100,6 @@ class GenericResponse:
address = self.result[idx]['address']
self.check_row_field(idx, field, value, base=address)
def match_row(self, row, context=None, field=None):
""" Match the result fields against the given behave table row.
"""
@@ -139,7 +135,6 @@ class GenericResponse:
else:
self.check_row_field(i, name, Field(value), base=subdict)
def check_row(self, idx, check, msg):
""" Assert for the condition 'check' and print 'msg' on fail together
with the contents of the failing result.
@@ -154,7 +149,6 @@ class GenericResponse:
assert check, _RowError(self.result[idx])
def check_row_field(self, idx, field, expected, base=None):
""" Check field 'field' of result 'idx' for the expected value
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}")
class SearchResponse(GenericResponse):
""" Specialised class for search and lookup responses.
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"
self.result[0]['namedetails'] = {}
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
elif child.tag == 'geokml':
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)
#
# 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.
from pathlib import Path
import importlib
import tempfile
import psycopg
@@ -13,10 +12,9 @@ from psycopg import sql as pysql
from nominatim_db import cli
from nominatim_db.config import Configuration
from nominatim_db.db.connection import Connection, register_hstore, execute_scalar
from nominatim_db.tools import refresh
from nominatim_db.db.connection import register_hstore, execute_scalar
from nominatim_db.tokenizer import factory as tokenizer_factory
from steps.utils import run_script
class NominatimEnvironment:
""" Collects all functions for the execution of Nominatim functions.
@@ -62,7 +60,6 @@ class NominatimEnvironment:
dbargs['password'] = self.db_pass
return psycopg.connect(**dbargs)
def write_nominatim_config(self, dbname):
""" Set up a custom test configuration that connects to the given
database. This sets up the environment variables so that they can
@@ -101,7 +98,6 @@ class NominatimEnvironment:
self.website_dir = tempfile.TemporaryDirectory()
def get_test_config(self):
cfg = Configuration(Path(self.website_dir.name), environ=self.test_env)
return cfg
@@ -122,14 +118,13 @@ class NominatimEnvironment:
return dsn
def db_drop_database(self, name):
""" Drop the database with the given name.
"""
with self.connect_database('postgres') as conn:
conn.autocommit = True
conn.execute(pysql.SQL('DROP DATABASE IF EXISTS')
+ pysql.Identifier(name))
+ pysql.Identifier(name))
def setup_template_db(self):
""" Setup a template database that already contains common test data.
@@ -153,13 +148,12 @@ class NominatimEnvironment:
'--osm2pgsql-cache', '1',
'--ignore-errors',
'--offline', '--index-noanalyse')
except:
except: # noqa: E722
self.db_drop_database(self.template_db)
raise
self.run_nominatim('refresh', '--functions')
def setup_api_db(self):
""" Setup a test against the API test database.
"""
@@ -184,13 +178,12 @@ class NominatimEnvironment:
csv_path = str(testdata / 'full_en_phrases_test.csv')
self.run_nominatim('special-phrases', '--import-from-csv', csv_path)
except:
except: # noqa: E722
self.db_drop_database(self.api_test_db)
raise
tokenizer_factory.get_tokenizer_for_db(self.get_test_config())
def setup_unknown_db(self):
""" Setup a test against a non-existing database.
"""
@@ -213,7 +206,7 @@ class NominatimEnvironment:
with self.connect_database(self.template_db) as conn:
conn.autocommit = True
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(
pysql.Identifier(self.test_db),
pysql.Identifier(self.template_db)))
@@ -250,7 +243,6 @@ class NominatimEnvironment:
return False
def reindex_placex(self, db):
""" Run the indexing step until all data in the placex has
been processed. Indexing during updates can produce more data
@@ -259,7 +251,6 @@ class NominatimEnvironment:
"""
self.run_nominatim('index')
def run_nominatim(self, *cmdline):
""" Run the nominatim command-line tool via the library.
"""
@@ -270,7 +261,6 @@ class NominatimEnvironment:
cli_args=cmdline,
environ=self.test_env)
def copy_from_place(self, db):
""" Copy data from place to the placex and location_property_osmline
tables invoking the appropriate triggers.
@@ -293,7 +283,6 @@ class NominatimEnvironment:
and osm_type='W'
and ST_GeometryType(geometry) = 'ST_LineString'""")
def create_api_request_func_starlette(self):
import nominatim_api.server.starlette.server
from asgi_lifespan import LifespanManager
@@ -311,7 +300,6 @@ class NominatimEnvironment:
return _request
def create_api_request_func_falcon(self):
import nominatim_api.server.falcon.server
import falcon.testing
@@ -326,6 +314,3 @@ class NominatimEnvironment:
return response.text, response.status_code
return _request

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Helper classes for filling the place table.
@@ -10,12 +10,13 @@ Helper classes for filling the place table.
import random
import string
class PlaceColumn:
""" Helper class to collect contents from a behave table row and
insert it into the place table.
"""
def __init__(self, context):
self.columns = {'admin_level' : 15}
self.columns = {'admin_level': 15}
self.context = context
self.geometry = None
@@ -98,7 +99,7 @@ class PlaceColumn:
""" Issue a delete for the given OSM object.
"""
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):
""" Insert the collected data into the database.

View File

@@ -2,20 +2,16 @@
#
# 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.
""" Steps that run queries against the API.
"""
from pathlib import Path
import json
import os
import re
import logging
import asyncio
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 check_functions import Bbox, check_for_attributes
from table_compare import NominatimID
@@ -68,7 +64,7 @@ def send_api_query(endpoint, params, fmt, context):
getattr(context, 'http_headers', {})))
@given(u'the HTTP header')
@given('the HTTP header')
def add_http_header(context):
if not hasattr(context, 'http_headers'):
context.http_headers = {}
@@ -77,7 +73,7 @@ def add_http_header(context):
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):
params = {}
if query:
@@ -90,7 +86,7 @@ def website_search_request(context, fmt, query, addr):
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):
params = {}
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)
@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):
params = {}
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)
@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):
params = {}
if query[0] in 'NWR':
@@ -130,38 +126,45 @@ def website_details_request(context, fmt, query):
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):
params = { 'osm_ids' : query }
params = {'osm_ids': query}
outp, status = send_api_query('lookup', params, fmt, context)
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):
params = {}
outp, status = send_api_query('status', params, fmt, context)
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):
context.execute_steps("Then a HTTP 200 is returned")
numres = len(context.response.result)
assert compare(operator, numres, int(number)), \
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):
assert context.response.errorcode == int(status), \
f"Return HTTP status is {context.response.errorcode}."\
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):
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):
context.execute_steps("Then a HTTP 200 is returned")
if fmt.strip() == 'html':
@@ -178,7 +181,7 @@ def step_impl(context, 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):
context.execute_steps("Then a HTTP 400 is returned")
assert context.response.format == fmt
@@ -188,32 +191,34 @@ def check_page_error(context, fmt):
else:
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):
context.execute_steps("Then a HTTP 200 is returned")
for line in context.table:
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']]
assert re.fullmatch(line['value'], value) is not None, \
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):
check_for_attributes(context.response.header, attrs,
'absent' if neg else 'present')
@then(u'results contain(?: in field (?P<field>.*))?')
def step_impl(context, field):
@then(r'results contain(?: in field (?P<field>.*))?')
def results_contain_in_field(context, field):
context.execute_steps("then at least 1 result is returned")
for line in context.table:
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):
for i in make_todo_list(context, lid):
check_for_attributes(context.response.result[i], attrs,
@@ -221,7 +226,7 @@ def validate_attributes(context, lid, neg, attrs):
@then(u'result addresses contain')
def step_impl(context):
def result_addresses_contain(context):
context.execute_steps("then at least 1 result is returned")
for line in context.table:
@@ -231,8 +236,9 @@ def step_impl(context):
if name != 'ID':
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")
addr_parts = context.response.result[int(lid)]['address']
@@ -243,7 +249,8 @@ def check_address(context, lid, neg, attrs):
else:
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):
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}"
@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):
expected = Bbox(coords)
@@ -269,7 +276,7 @@ def check_bounding_box_in_area(context, lid, coords):
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):
expected = Bbox(coords)
@@ -280,7 +287,7 @@ def check_centroid_in_area(context, lid, coords):
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):
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}"
else:
assert has_dupe, "No duplicates found"

View File

@@ -2,9 +2,8 @@
#
# 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.
import logging
from itertools import chain
import psycopg
@@ -13,9 +12,9 @@ from psycopg import sql as pysql
from place_inserter import PlaceColumn
from table_compare import NominatimID, DBRow
from nominatim_db.indexer import indexer
from nominatim_db.tokenizer import factory as tokenizer_factory
def check_database_integrity(context):
""" 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 = ''")
assert cur.fetchone()[0] == 0, "Empty word tokens found in word table"
# GIVEN ##################################
################################ GIVEN ##################################
@given("the (?P<named>named )?places")
def add_data_to_place_table(context, named):
""" 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)
cur.execute('ALTER TABLE place ENABLE TRIGGER place_before_insert')
@given("the relations")
def add_data_to_planet_relations(context):
""" Add entries into the osm2pgsql relation middle table. This is needed
@@ -77,9 +76,11 @@ def add_data_to_planet_relations(context):
else:
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)""",
(r['id'], last_node, last_way, parts, members, list(tags)))
else:
@@ -99,6 +100,7 @@ def add_data_to_planet_relations(context):
(r['id'], psycopg.types.json.Json(tags),
psycopg.types.json.Json(members)))
@given("the ways")
def add_data_to_planet_ways(context):
""" 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'
for r in context.table:
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:
tags = list(chain.from_iterable([(h[5:], r[h])
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)",
(r['id'], nodes, tags))
################################ WHEN ##################################
# WHEN ##################################
@when("importing")
def import_and_index_data_from_place_table(context):
@@ -136,6 +140,7 @@ def import_and_index_data_from_place_table(context):
# itself.
context.log_capture.buffer.clear()
@when("updating places")
def update_place_table(context):
""" 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')
@when("marking for delete (?P<oids>.*)")
def delete_places(context, oids):
""" Remove entries from the place table. Multiple ids may be given
@@ -184,7 +190,8 @@ def delete_places(context, oids):
# itself.
context.log_capture.buffer.clear()
################################ THEN ##################################
# THEN ##################################
@then("(?P<table>placex|place) contains(?P<exact> exactly)?")
def check_place_contents(context, table, exact):
@@ -201,7 +208,8 @@ def check_place_contents(context, table, exact):
expected_content = set()
for row in context.table:
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':
query += ' ,ST_X(centroid) as cx, ST_Y(centroid) as cy'
query += " FROM %s WHERE {}" % (table, )
@@ -261,17 +269,18 @@ def check_search_name_contents(context, exclude):
if not exclude:
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:
if exclude:
assert wid not in res[name], \
"Found term for {}/{}: {}".format(nid, name, wid)
"Found term for {}/{}: {}".format(nid, name, wid)
else:
assert wid in res[name], \
"Missing term for {}/{}: {}".format(nid, name, wid)
"Missing term for {}/{}: {}".format(nid, name, wid)
elif name != 'object':
assert db_row.contains(name, value), db_row.assert_msg(name, value)
@then("search_name has no entry for (?P<oid>.*)")
def check_search_name_has_entry(context, oid):
""" 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, \
"Found {} entries for ID {}".format(cur.rowcount, oid)
@then("location_postcode contains exactly")
def check_location_postcode(context):
""" 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:
cur.execute("SELECT *, ST_AsText(geometry) as geomtxt FROM location_postcode")
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 = {}
for row in cur:
key = (row['country_code'], row['postcode'])
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:
db_row = results.get((row['country'],row['postcode']))
db_row = results.get((row['country'], row['postcode']))
assert db_row is not None, \
f"Missing row for country '{row['country']}' postcode '{row['postcode']}'."
db_row.assert_row(row, ('country', 'postcode'))
@then("there are(?P<exclude> no)? word tokens for postcodes (?P<postcodes>.*)")
def check_word_table_for_postcodes(context, exclude, postcodes):
""" 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}"
else:
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")
def check_place_addressline(context):
@@ -352,11 +364,12 @@ def check_place_addressline(context):
WHERE place_id = %s AND address_place_id = %s""",
(pid, apid))
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:
DBRow(nid, res, context).assert_row(row, ('address', 'object'))
@then("place_addressline doesn't contain")
def check_place_addressline_exclude(context):
""" 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""",
(pid, apid))
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):
""" 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:
row = context.table[i]
if (int(row['start']) == res['startnumber']
and int(row['end']) == res['endnumber']):
and int(row['end']) == res['endnumber']):
todo.remove(i)
break
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)}"
@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
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
@@ -447,4 +462,3 @@ def check_place_contents(context, exact):
assert expected_content == actual, \
f"Missing entries: {expected_content - actual}\n" \
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
def get_osm2pgsql_options(nominatim_env, fname, append):
return dict(import_file=fname,
osm2pgsql='osm2pgsql',
@@ -25,8 +26,7 @@ def get_osm2pgsql_options(nominatim_env, fname, append):
flatnode_file='',
tablespaces=dict(slim_data='', slim_index='',
main_data='', main_index=''),
append=append
)
append=append)
def write_opl_file(opl, grid):
@@ -48,6 +48,7 @@ def write_opl_file(opl, grid):
return fd.name
@given('the lua style file')
def lua_style_file(context):
""" 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')
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.
The data is expected as attached text in OPL format.
@@ -102,13 +103,14 @@ def load_osm_file(context):
finally:
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.execute("""CREATE TRIGGER place_before_delete BEFORE DELETE ON place
FOR EACH ROW EXECUTE PROCEDURE place_delete()""")
cur.execute("""CREATE TRIGGER place_before_insert BEFORE INSERT ON place
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()
@@ -132,6 +134,7 @@ def update_from_osm_file(context):
finally:
os.remove(fname)
@when('indexing')
def index_database(context):
"""

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
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+))?")
class NominatimID:
""" Splits a unique identifier for places into its components.
As place_ids cannot be used for testing, we use a unique
@@ -146,10 +147,10 @@ class DBRow:
return str(actual) == expected
def _compare_place_id(self, actual, expected):
if expected == '0':
if expected == '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
def _has_centroid(self, expected):
@@ -165,13 +166,15 @@ class DBRow:
else:
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):
geom = self.context.osm.parse_geometry(expected)
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),
ST_SnapToGrid(ST_SetSRID({}::geometry, 4326), 0.00001, 0.00001))""")
cur.execute(pysql.SQL("""
SELECT ST_Equals(ST_SnapToGrid({}, 0.00001, 0.00001),
ST_SnapToGrid(ST_SetSRID({}::geometry, 4326), 0.00001, 0.00001))""")
.format(pysql.SQL(geom),
pysql.Literal(self.db_row['geomtxt'])))
return cur.fetchone()[0]
@@ -186,7 +189,8 @@ class DBRow:
else:
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):
if '+' in name:

View File

@@ -1,28 +0,0 @@
# SPDX-License-Identifier: GPL-2.0-only
#
# This file is part of Nominatim. (https://nominatim.org)
#
# Copyright (C) 2022 by the Nominatim developer community.
# For a full list of authors see the git log.
"""
Various smaller helps for step execution.
"""
import logging
import subprocess
LOG = logging.getLogger(__name__)
def run_script(cmd, **kwargs):
""" Run the given command, check that it is successful and output
when necessary.
"""
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
**kwargs)
(outp, outerr) = proc.communicate()
outp = outp.decode('utf-8')
outerr = outerr.decode('utf-8').replace('\\n', '\n')
LOG.debug("Run command: %s\n%s\n%s", cmd, outp, outerr)
assert proc.returncode == 0, "Script '{}' failed:\n{}\n{}\n".format(cmd[0], outp, outerr)
return outp, outerr

View File

@@ -2,14 +2,13 @@
#
# 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.
"""
Helper fixtures for API call tests.
"""
import pytest
import pytest_asyncio
import time
import datetime as dt
import sqlalchemy as sa
@@ -20,27 +19,25 @@ from nominatim_api.search.query_analyzer_factory import make_query_analyzer
from nominatim_db.tools import convert_sqlite
import nominatim_api.logging as loglib
class APITester:
def __init__(self):
self.api = napi.NominatimAPI()
self.async_to_sync(self.api._async_api.setup_database())
def async_to_sync(self, func):
""" Run an asynchronous function until completion using the
internal loop of the API.
"""
return self.api._loop.run_until_complete(func)
def add_data(self, table, data):
""" Insert data into the given table.
"""
sql = getattr(self.api._async_api._tables, table).insert()
self.async_to_sync(self.exec_async(sql, data))
def add_placex(self, **kw):
name = kw.get('name')
if isinstance(name, str):
@@ -50,30 +47,29 @@ class APITester:
geometry = kw.get('geometry', 'POINT(%f %f)' % centroid)
self.add_data('placex',
{'place_id': kw.get('place_id', 1000),
'osm_type': kw.get('osm_type', 'W'),
'osm_id': kw.get('osm_id', 4),
'class_': kw.get('class_', 'highway'),
'type': kw.get('type', 'residential'),
'name': name,
'address': kw.get('address'),
'extratags': kw.get('extratags'),
'parent_place_id': kw.get('parent_place_id'),
'linked_place_id': kw.get('linked_place_id'),
'admin_level': kw.get('admin_level', 15),
'country_code': kw.get('country_code'),
'housenumber': kw.get('housenumber'),
'postcode': kw.get('postcode'),
'wikipedia': kw.get('wikipedia'),
'rank_search': kw.get('rank_search', 30),
'rank_address': kw.get('rank_address', 30),
'importance': kw.get('importance'),
'centroid': 'POINT(%f %f)' % centroid,
'indexed_status': kw.get('indexed_status', 0),
'indexed_date': kw.get('indexed_date',
dt.datetime(2022, 12, 7, 14, 14, 46, 0)),
'geometry': geometry})
{'place_id': kw.get('place_id', 1000),
'osm_type': kw.get('osm_type', 'W'),
'osm_id': kw.get('osm_id', 4),
'class_': kw.get('class_', 'highway'),
'type': kw.get('type', 'residential'),
'name': name,
'address': kw.get('address'),
'extratags': kw.get('extratags'),
'parent_place_id': kw.get('parent_place_id'),
'linked_place_id': kw.get('linked_place_id'),
'admin_level': kw.get('admin_level', 15),
'country_code': kw.get('country_code'),
'housenumber': kw.get('housenumber'),
'postcode': kw.get('postcode'),
'wikipedia': kw.get('wikipedia'),
'rank_search': kw.get('rank_search', 30),
'rank_address': kw.get('rank_address', 30),
'importance': kw.get('importance'),
'centroid': 'POINT(%f %f)' % centroid,
'indexed_status': kw.get('indexed_status', 0),
'indexed_date': kw.get('indexed_date',
dt.datetime(2022, 12, 7, 14, 14, 46, 0)),
'geometry': geometry})
def add_address_placex(self, object_id, **kw):
self.add_placex(**kw)
@@ -85,46 +81,42 @@ class APITester:
'fromarea': kw.get('fromarea', False),
'isaddress': kw.get('isaddress', True)})
def add_osmline(self, **kw):
self.add_data('osmline',
{'place_id': kw.get('place_id', 10000),
'osm_id': kw.get('osm_id', 4004),
'parent_place_id': kw.get('parent_place_id'),
'indexed_date': kw.get('indexed_date',
dt.datetime(2022, 12, 7, 14, 14, 46, 0)),
'startnumber': kw.get('startnumber', 2),
'endnumber': kw.get('endnumber', 6),
'step': kw.get('step', 2),
'address': kw.get('address'),
'postcode': kw.get('postcode'),
'country_code': kw.get('country_code'),
'linegeo': kw.get('geometry', 'LINESTRING(1.1 -0.2, 1.09 -0.22)')})
{'place_id': kw.get('place_id', 10000),
'osm_id': kw.get('osm_id', 4004),
'parent_place_id': kw.get('parent_place_id'),
'indexed_date': kw.get('indexed_date',
dt.datetime(2022, 12, 7, 14, 14, 46, 0)),
'startnumber': kw.get('startnumber', 2),
'endnumber': kw.get('endnumber', 6),
'step': kw.get('step', 2),
'address': kw.get('address'),
'postcode': kw.get('postcode'),
'country_code': kw.get('country_code'),
'linegeo': kw.get('geometry', 'LINESTRING(1.1 -0.2, 1.09 -0.22)')})
def add_tiger(self, **kw):
self.add_data('tiger',
{'place_id': kw.get('place_id', 30000),
'parent_place_id': kw.get('parent_place_id'),
'startnumber': kw.get('startnumber', 2),
'endnumber': kw.get('endnumber', 6),
'step': kw.get('step', 2),
'postcode': kw.get('postcode'),
'linegeo': kw.get('geometry', 'LINESTRING(1.1 -0.2, 1.09 -0.22)')})
{'place_id': kw.get('place_id', 30000),
'parent_place_id': kw.get('parent_place_id'),
'startnumber': kw.get('startnumber', 2),
'endnumber': kw.get('endnumber', 6),
'step': kw.get('step', 2),
'postcode': kw.get('postcode'),
'linegeo': kw.get('geometry', 'LINESTRING(1.1 -0.2, 1.09 -0.22)')})
def add_postcode(self, **kw):
self.add_data('postcode',
{'place_id': kw.get('place_id', 1000),
'parent_place_id': kw.get('parent_place_id'),
'country_code': kw.get('country_code'),
'postcode': kw.get('postcode'),
'rank_search': kw.get('rank_search', 20),
'rank_address': kw.get('rank_address', 22),
'indexed_date': kw.get('indexed_date',
dt.datetime(2022, 12, 7, 14, 14, 46, 0)),
'geometry': kw.get('geometry', 'POINT(23 34)')})
{'place_id': kw.get('place_id', 1000),
'parent_place_id': kw.get('parent_place_id'),
'country_code': kw.get('country_code'),
'postcode': kw.get('postcode'),
'rank_search': kw.get('rank_search', 20),
'rank_address': kw.get('rank_address', 22),
'indexed_date': kw.get('indexed_date',
dt.datetime(2022, 12, 7, 14, 14, 46, 0)),
'geometry': kw.get('geometry', 'POINT(23 34)')})
def add_country(self, country_code, geometry):
self.add_data('country_grid',
@@ -132,14 +124,12 @@ class APITester:
'area': 0.1,
'geometry': geometry})
def add_country_name(self, country_code, names, partition=0):
self.add_data('country_name',
{'country_code': country_code,
'name': names,
'partition': partition})
def add_search_name(self, place_id, **kw):
centroid = kw.get('centroid', (23.0, 34.0))
self.add_data('search_name',
@@ -152,7 +142,6 @@ class APITester:
'country_code': kw.get('country_code', 'xx'),
'centroid': 'POINT(%f %f)' % centroid})
def add_class_type_table(self, cls, typ):
self.async_to_sync(
self.exec_async(sa.text(f"""CREATE TABLE place_classtype_{cls}_{typ}
@@ -160,7 +149,6 @@ class APITester:
WHERE class = '{cls}' AND type = '{typ}')
""")))
def add_word_table(self, content):
data = [dict(zip(['word_id', 'word_token', 'type', 'word', 'info'], c))
for c in content]
@@ -176,12 +164,10 @@ class APITester:
self.async_to_sync(_do_sql())
async def exec_async(self, sql, *args, **kwargs):
async with self.api._async_api.begin() as conn:
return await conn.execute(sql, *args, **kwargs)
async def create_tables(self):
async with self.api._async_api._engine.begin() as conn:
await conn.run_sync(self.api._async_api._tables.meta.create_all)
@@ -212,11 +198,12 @@ def frontend(request, event_loop, tmp_path):
db = str(tmp_path / 'test_nominatim_python_unittest.sqlite')
def mkapi(apiobj, options={'reverse'}):
apiobj.add_data('properties',
[{'property': 'tokenizer', 'value': 'icu'},
{'property': 'tokenizer_import_normalisation', 'value': ':: lower();'},
{'property': 'tokenizer_import_transliteration', 'value': "'1' > '/1/'; 'ä' > 'ä '"},
])
apiobj.add_data(
'properties',
[{'property': 'tokenizer', 'value': 'icu'},
{'property': 'tokenizer_import_normalisation', 'value': ':: lower();'},
{'property': 'tokenizer_import_transliteration',
'value': "'1' > '/1/'; 'ä' > 'ä '"}])
async def _do_sql():
async with apiobj.api._async_api.begin() as conn:

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Provides dummy implementations of ASGIAdaptor for testing.
@@ -13,6 +13,7 @@ import nominatim_api.v1.server_glue as glue
from nominatim_api.v1.format import dispatch as formatting
from nominatim_api.config import Configuration
class FakeError(BaseException):
def __init__(self, msg, status):
@@ -22,8 +23,10 @@ class FakeError(BaseException):
def __str__(self):
return f'{self.status} -- {self.msg}'
FakeResponse = namedtuple('FakeResponse', ['status', 'output', 'content_type'])
class FakeAdaptor(glue.ASGIAdaptor):
def __init__(self, params=None, headers=None, config=None):
@@ -31,23 +34,18 @@ class FakeAdaptor(glue.ASGIAdaptor):
self.headers = headers or {}
self._config = config or Configuration(None)
def get(self, name, default=None):
return self.params.get(name, default)
def get_header(self, name, default=None):
return self.headers.get(name, default)
def error(self, msg, status=400):
return FakeError(msg, status)
def create_response(self, status, output, num_results):
return FakeResponse(status, output, self.content_type)
def base_uri(self):
return 'http://test'
@@ -56,5 +54,3 @@ class FakeAdaptor(glue.ASGIAdaptor):
def formatting(self):
return formatting

View File

@@ -2,21 +2,18 @@
#
# 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.
"""
Tests for normalizing search queries.
"""
from pathlib import Path
import pytest
from icu import Transliterator
import nominatim_api.search.query as qmod
from nominatim_api.query_preprocessing.config import QueryConfig
from nominatim_api.query_preprocessing import normalize
def run_preprocessor_on(query, norm):
normalizer = Transliterator.createFromRules("normalization", norm)
proc = normalize.create(QueryConfig().set_normalizer(normalizer))

View File

@@ -7,16 +7,13 @@
"""
Tests for japanese phrase splitting.
"""
from pathlib import Path
import pytest
from icu import Transliterator
import nominatim_api.search.query as qmod
from nominatim_api.query_preprocessing.config import QueryConfig
from nominatim_api.query_preprocessing import split_japanese_phrases
def run_preprocessor_on(query):
proc = split_japanese_phrases.create(QueryConfig().set_normalizer(None))

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for tokenized query data structures.
@@ -11,6 +11,7 @@ import pytest
from nominatim_api.search import query
class MyToken(query.Token):
def get_category(self):
@@ -21,9 +22,11 @@ def mktoken(tid: int):
return MyToken(penalty=3.0, token=tid, count=1, addr_count=1,
lookup_word='foo')
@pytest.fixture
def qnode():
return query.QueryNode(query.BREAK_PHRASE, query.PHRASE_ANY, 0.0 ,'', '')
return query.QueryNode(query.BREAK_PHRASE, query.PHRASE_ANY, 0.0, '', '')
@pytest.mark.parametrize('ptype,ttype', [(query.PHRASE_ANY, 'W'),
(query.PHRASE_AMENITY, 'Q'),
@@ -132,4 +135,3 @@ def test_query_struct_amenity_two_words():
assert len(q.get_tokens(query.TokenRange(1, 2), query.TOKEN_PARTIAL)) == 1
assert len(q.get_tokens(query.TokenRange(1, 2), query.TOKEN_NEAR_ITEM)) == 0
assert len(q.get_tokens(query.TokenRange(1, 2), query.TOKEN_QUALIFIER)) == 1

View File

@@ -16,6 +16,7 @@ from nominatim_api.search.token_assignment import TokenAssignment
from nominatim_api.types import SearchDetails
import nominatim_api.search.db_searches as dbs
class MyToken(Token):
def get_category(self):
return 'this', 'that'
@@ -36,7 +37,6 @@ def make_query(*args):
token=tid, count=1, addr_count=1,
lookup_word=word))
return q
@@ -241,8 +241,7 @@ def test_name_and_address():
[(2, qmod.TOKEN_PARTIAL, [(2, 'b')]),
(2, qmod.TOKEN_WORD, [(101, 'b')])],
[(3, qmod.TOKEN_PARTIAL, [(3, 'c')]),
(3, qmod.TOKEN_WORD, [(102, 'c')])]
)
(3, qmod.TOKEN_WORD, [(102, 'c')])])
builder = SearchBuilder(q, SearchDetails())
searches = list(builder.build(TokenAssignment(name=TokenRange(0, 1),
@@ -267,8 +266,7 @@ def test_name_and_complex_address():
(3, qmod.TOKEN_WORD, [(101, 'bc')])],
[(3, qmod.TOKEN_PARTIAL, [(3, 'c')])],
[(4, qmod.TOKEN_PARTIAL, [(4, 'd')]),
(4, qmod.TOKEN_WORD, [(103, 'd')])]
)
(4, qmod.TOKEN_WORD, [(103, 'd')])])
builder = SearchBuilder(q, SearchDetails())
searches = list(builder.build(TokenAssignment(name=TokenRange(0, 1),
@@ -423,8 +421,8 @@ def test_infrequent_partials_in_name():
assert len(search.lookups) == 2
assert len(search.rankings) == 2
assert set((l.column, l.lookup_type.__name__) for l in search.lookups) == \
{('name_vector', 'LookupAll'), ('nameaddress_vector', 'Restrict')}
assert set((s.column, s.lookup_type.__name__) for s in search.lookups) == \
{('name_vector', 'LookupAll'), ('nameaddress_vector', 'Restrict')}
def test_frequent_partials_in_name_and_address():
@@ -435,10 +433,10 @@ def test_frequent_partials_in_name_and_address():
assert all(isinstance(s, dbs.PlaceSearch) for s in searches)
searches.sort(key=lambda s: s.penalty)
assert set((l.column, l.lookup_type.__name__) for l in searches[0].lookups) == \
{('name_vector', 'LookupAny'), ('nameaddress_vector', 'Restrict')}
assert set((l.column, l.lookup_type.__name__) for l in searches[1].lookups) == \
{('nameaddress_vector', 'LookupAll'), ('name_vector', 'LookupAll')}
assert set((s.column, s.lookup_type.__name__) for s in searches[0].lookups) == \
{('name_vector', 'LookupAny'), ('nameaddress_vector', 'Restrict')}
assert set((s.column, s.lookup_type.__name__) for s in searches[1].lookups) == \
{('nameaddress_vector', 'LookupAll'), ('name_vector', 'LookupAll')}
def test_too_frequent_partials_in_name_and_address():
@@ -449,5 +447,5 @@ def test_too_frequent_partials_in_name_and_address():
assert all(isinstance(s, dbs.PlaceSearch) for s in searches)
searches.sort(key=lambda s: s.penalty)
assert set((l.column, l.lookup_type.__name__) for l in searches[0].lookups) == \
{('name_vector', 'LookupAny'), ('nameaddress_vector', 'Restrict')}
assert set((s.column, s.lookup_type.__name__) for s in searches[0].lookups) == \
{('name_vector', 'LookupAny'), ('nameaddress_vector', 'Restrict')}

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for query analyzer for ICU tokenizer.
@@ -16,7 +16,8 @@ import nominatim_api.search.query as qmod
import nominatim_api.search.icu_tokenizer as tok
from nominatim_api.logging import set_log_output, get_and_disable
async def add_word(conn, word_id, word_token, wtype, word, info = None):
async def add_word(conn, word_id, word_token, wtype, word, info=None):
t = conn.t.meta.tables['word']
await conn.execute(t.insert(), {'word_id': word_id,
'word_token': word_token,
@@ -28,6 +29,7 @@ async def add_word(conn, word_id, word_token, wtype, word, info = None):
def make_phrase(query):
return [Phrase(qmod.PHRASE_ANY, s) for s in query.split(',')]
@pytest_asyncio.fixture
async def conn(table_factory):
""" Create an asynchronous SQLAlchemy engine for the test DB.
@@ -102,8 +104,7 @@ async def test_splitting_in_transliteration(conn):
@pytest.mark.asyncio
@pytest.mark.parametrize('term,order', [('23456', ['P', 'H', 'W', 'w']),
('3', ['H', 'W', 'w'])
])
('3', ['H', 'W', 'w'])])
async def test_penalty_postcodes_and_housenumbers(conn, term, order):
ana = await tok.create_query_analyzer(conn)
@@ -120,6 +121,7 @@ async def test_penalty_postcodes_and_housenumbers(conn, term, order):
assert [t[1] for t in torder] == order
@pytest.mark.asyncio
async def test_category_words_only_at_beginning(conn):
ana = await tok.create_query_analyzer(conn)

View File

@@ -16,6 +16,7 @@ import pytest
from nominatim_api.search.postcode_parser import PostcodeParser
from nominatim_api.search.query import QueryStruct, PHRASE_ANY, PHRASE_POSTCODE, PHRASE_STREET
@pytest.fixture
def pc_config(project_env):
country_file = project_env.project_dir / 'country_settings.yaml'
@@ -55,6 +56,7 @@ ky:
return project_env
def mk_query(inp):
query = QueryStruct([])
phrase_split = re.split(r"([ ,:'-])", inp)
@@ -80,6 +82,7 @@ def test_simple_postcode(pc_config, query, pos):
assert result == {(pos, pos + 1, '45325'), (pos, pos + 1, '453 25')}
def test_contained_postcode(pc_config):
parser = PostcodeParser(pc_config)
@@ -87,7 +90,6 @@ def test_contained_postcode(pc_config):
(0, 2, '12345 DX')}
@pytest.mark.parametrize('query,frm,to', [('345987', 0, 1), ('345 987', 0, 2),
('Aina 345 987', 1, 3),
('Aina 23 345 987 ff', 2, 4)])
@@ -98,6 +100,7 @@ def test_postcode_with_space(pc_config, query, frm, to):
assert result == {(frm, to, '345987')}
def test_overlapping_postcode(pc_config):
parser = PostcodeParser(pc_config)
@@ -131,6 +134,7 @@ def test_postcode_with_non_matching_country_prefix(pc_config):
assert not parser.parse(mk_query('ky12233'))
def test_postcode_inside_postcode_phrase(pc_config):
parser = PostcodeParser(pc_config)

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Test data types for search queries.
@@ -11,14 +11,15 @@ import pytest
import nominatim_api.search.query as nq
def test_token_range_equal():
assert nq.TokenRange(2, 3) == nq.TokenRange(2, 3)
assert not (nq.TokenRange(2, 3) != nq.TokenRange(2, 3))
@pytest.mark.parametrize('lop,rop', [((1, 2), (3, 4)),
((3, 4), (3, 5)),
((10, 12), (11, 12))])
((3, 4), (3, 5)),
((10, 12), (11, 12))])
def test_token_range_unequal(lop, rop):
assert not (nq.TokenRange(*lop) == nq.TokenRange(*rop))
assert nq.TokenRange(*lop) != nq.TokenRange(*rop)
@@ -28,17 +29,17 @@ def test_token_range_lt():
assert nq.TokenRange(1, 3) < nq.TokenRange(10, 12)
assert nq.TokenRange(5, 6) < nq.TokenRange(7, 8)
assert nq.TokenRange(1, 4) < nq.TokenRange(4, 5)
assert not(nq.TokenRange(5, 6) < nq.TokenRange(5, 6))
assert not(nq.TokenRange(10, 11) < nq.TokenRange(4, 5))
assert not (nq.TokenRange(5, 6) < nq.TokenRange(5, 6))
assert not (nq.TokenRange(10, 11) < nq.TokenRange(4, 5))
def test_token_rankge_gt():
assert nq.TokenRange(3, 4) > nq.TokenRange(1, 2)
assert nq.TokenRange(100, 200) > nq.TokenRange(10, 11)
assert nq.TokenRange(10, 11) > nq.TokenRange(4, 10)
assert not(nq.TokenRange(5, 6) > nq.TokenRange(5, 6))
assert not(nq.TokenRange(1, 2) > nq.TokenRange(3, 4))
assert not(nq.TokenRange(4, 10) > nq.TokenRange(3, 5))
assert not (nq.TokenRange(5, 6) > nq.TokenRange(5, 6))
assert not (nq.TokenRange(1, 2) > nq.TokenRange(3, 4))
assert not (nq.TokenRange(4, 10) > nq.TokenRange(3, 5))
def test_token_range_unimplemented_ops():
@@ -58,8 +59,7 @@ def test_query_extract_words():
words = q.extract_words(base_penalty=1.0)
assert set(words.keys()) \
== {'12', 'ab', 'hallo', '12 ab', 'ab 12', '12 ab 12'}
== {'12', 'ab', 'hallo', '12 ab', 'ab 12', '12 ab 12'}
assert sorted(words['12']) == [nq.TokenRange(0, 1, 1.0), nq.TokenRange(2, 3, 1.0)]
assert words['12 ab'] == [nq.TokenRange(0, 2, 1.1)]
assert words['hallo'] == [nq.TokenRange(3, 4, 1.0)]

View File

@@ -2,18 +2,17 @@
#
# 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.
"""
Tests for query analyzer creation.
"""
from pathlib import Path
import pytest
from nominatim_api.search.query_analyzer_factory import make_query_analyzer
from nominatim_api.search.icu_tokenizer import ICUQueryAnalyzer
@pytest.mark.asyncio
async def test_import_icu_tokenizer(table_factory, api):
table_factory('nominatim_properties',

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for running the country searcher.
@@ -48,6 +48,7 @@ def test_find_from_placex(apiobj, frontend):
assert results[0].place_id == 55
assert results[0].accuracy == 0.8
def test_find_from_fallback_countries(apiobj, frontend):
apiobj.add_country('ro', 'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))')
apiobj.add_country_name('ro', {'name': 'România'})
@@ -87,7 +88,6 @@ class TestCountryParameters:
apiobj.add_country('ro', 'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))')
apiobj.add_country_name('ro', {'name': 'România'})
@pytest.mark.parametrize('geom', [napi.GeometryFormat.GEOJSON,
napi.GeometryFormat.KML,
napi.GeometryFormat.SVG,
@@ -100,7 +100,6 @@ class TestCountryParameters:
assert len(results) == 1
assert geom.name.lower() in results[0].geometry
@pytest.mark.parametrize('pid,rids', [(76, [55]), (55, [])])
def test_exclude_place_id(self, apiobj, frontend, pid, rids):
results = run_search(apiobj, frontend, 0.5, ['yw', 'ro'],
@@ -108,7 +107,6 @@ class TestCountryParameters:
assert [r.place_id for r in results] == rids
@pytest.mark.parametrize('viewbox,rids', [((9, 9, 11, 11), [55]),
((-10, -10, -3, -3), [])])
def test_bounded_viewbox_in_placex(self, apiobj, frontend, viewbox, rids):
@@ -118,9 +116,8 @@ class TestCountryParameters:
assert [r.place_id for r in results] == rids
@pytest.mark.parametrize('viewbox,numres', [((0, 0, 1, 1), 1),
((-10, -10, -3, -3), 0)])
((-10, -10, -3, -3), 0)])
def test_bounded_viewbox_in_fallback(self, apiobj, frontend, viewbox, numres):
results = run_search(apiobj, frontend, 0.5, ['ro'],
details=SearchDetails.from_kwargs({'viewbox': viewbox,

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for running the near searcher.
@@ -12,8 +12,8 @@ import pytest
import nominatim_api as napi
from nominatim_api.types import SearchDetails
from nominatim_api.search.db_searches import NearSearch, PlaceSearch
from nominatim_api.search.db_search_fields import WeightedStrings, WeightedCategories,\
FieldLookup, FieldRanking, RankedTokens
from nominatim_api.search.db_search_fields import WeightedStrings, WeightedCategories, \
FieldLookup
from nominatim_api.search.db_search_lookups import LookupAll
@@ -80,7 +80,6 @@ class TestNearSearch:
apiobj.add_search_name(101, names=[56], country_code='mx',
centroid=(-10.3, 56.9))
def test_near_in_placex(self, apiobj, frontend):
apiobj.add_placex(place_id=22, class_='amenity', type='bank',
centroid=(5.6001, 4.2994))
@@ -91,7 +90,6 @@ class TestNearSearch:
assert [r.place_id for r in results] == [22]
def test_multiple_types_near_in_placex(self, apiobj, frontend):
apiobj.add_placex(place_id=22, class_='amenity', type='bank',
importance=0.002,
@@ -105,7 +103,6 @@ class TestNearSearch:
assert [r.place_id for r in results] == [22, 23]
def test_near_in_classtype(self, apiobj, frontend):
apiobj.add_placex(place_id=22, class_='amenity', type='bank',
centroid=(5.6, 4.34))
@@ -118,7 +115,6 @@ class TestNearSearch:
assert [r.place_id for r in results] == [22]
@pytest.mark.parametrize('cc,rid', [('us', 22), ('mx', 23)])
def test_restrict_by_country(self, apiobj, frontend, cc, rid):
apiobj.add_placex(place_id=22, class_='amenity', type='bank',
@@ -138,7 +134,6 @@ class TestNearSearch:
assert [r.place_id for r in results] == [rid]
@pytest.mark.parametrize('excluded,rid', [(22, 122), (122, 22)])
def test_exclude_place_by_id(self, apiobj, frontend, excluded, rid):
apiobj.add_placex(place_id=22, class_='amenity', type='bank',
@@ -148,13 +143,11 @@ class TestNearSearch:
centroid=(5.6001, 4.2994),
country_code='us')
results = run_search(apiobj, frontend, 0.1, [('amenity', 'bank')],
details=SearchDetails(excluded=[excluded]))
assert [r.place_id for r in results] == [rid]
@pytest.mark.parametrize('layer,rids', [(napi.DataLayer.POI, [22]),
(napi.DataLayer.MANMADE, [])])
def test_with_layer(self, apiobj, frontend, layer, rids):

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for running the generic place searcher.
@@ -14,12 +14,13 @@ import pytest
import nominatim_api as napi
from nominatim_api.types import SearchDetails
from nominatim_api.search.db_searches import PlaceSearch
from nominatim_api.search.db_search_fields import WeightedStrings, WeightedCategories,\
from nominatim_api.search.db_search_fields import WeightedStrings, WeightedCategories, \
FieldLookup, FieldRanking, RankedTokens
from nominatim_api.search.db_search_lookups import LookupAll, LookupAny, Restrict
APIOPTIONS = ['search']
def run_search(apiobj, frontend, global_penalty, lookup, ranking, count=2,
hnrs=[], pcs=[], ccodes=[], quals=[],
details=SearchDetails()):
@@ -55,29 +56,27 @@ class TestNameOnlySearches:
def fill_database(self, apiobj):
apiobj.add_placex(place_id=100, country_code='us',
centroid=(5.6, 4.3))
apiobj.add_search_name(100, names=[1,2,10,11], country_code='us',
apiobj.add_search_name(100, names=[1, 2, 10, 11], country_code='us',
centroid=(5.6, 4.3))
apiobj.add_placex(place_id=101, country_code='mx',
centroid=(-10.3, 56.9))
apiobj.add_search_name(101, names=[1,2,20,21], country_code='mx',
apiobj.add_search_name(101, names=[1, 2, 20, 21], country_code='mx',
centroid=(-10.3, 56.9))
@pytest.mark.parametrize('lookup_type', [LookupAll, Restrict])
@pytest.mark.parametrize('rank,res', [([10], [100, 101]),
([20], [101, 100])])
def test_lookup_all_match(self, apiobj, frontend, lookup_type, rank, res):
lookup = FieldLookup('name_vector', [1,2], lookup_type)
lookup = FieldLookup('name_vector', [1, 2], lookup_type)
ranking = FieldRanking('name_vector', 0.4, [RankedTokens(0.0, rank)])
results = run_search(apiobj, frontend, 0.1, [lookup], [ranking])
assert [r.place_id for r in results] == res
@pytest.mark.parametrize('lookup_type', [LookupAll, Restrict])
def test_lookup_all_partial_match(self, apiobj, frontend, lookup_type):
lookup = FieldLookup('name_vector', [1,20], lookup_type)
lookup = FieldLookup('name_vector', [1, 20], lookup_type)
ranking = FieldRanking('name_vector', 0.4, [RankedTokens(0.0, [21])])
results = run_search(apiobj, frontend, 0.1, [lookup], [ranking])
@@ -88,14 +87,13 @@ class TestNameOnlySearches:
@pytest.mark.parametrize('rank,res', [([10], [100, 101]),
([20], [101, 100])])
def test_lookup_any_match(self, apiobj, frontend, rank, res):
lookup = FieldLookup('name_vector', [11,21], LookupAny)
lookup = FieldLookup('name_vector', [11, 21], LookupAny)
ranking = FieldRanking('name_vector', 0.4, [RankedTokens(0.0, rank)])
results = run_search(apiobj, frontend, 0.1, [lookup], [ranking])
assert [r.place_id for r in results] == res
def test_lookup_any_partial_match(self, apiobj, frontend):
lookup = FieldLookup('name_vector', [20], LookupAll)
ranking = FieldRanking('name_vector', 0.4, [RankedTokens(0.0, [21])])
@@ -105,19 +103,17 @@ class TestNameOnlySearches:
assert len(results) == 1
assert results[0].place_id == 101
@pytest.mark.parametrize('cc,res', [('us', 100), ('mx', 101)])
def test_lookup_restrict_country(self, apiobj, frontend, cc, res):
lookup = FieldLookup('name_vector', [1,2], LookupAll)
lookup = FieldLookup('name_vector', [1, 2], LookupAll)
ranking = FieldRanking('name_vector', 0.4, [RankedTokens(0.0, [10])])
results = run_search(apiobj, frontend, 0.1, [lookup], [ranking], ccodes=[cc])
assert [r.place_id for r in results] == [res]
def test_lookup_restrict_placeid(self, apiobj, frontend):
lookup = FieldLookup('name_vector', [1,2], LookupAll)
lookup = FieldLookup('name_vector', [1, 2], LookupAll)
ranking = FieldRanking('name_vector', 0.4, [RankedTokens(0.0, [10])])
results = run_search(apiobj, frontend, 0.1, [lookup], [ranking],
@@ -125,7 +121,6 @@ class TestNameOnlySearches:
assert [r.place_id for r in results] == [100]
@pytest.mark.parametrize('geom', [napi.GeometryFormat.GEOJSON,
napi.GeometryFormat.KML,
napi.GeometryFormat.SVG,
@@ -139,7 +134,6 @@ class TestNameOnlySearches:
assert geom.name.lower() in results[0].geometry
@pytest.mark.parametrize('factor,npoints', [(0.0, 3), (1.0, 2)])
def test_return_simplified_geometry(self, apiobj, frontend, factor, npoints):
apiobj.add_placex(place_id=333, country_code='us',
@@ -162,7 +156,6 @@ class TestNameOnlySearches:
assert result.place_id == 333
assert len(geom['coordinates']) == npoints
@pytest.mark.parametrize('viewbox', ['5.0,4.0,6.0,5.0', '5.7,4.0,6.0,5.0'])
@pytest.mark.parametrize('wcount,rids', [(2, [100, 101]), (20000, [100])])
def test_prefer_viewbox(self, apiobj, frontend, viewbox, wcount, rids):
@@ -177,18 +170,16 @@ class TestNameOnlySearches:
details=SearchDetails.from_kwargs({'viewbox': viewbox}))
assert [r.place_id for r in results] == rids
@pytest.mark.parametrize('viewbox', ['5.0,4.0,6.0,5.0', '5.55,4.27,5.62,4.31'])
def test_force_viewbox(self, apiobj, frontend, viewbox):
lookup = FieldLookup('name_vector', [1, 2], LookupAll)
details=SearchDetails.from_kwargs({'viewbox': viewbox,
'bounded_viewbox': True})
details = SearchDetails.from_kwargs({'viewbox': viewbox,
'bounded_viewbox': True})
results = run_search(apiobj, frontend, 0.1, [lookup], [], details=details)
assert [r.place_id for r in results] == [100]
def test_prefer_near(self, apiobj, frontend):
lookup = FieldLookup('name_vector', [1, 2], LookupAll)
ranking = FieldRanking('name_vector', 0.4, [RankedTokens(0.0, [21])])
@@ -202,13 +193,12 @@ class TestNameOnlySearches:
results.sort(key=lambda r: -r.importance)
assert [r.place_id for r in results] == [100, 101]
@pytest.mark.parametrize('radius', [0.09, 0.11])
def test_force_near(self, apiobj, frontend, radius):
lookup = FieldLookup('name_vector', [1, 2], LookupAll)
details=SearchDetails.from_kwargs({'near': '5.6,4.3',
'near_radius': radius})
details = SearchDetails.from_kwargs({'near': '5.6,4.3',
'near_radius': radius})
results = run_search(apiobj, frontend, 0.1, [lookup], [], details=details)
@@ -228,7 +218,7 @@ class TestStreetWithHousenumber:
apiobj.add_placex(place_id=1000, class_='highway', type='residential',
rank_search=26, rank_address=26,
country_code='es')
apiobj.add_search_name(1000, names=[1,2,10,11],
apiobj.add_search_name(1000, names=[1, 2, 10, 11],
search_rank=26, address_rank=26,
country_code='es')
apiobj.add_placex(place_id=91, class_='place', type='house',
@@ -243,26 +233,24 @@ class TestStreetWithHousenumber:
apiobj.add_placex(place_id=2000, class_='highway', type='residential',
rank_search=26, rank_address=26,
country_code='pt')
apiobj.add_search_name(2000, names=[1,2,20,21],
apiobj.add_search_name(2000, names=[1, 2, 20, 21],
search_rank=26, address_rank=26,
country_code='pt')
@pytest.mark.parametrize('hnr,res', [('20', [91, 1]), ('20 a', [1]),
('21', [2]), ('22', [2, 92]),
('24', [93]), ('25', [])])
def test_lookup_by_single_housenumber(self, apiobj, frontend, hnr, res):
lookup = FieldLookup('name_vector', [1,2], LookupAll)
lookup = FieldLookup('name_vector', [1, 2], LookupAll)
ranking = FieldRanking('name_vector', 0.3, [RankedTokens(0.0, [10])])
results = run_search(apiobj, frontend, 0.1, [lookup], [ranking], hnrs=[hnr])
assert [r.place_id for r in results] == res + [1000, 2000]
@pytest.mark.parametrize('cc,res', [('es', [2, 1000]), ('pt', [92, 2000])])
def test_lookup_with_country_restriction(self, apiobj, frontend, cc, res):
lookup = FieldLookup('name_vector', [1,2], LookupAll)
lookup = FieldLookup('name_vector', [1, 2], LookupAll)
ranking = FieldRanking('name_vector', 0.3, [RankedTokens(0.0, [10])])
results = run_search(apiobj, frontend, 0.1, [lookup], [ranking], hnrs=['22'],
@@ -270,9 +258,8 @@ class TestStreetWithHousenumber:
assert [r.place_id for r in results] == res
def test_lookup_exclude_housenumber_placeid(self, apiobj, frontend):
lookup = FieldLookup('name_vector', [1,2], LookupAll)
lookup = FieldLookup('name_vector', [1, 2], LookupAll)
ranking = FieldRanking('name_vector', 0.3, [RankedTokens(0.0, [10])])
results = run_search(apiobj, frontend, 0.1, [lookup], [ranking], hnrs=['22'],
@@ -280,9 +267,8 @@ class TestStreetWithHousenumber:
assert [r.place_id for r in results] == [2, 1000, 2000]
def test_lookup_exclude_street_placeid(self, apiobj, frontend):
lookup = FieldLookup('name_vector', [1,2], LookupAll)
lookup = FieldLookup('name_vector', [1, 2], LookupAll)
ranking = FieldRanking('name_vector', 0.3, [RankedTokens(0.0, [10])])
results = run_search(apiobj, frontend, 0.1, [lookup], [ranking], hnrs=['22'],
@@ -290,9 +276,8 @@ class TestStreetWithHousenumber:
assert [r.place_id for r in results] == [2, 92, 2000]
def test_lookup_only_house_qualifier(self, apiobj, frontend):
lookup = FieldLookup('name_vector', [1,2], LookupAll)
lookup = FieldLookup('name_vector', [1, 2], LookupAll)
ranking = FieldRanking('name_vector', 0.3, [RankedTokens(0.0, [10])])
results = run_search(apiobj, frontend, 0.1, [lookup], [ranking], hnrs=['22'],
@@ -300,9 +285,8 @@ class TestStreetWithHousenumber:
assert [r.place_id for r in results] == [2, 92]
def test_lookup_only_street_qualifier(self, apiobj, frontend):
lookup = FieldLookup('name_vector', [1,2], LookupAll)
lookup = FieldLookup('name_vector', [1, 2], LookupAll)
ranking = FieldRanking('name_vector', 0.3, [RankedTokens(0.0, [10])])
results = run_search(apiobj, frontend, 0.1, [lookup], [ranking], hnrs=['22'],
@@ -310,10 +294,9 @@ class TestStreetWithHousenumber:
assert [r.place_id for r in results] == [1000, 2000]
@pytest.mark.parametrize('rank,found', [(26, True), (27, False), (30, False)])
def test_lookup_min_rank(self, apiobj, frontend, rank, found):
lookup = FieldLookup('name_vector', [1,2], LookupAll)
lookup = FieldLookup('name_vector', [1, 2], LookupAll)
ranking = FieldRanking('name_vector', 0.3, [RankedTokens(0.0, [10])])
results = run_search(apiobj, frontend, 0.1, [lookup], [ranking], hnrs=['22'],
@@ -321,7 +304,6 @@ class TestStreetWithHousenumber:
assert [r.place_id for r in results] == ([2, 92, 1000, 2000] if found else [2, 92])
@pytest.mark.parametrize('geom', [napi.GeometryFormat.GEOJSON,
napi.GeometryFormat.KML,
napi.GeometryFormat.SVG,
@@ -343,7 +325,7 @@ def test_very_large_housenumber(apiobj, frontend):
apiobj.add_placex(place_id=2000, class_='highway', type='residential',
rank_search=26, rank_address=26,
country_code='pt')
apiobj.add_search_name(2000, names=[1,2],
apiobj.add_search_name(2000, names=[1, 2],
search_rank=26, address_rank=26,
country_code='pt')
@@ -405,7 +387,6 @@ class TestInterpolations:
centroid=(10.0, 10.00001),
geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)')
@pytest.mark.parametrize('hnr,res', [('21', [992]), ('22', []), ('23', [991])])
def test_lookup_housenumber(self, apiobj, frontend, hnr, res):
lookup = FieldLookup('name_vector', [111], LookupAll)
@@ -414,7 +395,6 @@ class TestInterpolations:
assert [r.place_id for r in results] == res + [990]
@pytest.mark.parametrize('geom', [napi.GeometryFormat.GEOJSON,
napi.GeometryFormat.KML,
napi.GeometryFormat.SVG,
@@ -429,7 +409,6 @@ class TestInterpolations:
assert geom.name.lower() in results[0].geometry
class TestTiger:
@pytest.fixture(autouse=True)
@@ -453,7 +432,6 @@ class TestTiger:
centroid=(10.0, 10.00001),
geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)')
@pytest.mark.parametrize('hnr,res', [('21', [992]), ('22', []), ('23', [991])])
def test_lookup_housenumber(self, apiobj, frontend, hnr, res):
lookup = FieldLookup('name_vector', [111], LookupAll)
@@ -462,7 +440,6 @@ class TestTiger:
assert [r.place_id for r in results] == res + [990]
@pytest.mark.parametrize('geom', [napi.GeometryFormat.GEOJSON,
napi.GeometryFormat.KML,
napi.GeometryFormat.SVG,
@@ -513,15 +490,15 @@ class TestLayersRank30:
importance=0.0005,
address_rank=0, search_rank=30)
@pytest.mark.parametrize('layer,res', [(napi.DataLayer.ADDRESS, [223]),
(napi.DataLayer.POI, [224]),
(napi.DataLayer.ADDRESS | napi.DataLayer.POI, [223, 224]),
(napi.DataLayer.MANMADE, [225]),
(napi.DataLayer.RAILWAY, [226]),
(napi.DataLayer.NATURAL, [227]),
(napi.DataLayer.MANMADE | napi.DataLayer.NATURAL, [225, 227]),
(napi.DataLayer.MANMADE | napi.DataLayer.RAILWAY, [225, 226])])
@pytest.mark.parametrize('layer,res',
[(napi.DataLayer.ADDRESS, [223]),
(napi.DataLayer.POI, [224]),
(napi.DataLayer.ADDRESS | napi.DataLayer.POI, [223, 224]),
(napi.DataLayer.MANMADE, [225]),
(napi.DataLayer.RAILWAY, [226]),
(napi.DataLayer.NATURAL, [227]),
(napi.DataLayer.MANMADE | napi.DataLayer.NATURAL, [225, 227]),
(napi.DataLayer.MANMADE | napi.DataLayer.RAILWAY, [225, 226])])
def test_layers_rank30(self, apiobj, frontend, layer, res):
lookup = FieldLookup('name_vector', [34], LookupAny)

View File

@@ -2,14 +2,13 @@
#
# 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.
"""
Tests for running the POI searcher.
"""
import pytest
import nominatim_api as napi
from nominatim_api.types import SearchDetails
from nominatim_api.search.db_searches import PoiSearch
from nominatim_api.search.db_search_fields import WeightedStrings, WeightedCategories
@@ -84,14 +83,12 @@ class TestPoiSearchWithRestrictions:
else:
self.args = {'near': '34.3, 56.100021', 'near_radius': 0.001}
def test_unrestricted(self, apiobj, frontend):
results = run_search(apiobj, frontend, 0.1, [('highway', 'bus_stop')], [0.5],
details=SearchDetails.from_kwargs(self.args))
assert [r.place_id for r in results] == [1, 2]
def test_restict_country(self, apiobj, frontend):
results = run_search(apiobj, frontend, 0.1, [('highway', 'bus_stop')], [0.5],
ccodes=['de', 'nz'],
@@ -99,7 +96,6 @@ class TestPoiSearchWithRestrictions:
assert [r.place_id for r in results] == [2]
def test_restrict_by_viewbox(self, apiobj, frontend):
args = {'bounded_viewbox': True, 'viewbox': '34.299,56.0,34.3001,56.10001'}
args.update(self.args)

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for running the postcode searcher.
@@ -15,6 +15,7 @@ from nominatim_api.search.db_searches import PostcodeSearch
from nominatim_api.search.db_search_fields import WeightedStrings, FieldLookup, \
FieldRanking, RankedTokens
def run_search(apiobj, frontend, global_penalty, pcs, pc_penalties=None,
ccodes=[], lookup=[], ranking=[], details=SearchDetails()):
if pc_penalties is None:
@@ -85,26 +86,24 @@ class TestPostcodeSearchWithAddress:
apiobj.add_placex(place_id=1000, class_='place', type='village',
rank_search=22, rank_address=22,
country_code='ch')
apiobj.add_search_name(1000, names=[1,2,10,11],
apiobj.add_search_name(1000, names=[1, 2, 10, 11],
search_rank=22, address_rank=22,
country_code='ch')
apiobj.add_placex(place_id=2000, class_='place', type='village',
rank_search=22, rank_address=22,
country_code='pl')
apiobj.add_search_name(2000, names=[1,2,20,21],
apiobj.add_search_name(2000, names=[1, 2, 20, 21],
search_rank=22, address_rank=22,
country_code='pl')
def test_lookup_both(self, apiobj, frontend):
lookup = FieldLookup('name_vector', [1,2], 'restrict')
lookup = FieldLookup('name_vector', [1, 2], 'restrict')
ranking = FieldRanking('name_vector', 0.3, [RankedTokens(0.0, [10])])
results = run_search(apiobj, frontend, 0.1, ['12345'], lookup=[lookup], ranking=[ranking])
assert [r.place_id for r in results] == [100, 101]
def test_restrict_by_name(self, apiobj, frontend):
lookup = FieldLookup('name_vector', [10], 'restrict')
@@ -112,11 +111,10 @@ class TestPostcodeSearchWithAddress:
assert [r.place_id for r in results] == [100]
@pytest.mark.parametrize('coord,place_id', [((16.5, 5), 100),
((-45.1, 7.004), 101)])
def test_lookup_near(self, apiobj, frontend, coord, place_id):
lookup = FieldLookup('name_vector', [1,2], 'restrict')
lookup = FieldLookup('name_vector', [1, 2], 'restrict')
ranking = FieldRanking('name_vector', 0.3, [RankedTokens(0.0, [10])])
results = run_search(apiobj, frontend, 0.1, ['12345'],
@@ -126,7 +124,6 @@ class TestPostcodeSearchWithAddress:
assert [r.place_id for r in results] == [place_id]
@pytest.mark.parametrize('geom', [napi.GeometryFormat.GEOJSON,
napi.GeometryFormat.KML,
napi.GeometryFormat.SVG,
@@ -138,18 +135,16 @@ class TestPostcodeSearchWithAddress:
assert results
assert all(geom.name.lower() in r.geometry for r in results)
@pytest.mark.parametrize('viewbox, rids', [('-46,6,-44,8', [101,100]),
('16,4,18,6', [100,101])])
@pytest.mark.parametrize('viewbox, rids', [('-46,6,-44,8', [101, 100]),
('16,4,18,6', [100, 101])])
def test_prefer_viewbox(self, apiobj, frontend, viewbox, rids):
results = run_search(apiobj, frontend, 0.1, ['12345'],
details=SearchDetails.from_kwargs({'viewbox': viewbox}))
assert [r.place_id for r in results] == rids
@pytest.mark.parametrize('viewbox, rid', [('-46,6,-44,8', 101),
('16,4,18,6', 100)])
('16,4,18,6', 100)])
def test_restrict_to_viewbox(self, apiobj, frontend, viewbox, rid):
results = run_search(apiobj, frontend, 0.1, ['12345'],
details=SearchDetails.from_kwargs({'viewbox': viewbox,
@@ -157,7 +152,6 @@ class TestPostcodeSearchWithAddress:
assert [r.place_id for r in results] == [rid]
@pytest.mark.parametrize('coord,rids', [((17.05, 5), [100, 101]),
((-45, 7.1), [101, 100])])
def test_prefer_near(self, apiobj, frontend, coord, rids):
@@ -166,7 +160,6 @@ class TestPostcodeSearchWithAddress:
assert [r.place_id for r in results] == rids
@pytest.mark.parametrize('pid,rid', [(100, 101), (101, 100)])
def test_exclude(self, apiobj, frontend, pid, rid):
results = run_search(apiobj, frontend, 0.1, ['12345'],

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Test for creation of token assignments from tokenized queries.
@@ -11,7 +11,10 @@ import pytest
from nominatim_api.search.query import QueryStruct, Phrase, TokenRange, Token
import nominatim_api.search.query as qmod
from nominatim_api.search.token_assignment import yield_token_assignments, TokenAssignment, PENALTY_TOKENCHANGE
from nominatim_api.search.token_assignment import (yield_token_assignments,
TokenAssignment,
PENALTY_TOKENCHANGE)
class MyToken(Token):
def get_category(self):
@@ -102,8 +105,7 @@ def test_multiple_simple_words(btype):
TokenAssignment(penalty=penalty, name=TokenRange(1, 3),
address=[TokenRange(0, 1)]),
TokenAssignment(penalty=penalty, name=TokenRange(2, 3),
address=[TokenRange(0, 2)])
)
address=[TokenRange(0, 2)]))
def test_multiple_words_respect_phrase_break():
@@ -156,6 +158,7 @@ def test_housenumber_and_postcode():
address=[TokenRange(0, 1), TokenRange(2, 3)],
postcode=TokenRange(3, 4)))
def test_postcode_and_housenumber():
q = make_query((qmod.BREAK_START, qmod.PHRASE_ANY, [(1, qmod.TOKEN_PARTIAL)]),
(qmod.BREAK_WORD, qmod.PHRASE_ANY, [(2, qmod.TOKEN_POSTCODE)]),
@@ -211,11 +214,11 @@ def test_housenumber_many_phrases():
check_assignments(yield_token_assignments(q),
TokenAssignment(penalty=0.1,
name=TokenRange(4, 5),
housenumber=TokenRange(3, 4),\
housenumber=TokenRange(3, 4),
address=[TokenRange(0, 1), TokenRange(1, 2),
TokenRange(2, 3)]),
TokenAssignment(penalty=0.1,
housenumber=TokenRange(3, 4),\
housenumber=TokenRange(3, 4),
address=[TokenRange(0, 1), TokenRange(1, 2),
TokenRange(2, 3), TokenRange(4, 5)]))
@@ -299,7 +302,6 @@ def test_qualifier_at_beginning():
(qmod.BREAK_WORD, qmod.PHRASE_ANY, [(2, qmod.TOKEN_PARTIAL)]),
(qmod.BREAK_WORD, qmod.PHRASE_ANY, [(3, qmod.TOKEN_PARTIAL)]))
check_assignments(yield_token_assignments(q),
TokenAssignment(penalty=0.1, name=TokenRange(1, 3),
qualifier=TokenRange(0, 1)),
@@ -315,7 +317,6 @@ def test_qualifier_after_name():
(qmod.BREAK_WORD, qmod.PHRASE_ANY, [(4, qmod.TOKEN_PARTIAL)]),
(qmod.BREAK_WORD, qmod.PHRASE_ANY, [(5, qmod.TOKEN_PARTIAL)]))
check_assignments(yield_token_assignments(q),
TokenAssignment(penalty=0.2, name=TokenRange(0, 2),
qualifier=TokenRange(2, 3),
@@ -349,4 +350,3 @@ def test_qualifier_in_middle_of_phrase():
(qmod.BREAK_PHRASE, qmod.PHRASE_ANY, [(5, qmod.TOKEN_PARTIAL)]))
check_assignments(yield_token_assignments(q))

View File

@@ -2,12 +2,11 @@
#
# 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.
"""
Tests for enhanced connection class for API functions.
"""
from pathlib import Path
import pytest
import sqlalchemy as sa
@@ -76,7 +75,7 @@ async def test_get_db_property_existing(api):
@pytest.mark.asyncio
async def test_get_db_property_existing(api):
async def test_get_db_property_bad_name(api):
async with api.begin() as conn:
with pytest.raises(ValueError):
await conn.get_db_property('dfkgjd.rijg')

View File

@@ -2,20 +2,20 @@
#
# 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.
"""
Tests for the deletable v1 API call.
"""
import json
from pathlib import Path
import pytest
from fake_adaptor import FakeAdaptor, FakeError, FakeResponse
from fake_adaptor import FakeAdaptor
import nominatim_api.v1.server_glue as glue
class TestDeletableEndPoint:
@pytest.fixture(autouse=True)
@@ -25,14 +25,13 @@ class TestDeletableEndPoint:
content=[(345, 'N', 'boundary', 'administrative'),
(781, 'R', 'landuse', 'wood'),
(781, 'R', 'landcover', 'grass')])
table_factory('placex',
definition="""place_id bigint, osm_id bigint, osm_type char(1),
class text, type text, name HSTORE, country_code char(2)""",
content=[(1, 345, 'N', 'boundary', 'administrative', {'old_name': 'Former'}, 'ab'),
(2, 781, 'R', 'landuse', 'wood', {'name': 'Wood'}, 'cd'),
(3, 781, 'R', 'landcover', 'grass', None, 'cd')])
table_factory(
'placex',
definition="""place_id bigint, osm_id bigint, osm_type char(1),
class text, type text, name HSTORE, country_code char(2)""",
content=[(1, 345, 'N', 'boundary', 'administrative', {'old_name': 'Former'}, 'ab'),
(2, 781, 'R', 'landuse', 'wood', {'name': 'Wood'}, 'cd'),
(3, 781, 'R', 'landcover', 'grass', None, 'cd')])
@pytest.mark.asyncio
async def test_deletable(self, api):

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for details API call.
@@ -13,23 +13,24 @@ import pytest
import nominatim_api as napi
@pytest.mark.parametrize('idobj', (napi.PlaceID(332), napi.OsmID('W', 4),
napi.OsmID('W', 4, 'highway')))
def test_lookup_in_placex(apiobj, frontend, idobj):
import_date = dt.datetime(2022, 12, 7, 14, 14, 46, 0)
apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
class_='highway', type='residential',
name={'name': 'Road'}, address={'city': 'Barrow'},
extratags={'surface': 'paved'},
parent_place_id=34, linked_place_id=55,
admin_level=15, country_code='gb',
housenumber='4',
postcode='34425', wikipedia='en:Faa',
rank_search=27, rank_address=26,
importance=0.01,
centroid=(23, 34),
indexed_date=import_date,
geometry='LINESTRING(23 34, 23.1 34, 23.1 34.1, 23 34)')
class_='highway', type='residential',
name={'name': 'Road'}, address={'city': 'Barrow'},
extratags={'surface': 'paved'},
parent_place_id=34, linked_place_id=55,
admin_level=15, country_code='gb',
housenumber='4',
postcode='34425', wikipedia='en:Faa',
rank_search=27, rank_address=26,
importance=0.01,
centroid=(23, 34),
indexed_date=import_date,
geometry='LINESTRING(23 34, 23.1 34, 23.1 34.1, 23 34)')
api = frontend(apiobj, options={'details'})
result = api.details(idobj)
@@ -73,12 +74,12 @@ def test_lookup_in_placex(apiobj, frontend, idobj):
def test_lookup_in_placex_minimal_info(apiobj, frontend):
import_date = dt.datetime(2022, 12, 7, 14, 14, 46, 0)
apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
class_='highway', type='residential',
admin_level=15,
rank_search=27, rank_address=26,
centroid=(23, 34),
indexed_date=import_date,
geometry='LINESTRING(23 34, 23.1 34, 23.1 34.1, 23 34)')
class_='highway', type='residential',
admin_level=15,
rank_search=27, rank_address=26,
centroid=(23, 34),
indexed_date=import_date,
geometry='LINESTRING(23 34, 23.1 34, 23.1 34.1, 23 34)')
api = frontend(apiobj, options={'details'})
result = api.details(napi.PlaceID(332))
@@ -131,9 +132,9 @@ def test_lookup_in_placex_with_geometry(apiobj, frontend):
def test_lookup_placex_with_address_details(apiobj, frontend):
apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
class_='highway', type='residential', name='Street',
country_code='pl',
rank_search=27, rank_address=26)
class_='highway', type='residential', name='Street',
country_code='pl',
rank_search=27, rank_address=26)
apiobj.add_address_placex(332, fromarea=False, isaddress=False,
distance=0.0034,
place_id=1000, osm_type='N', osm_id=3333,
@@ -178,9 +179,9 @@ def test_lookup_placex_with_address_details(apiobj, frontend):
def test_lookup_place_with_linked_places_none_existing(apiobj, frontend):
apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
class_='highway', type='residential', name='Street',
country_code='pl', linked_place_id=45,
rank_search=27, rank_address=26)
class_='highway', type='residential', name='Street',
country_code='pl', linked_place_id=45,
rank_search=27, rank_address=26)
api = frontend(apiobj, options={'details'})
result = api.details(napi.PlaceID(332), linked_places=True)
@@ -190,17 +191,17 @@ def test_lookup_place_with_linked_places_none_existing(apiobj, frontend):
def test_lookup_place_with_linked_places_existing(apiobj, frontend):
apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
class_='highway', type='residential', name='Street',
country_code='pl', linked_place_id=45,
rank_search=27, rank_address=26)
class_='highway', type='residential', name='Street',
country_code='pl', linked_place_id=45,
rank_search=27, rank_address=26)
apiobj.add_placex(place_id=1001, osm_type='W', osm_id=5,
class_='highway', type='residential', name='Street',
country_code='pl', linked_place_id=332,
rank_search=27, rank_address=26)
class_='highway', type='residential', name='Street',
country_code='pl', linked_place_id=332,
rank_search=27, rank_address=26)
apiobj.add_placex(place_id=1002, osm_type='W', osm_id=6,
class_='highway', type='residential', name='Street',
country_code='pl', linked_place_id=332,
rank_search=27, rank_address=26)
class_='highway', type='residential', name='Street',
country_code='pl', linked_place_id=332,
rank_search=27, rank_address=26)
api = frontend(apiobj, options={'details'})
result = api.details(napi.PlaceID(332), linked_places=True)
@@ -221,9 +222,9 @@ def test_lookup_place_with_linked_places_existing(apiobj, frontend):
def test_lookup_place_with_parented_places_not_existing(apiobj, frontend):
apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
class_='highway', type='residential', name='Street',
country_code='pl', parent_place_id=45,
rank_search=27, rank_address=26)
class_='highway', type='residential', name='Street',
country_code='pl', parent_place_id=45,
rank_search=27, rank_address=26)
api = frontend(apiobj, options={'details'})
result = api.details(napi.PlaceID(332), parented_places=True)
@@ -233,17 +234,17 @@ def test_lookup_place_with_parented_places_not_existing(apiobj, frontend):
def test_lookup_place_with_parented_places_existing(apiobj, frontend):
apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
class_='highway', type='residential', name='Street',
country_code='pl', parent_place_id=45,
rank_search=27, rank_address=26)
class_='highway', type='residential', name='Street',
country_code='pl', parent_place_id=45,
rank_search=27, rank_address=26)
apiobj.add_placex(place_id=1001, osm_type='N', osm_id=5,
class_='place', type='house', housenumber='23',
country_code='pl', parent_place_id=332,
rank_search=30, rank_address=30)
class_='place', type='house', housenumber='23',
country_code='pl', parent_place_id=332,
rank_search=30, rank_address=30)
apiobj.add_placex(place_id=1002, osm_type='W', osm_id=6,
class_='highway', type='residential', name='Street',
country_code='pl', parent_place_id=332,
rank_search=27, rank_address=26)
class_='highway', type='residential', name='Street',
country_code='pl', parent_place_id=332,
rank_search=27, rank_address=26)
api = frontend(apiobj, options={'details'})
result = api.details(napi.PlaceID(332), parented_places=True)
@@ -332,9 +333,9 @@ def test_lookup_osmline_with_address_details(apiobj, frontend):
startnumber=2, endnumber=4, step=1,
parent_place_id=332)
apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
class_='highway', type='residential', name='Street',
country_code='pl',
rank_search=27, rank_address=26)
class_='highway', type='residential', name='Street',
country_code='pl',
rank_search=27, rank_address=26)
apiobj.add_address_placex(332, fromarea=False, isaddress=False,
distance=0.0034,
place_id=1000, osm_type='N', osm_id=3333,
@@ -432,9 +433,9 @@ def test_lookup_tiger_with_address_details(apiobj, frontend):
startnumber=2, endnumber=4, step=1,
parent_place_id=332)
apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
class_='highway', type='residential', name='Street',
country_code='us',
rank_search=27, rank_address=26)
class_='highway', type='residential', name='Street',
country_code='us',
rank_search=27, rank_address=26)
apiobj.add_address_placex(332, fromarea=False, isaddress=False,
distance=0.0034,
place_id=1000, osm_type='N', osm_id=3333,
@@ -571,6 +572,7 @@ def test_lookup_postcode_with_address_details(apiobj, frontend):
rank_address=4, distance=0.0)
]
@pytest.mark.parametrize('objid', [napi.PlaceID(1736),
napi.OsmID('W', 55),
napi.OsmID('N', 55, 'amenity')])
@@ -583,8 +585,8 @@ def test_lookup_missing_object(apiobj, frontend, objid):
@pytest.mark.parametrize('gtype', (napi.GeometryFormat.KML,
napi.GeometryFormat.SVG,
napi.GeometryFormat.TEXT))
napi.GeometryFormat.SVG,
napi.GeometryFormat.TEXT))
def test_lookup_unsupported_geometry(apiobj, frontend, gtype):
apiobj.add_placex(place_id=332)

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for lookup API call.
@@ -13,6 +13,7 @@ import pytest
import nominatim_api as napi
def test_lookup_empty_list(apiobj, frontend):
api = frontend(apiobj, options={'details'})
assert api.lookup([]) == []
@@ -28,17 +29,17 @@ def test_lookup_non_existing(apiobj, frontend):
napi.OsmID('W', 4, 'highway')))
def test_lookup_single_placex(apiobj, frontend, idobj):
apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
class_='highway', type='residential',
name={'name': 'Road'}, address={'city': 'Barrow'},
extratags={'surface': 'paved'},
parent_place_id=34, linked_place_id=55,
admin_level=15, country_code='gb',
housenumber='4',
postcode='34425', wikipedia='en:Faa',
rank_search=27, rank_address=26,
importance=0.01,
centroid=(23, 34),
geometry='LINESTRING(23 34, 23.1 34, 23.1 34.1, 23 34)')
class_='highway', type='residential',
name={'name': 'Road'}, address={'city': 'Barrow'},
extratags={'surface': 'paved'},
parent_place_id=34, linked_place_id=55,
admin_level=15, country_code='gb',
housenumber='4',
postcode='34425', wikipedia='en:Faa',
rank_search=27, rank_address=26,
importance=0.01,
centroid=(23, 34),
geometry='LINESTRING(23 34, 23.1 34, 23.1 34.1, 23 34)')
api = frontend(apiobj, options={'details'})
result = api.lookup([idobj])
@@ -79,17 +80,17 @@ def test_lookup_single_placex(apiobj, frontend, idobj):
def test_lookup_multiple_places(apiobj, frontend):
apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
class_='highway', type='residential',
name={'name': 'Road'}, address={'city': 'Barrow'},
extratags={'surface': 'paved'},
parent_place_id=34, linked_place_id=55,
admin_level=15, country_code='gb',
housenumber='4',
postcode='34425', wikipedia='en:Faa',
rank_search=27, rank_address=26,
importance=0.01,
centroid=(23, 34),
geometry='LINESTRING(23 34, 23.1 34, 23.1 34.1, 23 34)')
class_='highway', type='residential',
name={'name': 'Road'}, address={'city': 'Barrow'},
extratags={'surface': 'paved'},
parent_place_id=34, linked_place_id=55,
admin_level=15, country_code='gb',
housenumber='4',
postcode='34425', wikipedia='en:Faa',
rank_search=27, rank_address=26,
importance=0.01,
centroid=(23, 34),
geometry='LINESTRING(23 34, 23.1 34, 23.1 34.1, 23 34)')
apiobj.add_osmline(place_id=4924, osm_id=9928,
parent_place_id=12,
startnumber=1, endnumber=4, step=1,
@@ -97,7 +98,6 @@ def test_lookup_multiple_places(apiobj, frontend):
address={'city': 'Big'},
geometry='LINESTRING(23 34, 23 35)')
api = frontend(apiobj, options={'details'})
result = api.lookup((napi.OsmID('W', 1),
napi.OsmID('W', 4),
@@ -111,17 +111,17 @@ def test_lookup_multiple_places(apiobj, frontend):
@pytest.mark.parametrize('gtype', list(napi.GeometryFormat))
def test_simple_place_with_geometry(apiobj, frontend, gtype):
apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
class_='highway', type='residential',
name={'name': 'Road'}, address={'city': 'Barrow'},
extratags={'surface': 'paved'},
parent_place_id=34, linked_place_id=55,
admin_level=15, country_code='gb',
housenumber='4',
postcode='34425', wikipedia='en:Faa',
rank_search=27, rank_address=26,
importance=0.01,
centroid=(23, 34),
geometry='POLYGON((23 34, 23.1 34, 23.1 34.1, 23 34))')
class_='highway', type='residential',
name={'name': 'Road'}, address={'city': 'Barrow'},
extratags={'surface': 'paved'},
parent_place_id=34, linked_place_id=55,
admin_level=15, country_code='gb',
housenumber='4',
postcode='34425', wikipedia='en:Faa',
rank_search=27, rank_address=26,
importance=0.01,
centroid=(23, 34),
geometry='POLYGON((23 34, 23.1 34, 23.1 34.1, 23 34))')
api = frontend(apiobj, options={'details'})
result = api.lookup([napi.OsmID('W', 4)], geometry_output=gtype)
@@ -137,17 +137,17 @@ def test_simple_place_with_geometry(apiobj, frontend, gtype):
def test_simple_place_with_geometry_simplified(apiobj, frontend):
apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
class_='highway', type='residential',
name={'name': 'Road'}, address={'city': 'Barrow'},
extratags={'surface': 'paved'},
parent_place_id=34, linked_place_id=55,
admin_level=15, country_code='gb',
housenumber='4',
postcode='34425', wikipedia='en:Faa',
rank_search=27, rank_address=26,
importance=0.01,
centroid=(23, 34),
geometry='POLYGON((23 34, 22.999 34, 23.1 34, 23.1 34.1, 23 34))')
class_='highway', type='residential',
name={'name': 'Road'}, address={'city': 'Barrow'},
extratags={'surface': 'paved'},
parent_place_id=34, linked_place_id=55,
admin_level=15, country_code='gb',
housenumber='4',
postcode='34425', wikipedia='en:Faa',
rank_search=27, rank_address=26,
importance=0.01,
centroid=(23, 34),
geometry='POLYGON((23 34, 22.999 34, 23.1 34, 23.1 34.1, 23 34))')
api = frontend(apiobj, options={'details'})
result = api.lookup([napi.OsmID('W', 4)],
@@ -159,5 +159,5 @@ def test_simple_place_with_geometry_simplified(apiobj, frontend):
geom = json.loads(result[0].geometry['geojson'])
assert geom['type'] == 'Polygon'
assert geom['type'] == 'Polygon'
assert geom['coordinates'] == [[[23, 34], [23.1, 34], [23.1, 34.1], [23, 34]]]

View File

@@ -2,21 +2,21 @@
#
# 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.
"""
Tests for the deletable v1 API call.
"""
import json
import datetime as dt
from pathlib import Path
import pytest
from fake_adaptor import FakeAdaptor, FakeError, FakeResponse
from fake_adaptor import FakeAdaptor
import nominatim_api.v1.server_glue as glue
class TestPolygonsEndPoint:
@pytest.fixture(autouse=True)
@@ -35,13 +35,12 @@ class TestPolygonsEndPoint:
errormessage text,
prevgeometry geometry(Geometry,4326),
newgeometry geometry(Geometry,4326)""",
content=[(345, 'N', 'boundary', 'administrative',
{'name': 'Foo'}, 'xx', self.recent,
'some text', None, None),
(781, 'R', 'landuse', 'wood',
None, 'ds', self.now,
'Area reduced by lots', None, None)])
content=[(345, 'N', 'boundary', 'administrative',
{'name': 'Foo'}, 'xx', self.recent,
'some text', None, None),
(781, 'R', 'landuse', 'wood',
None, 'ds', self.now,
'Area reduced by lots', None, None)])
@pytest.mark.asyncio
async def test_polygons_simple(self, api):
@@ -63,7 +62,6 @@ class TestPolygonsEndPoint:
'errormessage': 'Area reduced by lots',
'updated': self.now.isoformat(sep=' ', timespec='seconds')}]
@pytest.mark.asyncio
async def test_polygons_days(self, api):
a = FakeAdaptor()
@@ -74,7 +72,6 @@ class TestPolygonsEndPoint:
assert [r['osm_id'] for r in results] == [781]
@pytest.mark.asyncio
async def test_polygons_class(self, api):
a = FakeAdaptor()
@@ -85,8 +82,6 @@ class TestPolygonsEndPoint:
assert [r['osm_id'] for r in results] == [781]
@pytest.mark.asyncio
async def test_polygons_reduced(self, api):
a = FakeAdaptor()

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for reverse API call.
@@ -18,6 +18,7 @@ import nominatim_api as napi
API_OPTIONS = {'reverse'}
def test_reverse_rank_30(apiobj, frontend):
apiobj.add_placex(place_id=223, class_='place', type='house',
housenumber='1',
@@ -35,7 +36,7 @@ def test_reverse_rank_30(apiobj, frontend):
def test_reverse_street(apiobj, frontend, country):
apiobj.add_placex(place_id=990, class_='highway', type='service',
rank_search=27, rank_address=27,
name = {'name': 'My Street'},
name={'name': 'My Street'},
centroid=(10.0, 10.0),
country_code=country,
geometry='LINESTRING(9.995 10, 10.005 10)')
@@ -57,16 +58,17 @@ def test_reverse_ignore_unindexed(apiobj, frontend):
assert result is None
@pytest.mark.parametrize('y,layer,place_id', [(0.7, napi.DataLayer.ADDRESS, 223),
(0.70001, napi.DataLayer.POI, 224),
(0.7, napi.DataLayer.ADDRESS | napi.DataLayer.POI, 224),
(0.70001, napi.DataLayer.ADDRESS | napi.DataLayer.POI, 223),
(0.7, napi.DataLayer.MANMADE, 225),
(0.7, napi.DataLayer.RAILWAY, 226),
(0.7, napi.DataLayer.NATURAL, 227),
(0.70003, napi.DataLayer.MANMADE | napi.DataLayer.RAILWAY, 225),
(0.70003, napi.DataLayer.MANMADE | napi.DataLayer.NATURAL, 225),
(5, napi.DataLayer.ADDRESS, 229)])
@pytest.mark.parametrize('y,layer,place_id',
[(0.7, napi.DataLayer.ADDRESS, 223),
(0.70001, napi.DataLayer.POI, 224),
(0.7, napi.DataLayer.ADDRESS | napi.DataLayer.POI, 224),
(0.70001, napi.DataLayer.ADDRESS | napi.DataLayer.POI, 223),
(0.7, napi.DataLayer.MANMADE, 225),
(0.7, napi.DataLayer.RAILWAY, 226),
(0.7, napi.DataLayer.NATURAL, 227),
(0.70003, napi.DataLayer.MANMADE | napi.DataLayer.RAILWAY, 225),
(0.70003, napi.DataLayer.MANMADE | napi.DataLayer.NATURAL, 225),
(5, napi.DataLayer.ADDRESS, 229)])
def test_reverse_rank_30_layers(apiobj, frontend, y, layer, place_id):
apiobj.add_placex(place_id=223, osm_type='N', class_='place', type='house',
housenumber='1',
@@ -108,14 +110,14 @@ def test_reverse_poi_layer_with_no_pois(apiobj, frontend):
api = frontend(apiobj, options=API_OPTIONS)
assert api.reverse((1.3, 0.70001), max_rank=29,
layers=napi.DataLayer.POI) is None
layers=napi.DataLayer.POI) is None
@pytest.mark.parametrize('with_geom', [True, False])
def test_reverse_housenumber_on_street(apiobj, frontend, with_geom):
apiobj.add_placex(place_id=990, class_='highway', type='service',
rank_search=27, rank_address=27,
name = {'name': 'My Street'},
name={'name': 'My Street'},
centroid=(10.0, 10.0),
geometry='LINESTRING(9.995 10, 10.005 10)')
apiobj.add_placex(place_id=991, class_='place', type='house',
@@ -125,7 +127,7 @@ def test_reverse_housenumber_on_street(apiobj, frontend, with_geom):
centroid=(10.0, 10.00001))
apiobj.add_placex(place_id=1990, class_='highway', type='service',
rank_search=27, rank_address=27,
name = {'name': 'Other Street'},
name={'name': 'Other Street'},
centroid=(10.0, 1.0),
geometry='LINESTRING(9.995 1, 10.005 1)')
apiobj.add_placex(place_id=1991, class_='place', type='house',
@@ -147,7 +149,7 @@ def test_reverse_housenumber_on_street(apiobj, frontend, with_geom):
def test_reverse_housenumber_interpolation(apiobj, frontend, with_geom):
apiobj.add_placex(place_id=990, class_='highway', type='service',
rank_search=27, rank_address=27,
name = {'name': 'My Street'},
name={'name': 'My Street'},
centroid=(10.0, 10.0),
geometry='LINESTRING(9.995 10, 10.005 10)')
apiobj.add_placex(place_id=991, class_='place', type='house',
@@ -162,7 +164,7 @@ def test_reverse_housenumber_interpolation(apiobj, frontend, with_geom):
geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)')
apiobj.add_placex(place_id=1990, class_='highway', type='service',
rank_search=27, rank_address=27,
name = {'name': 'Other Street'},
name={'name': 'Other Street'},
centroid=(10.0, 20.0),
geometry='LINESTRING(9.995 20, 10.005 20)')
apiobj.add_osmline(place_id=1992,
@@ -181,7 +183,7 @@ def test_reverse_housenumber_interpolation(apiobj, frontend, with_geom):
def test_reverse_housenumber_point_interpolation(apiobj, frontend):
apiobj.add_placex(place_id=990, class_='highway', type='service',
rank_search=27, rank_address=27,
name = {'name': 'My Street'},
name={'name': 'My Street'},
centroid=(10.0, 10.0),
geometry='LINESTRING(9.995 10, 10.005 10)')
apiobj.add_osmline(place_id=992,
@@ -199,7 +201,7 @@ def test_reverse_housenumber_point_interpolation(apiobj, frontend):
def test_reverse_tiger_number(apiobj, frontend):
apiobj.add_placex(place_id=990, class_='highway', type='service',
rank_search=27, rank_address=27,
name = {'name': 'My Street'},
name={'name': 'My Street'},
centroid=(10.0, 10.0),
country_code='us',
geometry='LINESTRING(9.995 10, 10.005 10)')
@@ -217,7 +219,7 @@ def test_reverse_tiger_number(apiobj, frontend):
def test_reverse_point_tiger(apiobj, frontend):
apiobj.add_placex(place_id=990, class_='highway', type='service',
rank_search=27, rank_address=27,
name = {'name': 'My Street'},
name={'name': 'My Street'},
centroid=(10.0, 10.0),
country_code='us',
geometry='LINESTRING(9.995 10, 10.005 10)')
@@ -393,14 +395,15 @@ def test_reverse_interpolation_geometry(apiobj, frontend):
geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)')
api = frontend(apiobj, options=API_OPTIONS)
assert api.reverse((10.0, 10.0), geometry_output=napi.GeometryFormat.TEXT)\
.geometry['text'] == 'POINT(10 10.00001)'
result = api.reverse((10.0, 10.0), geometry_output=napi.GeometryFormat.TEXT)
assert result.geometry['text'] == 'POINT(10 10.00001)'
def test_reverse_tiger_geometry(apiobj, frontend):
apiobj.add_placex(place_id=990, class_='highway', type='service',
rank_search=27, rank_address=27,
name = {'name': 'My Street'},
name={'name': 'My Street'},
centroid=(10.0, 10.0),
country_code='us',
geometry='LINESTRING(9.995 10, 10.005 10)')
@@ -411,7 +414,7 @@ def test_reverse_tiger_geometry(apiobj, frontend):
geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)')
apiobj.add_placex(place_id=1000, class_='highway', type='service',
rank_search=27, rank_address=27,
name = {'name': 'My Street'},
name={'name': 'My Street'},
centroid=(11.0, 11.0),
country_code='us',
geometry='LINESTRING(10.995 11, 11.005 11)')
@@ -426,8 +429,9 @@ def test_reverse_tiger_geometry(apiobj, frontend):
params = {'geometry_output': napi.GeometryFormat.GEOJSON}
output = api.reverse((10.0, 10.0), **params)
assert json.loads(output.geometry['geojson']) == {'coordinates': [10, 10.00001], 'type': 'Point'}
assert json.loads(output.geometry['geojson']) \
== {'coordinates': [10, 10.00001], 'type': 'Point'}
output = api.reverse((11.0, 11.0), **params)
assert json.loads(output.geometry['geojson']) == {'coordinates': [11, 11.00001], 'type': 'Point'}
assert json.loads(output.geometry['geojson']) \
== {'coordinates': [11, 11.00001], 'type': 'Point'}

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for search API calls.
@@ -10,17 +10,13 @@ Tests for search API calls.
These tests make sure that all Python code is correct and executable.
Functional tests can be found in the BDD test suite.
"""
import json
import pytest
import sqlalchemy as sa
import nominatim_api as napi
import nominatim_api.logging as loglib
API_OPTIONS = {'search'}
@pytest.fixture(autouse=True)
def setup_icu_tokenizer(apiobj):
""" Setup the properties needed for using the ICU tokenizer.
@@ -28,8 +24,9 @@ def setup_icu_tokenizer(apiobj):
apiobj.add_data('properties',
[{'property': 'tokenizer', 'value': 'icu'},
{'property': 'tokenizer_import_normalisation', 'value': ':: lower();'},
{'property': 'tokenizer_import_transliteration', 'value': "'1' > '/1/'; 'ä' > 'ä '"},
])
{'property': 'tokenizer_import_transliteration',
'value': "'1' > '/1/'; 'ä' > 'ä '"},
])
def test_search_no_content(apiobj, frontend):
@@ -64,7 +61,7 @@ def test_search_with_debug(apiobj, frontend, logtype):
api = frontend(apiobj, options=API_OPTIONS)
loglib.set_log_output(logtype)
results = api.search('TEST')
api.search('TEST')
assert loglib.get_and_disable()

View File

@@ -2,18 +2,17 @@
#
# 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.
"""
Tests for the status API call.
"""
import datetime as dt
import pytest
from nominatim_db.version import NominatimVersion
from nominatim_api.version import NOMINATIM_API_VERSION
import nominatim_api as napi
def test_status_no_extra_info(apiobj, frontend):
api = frontend(apiobj)
result = api.status()

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for loading of parameter dataclasses.
@@ -12,6 +12,7 @@ import pytest
from nominatim_api.errors import UsageError
import nominatim_api.types as typ
def test_no_params_defaults():
params = typ.LookupDetails.from_kwargs({})
@@ -24,7 +25,7 @@ def test_no_params_defaults():
('geometry_simplification', 'NaN')])
def test_bad_format_reverse(k, v):
with pytest.raises(UsageError):
params = typ.ReverseDetails.from_kwargs({k: v})
typ.ReverseDetails.from_kwargs({k: v})
@pytest.mark.parametrize('rin,rout', [(-23, 0), (0, 0), (1, 1),

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for export CLI function.
@@ -11,12 +11,13 @@ import pytest
import nominatim_db.cli
@pytest.fixture
def run_export(tmp_path, capsys):
def _exec(args):
cli_args = ['export', '--project-dir', str(tmp_path)] + args
assert 0 == nominatim_db.cli.nominatim(osm2pgsql_path='OSM2PGSQL NOT AVAILABLE',
cli_args=['export', '--project-dir', str(tmp_path)]
+ args)
cli_args=cli_args)
return capsys.readouterr().out.split('\r\n')
return _exec
@@ -25,9 +26,9 @@ def run_export(tmp_path, capsys):
@pytest.fixture(autouse=True)
def setup_database_with_context(apiobj):
apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
class_='highway', type='residential', name='Street',
country_code='pl', postcode='55674',
rank_search=27, rank_address=26)
class_='highway', type='residential', name='Street',
country_code='pl', postcode='55674',
rank_search=27, rank_address=26)
apiobj.add_address_placex(332, fromarea=False, isaddress=False,
distance=0.0034,
place_id=1000, osm_type='N', osm_id=3333,

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for the helper functions for v1 API.
@@ -11,6 +11,7 @@ import pytest
import nominatim_api.v1.helpers as helper
@pytest.mark.parametrize('inp', ['',
'abc',
'12 23',
@@ -35,40 +36,42 @@ def test_extract_coords_with_text_before():
def test_extract_coords_with_text_after():
assert ('abc', 12.456, -78.90) == helper.extract_coords_from_query('-78.90, 12.456 abc')
@pytest.mark.parametrize('inp', [' [12.456,-78.90] ', ' 12.456,-78.90 '])
def test_extract_coords_with_spaces(inp):
assert ('', -78.90, 12.456) == helper.extract_coords_from_query(inp)
@pytest.mark.parametrize('inp', ['40 26.767 N 79 58.933 W',
'40° 26.767 N 79° 58.933 W',
"40° 26.767' N 79° 58.933' W",
"40° 26.767'\n"
" N 79° 58.933' W",
'N 40 26.767, W 79 58.933',
'N 40°26.767, W 79°58.933',
' N 40°26.767, W 79°58.933',
"N 40°26.767', W 79°58.933'",
'40 26 46 N 79 58 56 W',
'40° 26 46″ N 79° 58 56″ W',
'40° 26 46.00″ N 79° 58 56.00″ W',
'40°2646″N 79°5856″W',
'N 40 26 46 W 79 58 56',
'N 40° 26 46″, W 79° 58 56″',
'N 40° 26\' 46", W 79° 58\' 56"',
'N 40° 26\' 46", W 79° 58\' 56"',
'40.446 -79.982',
'40.446,-79.982',
'40.446° N 79.982° W',
'N 40.446° W 79.982°',
'[40.446 -79.982]',
'[40.446, -79.982]',
' 40.446 , -79.982 ',
' 40.446 , -79.982 ',
' 40.446 , -79.982 ',
' 40.446 , -79.982 '])
'40° 26.767 N 79° 58.933 W',
"40° 26.767' N 79° 58.933' W",
"40° 26.767'\n"
" N 79° 58.933' W",
'N 40 26.767, W 79 58.933',
'N 40°26.767, W 79°58.933',
' N 40°26.767, W 79°58.933',
"N 40°26.767', W 79°58.933'",
'40 26 46 N 79 58 56 W',
'40° 26 46″ N 79° 58 56″ W',
'40° 26 46.00″ N 79° 58 56.00″ W',
'40°2646″N 79°5856″W',
'N 40 26 46 W 79 58 56',
'N 40° 26 46″, W 79° 58 56″',
'N 40° 26\' 46", W 79° 58\' 56"',
'N 40° 26\' 46", W 79° 58\' 56"',
'40.446 -79.982',
'40.446,-79.982',
'40.446° N 79.982° W',
'N 40.446° W 79.982°',
'[40.446 -79.982]',
'[40.446, -79.982]',
' 40.446 , -79.982 ',
' 40.446 , -79.982 ',
' 40.446 , -79.982 ',
' 40.446 , -79.982 '])
def test_extract_coords_formats(inp):
query, x, y = helper.extract_coords_from_query(inp)
@@ -108,9 +111,11 @@ def test_extract_category_good(inp):
assert cls == 'shop'
assert typ == 'fish'
def test_extract_category_only():
assert helper.extract_category_from_query('[shop=market]') == ('', 'shop', 'market')
@pytest.mark.parametrize('inp', ['house []', 'nothing', '[352]'])
def test_extract_category_no_match(inp):
def test_extract_category_no_match(inp):
assert helper.extract_category_from_query(inp) == (inp, None, None)

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Test functions for adapting results to the user's locale.
@@ -11,34 +11,36 @@ import pytest
from nominatim_api import Locales
def test_display_name_empty_names():
l = Locales(['en', 'de'])
assert l.display_name(None) == ''
assert l.display_name({}) == ''
def test_display_name_empty_names():
loc = Locales(['en', 'de'])
assert loc.display_name(None) == ''
assert loc.display_name({}) == ''
def test_display_name_none_localized():
l = Locales()
loc = Locales()
assert l.display_name({}) == ''
assert l.display_name({'name:de': 'DE', 'name': 'ALL'}) == 'ALL'
assert l.display_name({'ref': '34', 'name:de': 'DE'}) == '34'
assert loc.display_name({}) == ''
assert loc.display_name({'name:de': 'DE', 'name': 'ALL'}) == 'ALL'
assert loc.display_name({'ref': '34', 'name:de': 'DE'}) == '34'
def test_display_name_localized():
l = Locales(['en', 'de'])
loc = Locales(['en', 'de'])
assert l.display_name({}) == ''
assert l.display_name({'name:de': 'DE', 'name': 'ALL'}) == 'DE'
assert l.display_name({'ref': '34', 'name:de': 'DE'}) == 'DE'
assert loc.display_name({}) == ''
assert loc.display_name({'name:de': 'DE', 'name': 'ALL'}) == 'DE'
assert loc.display_name({'ref': '34', 'name:de': 'DE'}) == 'DE'
def test_display_name_preference():
l = Locales(['en', 'de'])
loc = Locales(['en', 'de'])
assert l.display_name({}) == ''
assert l.display_name({'name:de': 'DE', 'name:en': 'EN'}) == 'EN'
assert l.display_name({'official_name:en': 'EN', 'name:de': 'DE'}) == 'DE'
assert loc.display_name({}) == ''
assert loc.display_name({'name:de': 'DE', 'name:en': 'EN'}) == 'EN'
assert loc.display_name({'official_name:en': 'EN', 'name:de': 'DE'}) == 'DE'
@pytest.mark.parametrize('langstr,langlist',

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for formatting results for the V1 API.
@@ -22,6 +22,7 @@ STATUS_FORMATS = {'text', 'json'}
# StatusResult
def test_status_format_list():
assert set(v1_format.list_formats(napi.StatusResult)) == STATUS_FORMATS
@@ -36,11 +37,13 @@ def test_status_unsupported():
def test_status_format_text():
assert v1_format.format_result(napi.StatusResult(0, 'message here'), 'text', {}) == 'OK'
assert v1_format.format_result(napi.StatusResult(0, 'message here'), 'text', {}) \
== 'OK'
def test_status_format_text():
assert v1_format.format_result(napi.StatusResult(500, 'message here'), 'text', {}) == 'ERROR: message here'
def test_status_format_error_text():
assert v1_format.format_result(napi.StatusResult(500, 'message here'), 'text', {}) \
== 'ERROR: message here'
def test_status_format_json_minimal():
@@ -48,8 +51,9 @@ def test_status_format_json_minimal():
result = v1_format.format_result(status, 'json', {})
assert result == \
f'{{"status":700,"message":"Bad format.","software_version":"{napi.__version__}"}}'
assert json.loads(result) == {'status': 700,
'message': 'Bad format.',
'software_version': napi.__version__}
def test_status_format_json_full():
@@ -59,8 +63,11 @@ def test_status_format_json_full():
result = v1_format.format_result(status, 'json', {})
assert result == \
f'{{"status":0,"message":"OK","data_updated":"2010-02-07T20:20:03+00:00","software_version":"{napi.__version__}","database_version":"5.6"}}'
assert json.loads(result) == {'status': 0,
'message': 'OK',
'data_updated': '2010-02-07T20:20:03+00:00',
'software_version': napi.__version__,
'database_version': '5.6'}
# DetailedResult
@@ -86,7 +93,7 @@ def test_search_details_minimal():
'extratags': {},
'centroid': {'type': 'Point', 'coordinates': [1.0, 2.0]},
'geometry': {'type': 'Point', 'coordinates': [1.0, 2.0]},
}
}
def test_search_details_full():
@@ -110,7 +117,7 @@ def test_search_details_full():
rank_search=28,
importance=0.0443,
country_code='ll',
indexed_date = import_date
indexed_date=import_date
)
search.localize(napi.Locales())
@@ -140,7 +147,7 @@ def test_search_details_full():
'isarea': False,
'centroid': {'type': 'Point', 'coordinates': [56.947, -87.44]},
'geometry': {'type': 'Point', 'coordinates': [56.947, -87.44]},
}
}
@pytest.mark.parametrize('gtype,isarea', [('ST_Point', False),
@@ -149,9 +156,9 @@ def test_search_details_full():
('ST_MultiPolygon', True)])
def test_search_details_no_geometry(gtype, isarea):
search = napi.DetailedResult(napi.SourceTable.PLACEX,
('place', 'thing'),
napi.Point(1.0, 2.0),
geometry={'type': gtype})
('place', 'thing'),
napi.Point(1.0, 2.0),
geometry={'type': gtype})
result = v1_format.format_result(search, 'json', {})
js = json.loads(result)
@@ -161,16 +168,17 @@ def test_search_details_no_geometry(gtype, isarea):
def test_search_details_with_geometry():
search = napi.DetailedResult(napi.SourceTable.PLACEX,
('place', 'thing'),
napi.Point(1.0, 2.0),
geometry={'geojson': '{"type":"Point","coordinates":[56.947,-87.44]}'})
search = napi.DetailedResult(
napi.SourceTable.PLACEX,
('place', 'thing'),
napi.Point(1.0, 2.0),
geometry={'geojson': '{"type":"Point","coordinates":[56.947,-87.44]}'})
result = v1_format.format_result(search, 'json', {})
js = json.loads(result)
assert js['geometry'] == {'type': 'Point', 'coordinates': [56.947, -87.44]}
assert js['isarea'] == False
assert js['isarea'] is False
def test_search_details_with_icon_available():
@@ -226,7 +234,7 @@ def test_search_details_with_address_minimal():
@pytest.mark.parametrize('field,outfield', [('address_rows', 'address'),
('linked_rows', 'linked_places'),
('parented_rows', 'hierarchy')
])
])
def test_search_details_with_further_infos(field, outfield):
search = napi.DetailedResult(napi.SourceTable.PLACEX,
('place', 'thing'),
@@ -249,50 +257,49 @@ def test_search_details_with_further_infos(field, outfield):
js = json.loads(result)
assert js[outfield] == [{'localname': 'Trespass',
'place_id': 3498,
'osm_id': 442,
'osm_type': 'R',
'place_type': 'spec',
'class': 'bnd',
'type': 'note',
'admin_level': 4,
'rank_address': 10,
'distance': 0.034,
'isaddress': True}]
'place_id': 3498,
'osm_id': 442,
'osm_type': 'R',
'place_type': 'spec',
'class': 'bnd',
'type': 'note',
'admin_level': 4,
'rank_address': 10,
'distance': 0.034,
'isaddress': True}]
def test_search_details_grouped_hierarchy():
search = napi.DetailedResult(napi.SourceTable.PLACEX,
('place', 'thing'),
napi.Point(1.0, 2.0),
parented_rows =
[napi.AddressLine(place_id=3498,
osm_object=('R', 442),
category=('bnd', 'note'),
names={'name': 'Trespass'},
extratags={'access': 'no',
'place_type': 'spec'},
admin_level=4,
fromarea=True,
isaddress=True,
rank_address=10,
distance=0.034)
])
parented_rows=[napi.AddressLine(
place_id=3498,
osm_object=('R', 442),
category=('bnd', 'note'),
names={'name': 'Trespass'},
extratags={'access': 'no',
'place_type': 'spec'},
admin_level=4,
fromarea=True,
isaddress=True,
rank_address=10,
distance=0.034)])
result = v1_format.format_result(search, 'json', {'group_hierarchy': True})
js = json.loads(result)
assert js['hierarchy'] == {'note': [{'localname': 'Trespass',
'place_id': 3498,
'osm_id': 442,
'osm_type': 'R',
'place_type': 'spec',
'class': 'bnd',
'type': 'note',
'admin_level': 4,
'rank_address': 10,
'distance': 0.034,
'isaddress': True}]}
'place_id': 3498,
'osm_id': 442,
'osm_type': 'R',
'place_type': 'spec',
'class': 'bnd',
'type': 'note',
'admin_level': 4,
'rank_address': 10,
'distance': 0.034,
'isaddress': True}]}
def test_search_details_keywords_name():
@@ -307,7 +314,7 @@ def test_search_details_keywords_name():
js = json.loads(result)
assert js['keywords'] == {'name': [{'id': 23, 'token': 'foo'},
{'id': 24, 'token': 'foo'}],
{'id': 24, 'token': 'foo'}],
'address': []}
@@ -323,6 +330,5 @@ def test_search_details_keywords_address():
js = json.loads(result)
assert js['keywords'] == {'address': [{'id': 23, 'token': 'foo'},
{'id': 24, 'token': 'foo'}],
{'id': 24, 'token': 'foo'}],
'name': []}

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for formatting reverse results for the V1 API.
@@ -20,6 +20,7 @@ import nominatim_api as napi
FORMATS = ['json', 'jsonv2', 'geojson', 'geocodejson', 'xml']
@pytest.mark.parametrize('fmt', FORMATS)
def test_format_reverse_minimal(fmt):
reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
@@ -104,8 +105,7 @@ def test_format_reverse_with_address(fmt):
reverse.localize(napi.Locales())
raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
{'addressdetails': True})
{'addressdetails': True})
if fmt == 'xml':
root = ET.fromstring(raw)
@@ -168,7 +168,7 @@ def test_format_reverse_geocodejson_special_parts():
reverse.localize(napi.Locales())
raw = v1_format.format_result(napi.ReverseResults([reverse]), 'geocodejson',
{'addressdetails': True})
{'addressdetails': True})
props = json.loads(raw)['features'][0]['properties']['geocoding']
assert props['housenumber'] == '1'
@@ -184,8 +184,7 @@ def test_format_reverse_with_address_none(fmt):
address_rows=napi.AddressLines())
raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
{'addressdetails': True})
{'addressdetails': True})
if fmt == 'xml':
root = ET.fromstring(raw)
@@ -211,10 +210,10 @@ def test_format_reverse_with_extratags(fmt):
reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
('place', 'thing'),
napi.Point(1.0, 2.0),
extratags={'one': 'A', 'two':'B'})
extratags={'one': 'A', 'two': 'B'})
raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
{'extratags': True})
{'extratags': True})
if fmt == 'xml':
root = ET.fromstring(raw)
@@ -226,7 +225,7 @@ def test_format_reverse_with_extratags(fmt):
else:
extra = result['extratags']
assert extra == {'one': 'A', 'two':'B'}
assert extra == {'one': 'A', 'two': 'B'}
@pytest.mark.parametrize('fmt', ['json', 'jsonv2', 'geojson', 'xml'])
@@ -236,7 +235,7 @@ def test_format_reverse_with_extratags_none(fmt):
napi.Point(1.0, 2.0))
raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
{'extratags': True})
{'extratags': True})
if fmt == 'xml':
root = ET.fromstring(raw)
@@ -256,10 +255,10 @@ def test_format_reverse_with_namedetails_with_name(fmt):
reverse = napi.ReverseResult(napi.SourceTable.PLACEX,
('place', 'thing'),
napi.Point(1.0, 2.0),
names={'name': 'A', 'ref':'1'})
names={'name': 'A', 'ref': '1'})
raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
{'namedetails': True})
{'namedetails': True})
if fmt == 'xml':
root = ET.fromstring(raw)
@@ -271,7 +270,7 @@ def test_format_reverse_with_namedetails_with_name(fmt):
else:
extra = result['namedetails']
assert extra == {'name': 'A', 'ref':'1'}
assert extra == {'name': 'A', 'ref': '1'}
@pytest.mark.parametrize('fmt', ['json', 'jsonv2', 'geojson', 'xml'])
@@ -281,7 +280,7 @@ def test_format_reverse_with_namedetails_without_name(fmt):
napi.Point(1.0, 2.0))
raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
{'namedetails': True})
{'namedetails': True})
if fmt == 'xml':
root = ET.fromstring(raw)
@@ -303,7 +302,7 @@ def test_search_details_with_icon_available(fmt):
napi.Point(1.0, 2.0))
result = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
{'icon_base_url': 'foo'})
{'icon_base_url': 'foo'})
js = json.loads(result)
@@ -317,7 +316,6 @@ def test_search_details_with_icon_not_available(fmt):
napi.Point(1.0, 2.0))
result = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
{'icon_base_url': 'foo'})
{'icon_base_url': 'foo'})
assert 'icon' not in json.loads(result)

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for result datatype helper functions.
@@ -11,16 +11,15 @@ import struct
from binascii import hexlify
import pytest
import pytest_asyncio
import sqlalchemy as sa
from nominatim_api import SourceTable, DetailedResult, Point
import nominatim_api.results as nresults
def mkpoint(x, y):
return hexlify(struct.pack("=biidd", 1, 0x20000001, 4326, x, y)).decode('utf-8')
class FakeRow:
def __init__(self, **kwargs):
if 'parent_place_id' not in kwargs:
@@ -39,6 +38,7 @@ def test_minimal_detailed_result():
assert res.lat == 0.5
assert res.calculated_importance() == pytest.approx(0.00001)
def test_detailed_result_custom_importance():
res = DetailedResult(SourceTable.PLACEX,
('amenity', 'post_box'),

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for the Python web frameworks adaptor, v1 API.
@@ -121,7 +121,6 @@ class TestAdaptorRaiseError:
return excinfo.value
def test_without_content_set(self):
err = self.run_raise_error('TEST', 404)
@@ -129,7 +128,6 @@ class TestAdaptorRaiseError:
assert err.msg == 'ERROR 404: TEST'
assert err.status == 404
def test_json(self):
self.adaptor.content_type = 'application/json; charset=utf-8'
@@ -139,7 +137,6 @@ class TestAdaptorRaiseError:
assert content['code'] == 501
assert content['message'] == 'TEST'
def test_xml(self):
self.adaptor.content_type = 'text/xml; charset=utf-8'
@@ -235,7 +232,6 @@ class TestStatusEndpoint:
monkeypatch.setattr(napi.NominatimAPIAsync, 'status', _status)
@pytest.mark.asyncio
async def test_status_without_params(self):
a = FakeAdaptor()
@@ -247,7 +243,6 @@ class TestStatusEndpoint:
assert resp.status == 200
assert resp.content_type == 'text/plain; charset=utf-8'
@pytest.mark.asyncio
async def test_status_with_error(self):
a = FakeAdaptor()
@@ -259,7 +254,6 @@ class TestStatusEndpoint:
assert resp.status == 500
assert resp.content_type == 'text/plain; charset=utf-8'
@pytest.mark.asyncio
async def test_status_json_with_error(self):
a = FakeAdaptor(params={'format': 'json'})
@@ -271,7 +265,6 @@ class TestStatusEndpoint:
assert resp.status == 200
assert resp.content_type == 'application/json; charset=utf-8'
@pytest.mark.asyncio
async def test_status_bad_format(self):
a = FakeAdaptor(params={'format': 'foo'})
@@ -298,7 +291,6 @@ class TestDetailsEndpoint:
monkeypatch.setattr(napi.NominatimAPIAsync, 'details', _lookup)
@pytest.mark.asyncio
async def test_details_no_params(self):
a = FakeAdaptor()
@@ -306,7 +298,6 @@ class TestDetailsEndpoint:
with pytest.raises(FakeError, match='^400 -- .*Missing'):
await glue.details_endpoint(napi.NominatimAPIAsync(), a)
@pytest.mark.asyncio
async def test_details_by_place_id(self):
a = FakeAdaptor(params={'place_id': '4573'})
@@ -315,7 +306,6 @@ class TestDetailsEndpoint:
assert self.lookup_args[0].place_id == 4573
@pytest.mark.asyncio
async def test_details_by_osm_id(self):
a = FakeAdaptor(params={'osmtype': 'N', 'osmid': '45'})
@@ -326,7 +316,6 @@ class TestDetailsEndpoint:
assert self.lookup_args[0].osm_id == 45
assert self.lookup_args[0].osm_class is None
@pytest.mark.asyncio
async def test_details_with_debugging(self):
a = FakeAdaptor(params={'osmtype': 'N', 'osmid': '45', 'debug': '1'})
@@ -337,7 +326,6 @@ class TestDetailsEndpoint:
assert resp.content_type == 'text/html; charset=utf-8'
assert content.tag == 'html'
@pytest.mark.asyncio
async def test_details_no_result(self):
a = FakeAdaptor(params={'place_id': '4573'})
@@ -353,14 +341,14 @@ class TestReverseEndPoint:
@pytest.fixture(autouse=True)
def patch_reverse_func(self, monkeypatch):
self.result = napi.ReverseResult(napi.SourceTable.PLACEX,
('place', 'thing'),
napi.Point(1.0, 2.0))
('place', 'thing'),
napi.Point(1.0, 2.0))
async def _reverse(*args, **kwargs):
return self.result
monkeypatch.setattr(napi.NominatimAPIAsync, 'reverse', _reverse)
@pytest.mark.asyncio
@pytest.mark.parametrize('params', [{}, {'lat': '3.4'}, {'lon': '6.7'}])
async def test_reverse_no_params(self, params):
@@ -371,19 +359,6 @@ class TestReverseEndPoint:
with pytest.raises(FakeError, match='^400 -- (?s:.*)missing'):
await glue.reverse_endpoint(napi.NominatimAPIAsync(), a)
@pytest.mark.asyncio
@pytest.mark.parametrize('params', [{'lat': '45.6', 'lon': '4563'}])
async def test_reverse_success(self, params):
a = FakeAdaptor()
a.params = params
a.params['format'] = 'json'
res = await glue.reverse_endpoint(napi.NominatimAPIAsync(), a)
assert res == ''
@pytest.mark.asyncio
async def test_reverse_success(self):
a = FakeAdaptor()
@@ -392,7 +367,6 @@ class TestReverseEndPoint:
assert await glue.reverse_endpoint(napi.NominatimAPIAsync(), a)
@pytest.mark.asyncio
async def test_reverse_from_search(self):
a = FakeAdaptor()
@@ -413,12 +387,12 @@ class TestLookupEndpoint:
self.results = [napi.SearchResult(napi.SourceTable.PLACEX,
('place', 'thing'),
napi.Point(1.0, 2.0))]
async def _lookup(*args, **kwargs):
return napi.SearchResults(self.results)
monkeypatch.setattr(napi.NominatimAPIAsync, 'lookup', _lookup)
@pytest.mark.asyncio
async def test_lookup_no_params(self):
a = FakeAdaptor()
@@ -428,7 +402,6 @@ class TestLookupEndpoint:
assert res.output == '[]'
@pytest.mark.asyncio
@pytest.mark.parametrize('param', ['w', 'bad', ''])
async def test_lookup_bad_params(self, param):
@@ -440,7 +413,6 @@ class TestLookupEndpoint:
assert len(json.loads(res.output)) == 1
@pytest.mark.asyncio
@pytest.mark.parametrize('param', ['p234234', '4563'])
async def test_lookup_bad_osm_type(self, param):
@@ -452,7 +424,6 @@ class TestLookupEndpoint:
assert len(json.loads(res.output)) == 1
@pytest.mark.asyncio
async def test_lookup_working(self):
a = FakeAdaptor()
@@ -473,12 +444,12 @@ class TestSearchEndPointSearch:
self.results = [napi.SearchResult(napi.SourceTable.PLACEX,
('place', 'thing'),
napi.Point(1.0, 2.0))]
async def _search(*args, **kwargs):
return napi.SearchResults(self.results)
monkeypatch.setattr(napi.NominatimAPIAsync, 'search', _search)
@pytest.mark.asyncio
async def test_search_free_text(self):
a = FakeAdaptor()
@@ -488,7 +459,6 @@ class TestSearchEndPointSearch:
assert len(json.loads(res.output)) == 1
@pytest.mark.asyncio
async def test_search_free_text_xml(self):
a = FakeAdaptor()
@@ -500,7 +470,6 @@ class TestSearchEndPointSearch:
assert res.status == 200
assert res.output.index('something') > 0
@pytest.mark.asyncio
async def test_search_free_and_structured(self):
a = FakeAdaptor()
@@ -508,8 +477,7 @@ class TestSearchEndPointSearch:
a.params['city'] = 'ignored'
with pytest.raises(FakeError, match='^400 -- .*cannot be used together'):
res = await glue.search_endpoint(napi.NominatimAPIAsync(), a)
await glue.search_endpoint(napi.NominatimAPIAsync(), a)
@pytest.mark.asyncio
@pytest.mark.parametrize('dedupe,numres', [(True, 1), (False, 2)])
@@ -532,12 +500,12 @@ class TestSearchEndPointSearchAddress:
self.results = [napi.SearchResult(napi.SourceTable.PLACEX,
('place', 'thing'),
napi.Point(1.0, 2.0))]
async def _search(*args, **kwargs):
return napi.SearchResults(self.results)
monkeypatch.setattr(napi.NominatimAPIAsync, 'search_address', _search)
@pytest.mark.asyncio
async def test_search_structured(self):
a = FakeAdaptor()
@@ -555,12 +523,12 @@ class TestSearchEndPointSearchCategory:
self.results = [napi.SearchResult(napi.SourceTable.PLACEX,
('place', 'thing'),
napi.Point(1.0, 2.0))]
async def _search(*args, **kwargs):
return napi.SearchResults(self.results)
monkeypatch.setattr(napi.NominatimAPIAsync, 'search_category', _search)
@pytest.mark.asyncio
async def test_search_category(self):
a = FakeAdaptor()

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for warm-up CLI function.
@@ -11,6 +11,7 @@ import pytest
import nominatim_db.cli
@pytest.fixture(autouse=True)
def setup_database_with_context(apiobj, table_factory):
table_factory('word',
@@ -21,8 +22,9 @@ def setup_database_with_context(apiobj, table_factory):
apiobj.add_data('properties',
[{'property': 'tokenizer', 'value': 'icu'},
{'property': 'tokenizer_import_normalisation', 'value': ':: lower();'},
{'property': 'tokenizer_import_transliteration', 'value': "'1' > '/1/'; 'ä' > 'ä '"},
])
{'property': 'tokenizer_import_transliteration',
'value': "'1' > '/1/'; 'ä' > 'ä '"}
])
@pytest.mark.parametrize('args', [['--search-only'], ['--reverse-only']])

View File

@@ -2,12 +2,13 @@
#
# 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.
import pytest
import nominatim_db.cli
class MockParamCapture:
""" Mock that records the parameters with which a function was called
as well as the number of calls.

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for command line interface wrapper.
@@ -11,7 +11,6 @@ These tests just check that the various command line parameters route to the
correct functionality. They use a lot of monkeypatching to avoid executing
the actual functions.
"""
import importlib
import pytest
import nominatim_db.indexer.indexer
@@ -28,6 +27,7 @@ def test_cli_help(cli_call, capsys):
captured = capsys.readouterr()
assert captured.out.startswith('usage:')
def test_cli_version(cli_call, capsys):
""" Running nominatim tool --version prints a version string.
"""
@@ -46,7 +46,6 @@ class TestCliWithDb:
# Make sure tools.freeze.is_frozen doesn't report database as frozen. Monkeypatching failed
table_factory('place')
@pytest.mark.parametrize("name,oid", [('file', 'foo.osm'), ('diff', 'foo.osc')])
def test_cli_add_data_file_command(self, cli_call, mock_func_factory, name, oid):
mock_run_legacy = mock_func_factory(nominatim_db.tools.add_osm_data, 'add_data_from_file')
@@ -54,7 +53,6 @@ class TestCliWithDb:
assert mock_run_legacy.called == 1
@pytest.mark.parametrize("name,oid", [('node', 12), ('way', 8), ('relation', 32)])
def test_cli_add_data_object_command(self, cli_call, mock_func_factory, name, oid):
mock_run_legacy = mock_func_factory(nominatim_db.tools.add_osm_data, 'add_osm_object')
@@ -62,8 +60,6 @@ class TestCliWithDb:
assert mock_run_legacy.called == 1
def test_cli_add_data_tiger_data(self, cli_call, cli_tokenizer_mock, async_mock_func_factory):
mock = async_mock_func_factory(nominatim_db.tools.tiger_data, 'add_tiger_data')
@@ -80,7 +76,6 @@ class TestCliWithDb:
assert mock_drop.called == 1
assert mock_flatnode.called == 1
@pytest.mark.parametrize("params,do_bnds,do_ranks", [
([], 2, 2),
(['--boundaries-only'], 2, 0),
@@ -89,11 +84,14 @@ class TestCliWithDb:
def test_index_command(self, monkeypatch, async_mock_func_factory, table_factory,
params, do_bnds, do_ranks):
table_factory('import_status', 'indexed bool')
bnd_mock = async_mock_func_factory(nominatim_db.indexer.indexer.Indexer, 'index_boundaries')
rank_mock = async_mock_func_factory(nominatim_db.indexer.indexer.Indexer, 'index_by_rank')
postcode_mock = async_mock_func_factory(nominatim_db.indexer.indexer.Indexer, 'index_postcodes')
bnd_mock = async_mock_func_factory(nominatim_db.indexer.indexer.Indexer,
'index_boundaries')
rank_mock = async_mock_func_factory(nominatim_db.indexer.indexer.Indexer,
'index_by_rank')
postcode_mock = async_mock_func_factory(nominatim_db.indexer.indexer.Indexer,
'index_postcodes')
monkeypatch.setattr(nominatim_db.indexer.indexer.Indexer, 'has_pending',
monkeypatch.setattr(nominatim_db.indexer.indexer.Indexer, 'has_pending',
[False, True].pop)
assert self.call_nominatim('index', *params) == 0
@@ -102,7 +100,6 @@ class TestCliWithDb:
assert rank_mock.called == do_ranks
assert postcode_mock.called == do_ranks
def test_special_phrases_wiki_command(self, mock_func_factory):
func = mock_func_factory(nominatim_db.clicmd.special_phrases.SPImporter, 'import_phrases')
@@ -110,7 +107,6 @@ class TestCliWithDb:
assert func.called == 1
def test_special_phrases_csv_command(self, src_dir, mock_func_factory):
func = mock_func_factory(nominatim_db.clicmd.special_phrases.SPImporter, 'import_phrases')
testdata = src_dir / 'test' / 'testdb'
@@ -120,7 +116,6 @@ class TestCliWithDb:
assert func.called == 1
def test_special_phrases_csv_bad_file(self, src_dir):
testdata = src_dir / 'something349053905.csv'

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Test for the command line interface wrapper admin subcommand.
@@ -39,11 +39,13 @@ def test_admin_clean_deleted_relations(cli_call, mock_func_factory):
assert cli_call('admin', '--clean-deleted', '1 month') == 0
assert mock.called == 1
def test_admin_clean_deleted_relations_no_age(cli_call, mock_func_factory):
mock = mock_func_factory(nominatim_db.tools.admin, 'clean_deleted_relations')
mock_func_factory(nominatim_db.tools.admin, 'clean_deleted_relations')
assert cli_call('admin', '--clean-deleted') == 1
class TestCliAdminWithDb:
@pytest.fixture(autouse=True)
@@ -51,7 +53,6 @@ class TestCliAdminWithDb:
self.call_nominatim = cli_call
self.tokenizer_mock = cli_tokenizer_mock
@pytest.mark.parametrize("func, params", [('analyse_indexing', ('--analyse-indexing', ))])
def test_analyse_indexing(self, mock_func_factory, func, params):
mock = mock_func_factory(nominatim_db.tools.admin, func)

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for API access commands of command-line interface wrapper.
@@ -10,9 +10,9 @@ Tests for API access commands of command-line interface wrapper.
import json
import pytest
import nominatim_db.clicmd.api
import nominatim_api as napi
@pytest.mark.parametrize('call', ['search', 'reverse', 'lookup', 'details', 'status'])
def test_list_format(cli_call, call):
assert 0 == cli_call(call, '--list-formats')
@@ -30,13 +30,11 @@ class TestCliStatusCall:
monkeypatch.setattr(napi.NominatimAPI, 'status',
lambda self: napi.StatusResult(200, 'OK'))
def test_status_simple(self, cli_call, tmp_path):
result = cli_call('status', '--project-dir', str(tmp_path))
assert result == 0
def test_status_json_format(self, cli_call, tmp_path, capsys):
result = cli_call('status', '--project-dir', str(tmp_path),
'--format', 'json')
@@ -60,7 +58,6 @@ class TestCliDetailsCall:
('--way', '1'),
('--relation', '1'),
('--place_id', '10001')])
def test_details_json_format(self, cli_call, tmp_path, capsys, params):
result = cli_call('details', '--project-dir', str(tmp_path), *params)
@@ -75,15 +72,14 @@ class TestCliReverseCall:
def setup_reverse_mock(self, monkeypatch):
result = napi.ReverseResult(napi.SourceTable.PLACEX, ('place', 'thing'),
napi.Point(1.0, -3.0),
names={'name':'Name', 'name:fr': 'Nom'},
extratags={'extra':'Extra'},
names={'name': 'Name', 'name:fr': 'Nom'},
extratags={'extra': 'Extra'},
locale_name='Name',
display_name='Name')
monkeypatch.setattr(napi.NominatimAPI, 'reverse',
lambda *args, **kwargs: result)
def test_reverse_simple(self, cli_call, tmp_path, capsys):
result = cli_call('reverse', '--project-dir', str(tmp_path),
'--lat', '34', '--lon', '34')
@@ -96,7 +92,6 @@ class TestCliReverseCall:
assert 'extratags' not in out
assert 'namedetails' not in out
@pytest.mark.parametrize('param,field', [('--addressdetails', 'address'),
('--extratags', 'extratags'),
('--namedetails', 'namedetails')])
@@ -109,7 +104,6 @@ class TestCliReverseCall:
out = json.loads(capsys.readouterr().out)
assert field in out
def test_reverse_format(self, cli_call, tmp_path, capsys):
result = cli_call('reverse', '--project-dir', str(tmp_path),
'--lat', '34', '--lon', '34', '--format', 'geojson')
@@ -125,11 +119,11 @@ class TestCliLookupCall:
@pytest.fixture(autouse=True)
def setup_lookup_mock(self, monkeypatch):
result = napi.SearchResult(napi.SourceTable.PLACEX, ('place', 'thing'),
napi.Point(1.0, -3.0),
names={'name':'Name', 'name:fr': 'Nom'},
extratags={'extra':'Extra'},
locale_name='Name',
display_name='Name')
napi.Point(1.0, -3.0),
names={'name': 'Name', 'name:fr': 'Nom'},
extratags={'extra': 'Extra'},
locale_name='Name',
display_name='Name')
monkeypatch.setattr(napi.NominatimAPI, 'lookup',
lambda *args, **kwargs: napi.SearchResults([result]))
@@ -150,19 +144,18 @@ class TestCliLookupCall:
@pytest.mark.parametrize('endpoint, params', [('search', ('--query', 'Berlin')),
('search_address', ('--city', 'Berlin'))
])
])
def test_search(cli_call, tmp_path, capsys, monkeypatch, endpoint, params):
result = napi.SearchResult(napi.SourceTable.PLACEX, ('place', 'thing'),
napi.Point(1.0, -3.0),
names={'name':'Name', 'name:fr': 'Nom'},
extratags={'extra':'Extra'},
names={'name': 'Name', 'name:fr': 'Nom'},
extratags={'extra': 'Extra'},
locale_name='Name',
display_name='Name')
monkeypatch.setattr(napi.NominatimAPI, endpoint,
lambda *args, **kwargs: napi.SearchResults([result]))
result = cli_call('search', '--project-dir', str(tmp_path), *params)
assert result == 0

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for import command of the command-line interface wrapper.
@@ -24,15 +24,12 @@ class TestCliImportWithDb:
self.call_nominatim = cli_call
self.tokenizer_mock = cli_tokenizer_mock
def test_import_missing_file(self):
assert self.call_nominatim('import', '--osm-file', 'sfsafegwedgw.reh.erh') == 1
def test_import_bad_file(self):
assert self.call_nominatim('import', '--osm-file', '.') == 1
@pytest.mark.parametrize('with_updates', [True, False])
def test_import_full(self, mock_func_factory, async_mock_func_factory,
with_updates, place_table, property_table):
@@ -62,7 +59,6 @@ class TestCliImportWithDb:
cf_mock = mock_func_factory(nominatim_db.tools.refresh, 'create_functions')
assert self.call_nominatim(*params) == 0
assert self.tokenizer_mock.finalize_import_called
@@ -71,7 +67,6 @@ class TestCliImportWithDb:
for mock in mocks:
assert mock.called == 1, "Mock '{}' not called".format(mock.func_name)
def test_import_continue_load_data(self, mock_func_factory, async_mock_func_factory):
mocks = [
mock_func_factory(nominatim_db.tools.database_import, 'truncate_data_tables'),
@@ -89,7 +84,6 @@ class TestCliImportWithDb:
for mock in mocks:
assert mock.called == 1, "Mock '{}' not called".format(mock.func_name)
def test_import_continue_indexing(self, mock_func_factory, async_mock_func_factory,
placex_table, temp_db_conn):
mocks = [
@@ -107,7 +101,6 @@ class TestCliImportWithDb:
# Calling it again still works for the index
assert self.call_nominatim('import', '--continue', 'indexing') == 0
def test_import_continue_postprocess(self, mock_func_factory, async_mock_func_factory):
mocks = [
async_mock_func_factory(nominatim_db.tools.database_import, 'create_search_indices'),

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for command line interface wrapper for refresk command.
@@ -13,6 +13,7 @@ import nominatim_db.tools.refresh
import nominatim_db.tools.postcodes
import nominatim_db.indexer.indexer
class TestRefresh:
@pytest.fixture(autouse=True)
@@ -20,7 +21,6 @@ class TestRefresh:
self.call_nominatim = cli_call
self.tokenizer_mock = cli_tokenizer_mock
@pytest.mark.parametrize("command,func", [
('address-levels', 'load_address_levels_from_config'),
('wiki-data', 'import_wikipedia_articles'),
@@ -33,17 +33,14 @@ class TestRefresh:
assert self.call_nominatim('refresh', '--' + command) == 0
assert func_mock.called == 1
def test_refresh_word_count(self):
assert self.call_nominatim('refresh', '--word-count') == 0
assert self.tokenizer_mock.update_statistics_called
def test_refresh_word_tokens(self):
assert self.call_nominatim('refresh', '--word-tokens') == 0
assert self.tokenizer_mock.update_word_tokens_called
def test_refresh_postcodes(self, async_mock_func_factory, mock_func_factory, place_table):
func_mock = mock_func_factory(nominatim_db.tools.postcodes, 'update_postcodes')
idx_mock = async_mock_func_factory(nominatim_db.indexer.indexer.Indexer, 'index_postcodes')
@@ -52,12 +49,10 @@ class TestRefresh:
assert func_mock.called == 1
assert idx_mock.called == 1
def test_refresh_postcodes_no_place_table(self):
# Do nothing without the place table
assert self.call_nominatim('refresh', '--postcodes') == 0
def test_refresh_create_functions(self, mock_func_factory):
func_mock = mock_func_factory(nominatim_db.tools.refresh, 'create_functions')
@@ -65,17 +60,14 @@ class TestRefresh:
assert func_mock.called == 1
assert self.tokenizer_mock.update_sql_functions_called
def test_refresh_wikidata_file_not_found(self, monkeypatch):
monkeypatch.setenv('NOMINATIM_WIKIPEDIA_DATA_PATH', 'gjoiergjeroi345Q')
assert self.call_nominatim('refresh', '--wiki-data') == 1
def test_refresh_secondary_importance_file_not_found(self):
assert self.call_nominatim('refresh', '--secondary-importance') == 1
def test_refresh_secondary_importance_new_table(self, mock_func_factory):
mocks = [mock_func_factory(nominatim_db.tools.refresh, 'import_secondary_importance'),
mock_func_factory(nominatim_db.tools.refresh, 'create_functions')]
@@ -84,7 +76,6 @@ class TestRefresh:
assert mocks[0].called == 1
assert mocks[1].called == 1
def test_refresh_importance_computed_after_wiki_import(self, monkeypatch, mock_func_factory):
calls = []
monkeypatch.setattr(nominatim_db.tools.refresh, 'import_wikipedia_articles',
@@ -102,7 +93,8 @@ class TestRefresh:
('--data-object', 'N23', '--data-object', 'N24'),
('--data-area', 'R7723'),
('--data-area', 'r7723', '--data-area', 'r2'),
('--data-area', 'R9284425', '--data-object', 'n1234567894567')])
('--data-area', 'R9284425',
'--data-object', 'n1234567894567')])
def test_refresh_objects(self, params, mock_func_factory):
func_mock = mock_func_factory(nominatim_db.tools.refresh, 'invalidate_osm_object')
@@ -110,7 +102,6 @@ class TestRefresh:
assert func_mock.called == len(params)/2
@pytest.mark.parametrize('func', ('--data-object', '--data-area'))
@pytest.mark.parametrize('param', ('234', 'a55', 'R 453', 'Rel'))
def test_refresh_objects_bad_param(self, func, param, mock_func_factory):

View File

@@ -18,6 +18,7 @@ import nominatim_db.tools.replication
import nominatim_db.tools.refresh
from nominatim_db.db import status
@pytest.fixture
def tokenizer_mock(monkeypatch):
class DummyTokenizer:
@@ -40,7 +41,6 @@ def tokenizer_mock(monkeypatch):
return tok
@pytest.fixture
def init_status(temp_db_conn, status_table):
status.set_status(temp_db_conn, date=dt.datetime.now(dt.timezone.utc), seq=1)
@@ -62,16 +62,14 @@ class TestCliReplication:
def setup_cli_call(self, cli_call, temp_db):
self.call_nominatim = lambda *args: cli_call('replication', *args)
@pytest.fixture(autouse=True)
def setup_update_function(self, monkeypatch):
def _mock_updates(states):
monkeypatch.setattr(nominatim_db.tools.replication, 'update',
lambda *args, **kwargs: states.pop())
lambda *args, **kwargs: states.pop())
self.update_states = _mock_updates
@pytest.mark.parametrize("params,func", [
(('--init',), 'init_replication'),
(('--init', '--no-update-functions'), 'init_replication'),
@@ -88,20 +86,17 @@ class TestCliReplication:
if params == ('--init',):
assert umock.called == 1
def test_replication_update_bad_interval(self, monkeypatch):
monkeypatch.setenv('NOMINATIM_REPLICATION_UPDATE_INTERVAL', 'xx')
assert self.call_nominatim() == 1
def test_replication_update_bad_interval_for_geofabrik(self, monkeypatch):
monkeypatch.setenv('NOMINATIM_REPLICATION_URL',
'https://download.geofabrik.de/europe/italy-updates')
assert self.call_nominatim() == 1
def test_replication_update_continuous_no_index(self):
assert self.call_nominatim('--no-index') == 1
@@ -110,14 +105,12 @@ class TestCliReplication:
assert str(update_mock.last_args[1]['osm2pgsql']).endswith('OSM2PGSQL NOT AVAILABLE')
def test_replication_update_custom_osm2pgsql(self, monkeypatch, update_mock):
monkeypatch.setenv('NOMINATIM_OSM2PGSQL_BINARY', '/secret/osm2pgsql')
assert self.call_nominatim('--once', '--no-index') == 0
assert str(update_mock.last_args[1]['osm2pgsql']) == '/secret/osm2pgsql'
@pytest.mark.parametrize("update_interval", [60, 3600])
def test_replication_catchup(self, placex_table, monkeypatch, index_mock, update_interval):
monkeypatch.setenv('NOMINATIM_REPLICATION_UPDATE_INTERVAL', str(update_interval))
@@ -125,13 +118,11 @@ class TestCliReplication:
assert self.call_nominatim('--catch-up') == 0
def test_replication_update_custom_threads(self, update_mock):
assert self.call_nominatim('--once', '--no-index', '--threads', '4') == 0
assert update_mock.last_args[1]['threads'] == 4
def test_replication_update_continuous(self, index_mock):
self.update_states([nominatim_db.tools.replication.UpdateState.UP_TO_DATE,
nominatim_db.tools.replication.UpdateState.UP_TO_DATE])
@@ -141,7 +132,6 @@ class TestCliReplication:
assert index_mock.called == 2
def test_replication_update_continuous_no_change(self, mock_func_factory,
index_mock):
self.update_states([nominatim_db.tools.replication.UpdateState.NO_CHANGES,

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Test for loading dotenv configuration.
@@ -13,6 +13,7 @@ import pytest
from nominatim_db.config import Configuration, flatten_config_list
from nominatim_db.errors import UsageError
@pytest.fixture
def make_config():
""" Create a configuration object from the given project directory.
@@ -22,6 +23,7 @@ def make_config():
return _mk_config
@pytest.fixture
def make_config_path(tmp_path):
""" Create a configuration object with project and config directories
@@ -108,7 +110,7 @@ def test_get_libpq_dsn_convert_php(make_config, monkeypatch):
@pytest.mark.parametrize("val,expect", [('foo bar', "'foo bar'"),
("xy'z", "xy\\'z"),
])
])
def test_get_libpq_dsn_convert_php_special_chars(make_config, monkeypatch, val, expect):
config = make_config()
@@ -137,6 +139,7 @@ def test_get_bool(make_config, monkeypatch, value, result):
assert config.get_bool('FOOBAR') == result
def test_get_bool_empty(make_config):
config = make_config()
@@ -303,7 +306,7 @@ def test_load_subconf_env_absolute_not_found(make_config_path, monkeypatch, tmp_
(config.config_dir / 'test.yaml').write_text('cow: muh\ncat: miau\n')
with pytest.raises(UsageError, match='Config file not found.'):
rules = config.load_sub_configuration('test.yaml', config='MY_CONFIG')
config.load_sub_configuration('test.yaml', config='MY_CONFIG')
@pytest.mark.parametrize("location", ['project_dir', 'config_dir'])
@@ -326,7 +329,7 @@ def test_load_subconf_env_relative_not_found(make_config_path, monkeypatch):
(config.config_dir / 'test.yaml').write_text('cow: muh\ncat: miau\n')
with pytest.raises(UsageError, match='Config file not found.'):
rules = config.load_sub_configuration('test.yaml', config='MY_CONFIG')
config.load_sub_configuration('test.yaml', config='MY_CONFIG')
def test_load_subconf_json(make_config_path):
@@ -338,6 +341,7 @@ def test_load_subconf_json(make_config_path):
assert rules == dict(cow='muh', cat='miau')
def test_load_subconf_not_found(make_config_path):
config = make_config_path()
@@ -371,7 +375,7 @@ def test_load_subconf_include_relative(make_config_path, tmp_path, location):
config = make_config_path()
testfile = config.config_dir / 'test.yaml'
testfile.write_text(f'base: !include inc.yaml\n')
testfile.write_text('base: !include inc.yaml\n')
(getattr(config, location) / 'inc.yaml').write_text('first: 1\nsecond: 2\n')
rules = config.load_sub_configuration('test.yaml')
@@ -383,28 +387,28 @@ def test_load_subconf_include_bad_format(make_config_path):
config = make_config_path()
testfile = config.config_dir / 'test.yaml'
testfile.write_text(f'base: !include inc.txt\n')
testfile.write_text('base: !include inc.txt\n')
(config.config_dir / 'inc.txt').write_text('first: 1\nsecond: 2\n')
with pytest.raises(UsageError, match='Cannot handle config file format.'):
rules = config.load_sub_configuration('test.yaml')
config.load_sub_configuration('test.yaml')
def test_load_subconf_include_not_found(make_config_path):
config = make_config_path()
testfile = config.config_dir / 'test.yaml'
testfile.write_text(f'base: !include inc.txt\n')
testfile.write_text('base: !include inc.txt\n')
with pytest.raises(UsageError, match='Config file not found.'):
rules = config.load_sub_configuration('test.yaml')
config.load_sub_configuration('test.yaml')
def test_load_subconf_include_recursive(make_config_path):
config = make_config_path()
testfile = config.config_dir / 'test.yaml'
testfile.write_text(f'base: !include inc.yaml\n')
testfile.write_text('base: !include inc.yaml\n')
(config.config_dir / 'inc.yaml').write_text('- !include more.yaml\n- upper\n')
(config.config_dir / 'more.yaml').write_text('- the end\n')
@@ -435,6 +439,6 @@ def test_flatten_config_list_nested():
[[2, 3], [45, [56, 78], 66]],
'end'
]
assert flatten_config_list(content) == \
[34, {'first': '1st', 'second': '2nd'}, {},
2, 3, 45, 56, 78, 66, 'end']
[34, {'first': '1st', 'second': '2nd'}, {}, 2, 3, 45, 56, 78, 66, 'end']

View File

@@ -2,18 +2,18 @@
#
# 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.
"""
Test for loading extra Python modules.
"""
from pathlib import Path
import sys
import pytest
from nominatim_db.config import Configuration
@pytest.fixture
def test_config(src_dir, tmp_path):
""" Create a configuration object with project and config directories
@@ -31,6 +31,7 @@ def test_load_default_module(test_config):
assert isinstance(module.NOMINATIM_VERSION, tuple)
def test_load_default_module_with_hyphen(test_config):
module = test_config.load_plugin_module('place-info', 'nominatim_db.data')

View File

@@ -2,7 +2,7 @@
#
# 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.
import itertools
import sys
@@ -69,6 +69,7 @@ def temp_db_with_extensions(temp_db):
return temp_db
@pytest.fixture
def temp_db_conn(temp_db):
""" Connection to the test database.
@@ -100,8 +101,9 @@ def table_factory(temp_db_conn):
if content:
sql = pysql.SQL("INSERT INTO {} VALUES ({})")\
.format(pysql.Identifier(name),
pysql.SQL(',').join([pysql.Placeholder() for _ in range(len(content[0]))]))
cur.executemany(sql , content)
pysql.SQL(',').join([pysql.Placeholder()
for _ in range(len(content[0]))]))
cur.executemany(sql, content)
return mk_table
@@ -178,6 +180,7 @@ def place_row(place_table, temp_db_cursor):
return _insert
@pytest.fixture
def placex_table(temp_db_with_extensions, temp_db_conn):
""" Create an empty version of the place table.

View File

@@ -2,13 +2,14 @@
#
# 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.
"""
Specialised psycopg cursor with shortcut functions useful for testing.
"""
import psycopg
class CursorForTesting(psycopg.Cursor):
""" Extension to the DictCursor class that provides execution
short-cuts that simplify writing assertions.
@@ -22,7 +23,6 @@ class CursorForTesting(psycopg.Cursor):
assert self.rowcount == 1
return self.fetchone()[0]
def row_set(self, sql, params=None):
""" Execute a query and return the result as a set of tuples.
Fails when the SQL command returns duplicate rows.
@@ -34,7 +34,6 @@ class CursorForTesting(psycopg.Cursor):
return result
def table_exists(self, table):
""" Check that a table with the given name exists in the database.
"""
@@ -42,7 +41,6 @@ class CursorForTesting(psycopg.Cursor):
WHERE tablename = %s""", (table, ))
return num == 1
def index_exists(self, table, index):
""" Check that an indexwith the given name exists on the given table.
"""
@@ -51,7 +49,6 @@ class CursorForTesting(psycopg.Cursor):
(table, index))
return num == 1
def table_rows(self, table, where=None):
""" Return the number of rows in the given table.
"""

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for function that handle country properties.
@@ -12,6 +12,7 @@ import pytest
from nominatim_db.data import country_info
@pytest.fixture
def loaded_country(def_config):
country_info.setup_country_config(def_config)
@@ -115,8 +116,8 @@ def test_setup_country_config_languages_not_loaded(env_with_country_config):
info = country_info._CountryInfo()
info.load(config)
assert dict(info.items()) == {'de': {'partition': 3,
'languages': [],
'names': {'name': 'Deutschland'}}}
'languages': [],
'names': {'name': 'Deutschland'}}}
def test_setup_country_config_name_not_loaded(env_with_country_config):
@@ -132,8 +133,7 @@ def test_setup_country_config_name_not_loaded(env_with_country_config):
assert dict(info.items()) == {'de': {'partition': 3,
'languages': ['de'],
'names': {}
}}
'names': {}}}
def test_setup_country_config_names_not_loaded(env_with_country_config):
@@ -148,8 +148,7 @@ def test_setup_country_config_names_not_loaded(env_with_country_config):
assert dict(info.items()) == {'de': {'partition': 3,
'languages': ['de'],
'names': {}
}}
'names': {}}}
def test_setup_country_config_special_character(env_with_country_config):
@@ -157,8 +156,8 @@ def test_setup_country_config_special_character(env_with_country_config):
bq:
partition: 250
languages: nl
names:
name:
names:
name:
default: "\\N"
""")
@@ -167,5 +166,4 @@ def test_setup_country_config_special_character(env_with_country_config):
assert dict(info.items()) == {'bq': {'partition': 250,
'languages': ['nl'],
'names': {'name': '\x85'}
}}
'names': {'name': '\x85'}}}

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for specialised connection and cursor classes.
@@ -12,6 +12,7 @@ import psycopg
import nominatim_db.db.connection as nc
@pytest.fixture
def db(dsn):
with nc.connect(dsn) as conn:
@@ -36,6 +37,7 @@ def test_has_column(db, table_factory, name, result):
assert nc.table_has_column(db, 'stuff', name) == result
def test_connection_index_exists(db, table_factory, temp_db_cursor):
assert not nc.index_exists(db, 'some_index')
@@ -76,6 +78,7 @@ def test_drop_table_non_existing_force(db):
with pytest.raises(psycopg.ProgrammingError, match='.*does not exist.*'):
nc.drop_tables(db, 'dfkjgjriogjigjgjrdghehtre', if_exists=False)
def test_connection_server_version_tuple(db):
ver = nc.server_version_tuple(db)

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for property table manpulation.
@@ -11,6 +11,7 @@ import pytest
from nominatim_db.db import properties
@pytest.fixture
def property_factory(property_table, temp_db_cursor):
""" A function fixture that adds a property into the property table.

View File

@@ -2,16 +2,17 @@
#
# 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.
"""
Tests for SQL preprocessing.
"""
import pytest
import pytest_asyncio
import pytest_asyncio # noqa
from nominatim_db.db.sql_preprocessor import SQLPreprocessor
@pytest.fixture
def sql_factory(tmp_path):
def _mk_sql(sql_body):
@@ -26,6 +27,7 @@ def sql_factory(tmp_path):
return _mk_sql
@pytest.mark.parametrize("expr,ret", [
("'a'", 'a'),
("'{{db.partitions|join}}'", '012'),
@@ -61,8 +63,7 @@ def test_load_file_with_params(sql_preprocessor, sql_factory, temp_db_conn, temp
async def test_load_parallel_file(dsn, sql_preprocessor, tmp_path, temp_db_cursor):
(tmp_path / 'test.sql').write_text("""
CREATE TABLE foo (a TEXT);
CREATE TABLE foo2(a TEXT);""" +
"\n---\nCREATE TABLE bar (b INT);")
CREATE TABLE foo2(a TEXT);""" + "\n---\nCREATE TABLE bar (b INT);")
await sql_preprocessor.run_parallel_sql_file(dsn, 'test.sql', num_threads=4)

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for status table manipulation.
@@ -19,7 +19,8 @@ OSM_NODE_DATA = """\
<node id="45673" visible="true" version="1" changeset="2047" timestamp="2006-01-27T22:09:10Z" user="Foo" uid="111" lat="48.7586670" lon="8.1343060">
</node>
</osm>
"""
""" # noqa
def iso_date(date):
return dt.datetime.strptime(date, nominatim_db.db.status.ISODATE_FORMAT)\
@@ -43,7 +44,8 @@ def test_compute_database_date_from_osm2pgsql(table_factory, temp_db_conn, offli
def test_compute_database_date_from_osm2pgsql_nodata(table_factory, temp_db_conn):
table_factory('osm2pgsql_properties', 'property TEXT, value TEXT')
with pytest.raises(UsageError, match='Cannot determine database date from data in offline mode'):
with pytest.raises(UsageError,
match='Cannot determine database date from data in offline mode'):
nominatim_db.db.status.compute_database_date(temp_db_conn, offline=True)
@@ -56,6 +58,7 @@ def test_compute_database_date_valid(monkeypatch, place_row, temp_db_conn):
place_row(osm_type='N', osm_id=45673)
requested_url = []
def mock_url(url):
requested_url.append(url)
return OSM_NODE_DATA
@@ -72,6 +75,7 @@ def test_compute_database_broken_api(monkeypatch, place_row, temp_db_conn):
place_row(osm_type='N', osm_id=45673)
requested_url = []
def mock_url(url):
requested_url.append(url)
return '<osm version="0.6" generator="OpenStre'
@@ -86,8 +90,7 @@ def test_set_status_empty_table(temp_db_conn, temp_db_cursor):
date = dt.datetime.fromordinal(1000000).replace(tzinfo=dt.timezone.utc)
nominatim_db.db.status.set_status(temp_db_conn, date=date)
assert temp_db_cursor.row_set("SELECT * FROM import_status") == \
{(date, None, True)}
assert temp_db_cursor.row_set("SELECT * FROM import_status") == {(date, None, True)}
def test_set_status_filled_table(temp_db_conn, temp_db_cursor):
@@ -99,8 +102,7 @@ def test_set_status_filled_table(temp_db_conn, temp_db_cursor):
date = dt.datetime.fromordinal(1000100).replace(tzinfo=dt.timezone.utc)
nominatim_db.db.status.set_status(temp_db_conn, date=date, seq=456, indexed=False)
assert temp_db_cursor.row_set("SELECT * FROM import_status") == \
{(date, 456, False)}
assert temp_db_cursor.row_set("SELECT * FROM import_status") == {(date, 456, False)}
def test_set_status_missing_date(temp_db_conn, temp_db_cursor):
@@ -111,8 +113,7 @@ def test_set_status_missing_date(temp_db_conn, temp_db_cursor):
nominatim_db.db.status.set_status(temp_db_conn, date=None, seq=456, indexed=False)
assert temp_db_cursor.row_set("SELECT * FROM import_status") == \
{(date, 456, False)}
assert temp_db_cursor.row_set("SELECT * FROM import_status") == {(date, 456, False)}
def test_get_status_empty_table(temp_db_conn):
@@ -123,8 +124,7 @@ def test_get_status_success(temp_db_conn):
date = dt.datetime.fromordinal(1000000).replace(tzinfo=dt.timezone.utc)
nominatim_db.db.status.set_status(temp_db_conn, date=date, seq=667, indexed=False)
assert nominatim_db.db.status.get_status(temp_db_conn) == \
(date, 667, False)
assert nominatim_db.db.status.get_status(temp_db_conn) == (date, 667, False)
@pytest.mark.parametrize("old_state", [True, False])

View File

@@ -2,18 +2,17 @@
#
# 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.
"""
Tests for DB utility functions in db.utils
"""
import json
import pytest
import nominatim_db.db.utils as db_utils
from nominatim_db.errors import UsageError
def test_execute_file_success(dsn, temp_db_cursor, tmp_path):
tmpfile = tmp_path / 'test.sql'
tmpfile.write_text('CREATE TABLE test (id INT);\nINSERT INTO test VALUES(56);')
@@ -22,6 +21,7 @@ def test_execute_file_success(dsn, temp_db_cursor, tmp_path):
assert temp_db_cursor.row_set('SELECT * FROM test') == {(56, )}
def test_execute_file_bad_file(dsn, tmp_path):
with pytest.raises(FileNotFoundError):
db_utils.execute_file(dsn, tmp_path / 'test2.sql')

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tokenizer for testing.
@@ -10,11 +10,13 @@ Tokenizer for testing.
from nominatim_db.data.place_info import PlaceInfo
from nominatim_db.config import Configuration
def create(dsn, data_dir):
""" Create a new instance of the tokenizer provided by this module.
"""
return DummyTokenizer(dsn, data_dir)
class DummyTokenizer:
def __init__(self, dsn, data_dir):
@@ -23,23 +25,19 @@ class DummyTokenizer:
self.init_state = None
self.analyser_cache = {}
def init_new_db(self, *args, **kwargs):
assert self.init_state is None
self.init_state = "new"
def init_from_project(self, config):
assert isinstance(config, Configuration)
assert self.init_state is None
self.init_state = "loaded"
@staticmethod
def finalize_import(_):
pass
def name_analyzer(self):
return DummyNameAnalyzer(self.analyser_cache)
@@ -52,12 +50,10 @@ class DummyNameAnalyzer:
def __exit__(self, exc_type, exc_value, traceback):
self.close()
def __init__(self, cache):
self.analyser_cache = cache
cache['countries'] = []
def close(self):
pass

View File

@@ -2,18 +2,19 @@
#
# 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.
"""
Tests for running the indexing.
"""
import itertools
import pytest
import pytest_asyncio
import pytest_asyncio # noqa
from nominatim_db.indexer import indexer
from nominatim_db.tokenizer import factory
class IndexerTestDB:
def __init__(self, conn):
@@ -232,6 +233,7 @@ async def test_index_partial_with_30(test_db, threads, test_tokenizer):
SELECT count(*) FROM placex
WHERE indexed_status = 0 AND rank_address between 1 and 27""") == 0
@pytest.mark.parametrize("threads", [1, 15])
@pytest.mark.asyncio
async def test_index_boundaries(test_db, threads, test_tokenizer):

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Legacy word table for testing with functions to prefil and test contents
@@ -10,6 +10,7 @@ of the table.
"""
from nominatim_db.db.connection import execute_scalar
class MockIcuWordTable:
""" A word table for testing using legacy word table structure.
"""
@@ -31,7 +32,6 @@ class MockIcuWordTable:
(word_id, word or word_token, word))
self.conn.commit()
def add_special(self, word_token, word, cls, typ, oper):
with self.conn.cursor() as cur:
cur.execute("""INSERT INTO word (word_token, type, word, info)
@@ -42,7 +42,6 @@ class MockIcuWordTable:
""", (word_token, word, cls, typ, oper))
self.conn.commit()
def add_country(self, country_code, word_token):
with self.conn.cursor() as cur:
cur.execute("""INSERT INTO word (word_token, type, word)
@@ -50,7 +49,6 @@ class MockIcuWordTable:
(word_token, country_code))
self.conn.commit()
def add_postcode(self, word_token, postcode):
with self.conn.cursor() as cur:
cur.execute("""INSERT INTO word (word_token, type, word)
@@ -58,7 +56,6 @@ class MockIcuWordTable:
""", (word_token, postcode))
self.conn.commit()
def add_housenumber(self, word_id, word_tokens, word=None):
with self.conn.cursor() as cur:
if isinstance(word_tokens, str):
@@ -71,24 +68,21 @@ class MockIcuWordTable:
word = word_tokens[0]
for token in word_tokens:
cur.execute("""INSERT INTO word (word_id, word_token, type, word, info)
VALUES (%s, %s, 'H', %s, jsonb_build_object('lookup', %s::text))
VALUES (%s, %s, 'H', %s,
jsonb_build_object('lookup', %s::text))
""", (word_id, token, word, word_tokens[0]))
self.conn.commit()
def count(self):
return execute_scalar(self.conn, "SELECT count(*) FROM word")
def count_special(self):
return execute_scalar(self.conn, "SELECT count(*) FROM word WHERE type = 'S'")
def count_housenumbers(self):
return execute_scalar(self.conn, "SELECT count(*) FROM word WHERE type = 'H'")
def get_special(self):
with self.conn.cursor() as cur:
cur.execute("SELECT word_token, info, word FROM word WHERE type = 'S'")
@@ -97,7 +91,6 @@ class MockIcuWordTable:
assert len(result) == cur.rowcount, "Word table has duplicates."
return result
def get_country(self):
with self.conn.cursor() as cur:
cur.execute("SELECT word, word_token FROM word WHERE type = 'C'")
@@ -105,15 +98,12 @@ class MockIcuWordTable:
assert len(result) == cur.rowcount, "Word table has duplicates."
return result
def get_postcodes(self):
with self.conn.cursor() as cur:
cur.execute("SELECT word FROM word WHERE type = 'P'")
return set((row[0] for row in cur))
def get_partial_words(self):
with self.conn.cursor() as cur:
cur.execute("SELECT word_token, info FROM word WHERE type ='w'")
return set(((row[0], row[1]['count']) for row in cur))

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Custom mocks for testing.
@@ -11,8 +11,6 @@ import itertools
from nominatim_db.db import properties
# This must always point to the mock word table for the default tokenizer.
from mock_icu_word_table import MockIcuWordTable as MockWordTable
class MockPlacexTable:
""" A placex table for testing.
@@ -58,7 +56,8 @@ class MockPlacexTable:
type, name, admin_level, address,
housenumber, rank_search,
extratags, geometry, country_code)
VALUES(nextval('seq_place'), %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)""",
VALUES(nextval('seq_place'), %s, %s, %s, %s, %s, %s,
%s, %s, %s, %s, %s, %s)""",
(osm_type, osm_id or next(self.idseq), cls, typ, names,
admin_level, address, housenumber, rank_search,
extratags, 'SRID=4326;' + geom,
@@ -72,13 +71,11 @@ class MockPropertyTable:
def __init__(self, conn):
self.conn = conn
def set(self, name, value):
""" Set a property in the table to the given value.
"""
properties.set_property(self.conn, name, value)
def get(self, name):
""" Set a property in the table to the given value.
"""

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for the sanitizer that normalizes housenumbers.
@@ -12,11 +12,12 @@ import pytest
from nominatim_db.tokenizer.place_sanitizer import PlaceSanitizer
from nominatim_db.data.place_info import PlaceInfo
@pytest.fixture
def sanitize(request, def_config):
sanitizer_args = {'step': 'clean-housenumbers'}
for mark in request.node.iter_markers(name="sanitizer_params"):
sanitizer_args.update({k.replace('_', '-') : v for k,v in mark.kwargs.items()})
sanitizer_args.update({k.replace('_', '-'): v for k, v in mark.kwargs.items()})
def _run(**kwargs):
place = PlaceInfo({'address': kwargs})

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for the sanitizer that normalizes postcodes.
@@ -13,12 +13,13 @@ from nominatim_db.tokenizer.place_sanitizer import PlaceSanitizer
from nominatim_db.data.place_info import PlaceInfo
from nominatim_db.data import country_info
@pytest.fixture
def sanitize(def_config, request):
country_info.setup_country_config(def_config)
sanitizer_args = {'step': 'clean-postcodes'}
for mark in request.node.iter_markers(name="sanitizer_params"):
sanitizer_args.update({k.replace('_', '-') : v for k,v in mark.kwargs.items()})
sanitizer_args.update({k.replace('_', '-'): v for k, v in mark.kwargs.items()})
def _run(country=None, **kwargs):
pi = {'address': kwargs}

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for sanitizer that clean up TIGER tags.
@@ -12,16 +12,17 @@ import pytest
from nominatim_db.tokenizer.place_sanitizer import PlaceSanitizer
from nominatim_db.data.place_info import PlaceInfo
class TestCleanTigerTags:
@pytest.fixture(autouse=True)
def setup_country(self, def_config):
self.config = def_config
def run_sanitizer_on(self, addr):
place = PlaceInfo({'address': addr})
_, outaddr = PlaceSanitizer([{'step': 'clean-tiger-tags'}], self.config).process_names(place)
_, outaddr = PlaceSanitizer([{'step': 'clean-tiger-tags'}],
self.config).process_names(place)
return sorted([(p.name, p.kind, p.suffix) for p in outaddr])
@@ -31,13 +32,11 @@ class TestCleanTigerTags:
assert self.run_sanitizer_on({'tiger:county': inname})\
== [(outname, 'county', 'tiger')]
@pytest.mark.parametrize('name', ('Hamilton', 'Big, Road', ''))
def test_badly_formatted(self, name):
assert self.run_sanitizer_on({'tiger:county': name})\
== [(name, 'county', 'tiger')]
def test_unmatched(self):
assert self.run_sanitizer_on({'tiger:country': 'US'})\
== [('US', 'tiger', 'country')]

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for the sanitizer that normalizes housenumbers.
@@ -22,18 +22,15 @@ class TestWithDefault:
def run_sanitizer_on(self, type, **kwargs):
place = PlaceInfo({type: {k.replace('_', ':'): v for k, v in kwargs.items()},
'country_code': 'de', 'rank_address': 30})
'country_code': 'de', 'rank_address': 30})
sanitizer_args = {'step': 'delete-tags'}
name, address = PlaceSanitizer([sanitizer_args],
self.config).process_names(place)
return {
'name': sorted([(p.name, p.kind, p.suffix or '') for p in name]),
'address': sorted([(p.name, p.kind, p.suffix or '') for p in address])
}
self.config).process_names(place)
return {'name': sorted([(p.name, p.kind, p.suffix or '') for p in name]),
'address': sorted([(p.name, p.kind, p.suffix or '') for p in address])}
def test_on_name(self):
res = self.run_sanitizer_on('name', name='foo', ref='bar', ref_abc='baz')
@@ -44,7 +41,7 @@ class TestWithDefault:
res = self.run_sanitizer_on('address', name='foo', ref='bar', ref_abc='baz')
assert res.get('address') == [('bar', 'ref', ''), ('baz', 'ref', 'abc'),
('foo', 'name', '')]
('foo', 'name', '')]
class TestTypeField:
@@ -56,15 +53,13 @@ class TestTypeField:
def run_sanitizer_on(self, type, **kwargs):
place = PlaceInfo({'name': {k.replace('_', ':'): v for k, v in kwargs.items()},
'country_code': 'de', 'rank_address': 30})
'country_code': 'de', 'rank_address': 30})
sanitizer_args = {
'step': 'delete-tags',
'type': type,
}
sanitizer_args = {'step': 'delete-tags',
'type': type}
name, _ = PlaceSanitizer([sanitizer_args],
self.config).process_names(place)
self.config).process_names(place)
return sorted([(p.name, p.kind, p.suffix or '') for p in name])
@@ -77,7 +72,8 @@ class TestTypeField:
res = self.run_sanitizer_on('address', name='foo', ref='bar', ref_abc='baz')
assert res == [('bar', 'ref', ''), ('baz', 'ref', 'abc'),
('foo', 'name', '')]
('foo', 'name', '')]
class TestFilterKind:
@@ -88,15 +84,13 @@ class TestFilterKind:
def run_sanitizer_on(self, filt, **kwargs):
place = PlaceInfo({'name': {k.replace('_', ':'): v for k, v in kwargs.items()},
'country_code': 'de', 'rank_address': 30})
'country_code': 'de', 'rank_address': 30})
sanitizer_args = {
'step': 'delete-tags',
'filter-kind': filt,
}
sanitizer_args = {'step': 'delete-tags',
'filter-kind': filt}
name, _ = PlaceSanitizer([sanitizer_args],
self.config).process_names(place)
self.config).process_names(place)
return sorted([(p.name, p.kind, p.suffix or '') for p in name])
@@ -106,7 +100,6 @@ class TestFilterKind:
assert res == [('bar', 'ref', 'abc'), ('foo', 'ref', '')]
def test_single_pattern(self):
res = self.run_sanitizer_on(['.*name'],
name_fr='foo', ref_fr='foo', namexx_fr='bar',
@@ -114,7 +107,6 @@ class TestFilterKind:
assert res == [('bar', 'namexx', 'fr'), ('foo', 'ref', 'fr')]
def test_multiple_patterns(self):
res = self.run_sanitizer_on(['.*name', 'ref'],
name_fr='foo', ref_fr='foo', oldref_fr='foo',
@@ -132,19 +124,16 @@ class TestRankAddress:
def run_sanitizer_on(self, rank_addr, **kwargs):
place = PlaceInfo({'name': {k.replace('_', ':'): v for k, v in kwargs.items()},
'country_code': 'de', 'rank_address': 30})
'country_code': 'de', 'rank_address': 30})
sanitizer_args = {
'step': 'delete-tags',
'rank_address': rank_addr
}
sanitizer_args = {'step': 'delete-tags',
'rank_address': rank_addr}
name, _ = PlaceSanitizer([sanitizer_args],
self.config).process_names(place)
self.config).process_names(place)
return sorted([(p.name, p.kind, p.suffix or '') for p in name])
def test_single_rank(self):
res = self.run_sanitizer_on('30', name='foo', ref='bar')
@@ -185,33 +174,29 @@ class TestSuffix:
def run_sanitizer_on(self, suffix, **kwargs):
place = PlaceInfo({'name': {k.replace('_', ':'): v for k, v in kwargs.items()},
'country_code': 'de', 'rank_address': 30})
'country_code': 'de', 'rank_address': 30})
sanitizer_args = {
'step': 'delete-tags',
'suffix': suffix,
}
sanitizer_args = {'step': 'delete-tags',
'suffix': suffix}
name, _ = PlaceSanitizer([sanitizer_args],
self.config).process_names(place)
self.config).process_names(place)
return sorted([(p.name, p.kind, p.suffix or '') for p in name])
def test_single_suffix(self):
res = self.run_sanitizer_on('abc', name='foo', name_abc='foo',
name_pqr='bar', ref='bar', ref_abc='baz')
name_pqr='bar', ref='bar', ref_abc='baz')
assert res == [('bar', 'name', 'pqr'), ('bar', 'ref', ''), ('foo', 'name', '')]
def test_multiple_suffix(self):
res = self.run_sanitizer_on(['abc.*', 'pqr'], name='foo', name_abcxx='foo',
ref_pqr='bar', name_pqrxx='baz')
ref_pqr='bar', name_pqrxx='baz')
assert res == [('baz', 'name', 'pqrxx'), ('foo', 'name', '')]
class TestCountryCodes:
@pytest.fixture(autouse=True)
@@ -221,19 +206,16 @@ class TestCountryCodes:
def run_sanitizer_on(self, country_code, **kwargs):
place = PlaceInfo({'name': {k.replace('_', ':'): v for k, v in kwargs.items()},
'country_code': 'de', 'rank_address': 30})
'country_code': 'de', 'rank_address': 30})
sanitizer_args = {
'step': 'delete-tags',
'country_code': country_code,
}
sanitizer_args = {'step': 'delete-tags',
'country_code': country_code}
name, _ = PlaceSanitizer([sanitizer_args],
self.config).process_names(place)
self.config).process_names(place)
return sorted([(p.name, p.kind) for p in name])
def test_single_country_code_pass(self):
res = self.run_sanitizer_on('de', name='foo', ref='bar')
@@ -259,6 +241,7 @@ class TestCountryCodes:
assert res == [('bar', 'ref'), ('foo', 'name')]
class TestAllParameters:
@pytest.fixture(autouse=True)
@@ -268,7 +251,7 @@ class TestAllParameters:
def run_sanitizer_on(self, country_code, rank_addr, suffix, **kwargs):
place = PlaceInfo({'name': {k.replace('_', ':'): v for k, v in kwargs.items()},
'country_code': 'de', 'rank_address': 30})
'country_code': 'de', 'rank_address': 30})
sanitizer_args = {
'step': 'delete-tags',
@@ -281,11 +264,10 @@ class TestAllParameters:
}
name, _ = PlaceSanitizer([sanitizer_args],
self.config).process_names(place)
self.config).process_names(place)
return sorted([(p.name, p.kind, p.suffix or '') for p in name])
def test_string_arguments_pass(self):
res = self.run_sanitizer_on('de', '25-30', r'[\s\S]*',
name='foo', ref='foo', name_abc='bar', ref_abc='baz')

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for sanitizer configuration helper functions.
@@ -12,6 +12,7 @@ import pytest
from nominatim_db.errors import UsageError
from nominatim_db.tokenizer.sanitizers.config import SanitizerConfig
def test_string_list_default_empty():
assert SanitizerConfig().get_string_list('op') == []
@@ -53,7 +54,7 @@ def test_create_split_regex_no_params_unsplit(inp):
('ying;;yang', ['ying', 'yang']),
(';a; ;c;d,', ['', 'a', '', 'c', 'd', '']),
('1, 3 ,5', ['1', '3', '5'])
])
])
def test_create_split_regex_no_params_split(inp, outp):
regex = SanitizerConfig().get_delimiter()
@@ -70,7 +71,7 @@ def test_create_split_regex_custom(delimiter):
def test_create_split_regex_empty_delimiter():
with pytest.raises(UsageError):
regex = SanitizerConfig({'delimiters': ''}).get_delimiter()
SanitizerConfig({'delimiters': ''}).get_delimiter()
@pytest.mark.parametrize('inp', ('name', 'name:de', 'na\\me', '.*', ''))
@@ -96,12 +97,12 @@ def test_create_name_filter_no_param_default_fail_all(inp):
def test_create_name_filter_no_param_default_invalid_string():
with pytest.raises(ValueError):
filt = SanitizerConfig().get_filter('name', 'abc')
SanitizerConfig().get_filter('name', 'abc')
def test_create_name_filter_no_param_default_empty_list():
with pytest.raises(ValueError):
filt = SanitizerConfig().get_filter('name', [])
SanitizerConfig().get_filter('name', [])
@pytest.mark.parametrize('kind', ('de', 'name:de', 'ende'))
@@ -121,7 +122,7 @@ def test_create_kind_filter_default_negetive(kind):
@pytest.mark.parametrize('kind', ('lang', 'lang:de', 'langxx'))
def test_create_kind_filter_custom_regex_positive(kind):
filt = SanitizerConfig({'filter-kind': 'lang.*'}
).get_filter('filter-kind', ['.*fr'])
).get_filter('filter-kind', ['.*fr'])
assert filt(kind)
@@ -136,7 +137,7 @@ def test_create_kind_filter_custom_regex_negative(kind):
@pytest.mark.parametrize('kind', ('name', 'fr', 'name:fr', 'frfr', '34'))
def test_create_kind_filter_many_positive(kind):
filt = SanitizerConfig({'filter-kind': ['.*fr', 'name', r'\d+']}
).get_filter('filter-kind')
).get_filter('filter-kind')
assert filt(kind)
@@ -144,6 +145,6 @@ def test_create_kind_filter_many_positive(kind):
@pytest.mark.parametrize('kind', ('name:de', 'fridge', 'a34', '.*', '\\'))
def test_create_kind_filter_many_negative(kind):
filt = SanitizerConfig({'filter-kind': ['.*fr', 'name', r'\d+']}
).get_filter('filter-kind')
).get_filter('filter-kind')
assert not filt(kind)

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for the sanitizer that splits multivalue lists.
@@ -14,20 +14,19 @@ from nominatim_db.data.place_info import PlaceInfo
from nominatim_db.errors import UsageError
class TestSplitName:
@pytest.fixture(autouse=True)
def setup_country(self, def_config):
self.config = def_config
def run_sanitizer_on(self, **kwargs):
place = PlaceInfo({'name': kwargs})
name, _ = PlaceSanitizer([{'step': 'split-name-list'}], self.config).process_names(place)
return sorted([(p.name, p.kind, p.suffix) for p in name])
def sanitize_with_delimiter(self, delimiter, name):
place = PlaceInfo({'name': {'name': name}})
san = PlaceSanitizer([{'step': 'split-name-list', 'delimiters': delimiter}],
@@ -36,12 +35,10 @@ class TestSplitName:
return sorted([p.name for p in name])
def test_simple(self):
assert self.run_sanitizer_on(name='ABC') == [('ABC', 'name', None)]
assert self.run_sanitizer_on(name='') == [('', 'name', None)]
def test_splits(self):
assert self.run_sanitizer_on(name='A;B;C') == [('A', 'name', None),
('B', 'name', None),
@@ -49,7 +46,6 @@ class TestSplitName:
assert self.run_sanitizer_on(short_name=' House, boat ') == [('House', 'short_name', None),
('boat', 'short_name', None)]
def test_empty_fields(self):
assert self.run_sanitizer_on(name='A;;B') == [('A', 'name', None),
('B', 'name', None)]
@@ -58,14 +54,12 @@ class TestSplitName:
assert self.run_sanitizer_on(name=' ;B') == [('B', 'name', None)]
assert self.run_sanitizer_on(name='B,') == [('B', 'name', None)]
def test_custom_delimiters(self):
assert self.sanitize_with_delimiter(':', '12:45,3') == ['12', '45,3']
assert self.sanitize_with_delimiter('\\', 'a;\\b!#@ \\') == ['a;', 'b!#@']
assert self.sanitize_with_delimiter('[]', 'foo[to]be') == ['be', 'foo', 'to']
assert self.sanitize_with_delimiter(' ', 'morning sun') == ['morning', 'sun']
def test_empty_delimiter_set(self):
with pytest.raises(UsageError):
self.sanitize_with_delimiter('', 'abc')

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for the sanitizer that handles braced suffixes.
@@ -12,6 +12,7 @@ import pytest
from nominatim_db.tokenizer.place_sanitizer import PlaceSanitizer
from nominatim_db.data.place_info import PlaceInfo
class TestStripBrace:
@pytest.fixture(autouse=True)
@@ -24,23 +25,19 @@ class TestStripBrace:
return sorted([(p.name, p.kind, p.suffix) for p in name])
def test_no_braces(self):
assert self.run_sanitizer_on(name='foo', ref='23') == [('23', 'ref', None),
('foo', 'name', None)]
def test_simple_braces(self):
assert self.run_sanitizer_on(name='Halle (Saale)', ref='3')\
== [('3', 'ref', None), ('Halle', 'name', None), ('Halle (Saale)', 'name', None)]
assert self.run_sanitizer_on(name='ack ( bar')\
== [('ack', 'name', None), ('ack ( bar', 'name', None)]
assert self.run_sanitizer_on(name='Halle (Saale)', ref='3') \
== [('3', 'ref', None), ('Halle', 'name', None), ('Halle (Saale)', 'name', None)]
assert self.run_sanitizer_on(name='ack ( bar') \
== [('ack', 'name', None), ('ack ( bar', 'name', None)]
def test_only_braces(self):
assert self.run_sanitizer_on(name='(maybe)') == [('(maybe)', 'name', None)]
def test_double_braces(self):
assert self.run_sanitizer_on(name='a((b))') == [('a', 'name', None),
('a((b))', 'name', None)]

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for the sanitizer that enables language-dependent analyzers.
@@ -13,13 +13,13 @@ from nominatim_db.data.place_info import PlaceInfo
from nominatim_db.tokenizer.place_sanitizer import PlaceSanitizer
from nominatim_db.data.country_info import setup_country_config
class TestWithDefaults:
@pytest.fixture(autouse=True)
def setup_country(self, def_config):
self.config = def_config
def run_sanitizer_on(self, country, **kwargs):
place = PlaceInfo({'name': {k.replace('_', ':'): v for k, v in kwargs.items()},
'country_code': country})
@@ -28,19 +28,16 @@ class TestWithDefaults:
return sorted([(p.name, p.kind, p.suffix, p.attr) for p in name])
def test_no_names(self):
assert self.run_sanitizer_on('de') == []
def test_simple(self):
res = self.run_sanitizer_on('fr', name='Foo',name_de='Zoo', ref_abc='M')
res = self.run_sanitizer_on('fr', name='Foo', name_de='Zoo', ref_abc='M')
assert res == [('Foo', 'name', None, {}),
('M', 'ref', 'abc', {'analyzer': 'abc'}),
('Zoo', 'name', 'de', {'analyzer': 'de'})]
@pytest.mark.parametrize('suffix', ['DE', 'asbc'])
def test_illegal_suffix(self, suffix):
assert self.run_sanitizer_on('fr', **{'name_' + suffix: 'Foo'}) \
@@ -53,7 +50,6 @@ class TestFilterKind:
def setup_country(self, def_config):
self.config = def_config
def run_sanitizer_on(self, filt, **kwargs):
place = PlaceInfo({'name': {k.replace('_', ':'): v for k, v in kwargs.items()},
'country_code': 'de'})
@@ -63,17 +59,15 @@ class TestFilterKind:
return sorted([(p.name, p.kind, p.suffix, p.attr) for p in name])
def test_single_exact_name(self):
res = self.run_sanitizer_on(['name'], name_fr='A', ref_fr='12',
shortname_fr='C', name='D')
shortname_fr='C', name='D')
assert res == [('12', 'ref', 'fr', {}),
('A', 'name', 'fr', {'analyzer': 'fr'}),
('C', 'shortname', 'fr', {}),
('D', 'name', None, {})]
def test_single_pattern(self):
res = self.run_sanitizer_on(['.*name'],
name_fr='A', ref_fr='12', namexx_fr='B',
@@ -85,7 +79,6 @@ class TestFilterKind:
('C', 'shortname', 'fr', {'analyzer': 'fr'}),
('D', 'name', None, {})]
def test_multiple_patterns(self):
res = self.run_sanitizer_on(['.*name', 'ref'],
name_fr='A', ref_fr='12', oldref_fr='X',
@@ -106,7 +99,6 @@ class TestDefaultCountry:
setup_country_config(def_config)
self.config = def_config
def run_sanitizer_append(self, mode, country, **kwargs):
place = PlaceInfo({'name': {k.replace('_', ':'): v for k, v in kwargs.items()},
'country_code': country})
@@ -122,7 +114,6 @@ class TestDefaultCountry:
return sorted([(p.name, p.attr.get('analyzer', '')) for p in name])
def run_sanitizer_replace(self, mode, country, **kwargs):
place = PlaceInfo({'name': {k.replace('_', ':'): v for k, v in kwargs.items()},
'country_code': country})
@@ -138,7 +129,6 @@ class TestDefaultCountry:
return sorted([(p.name, p.attr.get('analyzer', '')) for p in name])
def test_missing_country(self):
place = PlaceInfo({'name': {'name': 'something'}})
name, _ = PlaceSanitizer([{'step': 'tag-analyzer-by-language',
@@ -151,59 +141,50 @@ class TestDefaultCountry:
assert name[0].suffix is None
assert 'analyzer' not in name[0].attr
def test_mono_unknown_country(self):
expect = [('XX', '')]
assert self.run_sanitizer_replace('mono', 'xx', name='XX') == expect
assert self.run_sanitizer_append('mono', 'xx', name='XX') == expect
def test_mono_monoling_replace(self):
res = self.run_sanitizer_replace('mono', 'de', name='Foo')
assert res == [('Foo', 'de')]
def test_mono_monoling_append(self):
res = self.run_sanitizer_append('mono', 'de', name='Foo')
assert res == [('Foo', ''), ('Foo', 'de')]
def test_mono_multiling(self):
expect = [('XX', '')]
assert self.run_sanitizer_replace('mono', 'ch', name='XX') == expect
assert self.run_sanitizer_append('mono', 'ch', name='XX') == expect
def test_all_unknown_country(self):
expect = [('XX', '')]
assert self.run_sanitizer_replace('all', 'xx', name='XX') == expect
assert self.run_sanitizer_append('all', 'xx', name='XX') == expect
def test_all_monoling_replace(self):
res = self.run_sanitizer_replace('all', 'de', name='Foo')
assert res == [('Foo', 'de')]
def test_all_monoling_append(self):
res = self.run_sanitizer_append('all', 'de', name='Foo')
assert res == [('Foo', ''), ('Foo', 'de')]
def test_all_multiling_append(self):
res = self.run_sanitizer_append('all', 'ch', name='XX')
assert res == [('XX', ''),
('XX', 'de'), ('XX', 'fr'), ('XX', 'it'), ('XX', 'rm')]
def test_all_multiling_replace(self):
res = self.run_sanitizer_replace('all', 'ch', name='XX')
@@ -216,7 +197,6 @@ class TestCountryWithWhitelist:
def setup_country(self, def_config):
self.config = def_config
def run_sanitizer_on(self, mode, country, **kwargs):
place = PlaceInfo({'name': {k.replace('_', ':'): v for k, v in kwargs.items()},
'country_code': country})
@@ -233,21 +213,17 @@ class TestCountryWithWhitelist:
return sorted([(p.name, p.attr.get('analyzer', '')) for p in name])
def test_mono_monoling(self):
assert self.run_sanitizer_on('mono', 'de', name='Foo') == [('Foo', 'de')]
assert self.run_sanitizer_on('mono', 'pt', name='Foo') == [('Foo', '')]
def test_mono_multiling(self):
assert self.run_sanitizer_on('mono', 'ca', name='Foo') == [('Foo', '')]
def test_all_monoling(self):
assert self.run_sanitizer_on('all', 'de', name='Foo') == [('Foo', 'de')]
assert self.run_sanitizer_on('all', 'pt', name='Foo') == [('Foo', '')]
def test_all_multiling(self):
assert self.run_sanitizer_on('all', 'ca', name='Foo') == [('Foo', 'fr')]
assert self.run_sanitizer_on('all', 'ch', name='Foo') \
@@ -260,7 +236,6 @@ class TestWhiteList:
def setup_country(self, def_config):
self.config = def_config
def run_sanitizer_on(self, whitelist, **kwargs):
place = PlaceInfo({'name': {k.replace('_', ':'): v for k, v in kwargs.items()}})
name, _ = PlaceSanitizer([{'step': 'tag-analyzer-by-language',
@@ -275,14 +250,11 @@ class TestWhiteList:
return sorted([(p.name, p.attr.get('analyzer', '')) for p in name])
def test_in_whitelist(self):
assert self.run_sanitizer_on(['de', 'xx'], ref_xx='123') == [('123', 'xx')]
def test_not_in_whitelist(self):
assert self.run_sanitizer_on(['de', 'xx'], ref_yy='123') == [('123', '')]
def test_empty_whitelist(self):
assert self.run_sanitizer_on([], ref_yy='123') == [('123', '')]

View File

@@ -2,86 +2,86 @@
#
# 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.
from typing import Mapping, Optional, List
import pytest
from nominatim_db.data.place_info import PlaceInfo
from nominatim_db.data.place_name import PlaceName
from nominatim_db.tokenizer.place_sanitizer import PlaceSanitizer
class TestTagJapanese:
@pytest.fixture(autouse=True)
def setup_country(self, def_config):
self.config = def_config
def run_sanitizer_on(self,type, **kwargs):
def run_sanitizer_on(self, type, **kwargs):
place = PlaceInfo({
'address': kwargs,
'country_code': 'jp'
})
sanitizer_args = {'step': 'tag-japanese'}
_, address = PlaceSanitizer([sanitizer_args], self.config).process_names(place)
tmp_list = [(p.name,p.kind) for p in address]
tmp_list = [(p.name, p.kind) for p in address]
return sorted(tmp_list)
def test_on_address(self):
res = self.run_sanitizer_on('address', name='foo', ref='bar', ref_abc='baz')
assert res == [('bar','ref'),('baz','ref_abc'),('foo','name')]
assert res == [('bar', 'ref'), ('baz', 'ref_abc'), ('foo', 'name')]
def test_housenumber(self):
res = self.run_sanitizer_on('address', housenumber='2')
assert res == [('2','housenumber')]
assert res == [('2', 'housenumber')]
def test_blocknumber(self):
res = self.run_sanitizer_on('address', block_number='6')
assert res == [('6','housenumber')]
assert res == [('6', 'housenumber')]
def test_neighbourhood(self):
res = self.run_sanitizer_on('address', neighbourhood='8')
assert res == [('8','place')]
assert res == [('8', 'place')]
def test_quarter(self):
res = self.run_sanitizer_on('address', quarter='kase')
assert res==[('kase','place')]
assert res == [('kase', 'place')]
def test_housenumber_blocknumber(self):
res = self.run_sanitizer_on('address', housenumber='2', block_number='6')
assert res == [('6-2','housenumber')]
assert res == [('6-2', 'housenumber')]
def test_quarter_neighbourhood(self):
res = self.run_sanitizer_on('address', quarter='kase', neighbourhood='8')
assert res == [('kase8','place')]
assert res == [('kase8', 'place')]
def test_blocknumber_housenumber_quarter(self):
res = self.run_sanitizer_on('address', block_number='6', housenumber='2', quarter='kase')
assert res == [('6-2','housenumber'),('kase','place')]
assert res == [('6-2', 'housenumber'), ('kase', 'place')]
def test_blocknumber_housenumber_quarter_neighbourhood(self):
res = self.run_sanitizer_on('address', block_number='6', housenumber='2', neighbourhood='8')
assert res == [('6-2','housenumber'),('8','place')]
assert res == [('6-2', 'housenumber'), ('8', 'place')]
def test_blocknumber_quarter_neighbourhood(self):
res = self.run_sanitizer_on('address',block_number='6', quarter='kase', neighbourhood='8')
assert res == [('6','housenumber'),('kase8','place')]
res = self.run_sanitizer_on('address', block_number='6', quarter='kase', neighbourhood='8')
assert res == [('6', 'housenumber'), ('kase8', 'place')]
def test_blocknumber_quarter(self):
res = self.run_sanitizer_on('address',block_number='6', quarter='kase')
assert res == [('6','housenumber'),('kase','place')]
res = self.run_sanitizer_on('address', block_number='6', quarter='kase')
assert res == [('6', 'housenumber'), ('kase', 'place')]
def test_blocknumber_neighbourhood(self):
res = self.run_sanitizer_on('address',block_number='6', neighbourhood='8')
assert res == [('6','housenumber'),('8','place')]
res = self.run_sanitizer_on('address', block_number='6', neighbourhood='8')
assert res == [('6', 'housenumber'), ('8', 'place')]
def test_housenumber_quarter_neighbourhood(self):
res = self.run_sanitizer_on('address',housenumber='2', quarter='kase', neighbourhood='8')
assert res == [('2','housenumber'),('kase8','place')]
res = self.run_sanitizer_on('address', housenumber='2', quarter='kase', neighbourhood='8')
assert res == [('2', 'housenumber'), ('kase8', 'place')]
def test_housenumber_quarter(self):
res = self.run_sanitizer_on('address',housenumber='2', quarter='kase')
assert res == [('2','housenumber'),('kase','place')]
res = self.run_sanitizer_on('address', housenumber='2', quarter='kase')
assert res == [('2', 'housenumber'), ('kase', 'place')]
def test_housenumber_blocknumber_neighbourhood_quarter(self):
res = self.run_sanitizer_on('address', block_number='6', housenumber='2', quarter='kase', neighbourhood='8')
assert res == [('6-2','housenumber'),('kase8','place')]
res = self.run_sanitizer_on('address', block_number='6', housenumber='2',
quarter='kase', neighbourhood='8')
assert res == [('6-2', 'housenumber'), ('kase8', 'place')]

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for creating new tokenizers.
@@ -27,7 +27,6 @@ class TestFactory:
def init_env(self, project_env, property_table, tokenizer_mock):
self.config = project_env
def test_setup_dummy_tokenizer(self, temp_db_conn):
tokenizer = factory.create_tokenizer(self.config)
@@ -37,7 +36,6 @@ class TestFactory:
assert properties.get_property(temp_db_conn, 'tokenizer') == 'dummy'
def test_setup_tokenizer_dir_exists(self):
(self.config.project_dir / 'tokenizer').mkdir()
@@ -46,14 +44,12 @@ class TestFactory:
assert isinstance(tokenizer, DummyTokenizer)
assert tokenizer.init_state == "new"
def test_setup_tokenizer_dir_failure(self):
(self.config.project_dir / 'tokenizer').write_text("foo")
with pytest.raises(UsageError):
factory.create_tokenizer(self.config)
def test_load_tokenizer(self):
factory.create_tokenizer(self.config)
@@ -62,7 +58,6 @@ class TestFactory:
assert isinstance(tokenizer, DummyTokenizer)
assert tokenizer.init_state == "loaded"
def test_load_repopulate_tokenizer_dir(self):
factory.create_tokenizer(self.config)
@@ -71,7 +66,6 @@ class TestFactory:
factory.get_tokenizer_for_db(self.config)
assert (self.config.project_dir / 'tokenizer').exists()
def test_load_missing_property(self, temp_db_cursor):
factory.create_tokenizer(self.config)

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for ICU tokenizer.
@@ -20,6 +20,7 @@ from nominatim_db.data.place_info import PlaceInfo
from mock_icu_word_table import MockIcuWordTable
@pytest.fixture
def word_table(temp_db_conn):
return MockIcuWordTable(temp_db_conn)
@@ -89,6 +90,7 @@ def analyzer(tokenizer_factory, test_config, monkeypatch,
return _mk_analyser
@pytest.fixture
def sql_functions(temp_db_conn, def_config, src_dir):
orig_sql = def_config.lib_dir.sql
@@ -152,19 +154,19 @@ LANGUAGE plpgsql;
""")
def test_init_new(tokenizer_factory, test_config, db_prop):
tok = tokenizer_factory()
tok.init_new_db(test_config)
assert db_prop(nominatim_db.tokenizer.icu_rule_loader.DBCFG_IMPORT_NORM_RULES) \
.startswith(':: lower ();')
prop = db_prop(nominatim_db.tokenizer.icu_rule_loader.DBCFG_IMPORT_NORM_RULES)
assert prop.startswith(':: lower ();')
def test_init_word_table(tokenizer_factory, test_config, place_row, temp_db_cursor):
place_row(names={'name' : 'Test Area', 'ref' : '52'})
place_row(names={'name' : 'No Area'})
place_row(names={'name' : 'Holzstrasse'})
place_row(names={'name': 'Test Area', 'ref': '52'})
place_row(names={'name': 'No Area'})
place_row(names={'name': 'Holzstrasse'})
tok = tokenizer_factory()
tok.init_new_db(test_config)
@@ -259,12 +261,10 @@ class TestPostcodes:
self.analyzer = anl
yield anl
def process_postcode(self, cc, postcode):
return self.analyzer.process_place(PlaceInfo({'country_code': cc,
'address': {'postcode': postcode}}))
def test_update_postcodes_deleted(self, word_table):
word_table.add_postcode(' 1234', '1234')
word_table.add_postcode(' 5678', '5678')
@@ -273,20 +273,17 @@ class TestPostcodes:
assert word_table.count() == 0
def test_process_place_postcode_simple(self, word_table):
info = self.process_postcode('de', '12345')
assert info['postcode'] == '12345'
def test_process_place_postcode_with_space(self, word_table):
info = self.process_postcode('in', '123 567')
assert info['postcode'] == '123567'
def test_update_special_phrase_empty_table(analyzer, word_table):
with analyzer() as anl:
anl.update_special_phrases([
@@ -296,9 +293,9 @@ def test_update_special_phrase_empty_table(analyzer, word_table):
], True)
assert word_table.get_special() \
== {('KÖNIG BEI', 'König bei', 'amenity', 'royal', 'near'),
('KÖNIGE', 'Könige', 'amenity', 'royal', None),
('STREET', 'street', 'highway', 'primary', 'in')}
== {('KÖNIG BEI', 'König bei', 'amenity', 'royal', 'near'),
('KÖNIGE', 'Könige', 'amenity', 'royal', None),
('STREET', 'street', 'highway', 'primary', 'in')}
def test_update_special_phrase_delete_all(analyzer, word_table):
@@ -339,9 +336,9 @@ def test_update_special_phrase_modify(analyzer, word_table):
], True)
assert word_table.get_special() \
== {('PRISON', 'prison', 'amenity', 'prison', 'in'),
('BAR', 'bar', 'highway', 'road', None),
('GARDEN', 'garden', 'leisure', 'garden', 'near')}
== {('PRISON', 'prison', 'amenity', 'prison', 'in'),
('BAR', 'bar', 'highway', 'road', None),
('GARDEN', 'garden', 'leisure', 'garden', 'near')}
def test_add_country_names_new(analyzer, word_table):
@@ -370,7 +367,6 @@ class TestPlaceNames:
self.analyzer = anl
yield anl
def expect_name_terms(self, info, *expected_terms):
tokens = self.analyzer.get_word_token_info(expected_terms)
for token in tokens:
@@ -378,34 +374,29 @@ class TestPlaceNames:
assert eval(info['names']) == set((t[2] for t in tokens))
def process_named_place(self, names):
return self.analyzer.process_place(PlaceInfo({'name': names}))
def test_simple_names(self):
info = self.process_named_place({'name': 'Soft bAr', 'ref': '34'})
self.expect_name_terms(info, '#Soft bAr', '#34', 'Soft', 'bAr', '34')
@pytest.mark.parametrize('sep', [',' , ';'])
@pytest.mark.parametrize('sep', [',', ';'])
def test_names_with_separator(self, sep):
info = self.process_named_place({'name': sep.join(('New York', 'Big Apple'))})
self.expect_name_terms(info, '#New York', '#Big Apple',
'new', 'york', 'big', 'apple')
def test_full_names_with_bracket(self):
info = self.process_named_place({'name': 'Houseboat (left)'})
self.expect_name_terms(info, '#Houseboat (left)', '#Houseboat',
'houseboat', 'left')
def test_country_name(self, word_table):
place = PlaceInfo({'name' : {'name': 'Norge'},
place = PlaceInfo({'name': {'name': 'Norge'},
'country_code': 'no',
'rank_address': 4,
'class': 'boundary',
@@ -427,18 +418,15 @@ class TestPlaceAddress:
self.analyzer = anl
yield anl
@pytest.fixture
def getorcreate_hnr_id(self, temp_db_cursor):
temp_db_cursor.execute("""CREATE OR REPLACE FUNCTION getorcreate_hnr_id(lookup_term TEXT)
RETURNS INTEGER AS $$
SELECT -nextval('seq_word')::INTEGER; $$ LANGUAGE SQL""")
def process_address(self, **kwargs):
return self.analyzer.process_place(PlaceInfo({'address': kwargs}))
def name_token_set(self, *expected_terms):
tokens = self.analyzer.get_word_token_info(expected_terms)
for token in tokens:
@@ -446,14 +434,12 @@ class TestPlaceAddress:
return set((t[2] for t in tokens))
@pytest.mark.parametrize('pcode', ['12345', 'AB 123', '34-345'])
def test_process_place_postcode(self, word_table, pcode):
info = self.process_address(postcode=pcode)
assert info['postcode'] == pcode
@pytest.mark.parametrize('hnr', ['123a', '1', '101'])
def test_process_place_housenumbers_simple(self, hnr, getorcreate_hnr_id):
info = self.process_address(housenumber=hnr)
@@ -461,7 +447,6 @@ class TestPlaceAddress:
assert info['hnr'] == hnr.upper()
assert info['hnr_tokens'] == "{-1}"
def test_process_place_housenumbers_duplicates(self, getorcreate_hnr_id):
info = self.process_address(housenumber='134',
conscriptionnumber='134',
@@ -470,7 +455,6 @@ class TestPlaceAddress:
assert set(info['hnr'].split(';')) == set(('134', '99A'))
assert info['hnr_tokens'] == "{-1,-2}"
def test_process_place_housenumbers_cached(self, getorcreate_hnr_id):
info = self.process_address(housenumber="45")
assert info['hnr_tokens'] == "{-1}"
@@ -484,37 +468,32 @@ class TestPlaceAddress:
info = self.process_address(housenumber="41")
assert eval(info['hnr_tokens']) == {-3}
def test_process_place_street(self):
self.analyzer.process_place(PlaceInfo({'name': {'name' : 'Grand Road'}}))
self.analyzer.process_place(PlaceInfo({'name': {'name': 'Grand Road'}}))
info = self.process_address(street='Grand Road')
assert eval(info['street']) == self.name_token_set('#Grand Road')
def test_process_place_nonexisting_street(self):
info = self.process_address(street='Grand Road')
assert info['street'] == '{}'
def test_process_place_multiple_street_tags(self):
self.analyzer.process_place(PlaceInfo({'name': {'name' : 'Grand Road',
self.analyzer.process_place(PlaceInfo({'name': {'name': 'Grand Road',
'ref': '05989'}}))
info = self.process_address(**{'street': 'Grand Road',
'street:sym_ul': '05989'})
'street:sym_ul': '05989'})
assert eval(info['street']) == self.name_token_set('#Grand Road', '#05989')
def test_process_place_street_empty(self):
info = self.process_address(street='🜵')
assert info['street'] == '{}'
def test_process_place_street_from_cache(self):
self.analyzer.process_place(PlaceInfo({'name': {'name' : 'Grand Road'}}))
self.analyzer.process_place(PlaceInfo({'name': {'name': 'Grand Road'}}))
self.process_address(street='Grand Road')
# request address again
@@ -522,25 +501,21 @@ class TestPlaceAddress:
assert eval(info['street']) == self.name_token_set('#Grand Road')
def test_process_place_place(self):
info = self.process_address(place='Honu Lulu')
assert eval(info['place']) == self.name_token_set('HONU', 'LULU', '#HONU LULU')
def test_process_place_place_extra(self):
info = self.process_address(**{'place:en': 'Honu Lulu'})
assert 'place' not in info
def test_process_place_place_empty(self):
info = self.process_address(place='🜵')
assert 'place' not in info
def test_process_place_address_terms(self):
info = self.process_address(country='de', city='Zwickau', state='Sachsen',
suburb='Zwickau', street='Hauptstr',
@@ -549,19 +524,17 @@ class TestPlaceAddress:
city = self.name_token_set('ZWICKAU', '#ZWICKAU')
state = self.name_token_set('SACHSEN', '#SACHSEN')
result = {k: eval(v) for k,v in info['addr'].items()}
result = {k: eval(v) for k, v in info['addr'].items()}
assert result == {'city': city, 'suburb': city, 'state': state}
def test_process_place_multiple_address_terms(self):
info = self.process_address(**{'city': 'Bruxelles', 'city:de': 'Brüssel'})
result = {k: eval(v) for k,v in info['addr'].items()}
result = {k: eval(v) for k, v in info['addr'].items()}
assert result == {'city': self.name_token_set('Bruxelles', '#Bruxelles')}
def test_process_place_address_terms_empty(self):
info = self.process_address(country='de', city=' ', street='Hauptstr',
full='right behind the church')
@@ -575,22 +548,21 @@ class TestPlaceHousenumberWithAnalyser:
def setup(self, analyzer, sql_functions):
hnr = {'step': 'clean-housenumbers',
'filter-kind': ['housenumber', 'conscriptionnumber', 'streetnumber']}
with analyzer(trans=(":: upper()", "'🜵' > ' '"), sanitizers=[hnr], with_housenumber=True) as anl:
with analyzer(trans=(":: upper()", "'🜵' > ' '"), sanitizers=[hnr],
with_housenumber=True) as anl:
self.analyzer = anl
yield anl
@pytest.fixture
def getorcreate_hnr_id(self, temp_db_cursor):
temp_db_cursor.execute("""CREATE OR REPLACE FUNCTION create_analyzed_hnr_id(norm_term TEXT, lookup_terms TEXT[])
RETURNS INTEGER AS $$
SELECT -nextval('seq_word')::INTEGER; $$ LANGUAGE SQL""")
temp_db_cursor.execute("""
CREATE OR REPLACE FUNCTION create_analyzed_hnr_id(norm_term TEXT, lookup_terms TEXT[])
RETURNS INTEGER AS $$
SELECT -nextval('seq_word')::INTEGER; $$ LANGUAGE SQL""")
def process_address(self, **kwargs):
return self.analyzer.process_place(PlaceInfo({'address': kwargs}))
def name_token_set(self, *expected_terms):
tokens = self.analyzer.get_word_token_info(expected_terms)
for token in tokens:
@@ -598,7 +570,6 @@ class TestPlaceHousenumberWithAnalyser:
return set((t[2] for t in tokens))
@pytest.mark.parametrize('hnr', ['123 a', '1', '101'])
def test_process_place_housenumbers_simple(self, hnr, getorcreate_hnr_id):
info = self.process_address(housenumber=hnr)
@@ -606,7 +577,6 @@ class TestPlaceHousenumberWithAnalyser:
assert info['hnr'] == hnr.upper()
assert info['hnr_tokens'] == "{-1}"
def test_process_place_housenumbers_duplicates(self, getorcreate_hnr_id):
info = self.process_address(housenumber='134',
conscriptionnumber='134',
@@ -615,7 +585,6 @@ class TestPlaceHousenumberWithAnalyser:
assert set(info['hnr'].split(';')) == set(('134', '99 A'))
assert info['hnr_tokens'] == "{-1,-2}"
def test_process_place_housenumbers_cached(self, getorcreate_hnr_id):
info = self.process_address(housenumber="45")
assert info['hnr_tokens'] == "{-1}"
@@ -637,7 +606,6 @@ class TestUpdateWordTokens:
table_factory('search_name', 'place_id BIGINT, name_vector INT[]')
self.tok = tokenizer_factory()
@pytest.fixture
def search_entry(self, temp_db_cursor):
place_id = itertools.count(1000)
@@ -648,7 +616,6 @@ class TestUpdateWordTokens:
return _insert
@pytest.fixture(params=['simple', 'analyzed'])
def add_housenumber(self, request, word_table):
if request.param == 'simple':
@@ -660,7 +627,6 @@ class TestUpdateWordTokens:
return _make
@pytest.mark.parametrize('hnr', ('1a', '1234567', '34 5'))
def test_remove_unused_housenumbers(self, add_housenumber, word_table, hnr):
word_table.add_housenumber(1000, hnr)
@@ -669,7 +635,6 @@ class TestUpdateWordTokens:
self.tok.update_word_tokens()
assert word_table.count_housenumbers() == 0
def test_keep_unused_numeral_housenumbers(self, add_housenumber, word_table):
add_housenumber(1000, '5432')
@@ -677,8 +642,8 @@ class TestUpdateWordTokens:
self.tok.update_word_tokens()
assert word_table.count_housenumbers() == 1
def test_keep_housenumbers_from_search_name_table(self, add_housenumber, word_table, search_entry):
def test_keep_housenumbers_from_search_name_table(self, add_housenumber,
word_table, search_entry):
add_housenumber(9999, '5432a')
add_housenumber(9991, '9 a')
search_entry(123, 9999, 34)
@@ -687,8 +652,8 @@ class TestUpdateWordTokens:
self.tok.update_word_tokens()
assert word_table.count_housenumbers() == 1
def test_keep_housenumbers_from_placex_table(self, add_housenumber, word_table, placex_table):
def test_keep_housenumbers_from_placex_table(self, add_housenumber, word_table,
placex_table):
add_housenumber(9999, '5432a')
add_housenumber(9990, '34z')
placex_table.add(housenumber='34z')
@@ -698,8 +663,8 @@ class TestUpdateWordTokens:
self.tok.update_word_tokens()
assert word_table.count_housenumbers() == 1
def test_keep_housenumbers_from_placex_table_hnr_list(self, add_housenumber, word_table, placex_table):
def test_keep_housenumbers_from_placex_table_hnr_list(self, add_housenumber,
word_table, placex_table):
add_housenumber(9991, '9 b')
add_housenumber(9990, '34z')
placex_table.add(housenumber='9 a;9 b;9 c')

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for converting a config file to ICU rules.
@@ -19,17 +19,16 @@ from icu import Transliterator
CONFIG_SECTIONS = ('normalization', 'transliteration', 'token-analysis')
class TestIcuRuleLoader:
@pytest.fixture(autouse=True)
def init_env(self, project_env):
self.project_env = project_env
def write_config(self, content):
(self.project_env.project_dir / 'icu_tokenizer.yaml').write_text(dedent(content))
def config_rules(self, *variants):
content = dedent("""\
normalization:
@@ -49,14 +48,12 @@ class TestIcuRuleLoader:
content += '\n'.join((" - " + s for s in variants)) + '\n'
self.write_config(content)
def get_replacements(self, *variants):
self.config_rules(*variants)
loader = ICURuleLoader(self.project_env)
rules = loader.analysis[None].config['replacements']
return sorted((k, sorted(v)) for k,v in rules)
return sorted((k, sorted(v)) for k, v in rules)
def test_empty_rule_set(self):
self.write_config("""\
@@ -72,16 +69,14 @@ class TestIcuRuleLoader:
assert rules.get_normalization_rules() == ''
assert rules.get_transliteration_rules() == ''
@pytest.mark.parametrize("section", CONFIG_SECTIONS)
def test_missing_section(self, section):
rule_cfg = { s: [] for s in CONFIG_SECTIONS if s != section}
rule_cfg = {s: [] for s in CONFIG_SECTIONS if s != section}
self.write_config(yaml.dump(rule_cfg))
with pytest.raises(UsageError):
ICURuleLoader(self.project_env)
def test_get_search_rules(self):
self.config_rules()
loader = ICURuleLoader(self.project_env)
@@ -97,7 +92,6 @@ class TestIcuRuleLoader:
assert trans.transliterate(" Αθήνα ") == " athēna "
assert trans.transliterate(" проспект ") == " prospekt "
def test_get_normalization_rules(self):
self.config_rules()
loader = ICURuleLoader(self.project_env)
@@ -106,7 +100,6 @@ class TestIcuRuleLoader:
assert trans.transliterate(" проспект-Prospekt ") == " проспект prospekt "
def test_get_transliteration_rules(self):
self.config_rules()
loader = ICURuleLoader(self.project_env)
@@ -115,7 +108,6 @@ class TestIcuRuleLoader:
assert trans.transliterate(" проспект-Prospekt ") == " prospekt Prospekt "
def test_transliteration_rules_from_file(self):
self.write_config("""\
normalization:
@@ -135,7 +127,6 @@ class TestIcuRuleLoader:
assert trans.transliterate(" axxt ") == " byt "
def test_search_rules(self):
self.config_rules('~street => s,st', 'master => mstr')
proc = ICURuleLoader(self.project_env).make_token_analysis()
@@ -144,7 +135,6 @@ class TestIcuRuleLoader:
assert proc.search.transliterate('Earnes St').strip() == 'earnes st'
assert proc.search.transliterate('Nostreet').strip() == 'nostreet'
@pytest.mark.parametrize("variant", ['foo > bar', 'foo -> bar -> bar',
'~foo~ -> bar', 'fo~ o -> bar'])
def test_invalid_variant_description(self, variant):
@@ -157,25 +147,21 @@ class TestIcuRuleLoader:
assert repl == [(' foo ', [' bar', ' foo'])]
def test_replace_full(self):
repl = self.get_replacements("foo => bar")
assert repl == [(' foo ', [' bar'])]
def test_add_suffix_no_decompose(self):
repl = self.get_replacements("~berg |-> bg")
assert repl == [(' berg ', [' berg', ' bg']),
('berg ', ['berg', 'bg'])]
def test_replace_suffix_no_decompose(self):
repl = self.get_replacements("~berg |=> bg")
assert repl == [(' berg ', [' bg']),('berg ', ['bg'])]
assert repl == [(' berg ', [' bg']), ('berg ', ['bg'])]
def test_add_suffix_decompose(self):
repl = self.get_replacements("~berg -> bg")
@@ -183,26 +169,22 @@ class TestIcuRuleLoader:
assert repl == [(' berg ', [' berg', ' bg', 'berg', 'bg']),
('berg ', [' berg', ' bg', 'berg', 'bg'])]
def test_replace_suffix_decompose(self):
repl = self.get_replacements("~berg => bg")
assert repl == [(' berg ', [' bg', 'bg']),
('berg ', [' bg', 'bg'])]
def test_add_prefix_no_compose(self):
repl = self.get_replacements("hinter~ |-> hnt")
assert repl == [(' hinter', [' hinter', ' hnt']),
(' hinter ', [' hinter', ' hnt'])]
def test_replace_prefix_no_compose(self):
repl = self.get_replacements("hinter~ |=> hnt")
assert repl == [(' hinter', [' hnt']), (' hinter ', [' hnt'])]
assert repl == [(' hinter', [' hnt']), (' hinter ', [' hnt'])]
def test_add_prefix_compose(self):
repl = self.get_replacements("hinter~-> h")
@@ -210,45 +192,38 @@ class TestIcuRuleLoader:
assert repl == [(' hinter', [' h', ' h ', ' hinter', ' hinter ']),
(' hinter ', [' h', ' h', ' hinter', ' hinter'])]
def test_replace_prefix_compose(self):
repl = self.get_replacements("hinter~=> h")
assert repl == [(' hinter', [' h', ' h ']),
(' hinter ', [' h', ' h'])]
def test_add_beginning_only(self):
repl = self.get_replacements("^Premier -> Pr")
assert repl == [('^ premier ', ['^ pr', '^ premier'])]
def test_replace_beginning_only(self):
repl = self.get_replacements("^Premier => Pr")
assert repl == [('^ premier ', ['^ pr'])]
def test_add_final_only(self):
repl = self.get_replacements("road$ -> rd")
assert repl == [(' road ^', [' rd ^', ' road ^'])]
def test_replace_final_only(self):
repl = self.get_replacements("road$ => rd")
assert repl == [(' road ^', [' rd ^'])]
def test_decompose_only(self):
repl = self.get_replacements("~foo -> foo")
assert repl == [(' foo ', [' foo', 'foo']),
('foo ', [' foo', 'foo'])]
def test_add_suffix_decompose_end_only(self):
repl = self.get_replacements("~berg |-> bg", "~berg$ -> bg")
@@ -257,7 +232,6 @@ class TestIcuRuleLoader:
('berg ', ['berg', 'bg']),
('berg ^', [' berg ^', ' bg ^', 'berg ^', 'bg ^'])]
def test_replace_suffix_decompose_end_only(self):
repl = self.get_replacements("~berg |=> bg", "~berg$ => bg")
@@ -266,7 +240,6 @@ class TestIcuRuleLoader:
('berg ', ['bg']),
('berg ^', [' bg ^', 'bg ^'])]
def test_add_multiple_suffix(self):
repl = self.get_replacements("~berg,~burg -> bg")

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for execution of the sanitztion step.
@@ -50,13 +50,13 @@ def test_placeinfo_has_attr():
def test_sanitizer_default(def_config):
san = sanitizer.PlaceSanitizer([{'step': 'split-name-list'}], def_config)
name, address = san.process_names(PlaceInfo({'name': {'name:de:de': '1;2;3'},
'address': {'street': 'Bald'}}))
name, address = san.process_names(PlaceInfo({'name': {'name:de:de': '1;2;3'},
'address': {'street': 'Bald'}}))
assert len(name) == 3
assert all(isinstance(n, sanitizer.PlaceName) for n in name)
assert all(n.kind == 'name' for n in name)
assert all(n.suffix == 'de:de' for n in name)
assert all(n.kind == 'name' for n in name)
assert all(n.suffix == 'de:de' for n in name)
assert len(address) == 1
assert all(isinstance(n, sanitizer.PlaceName) for n in address)
@@ -66,7 +66,7 @@ def test_sanitizer_default(def_config):
def test_sanitizer_empty_list(def_config, rules):
san = sanitizer.PlaceSanitizer(rules, def_config)
name, address = san.process_names(PlaceInfo({'name': {'name:de:de': '1;2;3'}}))
name, address = san.process_names(PlaceInfo({'name': {'name:de:de': '1;2;3'}}))
assert len(name) == 1
assert all(isinstance(n, sanitizer.PlaceName) for n in name)
@@ -74,4 +74,4 @@ def test_sanitizer_empty_list(def_config, rules):
def test_sanitizer_missing_step_definition(def_config):
with pytest.raises(UsageError):
san = sanitizer.PlaceSanitizer([{'id': 'split-name-list'}], def_config)
sanitizer.PlaceSanitizer([{'id': 'split-name-list'}], def_config)

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for special postcode analysis and variant generation.
@@ -13,7 +13,6 @@ from icu import Transliterator
import nominatim_db.tokenizer.token_analysis.postcodes as module
from nominatim_db.data.place_name import PlaceName
from nominatim_db.errors import UsageError
DEFAULT_NORMALIZATION = """ :: NFD ();
'🜳' > ' ';
@@ -27,9 +26,10 @@ DEFAULT_TRANSLITERATION = """ :: Latin ();
'🜵' > ' ';
"""
@pytest.fixture
def analyser():
rules = { 'analyzer': 'postcodes'}
rules = {'analyzer': 'postcodes'}
config = module.configure(rules, DEFAULT_NORMALIZATION)
trans = Transliterator.createFromRules("test_trans", DEFAULT_TRANSLITERATION)

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for import name normalisation and variant generation.
@@ -26,8 +26,9 @@ DEFAULT_TRANSLITERATION = """ :: Latin ();
'🜵' > ' ';
"""
def make_analyser(*variants, variant_only=False):
rules = { 'analyzer': 'generic', 'variants': [{'words': variants}]}
rules = {'analyzer': 'generic', 'variants': [{'words': variants}]}
if variant_only:
rules['mode'] = 'variant-only'
trans = Transliterator.createFromRules("test_trans", DEFAULT_TRANSLITERATION)
@@ -43,7 +44,7 @@ def get_normalized_variants(proc, name):
def test_no_variants():
rules = { 'analyzer': 'generic' }
rules = {'analyzer': 'generic'}
trans = Transliterator.createFromRules("test_trans", DEFAULT_TRANSLITERATION)
norm = Transliterator.createFromRules("test_norm", DEFAULT_NORMALIZATION)
config = module.configure(rules, norm, trans)
@@ -62,35 +63,36 @@ def test_variants_empty():
VARIANT_TESTS = [
(('~strasse,~straße -> str', '~weg => weg'), "hallo", {'hallo'}),
(('weg => wg',), "holzweg", {'holzweg'}),
(('weg -> wg',), "holzweg", {'holzweg'}),
(('~weg => weg',), "holzweg", {'holz weg', 'holzweg'}),
(('~weg -> weg',), "holzweg", {'holz weg', 'holzweg'}),
(('~weg => w',), "holzweg", {'holz w', 'holzw'}),
(('~weg -> w',), "holzweg", {'holz weg', 'holzweg', 'holz w', 'holzw'}),
(('~weg => weg',), "Meier Weg", {'meier weg', 'meierweg'}),
(('~weg -> weg',), "Meier Weg", {'meier weg', 'meierweg'}),
(('~weg => w',), "Meier Weg", {'meier w', 'meierw'}),
(('~weg -> w',), "Meier Weg", {'meier weg', 'meierweg', 'meier w', 'meierw'}),
(('weg => wg',), "Meier Weg", {'meier wg'}),
(('weg -> wg',), "Meier Weg", {'meier weg', 'meier wg'}),
(('~strasse,~straße -> str', '~weg => weg'), "Bauwegstraße",
(('~strasse,~straße -> str', '~weg => weg'), "hallo", {'hallo'}),
(('weg => wg',), "holzweg", {'holzweg'}),
(('weg -> wg',), "holzweg", {'holzweg'}),
(('~weg => weg',), "holzweg", {'holz weg', 'holzweg'}),
(('~weg -> weg',), "holzweg", {'holz weg', 'holzweg'}),
(('~weg => w',), "holzweg", {'holz w', 'holzw'}),
(('~weg -> w',), "holzweg", {'holz weg', 'holzweg', 'holz w', 'holzw'}),
(('~weg => weg',), "Meier Weg", {'meier weg', 'meierweg'}),
(('~weg -> weg',), "Meier Weg", {'meier weg', 'meierweg'}),
(('~weg => w',), "Meier Weg", {'meier w', 'meierw'}),
(('~weg -> w',), "Meier Weg", {'meier weg', 'meierweg', 'meier w', 'meierw'}),
(('weg => wg',), "Meier Weg", {'meier wg'}),
(('weg -> wg',), "Meier Weg", {'meier weg', 'meier wg'}),
(('~strasse,~straße -> str', '~weg => weg'), "Bauwegstraße",
{'bauweg straße', 'bauweg str', 'bauwegstraße', 'bauwegstr'}),
(('am => a', 'bach => b'), "am bach", {'a b'}),
(('am => a', '~bach => b'), "am bach", {'a b'}),
(('am -> a', '~bach -> b'), "am bach", {'am bach', 'a bach', 'am b', 'a b'}),
(('am -> a', '~bach -> b'), "ambach", {'ambach', 'am bach', 'amb', 'am b'}),
(('saint -> s,st', 'street -> st'), "Saint Johns Street",
(('am => a', 'bach => b'), "am bach", {'a b'}),
(('am => a', '~bach => b'), "am bach", {'a b'}),
(('am -> a', '~bach -> b'), "am bach", {'am bach', 'a bach', 'am b', 'a b'}),
(('am -> a', '~bach -> b'), "ambach", {'ambach', 'am bach', 'amb', 'am b'}),
(('saint -> s,st', 'street -> st'), "Saint Johns Street",
{'saint johns street', 's johns street', 'st johns street',
'saint johns st', 's johns st', 'st johns st'}),
(('river$ -> r',), "River Bend Road", {'river bend road'}),
(('river$ -> r',), "Bent River", {'bent river', 'bent r'}),
(('^north => n',), "North 2nd Street", {'n 2nd street'}),
(('^north => n',), "Airport North", {'airport north'}),
(('am -> a',), "am am am am am am am am", {'am am am am am am am am'}),
(('am => a',), "am am am am am am am am", {'a a a a a a a a'})
]
(('river$ -> r',), "River Bend Road", {'river bend road'}),
(('river$ -> r',), "Bent River", {'bent river', 'bent r'}),
(('^north => n',), "North 2nd Street", {'n 2nd street'}),
(('^north => n',), "Airport North", {'airport north'}),
(('am -> a',), "am am am am am am am am", {'am am am am am am am am'}),
(('am => a',), "am am am am am am am am", {'a a a a a a a a'})
]
@pytest.mark.parametrize("rules,name,variants", VARIANT_TESTS)
def test_variants(rules, name, variants):
@@ -103,10 +105,11 @@ def test_variants(rules, name, variants):
VARIANT_ONLY_TESTS = [
(('weg => wg',), "hallo", set()),
(('weg => wg',), "Meier Weg", {'meier wg'}),
(('weg -> wg',), "Meier Weg", {'meier wg'}),
]
(('weg => wg',), "hallo", set()),
(('weg => wg',), "Meier Weg", {'meier wg'}),
(('weg -> wg',), "Meier Weg", {'meier wg'}),
]
@pytest.mark.parametrize("rules,name,variants", VARIANT_ONLY_TESTS)
def test_variants_only(rules, name, variants):
@@ -122,17 +125,15 @@ class TestGetReplacements:
@staticmethod
def configure_rules(*variants):
rules = { 'analyzer': 'generic', 'variants': [{'words': variants}]}
rules = {'analyzer': 'generic', 'variants': [{'words': variants}]}
trans = Transliterator.createFromRules("test_trans", DEFAULT_TRANSLITERATION)
norm = Transliterator.createFromRules("test_norm", DEFAULT_NORMALIZATION)
return module.configure(rules, norm, trans)
def get_replacements(self, *variants):
config = self.configure_rules(*variants)
return sorted((k, sorted(v)) for k,v in config['replacements'])
return sorted((k, sorted(v)) for k, v in config['replacements'])
@pytest.mark.parametrize("variant", ['foo > bar', 'foo -> bar -> bar',
'~foo~ -> bar', 'fo~ o -> bar'])
@@ -140,38 +141,32 @@ class TestGetReplacements:
with pytest.raises(UsageError):
self.configure_rules(variant)
@pytest.mark.parametrize("rule", ["!!! -> bar", "bar => !!!"])
def test_ignore_unnormalizable_terms(self, rule):
repl = self.get_replacements(rule)
assert repl == []
def test_add_full(self):
repl = self.get_replacements("foo -> bar")
assert repl == [(' foo ', [' bar', ' foo'])]
def test_replace_full(self):
repl = self.get_replacements("foo => bar")
assert repl == [(' foo ', [' bar'])]
def test_add_suffix_no_decompose(self):
repl = self.get_replacements("~berg |-> bg")
assert repl == [(' berg ', [' berg', ' bg']),
('berg ', ['berg', 'bg'])]
def test_replace_suffix_no_decompose(self):
repl = self.get_replacements("~berg |=> bg")
assert repl == [(' berg ', [' bg']),('berg ', ['bg'])]
assert repl == [(' berg ', [' bg']), ('berg ', ['bg'])]
def test_add_suffix_decompose(self):
repl = self.get_replacements("~berg -> bg")
@@ -179,26 +174,22 @@ class TestGetReplacements:
assert repl == [(' berg ', [' berg', ' bg', 'berg', 'bg']),
('berg ', [' berg', ' bg', 'berg', 'bg'])]
def test_replace_suffix_decompose(self):
repl = self.get_replacements("~berg => bg")
assert repl == [(' berg ', [' bg', 'bg']),
('berg ', [' bg', 'bg'])]
def test_add_prefix_no_compose(self):
repl = self.get_replacements("hinter~ |-> hnt")
assert repl == [(' hinter', [' hinter', ' hnt']),
(' hinter ', [' hinter', ' hnt'])]
def test_replace_prefix_no_compose(self):
repl = self.get_replacements("hinter~ |=> hnt")
assert repl == [(' hinter', [' hnt']), (' hinter ', [' hnt'])]
assert repl == [(' hinter', [' hnt']), (' hinter ', [' hnt'])]
def test_add_prefix_compose(self):
repl = self.get_replacements("hinter~-> h")
@@ -206,45 +197,38 @@ class TestGetReplacements:
assert repl == [(' hinter', [' h', ' h ', ' hinter', ' hinter ']),
(' hinter ', [' h', ' h', ' hinter', ' hinter'])]
def test_replace_prefix_compose(self):
repl = self.get_replacements("hinter~=> h")
assert repl == [(' hinter', [' h', ' h ']),
(' hinter ', [' h', ' h'])]
def test_add_beginning_only(self):
repl = self.get_replacements("^Premier -> Pr")
assert repl == [('^ premier ', ['^ pr', '^ premier'])]
def test_replace_beginning_only(self):
repl = self.get_replacements("^Premier => Pr")
assert repl == [('^ premier ', ['^ pr'])]
def test_add_final_only(self):
repl = self.get_replacements("road$ -> rd")
assert repl == [(' road ^', [' rd ^', ' road ^'])]
def test_replace_final_only(self):
repl = self.get_replacements("road$ => rd")
assert repl == [(' road ^', [' rd ^'])]
def test_decompose_only(self):
repl = self.get_replacements("~foo -> foo")
assert repl == [(' foo ', [' foo', 'foo']),
('foo ', [' foo', 'foo'])]
def test_add_suffix_decompose_end_only(self):
repl = self.get_replacements("~berg |-> bg", "~berg$ -> bg")
@@ -253,7 +237,6 @@ class TestGetReplacements:
('berg ', ['berg', 'bg']),
('berg ^', [' berg ^', ' bg ^', 'berg ^', 'bg ^'])]
def test_replace_suffix_decompose_end_only(self):
repl = self.get_replacements("~berg |=> bg", "~berg$ => bg")
@@ -262,7 +245,6 @@ class TestGetReplacements:
('berg ', ['bg']),
('berg ^', [' bg ^', 'bg ^'])]
@pytest.mark.parametrize('rule', ["~berg,~burg -> bg",
"~berg, ~burg -> bg",
"~berg,,~burg -> bg"])

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for generic token analysis, mutation part.
@@ -24,37 +24,34 @@ DEFAULT_TRANSLITERATION = """ :: Latin ();
'🜵' > ' ';
"""
class TestMutationNoVariants:
def make_analyser(self, *mutations):
rules = { 'analyzer': 'generic',
'mutations': [ {'pattern': m[0], 'replacements': m[1]}
for m in mutations]
}
rules = {'analyzer': 'generic',
'mutations': [{'pattern': m[0], 'replacements': m[1]}
for m in mutations]
}
trans = Transliterator.createFromRules("test_trans", DEFAULT_TRANSLITERATION)
norm = Transliterator.createFromRules("test_norm", DEFAULT_NORMALIZATION)
config = module.configure(rules, norm, trans)
self.analysis = module.create(norm, trans, config)
def variants(self, name):
norm = Transliterator.createFromRules("test_norm", DEFAULT_NORMALIZATION)
return set(self.analysis.compute_variants(norm.transliterate(name).strip()))
@pytest.mark.parametrize('pattern', ('(capture)', ['a list']))
def test_bad_pattern(self, pattern):
with pytest.raises(UsageError):
self.make_analyser((pattern, ['b']))
@pytest.mark.parametrize('replacements', (None, 'a string'))
def test_bad_replacement(self, replacements):
with pytest.raises(UsageError):
self.make_analyser(('a', replacements))
def test_simple_replacement(self):
self.make_analyser(('a', ['b']))
@@ -62,27 +59,23 @@ class TestMutationNoVariants:
assert self.variants('abba') == {'bbbb'}
assert self.variants('2 aar') == {'2 bbr'}
def test_multichar_replacement(self):
self.make_analyser(('1 1', ['1 1 1']))
assert self.variants('1 1456') == {'1 1 1456'}
assert self.variants('1 1 1') == {'1 1 1 1'}
def test_removement_replacement(self):
self.make_analyser((' ', [' ', '']))
assert self.variants('A 345') == {'a 345', 'a345'}
assert self.variants('a g b') == {'a g b', 'ag b', 'a gb', 'agb'}
def test_regex_pattern(self):
self.make_analyser(('[^a-z]+', ['XXX', ' ']))
assert self.variants('a-34n12') == {'aXXXnXXX', 'aXXXn', 'a nXXX', 'a n'}
def test_multiple_mutations(self):
self.make_analyser(('ä', ['ä', 'ae']), ('ö', ['ö', 'oe']))

View File

@@ -10,6 +10,7 @@ Tests for simplified trie structure.
from nominatim_db.tokenizer.token_analysis.simple_trie import SimpleTrie
def test_single_item_trie():
t = SimpleTrie([('foob', 42)])
@@ -18,6 +19,7 @@ def test_single_item_trie():
assert t.longest_prefix('foob') == (42, 4)
assert t.longest_prefix('123foofoo', 3) == (None, 3)
def test_complex_item_tree():
t = SimpleTrie([('a', 1),
('b', 2),

View File

@@ -2,10 +2,11 @@
#
# 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.
import pytest
@pytest.fixture
def osm2pgsql_options(temp_db, tmp_path):
""" A standard set of options for osm2pgsql

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for functions to add additional data to the database.
@@ -13,6 +13,7 @@ import pytest
from nominatim_db.tools import add_osm_data
class CaptureGetUrl:
def __init__(self, monkeypatch):
@@ -29,6 +30,7 @@ def setup_delete_postprocessing(temp_db_cursor):
temp_db_cursor.execute("""CREATE OR REPLACE FUNCTION flush_deleted_places()
RETURNS INTEGER AS $$ SELECT 1 $$ LANGUAGE SQL""")
def test_import_osm_file_simple(dsn, table_factory, osm2pgsql_options, capfd):
assert add_osm_data.add_data_from_file(dsn, Path('change.osm'), osm2pgsql_options) == 0

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for maintenance and analysis functions.
@@ -14,6 +14,7 @@ from nominatim_db.tools import admin
from nominatim_db.tokenizer import factory
from nominatim_db.db.sql_preprocessor import SQLPreprocessor
@pytest.fixture(autouse=True)
def create_placex_table(project_env, tokenizer_mock, temp_db_cursor, placex_table):
""" All tests in this module require the placex table to be set up.
@@ -76,7 +77,8 @@ def test_analyse_indexing_with_osm_id(project_env, temp_db_cursor):
class TestAdminCleanDeleted:
@pytest.fixture(autouse=True)
def setup_polygon_delete(self, project_env, table_factory, place_table, osmline_table, temp_db_cursor, temp_db_conn, def_config, src_dir):
def setup_polygon_delete(self, project_env, table_factory, place_table,
osmline_table, temp_db_cursor, temp_db_conn, def_config, src_dir):
""" Set up place_force_delete function and related tables
"""
self.project_env = project_env
@@ -87,12 +89,14 @@ class TestAdminCleanDeleted:
class TEXT NOT NULL,
type TEXT NOT NULL""",
((100, 'N', 'boundary', 'administrative'),
(145, 'N', 'boundary', 'administrative'),
(175, 'R', 'landcover', 'grass')))
temp_db_cursor.execute("""INSERT INTO placex (place_id, osm_id, osm_type, class, type, indexed_date, indexed_status)
VALUES(1, 100, 'N', 'boundary', 'administrative', current_date - INTERVAL '1 month', 1),
(2, 145, 'N', 'boundary', 'administrative', current_date - INTERVAL '3 month', 1),
(3, 175, 'R', 'landcover', 'grass', current_date - INTERVAL '3 months', 1)""")
(145, 'N', 'boundary', 'administrative'),
(175, 'R', 'landcover', 'grass')))
temp_db_cursor.execute("""
INSERT INTO placex (place_id, osm_id, osm_type, class, type,
indexed_date, indexed_status)
VALUES(1, 100, 'N', 'boundary', 'administrative', current_date - INTERVAL '1 month', 1),
(2, 145, 'N', 'boundary', 'administrative', current_date - INTERVAL '3 month', 1),
(3, 175, 'R', 'landcover', 'grass', current_date - INTERVAL '3 months', 1)""")
# set up tables and triggers for utils function
table_factory('place_to_be_deleted',
"""osm_id BIGINT,
@@ -116,33 +120,42 @@ class TestAdminCleanDeleted:
sqlproc = SQLPreprocessor(temp_db_conn, def_config)
sqlproc.run_sql_file(temp_db_conn, 'functions/utils.sql')
def_config.lib_dir.sql = orig_sql
def test_admin_clean_deleted_no_records(self):
admin.clean_deleted_relations(self.project_env, age='1 year')
assert self.temp_db_cursor.row_set('SELECT osm_id, osm_type, class, type, indexed_status FROM placex') == {(100, 'N', 'boundary', 'administrative', 1),
(145, 'N', 'boundary', 'administrative', 1),
(175, 'R', 'landcover', 'grass', 1)}
assert self.temp_db_cursor.table_rows('import_polygon_delete') == 3
rowset = self.temp_db_cursor.row_set(
'SELECT osm_id, osm_type, class, type, indexed_status FROM placex')
assert rowset == {(100, 'N', 'boundary', 'administrative', 1),
(145, 'N', 'boundary', 'administrative', 1),
(175, 'R', 'landcover', 'grass', 1)}
assert self.temp_db_cursor.table_rows('import_polygon_delete') == 3
@pytest.mark.parametrize('test_age', ['T week', '1 welk', 'P1E'])
def test_admin_clean_deleted_bad_age(self, test_age):
with pytest.raises(UsageError):
admin.clean_deleted_relations(self.project_env, age = test_age)
admin.clean_deleted_relations(self.project_env, age=test_age)
def test_admin_clean_deleted_partial(self):
admin.clean_deleted_relations(self.project_env, age = '2 months')
assert self.temp_db_cursor.row_set('SELECT osm_id, osm_type, class, type, indexed_status FROM placex') == {(100, 'N', 'boundary', 'administrative', 1),
(145, 'N', 'boundary', 'administrative', 100),
(175, 'R', 'landcover', 'grass', 100)}
admin.clean_deleted_relations(self.project_env, age='2 months')
rowset = self.temp_db_cursor.row_set(
'SELECT osm_id, osm_type, class, type, indexed_status FROM placex')
assert rowset == {(100, 'N', 'boundary', 'administrative', 1),
(145, 'N', 'boundary', 'administrative', 100),
(175, 'R', 'landcover', 'grass', 100)}
assert self.temp_db_cursor.table_rows('import_polygon_delete') == 1
@pytest.mark.parametrize('test_age', ['1 week', 'P3D', '5 hours'])
def test_admin_clean_deleted(self, test_age):
admin.clean_deleted_relations(self.project_env, age = test_age)
assert self.temp_db_cursor.row_set('SELECT osm_id, osm_type, class, type, indexed_status FROM placex') == {(100, 'N', 'boundary', 'administrative', 100),
(145, 'N', 'boundary', 'administrative', 100),
(175, 'R', 'landcover', 'grass', 100)}
admin.clean_deleted_relations(self.project_env, age=test_age)
rowset = self.temp_db_cursor.row_set(
'SELECT osm_id, osm_type, class, type, indexed_status FROM placex')
assert rowset == {(100, 'N', 'boundary', 'administrative', 100),
(145, 'N', 'boundary', 'administrative', 100),
(175, 'R', 'landcover', 'grass', 100)}
assert self.temp_db_cursor.table_rows('import_polygon_delete') == 0

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for database integrity checks.
@@ -12,6 +12,7 @@ import pytest
from nominatim_db.tools import check_database as chkdb
import nominatim_db.version
def test_check_database_unknown_db(def_config, monkeypatch):
monkeypatch.setenv('NOMINATIM_DATABASE_DSN', 'pgsql:dbname=fjgkhughwgh2423gsags')
assert chkdb.check_database(def_config) == 1
@@ -35,6 +36,7 @@ def test_check_database_version_good(property_table, temp_db_conn, def_config):
str(nominatim_db.version.NOMINATIM_VERSION))
assert chkdb.check_database_version(temp_db_conn, def_config) == chkdb.CheckState.OK
def test_check_database_version_bad(property_table, temp_db_conn, def_config):
property_table.set('database_version', '3.9.9-9')
assert chkdb.check_database_version(temp_db_conn, def_config) == chkdb.CheckState.FATAL

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for functions to import a new database.
@@ -10,13 +10,14 @@ Tests for functions to import a new database.
from pathlib import Path
import pytest
import pytest_asyncio
import pytest_asyncio # noqa
import psycopg
from psycopg import sql as pysql
from nominatim_db.tools import database_import
from nominatim_db.errors import UsageError
class TestDatabaseSetup:
DBNAME = 'test_nominatim_python_unittest'
@@ -31,18 +32,15 @@ class TestDatabaseSetup:
with conn.cursor() as cur:
cur.execute(f'DROP DATABASE IF EXISTS {self.DBNAME}')
@pytest.fixture
def cursor(self):
with psycopg.connect(dbname=self.DBNAME) as conn:
with conn.cursor() as cur:
yield cur
def conn(self):
return psycopg.connect(dbname=self.DBNAME)
def test_setup_skeleton(self):
database_import.setup_database_skeleton(f'dbname={self.DBNAME}')
@@ -51,25 +49,21 @@ class TestDatabaseSetup:
with conn.cursor() as cur:
cur.execute('CREATE TABLE t (h HSTORE, geom GEOMETRY(Geometry, 4326))')
def test_unsupported_pg_version(self, monkeypatch):
monkeypatch.setattr(database_import, 'POSTGRESQL_REQUIRED_VERSION', (100, 4))
with pytest.raises(UsageError, match='PostgreSQL server is too old.'):
database_import.setup_database_skeleton(f'dbname={self.DBNAME}')
def test_create_db_explicit_ro_user(self):
database_import.setup_database_skeleton(f'dbname={self.DBNAME}',
rouser='postgres')
def test_create_db_missing_ro_user(self):
with pytest.raises(UsageError, match='Missing read-only user.'):
database_import.setup_database_skeleton(f'dbname={self.DBNAME}',
rouser='sdfwkjkjgdugu2;jgsafkljas;')
def test_setup_extensions_old_postgis(self, monkeypatch):
monkeypatch.setattr(database_import, 'POSTGIS_REQUIRED_VERSION', (50, 50))
@@ -173,7 +167,7 @@ def test_truncate_database_tables(temp_db_conn, temp_db_cursor, table_factory, w
@pytest.mark.parametrize("threads", (1, 5))
@pytest.mark.asyncio
async def test_load_data(dsn, place_row, placex_table, osmline_table,
temp_db_cursor, threads):
temp_db_cursor, threads):
for func in ('precompute_words', 'getorcreate_housenumber_id', 'make_standard_name'):
temp_db_cursor.execute(pysql.SQL("""CREATE FUNCTION {} (src TEXT)
RETURNS TEXT AS $$ SELECT 'a'::TEXT $$ LANGUAGE SQL
@@ -198,11 +192,9 @@ class TestSetupSQL:
self.config = def_config
def write_sql(self, fname, content):
(self.config.lib_dir.sql / fname).write_text(content)
@pytest.mark.parametrize("reverse", [True, False])
def test_create_tables(self, temp_db_conn, temp_db_cursor, reverse):
self.write_sql('tables.sql',
@@ -213,7 +205,6 @@ class TestSetupSQL:
temp_db_cursor.scalar('SELECT test()') == reverse
def test_create_table_triggers(self, temp_db_conn, temp_db_cursor):
self.write_sql('table-triggers.sql',
"""CREATE FUNCTION test() RETURNS TEXT
@@ -223,7 +214,6 @@ class TestSetupSQL:
temp_db_cursor.scalar('SELECT test()') == 'a'
def test_create_partition_tables(self, temp_db_conn, temp_db_cursor):
self.write_sql('partition-tables.src.sql',
"""CREATE FUNCTION test() RETURNS TEXT
@@ -233,7 +223,6 @@ class TestSetupSQL:
temp_db_cursor.scalar('SELECT test()') == 'b'
@pytest.mark.parametrize("drop", [True, False])
@pytest.mark.asyncio
async def test_create_search_indices(self, temp_db_conn, temp_db_cursor, drop):

View File

@@ -2,19 +2,14 @@
#
# 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.
"""
Tests for tools.exec_utils module.
"""
from pathlib import Path
import subprocess
import pytest
from nominatim_db.config import Configuration
import nominatim_db.tools.exec_utils as exec_utils
def test_run_osm2pgsql(osm2pgsql_options):
osm2pgsql_options['append'] = False
osm2pgsql_options['import_file'] = 'foo.bar'

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for freeze functions (removing unused database parts).
@@ -26,6 +26,7 @@ NOMINATIM_DROP_TABLES = [
'wikipedia_article', 'wikipedia_redirect'
]
def test_drop_tables(temp_db_conn, temp_db_cursor, table_factory):
for table in NOMINATIM_RUNTIME_TABLES + NOMINATIM_DROP_TABLES:
table_factory(table)
@@ -42,6 +43,7 @@ def test_drop_tables(temp_db_conn, temp_db_cursor, table_factory):
assert freeze.is_frozen(temp_db_conn)
def test_drop_flatnode_file_no_file():
freeze.drop_flatnode_file(None)

View File

@@ -2,20 +2,17 @@
#
# 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.
"""
Tests for import special phrases methods
of the class SPImporter.
"""
from shutil import copyfile
import pytest
from nominatim_db.tools.special_phrases.sp_importer import SPImporter
from nominatim_db.tools.special_phrases.sp_wiki_loader import SPWikiLoader
from nominatim_db.tools.special_phrases.special_phrase import SpecialPhrase
from nominatim_db.errors import UsageError
from cursor import CursorForTesting
@pytest.fixture
def sp_importer(temp_db_conn, def_config, monkeypatch):
@@ -53,6 +50,7 @@ def test_fetch_existing_place_classtype_tables(sp_importer, table_factory):
contained_table = sp_importer.table_phrases_to_delete.pop()
assert contained_table == 'place_classtype_testclasstypetable'
def test_check_sanity_class(sp_importer):
"""
Check for _check_sanity() method.
@@ -65,6 +63,7 @@ def test_check_sanity_class(sp_importer):
assert sp_importer._check_sanity(SpecialPhrase('en', 'class', 'type', ''))
def test_load_white_and_black_lists(sp_importer):
"""
Test that _load_white_and_black_lists() well return
@@ -93,6 +92,7 @@ def test_create_place_classtype_indexes(temp_db_with_extensions,
assert check_placeid_and_centroid_indexes(temp_db_cursor, phrase_class, phrase_type)
def test_create_place_classtype_table(temp_db_conn, temp_db_cursor, placex_table, sp_importer):
"""
Test that _create_place_classtype_table() create
@@ -105,6 +105,7 @@ def test_create_place_classtype_table(temp_db_conn, temp_db_cursor, placex_table
assert check_table_exist(temp_db_cursor, phrase_class, phrase_type)
def test_grant_access_to_web_user(temp_db_conn, temp_db_cursor, table_factory,
def_config, sp_importer):
"""
@@ -120,7 +121,9 @@ def test_grant_access_to_web_user(temp_db_conn, temp_db_cursor, table_factory,
sp_importer._grant_access_to_webuser(phrase_class, phrase_type)
temp_db_conn.commit()
assert check_grant_access(temp_db_cursor, def_config.DATABASE_WEBUSER, phrase_class, phrase_type)
assert check_grant_access(temp_db_cursor, def_config.DATABASE_WEBUSER,
phrase_class, phrase_type)
def test_create_place_classtype_table_and_indexes(
temp_db_cursor, def_config, placex_table,
@@ -141,6 +144,7 @@ def test_create_place_classtype_table_and_indexes(
assert check_placeid_and_centroid_indexes(temp_db_cursor, pair[0], pair[1])
assert check_grant_access(temp_db_cursor, def_config.DATABASE_WEBUSER, pair[0], pair[1])
def test_remove_non_existent_tables_from_db(sp_importer, default_phrases,
temp_db_conn, temp_db_cursor):
"""
@@ -168,7 +172,7 @@ def test_remove_non_existent_tables_from_db(sp_importer, default_phrases,
temp_db_conn.commit()
assert temp_db_cursor.row_set(query_tables) \
== {('place_classtype_testclasstypetable_to_keep', )}
== {('place_classtype_testclasstypetable_to_keep', )}
@pytest.mark.parametrize("should_replace", [(True), (False)])
@@ -182,8 +186,8 @@ def test_import_phrases(monkeypatch, temp_db_cursor, def_config, sp_importer,
It should also update the database well by deleting or preserving existing entries
of the database.
"""
#Add some data to the database before execution in order to test
#what is deleted and what is preserved.
# Add some data to the database before execution in order to test
# what is deleted and what is preserved.
table_factory('place_classtype_amenity_animal_shelter')
table_factory('place_classtype_wrongclass_wrongtype')
@@ -209,6 +213,7 @@ def test_import_phrases(monkeypatch, temp_db_cursor, def_config, sp_importer,
if should_replace:
assert not temp_db_cursor.table_exists('place_classtype_wrongclass_wrongtype')
def check_table_exist(temp_db_cursor, phrase_class, phrase_type):
"""
Verify that the place_classtype table exists for the given
@@ -231,6 +236,7 @@ def check_grant_access(temp_db_cursor, user, phrase_class, phrase_type):
AND privilege_type='SELECT'""".format(table_name, user))
return temp_db_cursor.fetchone()
def check_placeid_and_centroid_indexes(temp_db_cursor, phrase_class, phrase_type):
"""
Check that the place_id index and centroid index exist for the

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for migration functions
@@ -11,9 +11,9 @@ import pytest
from nominatim_db.tools import migration
from nominatim_db.errors import UsageError
from nominatim_db.db.connection import server_version_tuple
import nominatim_db.version
class DummyTokenizer:
def update_sql_functions(self, config):
@@ -49,6 +49,7 @@ def test_run_single_migration(temp_db_with_extensions, def_config, temp_db_curso
str(nominatim_db.version.NominatimVersion(*oldversion)))
done = {'old': False, 'new': False}
def _migration(**_):
""" Dummy migration"""
done['new'] = True
@@ -69,7 +70,7 @@ def test_run_single_migration(temp_db_with_extensions, def_config, temp_db_curso
assert property_table.get('database_version') == str(nominatim_db.version.NOMINATIM_VERSION)
###### Tests for specific migrations
# Tests for specific migrations
#
# Each migration should come with two tests:
# 1. Test that migration from old to new state works as expected.

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for functions to maintain the artificial postcode table.
@@ -15,6 +15,7 @@ from nominatim_db.tools import postcodes
from nominatim_db.data import country_info
import dummy_tokenizer
class MockPostcodeTable:
""" A location_postcode table for testing.
"""
@@ -35,7 +36,7 @@ class MockPostcodeTable:
RETURNS TEXT AS $$ BEGIN RETURN postcode; END; $$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION get_country_code(place geometry)
RETURNS TEXT AS $$ BEGIN
RETURNS TEXT AS $$ BEGIN
RETURN null;
END; $$ LANGUAGE plpgsql;
""")
@@ -51,7 +52,6 @@ class MockPostcodeTable:
(country, postcode, x, y))
self.conn.commit()
@property
def row_set(self):
with self.conn.cursor() as cur:
@@ -180,7 +180,7 @@ def test_postcodes_extern(dsn, postcode_table, tmp_path,
('xx', 'CD 4511', -10, -5)}
def test_postcodes_extern_bad_column(dsn, postcode_table, tmp_path,
def test_postcodes_extern_bad_column(dsn, postcode_table, tmp_path,
insert_implicit_postcode, tokenizer):
insert_implicit_postcode(1, 'xx', 'POINT(10 12)', dict(postcode='AB 4511'))
@@ -204,6 +204,7 @@ def test_postcodes_extern_bad_number(dsn, insert_implicit_postcode,
assert postcode_table.row_set == {('xx', 'AB 4511', 10, 12),
('xx', 'CD 4511', -10, -5)}
def test_can_compute(dsn, table_factory):
assert not postcodes.can_compute(dsn)
table_factory('place')
@@ -211,10 +212,10 @@ def test_can_compute(dsn, table_factory):
def test_no_placex_entry(dsn, tmp_path, temp_db_cursor, place_row, postcode_table, tokenizer):
#Rewrite the get_country_code function to verify its execution.
# Rewrite the get_country_code function to verify its execution.
temp_db_cursor.execute("""
CREATE OR REPLACE FUNCTION get_country_code(place geometry)
RETURNS TEXT AS $$ BEGIN
RETURNS TEXT AS $$ BEGIN
RETURN 'yy';
END; $$ LANGUAGE plpgsql;
""")
@@ -224,11 +225,12 @@ def test_no_placex_entry(dsn, tmp_path, temp_db_cursor, place_row, postcode_tabl
assert postcode_table.row_set == {('yy', 'AB 4511', 10, 12)}
def test_discard_badly_formatted_postcodes(dsn, tmp_path, temp_db_cursor, place_row, postcode_table, tokenizer):
#Rewrite the get_country_code function to verify its execution.
def test_discard_badly_formatted_postcodes(dsn, tmp_path, temp_db_cursor, place_row,
postcode_table, tokenizer):
# Rewrite the get_country_code function to verify its execution.
temp_db_cursor.execute("""
CREATE OR REPLACE FUNCTION get_country_code(place geometry)
RETURNS TEXT AS $$ BEGIN
RETURNS TEXT AS $$ BEGIN
RETURN 'fr';
END; $$ LANGUAGE plpgsql;
""")

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Test for various refresh functions.
@@ -12,7 +12,7 @@ from pathlib import Path
import pytest
from nominatim_db.tools import refresh
from nominatim_db.db.connection import postgis_version_tuple
def test_refresh_import_wikipedia_not_existing(dsn):
assert refresh.import_wikipedia_articles(dsn, Path('.')) == 1
@@ -21,6 +21,7 @@ def test_refresh_import_wikipedia_not_existing(dsn):
def test_refresh_import_secondary_importance_non_existing(dsn):
assert refresh.import_secondary_importance(dsn, Path('.')) == 1
def test_refresh_import_secondary_importance_testdb(dsn, src_dir, temp_db_conn, temp_db_cursor):
temp_db_cursor.execute('CREATE EXTENSION postgis')
temp_db_cursor.execute('CREATE EXTENSION postgis_raster')

View File

@@ -2,23 +2,24 @@
#
# 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.
"""
Tests for function for importing address ranks.
"""
import json
from pathlib import Path
import pytest
from nominatim_db.tools.refresh import load_address_levels, load_address_levels_from_config
def test_load_ranks_def_config(temp_db_conn, temp_db_cursor, def_config):
load_address_levels_from_config(temp_db_conn, def_config)
assert temp_db_cursor.table_rows('address_levels') > 0
def test_load_ranks_from_project_dir(project_env, temp_db_conn, temp_db_cursor):
test_file = project_env.project_dir / 'address-levels.json'
test_file.write_text('[{"tags":{"place":{"sea":2}}}]')
@@ -43,14 +44,14 @@ def test_load_ranks_country(temp_db_conn, temp_db_cursor):
"tags": {"place": {"village": 15}}},
{"countries": ['uk', 'us'],
"tags": {"place": {"village": 16}}}
])
])
assert temp_db_cursor.row_set('SELECT * FROM levels') == \
set([(None, 'place', 'village', 14, 14),
('de', 'place', 'village', 15, 15),
('uk', 'place', 'village', 16, 16),
('us', 'place', 'village', 16, 16),
])
])
def test_load_ranks_default_value(temp_db_conn, temp_db_cursor):
@@ -58,33 +59,33 @@ def test_load_ranks_default_value(temp_db_conn, temp_db_cursor):
[{"tags": {"boundary": {"": 28}}},
{"countries": ['hu'],
"tags": {"boundary": {"": 29}}}
])
])
assert temp_db_cursor.row_set('SELECT * FROM levels') == \
set([(None, 'boundary', None, 28, 28),
('hu', 'boundary', None, 29, 29),
])
])
def test_load_ranks_multiple_keys(temp_db_conn, temp_db_cursor):
load_address_levels(temp_db_conn, 'levels',
[{"tags": {"place": {"city": 14},
"boundary": {"administrative2" : 4}}
}])
"boundary": {"administrative2": 4}}
}])
assert temp_db_cursor.row_set('SELECT * FROM levels') == \
set([(None, 'place', 'city', 14, 14),
(None, 'boundary', 'administrative2', 4, 4),
])
])
def test_load_ranks_address(temp_db_conn, temp_db_cursor):
load_address_levels(temp_db_conn, 'levels',
[{"tags": {"place": {"city": 14,
"town" : [14, 13]}}
}])
"town": [14, 13]}}
}])
assert temp_db_cursor.row_set('SELECT * FROM levels') == \
set([(None, 'place', 'city', 14, 14),
(None, 'place', 'town', 14, 13),
])
])

View File

@@ -2,7 +2,7 @@
#
# 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.
"""
Tests for creating PL/pgSQL functions for Nominatim.
@@ -11,6 +11,7 @@ import pytest
from nominatim_db.tools.refresh import create_functions
class TestCreateFunctions:
@pytest.fixture(autouse=True)
def init_env(self, sql_preprocessor, temp_db_conn, def_config, tmp_path):
@@ -18,12 +19,10 @@ class TestCreateFunctions:
self.config = def_config
def_config.lib_dir.sql = tmp_path
def write_functions(self, content):
sqlfile = self.config.lib_dir.sql / 'functions.sql'
sqlfile.write_text(content)
def test_create_functions(self, temp_db_cursor):
self.write_functions("""CREATE OR REPLACE FUNCTION test() RETURNS INTEGER
AS $$
@@ -37,7 +36,6 @@ class TestCreateFunctions:
assert temp_db_cursor.scalar('SELECT test()') == 43
@pytest.mark.parametrize("dbg,ret", ((True, 43), (False, 22)))
def test_create_functions_with_template(self, temp_db_cursor, dbg, ret):
self.write_functions("""CREATE OR REPLACE FUNCTION test() RETURNS INTEGER

View File

@@ -12,7 +12,10 @@ import csv
import pytest
from nominatim_db.tools.refresh import import_wikipedia_articles, recompute_importance, create_functions
from nominatim_db.tools.refresh import (import_wikipedia_articles,
recompute_importance,
create_functions)
@pytest.fixture
def wiki_csv(tmp_path, sql_preprocessor):
@@ -25,7 +28,7 @@ def wiki_csv(tmp_path, sql_preprocessor):
for lang, title, importance, wd in data:
writer.writerow({'language': lang, 'type': 'a',
'title': title, 'importance': str(importance),
'wikidata_id' : wd})
'wikidata_id': wd})
return tmp_path
return _import

Some files were not shown because too many files have changed in this diff Show More