implement BDD osm2pgsql tests with pytest-bdd

This commit is contained in:
Sarah Hoffmann
2025-03-31 09:39:01 +02:00
parent 0f725b1880
commit fb440f29a2
18 changed files with 2409 additions and 17 deletions

View File

@@ -11,18 +11,24 @@ import sys
import json
from pathlib import Path
import pytest
from pytest_bdd.parsers import re as step_parse
from pytest_bdd import when, then
from utils.api_runner import APIRunner
from utils.api_result import APIResult
from utils.checks import ResultAttr, COMPARATOR_TERMS
# always test against the source
SRC_DIR = (Path(__file__) / '..' / '..' / '..').resolve()
sys.path.insert(0, str(SRC_DIR / 'src'))
import pytest
from pytest_bdd.parsers import re as step_parse
from pytest_bdd import given, when, then
pytest.register_assert_rewrite('utils')
from utils.api_runner import APIRunner
from utils.api_result import APIResult
from utils.checks import ResultAttr, COMPARATOR_TERMS
from utils.geometry_alias import ALIASES
from utils.grid import Grid
from utils.db import DBManager
from nominatim_db.config import Configuration
def _strlist(inp):
return [s.strip() for s in inp.split(',')]
@@ -60,6 +66,35 @@ def datatable():
return None
@pytest.fixture
def node_grid():
""" Default fixture for node grids. Nothing set.
"""
return Grid([[]], None, None)
@pytest.fixture(scope='session')
def template_db(pytestconfig):
""" Create a template database containing the extensions and base data
needed by Nominatim. Using the template instead of doing the full
setup can speed up the tests.
The template database will only be created if it does not exist yet
or a purge has been explicitly requested.
"""
dbm = DBManager(purge=pytestconfig.option.NOMINATIM_PURGE)
template_db = pytestconfig.getini('nominatim_template_db')
template_config = Configuration(
None, environ={'NOMINATIM_DATABASE_DSN': f"pgsql:dbname={template_db}"})
dbm.setup_template_db(template_config)
return template_db
@when(step_parse(r'reverse geocoding (?P<lat>[\d.-]*),(?P<lon>[\d.-]*)'),
target_fixture='nominatim_result')
def reverse_geocode_via_api(test_config_env, pytestconfig, datatable, lat, lon):
@@ -223,3 +258,23 @@ def check_specific_result_for_fields(nominatim_result, datatable, num, field):
for k, v in pairs:
assert ResultAttr(nominatim_result.result[num], prefix + k) == v
@given(step_parse(r'the (?P<step>[0-9.]+ )?grid(?: with origin (?P<origin>.*))?'),
target_fixture='node_grid')
def set_node_grid(datatable, step, origin):
if step is not None:
step = float(step)
if origin:
if ',' in origin:
coords = origin.split(',')
if len(coords) != 2:
raise RuntimeError('Grid origin expects origin with x,y coordinates.')
origin = list(map(float, coords))
elif origin in ALIASES:
origin = ALIASES[origin]
else:
raise RuntimeError('Grid origin must be either coordinate or alias.')
return Grid(datatable, step, origin)

View File

@@ -136,8 +136,8 @@ Feature: Json output for Reverse API
Then a HTTP 200 is returned
And the result is valid json
And the result contains
| geotext!fm |
| LINESTRING\(9.5039353 47.0657546, ?9.5040437 47.0657781, ?9.5040808 47.065787, ?9.5054298 47.0661407\) |
| geotext!wkt |
| 9.5039353 47.0657546, 9.5040437 47.0657781, 9.5040808 47.065787, 9.5054298 47.0661407 |
Examples:
| format |

View File

@@ -92,8 +92,8 @@ Feature: XML output for Reverse API
Then a HTTP 200 is returned
And the result is valid xml
And the result contains
| geotext!fm |
| LINESTRING\(9.5039353 47.0657546, ?9.5040437 47.0657781, ?9.5040808 47.065787, ?9.5054298 47.0661407\) |
| geotext!wkt |
| 9.5039353 47.0657546, 9.5040437 47.0657781, 9.5040808 47.065787, 9.5054298 47.0661407 |
Scenario: Reverse XML - Output of SVG
When sending v1/reverse with format xml

View File

@@ -0,0 +1,35 @@
Feature: Import of objects with broken geometries by osm2pgsql
Scenario: Import way with double nodes
When loading osm data
"""
n100 x0 y0
n101 x0 y0.1
n102 x0.1 y0.2
w1 Thighway=primary Nn100,n101,n101,n102
"""
Then place contains
| object | class | type | geometry!wkt |
| W1 | highway | primary | 0 0, 0 0.1, 0.1 0.2 |
Scenario: Import of ballon areas
Given the grid
| 2 | | 3 |
| 1 | | 4 |
| 5 | | |
When loading osm data
"""
n1
n2
n3
n4
n5
w1 Thighway=unclassified Nn1,n2,n3,n4,n1,n5
w2 Thighway=unclassified Nn1,n2,n3,n4,n1
w3 Thighway=unclassified Nn1,n2,n3,n4,n3
"""
Then place contains
| object | geometry!wkt |
| W1 | 1,2,3,4,1,5 |
| W2 | (1,2,3,4,1) |
| W3 | 1,2,3,4 |

View File

@@ -0,0 +1,318 @@
Feature: Import with custom styles by osm2pgsql
Tests for the example customizations given in the documentation.
Scenario: Custom main tags (set new ones)
Given the lua style file
"""
local flex = require('import-full')
flex.set_main_tags{
boundary = {administrative = 'named'},
highway = {'always', street_lamp = 'named'},
landuse = 'fallback'
}
"""
When loading osm data
"""
n10 Tboundary=administrative x0 y0
n11 Tboundary=administrative,name=Foo x0 y0
n12 Tboundary=electoral x0 y0
n13 Thighway=primary x0 y0
n14 Thighway=street_lamp x0 y0
n15 Thighway=primary,landuse=street x0 y0
"""
Then place contains exactly
| object | class | type |
| N11 | boundary | administrative |
| N13 | highway | primary |
| N15 | highway | primary |
Scenario: Custom main tags (modify existing)
Given the lua style file
"""
local flex = require('import-full')
flex.modify_main_tags{
amenity = {prison = 'delete'},
highway = {stop = 'named'},
aeroway = 'named'
}
"""
When loading osm data
"""
n10 Tamenity=hotel x0 y0
n11 Tamenity=prison x0 y0
n12 Thighway=stop x0 y0
n13 Thighway=stop,name=BigStop x0 y0
n14 Thighway=give_way x0 y0
n15 Thighway=bus_stop x0 y0
n16 Taeroway=no,name=foo x0 y0
n17 Taeroway=taxiway,name=D15 x0 y0
"""
Then place contains exactly
| object | class | type |
| N10 | amenity | hotel |
| N13 | highway | stop |
| N15 | highway | bus_stop |
| N17 | aeroway | taxiway |
Scenario: Prefiltering tags
Given the lua style file
"""
local flex = require('import-full')
flex.set_prefilters{
delete_keys = {'source', 'source:*'},
extra_tags = {amenity = {'yes', 'no'}}
}
flex.set_main_tags{
amenity = 'always',
tourism = 'always'
}
"""
When loading osm data
"""
n1 Tamenity=yes x0 y6
n2 Tamenity=hospital,source=survey x3 y6
n3 Ttourism=hotel,amenity=yes x0 y0
n4 Ttourism=hotel,amenity=telephone x0 y0
"""
Then place contains exactly
| object | class | extratags!dict |
| N2 | amenity | - |
| N3 | tourism | 'amenity': 'yes' |
| N4 | tourism | - |
| N4 | amenity | - |
Scenario: Ignore some tags
Given the lua style file
"""
local flex = require('import-extratags')
flex.ignore_keys{'ref:*', 'surface'}
"""
When loading osm data
"""
n100 Thighway=residential,ref=34,ref:bodo=34,surface=gray,extra=1 x0 y0
"""
Then place contains exactly
| object | name!dict | extratags!dict |
| N100 | 'ref' : '34' | 'extra': '1' |
Scenario: Add for extratags
Given the lua style file
"""
local flex = require('import-full')
flex.add_for_extratags{'ref:*', 'surface'}
"""
When loading osm data
"""
n100 Thighway=residential,ref=34,ref:bodo=34,surface=gray,extra=1 x0 y0
"""
Then place contains exactly
| object | name!dict | extratags!dict |
| N100 | 'ref' : '34' | 'ref:bodo': '34', 'surface': 'gray' |
Scenario: Name tags
Given the lua style file
"""
local flex = require('flex-base')
flex.set_main_tags{highway = {traffic_light = 'named'}}
flex.set_name_tags{main = {'name', 'name:*'},
extra = {'ref'}
}
"""
When loading osm data
"""
n1 Thighway=stop,name=Something x0 y0
n2 Thighway=traffic_light,ref=453-4 x0 y0
n3 Thighway=traffic_light,name=Greens x0 y0
n4 Thighway=traffic_light,name=Red,ref=45 x0 y0
"""
Then place contains exactly
| object | class | name!dict |
| N3 | highway | 'name': 'Greens' |
| N4 | highway | 'name': 'Red', 'ref': '45' |
Scenario: Modify name tags
Given the lua style file
"""
local flex = require('import-full')
flex.modify_name_tags{house = {}, extra = {'o'}}
"""
When loading osm data
"""
n1 Ttourism=hotel,ref=45,o=good
n2 Taddr:housename=Old,addr:street=Away
"""
Then place contains exactly
| object | class | name!dict |
| N1 | tourism | 'o': 'good' |
Scenario: Address tags
Given the lua style file
"""
local flex = require('import-full')
flex.set_address_tags{
main = {'addr:housenumber'},
extra = {'addr:*'},
postcode = {'postal_code', 'postcode', 'addr:postcode'},
country = {'country-code', 'ISO3166-1'}
}
"""
When loading osm data
"""
n1 Ttourism=hotel,addr:street=Foo x0 y0
n2 Taddr:housenumber=23,addr:street=Budd,postal_code=5567 x0 y0
n3 Taddr:street=None,addr:city=Where x0 y0
"""
Then place contains exactly
| object | class | type | address!dict |
| N1 | tourism | hotel | 'street': 'Foo' |
| N2 | place | house | 'housenumber': '23', 'street': 'Budd', 'postcode': '5567' |
Scenario: Modify address tags
Given the lua style file
"""
local flex = require('import-full')
flex.set_address_tags{
extra = {'addr:*'},
}
"""
When loading osm data
"""
n2 Taddr:housenumber=23,addr:street=Budd,is_in:city=Faraway,postal_code=5567 x0 y0
"""
Then place contains exactly
| object | class | type | address!dict |
| N2 | place | house | 'housenumber': '23', 'street': 'Budd', 'postcode': '5567' |
Scenario: Unused handling (delete)
Given the lua style file
"""
local flex = require('import-full')
flex.set_address_tags{
main = {'addr:housenumber'},
extra = {'addr:*', 'tiger:county'}
}
flex.set_unused_handling{delete_keys = {'tiger:*'}}
"""
When loading osm data
"""
n1 Ttourism=hotel,tiger:county=Fargo x0 y0
n2 Ttourism=hotel,tiger:xxd=56,else=other x0 y0
"""
Then place contains exactly
| object | class | type | address!dict | extratags!dict |
| N1 | tourism | hotel | 'tiger:county': 'Fargo' | - |
| N2 | tourism | hotel | - | 'else': 'other' |
Scenario: Unused handling (extra)
Given the lua style file
"""
local flex = require('flex-base')
flex.set_main_tags{highway = 'always',
wikipedia = 'extra'}
flex.add_for_extratags{'wikipedia:*', 'wikidata'}
flex.set_unused_handling{extra_keys = {'surface'}}
"""
When loading osm data
"""
n100 Thighway=path,foo=bar,wikipedia=en:Path x0 y0
n234 Thighway=path,surface=rough x0 y0
n445 Thighway=path,name=something x0 y0
n446 Thighway=path,wikipedia:en=Path,wikidata=Q23 x0 y0
n567 Thighway=path,surface=dirt,wikipedia:en=Path x0 y0
"""
Then place contains exactly
| object | class | type | extratags!dict |
| N100 | highway | path | 'wikipedia': 'en:Path' |
| N234 | highway | path | 'surface': 'rough' |
| N445 | highway | path | - |
| N446 | highway | path | 'wikipedia:en': 'Path', 'wikidata': 'Q23' |
| N567 | highway | path | 'surface': 'dirt', 'wikipedia:en': 'Path' |
Scenario: Additional relation types
Given the lua style file
"""
local flex = require('import-full')
flex.RELATION_TYPES['site'] = flex.relation_as_multipolygon
"""
And the grid
| 1 | 2 |
| 4 | 3 |
When loading osm data
"""
n1
n2
n3
n4
w1 Nn1,n2,n3,n4,n1
r1 Ttype=multipolygon,amenity=school Mw1@
r2 Ttype=site,amenity=school Mw1@
"""
Then place contains exactly
| object | class | type |
| R1 | amenity | school |
| R2 | amenity | school |
Scenario: Exclude country relations
Given the lua style file
"""
local flex = require('import-full')
function osm2pgsql.process_relation(object)
if object.tags.boundary ~= 'administrative' or object.tags.admin_level ~= '2' then
flex.process_relation(object)
end
end
"""
And the grid
| 1 | 2 |
| 4 | 3 |
When loading osm data
"""
n1
n2
n3
n4
w1 Nn1,n2,n3,n4,n1
r1 Ttype=multipolygon,boundary=administrative,admin_level=4,name=Small Mw1@
r2 Ttype=multipolygon,boundary=administrative,admin_level=2,name=Big Mw1@
"""
Then place contains exactly
| object | class | type |
| R1 | boundary | administrative |
Scenario: Customize processing functions
Given the lua style file
"""
local flex = require('import-full')
local original_process_tags = flex.process_tags
function flex.process_tags(o)
if o.object.tags.highway ~= nil and o.object.tags.access == 'no' then
return
end
original_process_tags(o)
end
"""
When loading osm data
"""
n1 Thighway=residential x0 y0
n2 Thighway=residential,access=no x0 y0
"""
Then place contains exactly
| object | class | type |
| N1 | highway | residential |

View File

@@ -0,0 +1,10 @@
Feature: Import of relations by osm2pgsql
Testing specific relation problems related to members.
Scenario: Don't import empty waterways
When loading osm data
"""
n1 Tamenity=prison,name=foo
r1 Ttype=waterway,waterway=river,name=XZ Mn1@
"""
Then place has no entry for R1

View File

@@ -0,0 +1,42 @@
Feature: Import of simple objects by osm2pgsql
Testing basic tagging in osm2pgsql imports.
Scenario: Import simple objects
When loading osm data
"""
n1 Tamenity=prison,name=foo x34.3 y-23
n100 x0 y0
n101 x0 y0.1
n102 x0.1 y0.2
n200 x0 y0
n201 x0 y1
n202 x1 y1
n203 x1 y0
w1 Tshop=toys,name=tata Nn100,n101,n102
w2 Tref=45 Nn200,n201,n202,n203,n200
r1 Ttype=multipolygon,tourism=hotel,name=XZ Mn1@,w2@
"""
Then place contains exactly
| object | class | type | name!dict | geometry!wkt |
| N1 | amenity | prison | 'name' : 'foo' | 34.3 -23 |
| W1 | shop | toys | 'name' : 'tata' | 0 0, 0 0.1, 0.1 0.2 |
| R1 | tourism | hotel | 'name' : 'XZ' | (0 0, 0 1, 1 1, 1 0, 0 0) |
Scenario: Import object with two main tags
When loading osm data
"""
n1 Ttourism=hotel,amenity=restaurant,name=foo
"""
Then place contains exactly
| object | class | type | name!dict |
| N1 | tourism | hotel | 'name' : 'foo' |
| N1 | amenity | restaurant | 'name' : 'foo' |
Scenario: Import stand-alone house number with postcode
When loading osm data
"""
n1 Taddr:housenumber=4,addr:postcode=3345
"""
Then place contains exactly
| object | class | type |
| N1 | place | house |

View File

@@ -0,0 +1,289 @@
Feature: Tag evaluation
Tests if tags are correctly imported into the place table
Scenario: Main tags as fallback
When loading osm data
"""
n100 Tjunction=yes,highway=bus_stop
n101 Tjunction=yes,name=Bar
n200 Tbuilding=yes,amenity=cafe
n201 Tbuilding=yes,name=Intersting
n202 Tbuilding=yes
"""
Then place contains exactly
| object | class | type |
| N100 | highway | bus_stop |
| N101 | junction | yes |
| N200 | amenity | cafe |
| N201 | building | yes |
Scenario: Name and reg tags
When loading osm data
"""
n2001 Thighway=road,name=Foo,alt_name:de=Bar,ref=45
n2002 Thighway=road,name:prefix=Pre,name:suffix=Post,ref:de=55
n2003 Thighway=yes,name:%20%de=Foo,name=real1
n2004 Thighway=yes,name:%a%de=Foo,name=real2
n2005 Thighway=yes,name:%9%de=Foo,name:\\=real3
n2006 Thighway=yes,name:%9%de=Foo,name=rea\l3
"""
Then place contains exactly
| object | class | type | name!dict |
| N2001 | highway | road | 'name': 'Foo', 'alt_name:de': 'Bar', 'ref': '45' |
| N2002 | highway | road | - |
| N2003 | highway | yes | 'name: de': 'Foo', 'name': 'real1' |
| N2004 | highway | yes | 'name:\\nde': 'Foo', 'name': 'real2' |
| N2005 | highway | yes | 'name:\tde': 'Foo', r'name:\\\\': 'real3' |
| N2006 | highway | yes | 'name:\tde': 'Foo', 'name': r'rea\l3' |
And place contains
| object | extratags!dict |
| N2002 | 'name:prefix': 'Pre', 'name:suffix': 'Post', 'ref:de': '55' |
Scenario: Name when using with_name flag
When loading osm data
"""
n3001 Tbridge=yes,bridge:name=GoldenGate
n3002 Tbridge=yes,bridge:name:en=Rainbow
"""
Then place contains exactly
| object | class | type | name!dict |
| N3001 | bridge | yes | 'name': 'GoldenGate' |
| N3002 | bridge | yes | 'name:en': 'Rainbow' |
Scenario: Address tags
When loading osm data
"""
n4001 Taddr:housenumber=34,addr:city=Esmarald,addr:county=Land
n4002 Taddr:streetnumber=10,is_in:city=Rootoo,is_in=Gold
"""
Then place contains exactly
| object | class | address!dict |
| N4001 | place | 'housenumber': '34', 'city': 'Esmarald', 'county': 'Land' |
| N4002 | place | 'streetnumber': '10', 'city': 'Rootoo' |
Scenario: Country codes
When loading osm data
"""
n5001 Tshop=yes,country_code=DE
n5002 Tshop=yes,country_code=toolong
n5003 Tshop=yes,country_code=x
n5004 Tshop=yes,addr:country=us
n5005 Tshop=yes,country=be
n5006 Tshop=yes,addr:country=France
"""
Then place contains exactly
| object | class | address!dict |
| N5001 | shop | 'country': 'DE' |
| N5002 | shop | - |
| N5003 | shop | - |
| N5004 | shop | 'country': 'us' |
| N5005 | shop | - |
| N5006 | shop | - |
Scenario: Postcodes
When loading osm data
"""
n6001 Tshop=bank,addr:postcode=12345
n6002 Tshop=bank,tiger:zip_left=34343
n6003 Tshop=bank,is_in:postcode=9009
"""
Then place contains exactly
| object | class | address!dict |
| N6001 | shop | 'postcode': '12345' |
| N6002 | shop | 'postcode': '34343' |
| N6003 | shop | - |
Scenario: Postcode areas
When loading osm data
"""
n1 x12.36853 y51.50618
n2 x12.36853 y51.42362
n3 x12.63666 y51.42362
n4 x12.63666 y51.50618
w1 Tboundary=postal_code,ref=3456 Nn1,n2,n3,n4,n1
"""
Then place contains exactly
| object | class | type | name!dict |
| W1 | boundary | postal_code | 'ref': '3456' |
Scenario: Main with extra
When loading osm data
"""
n7001 Thighway=primary,bridge=yes,name=1
n7002 Thighway=primary,bridge=yes,bridge:name=1
"""
Then place contains exactly
| object | class | type | name!dict | extratags!dict |
| N7001 | highway | primary | 'name': '1' | 'bridge': 'yes' |
| N7002 | highway | primary | - | 'bridge': 'yes', 'bridge:name': '1' |
| N7002 | bridge | yes | 'name': '1' | 'highway': 'primary', 'bridge:name': '1' |
Scenario: Global fallback and skipping
When loading osm data
"""
n8001 Tshop=shoes,note:de=Nein,xx=yy
n8002 Tshop=shoes,natural=no,ele=234
n8003 Tshop=shoes,name:source=survey
"""
Then place contains exactly
| object | class | name!dict | extratags!dict |
| N8001 | shop | - | 'xx': 'yy' |
| N8002 | shop | - | 'ele': '234' |
| N8003 | shop | - | - |
Scenario: Admin levels
When loading osm data
"""
n9001 Tplace=city
n9002 Tplace=city,admin_level=16
n9003 Tplace=city,admin_level=x
n9004 Tplace=city,admin_level=1
n9005 Tplace=city,admin_level=0
n9006 Tplace=city,admin_level=2.5
"""
Then place contains exactly
| object | class | admin_level |
| N9001 | place | 15 |
| N9002 | place | 15 |
| N9003 | place | 15 |
| N9004 | place | 1 |
| N9005 | place | 15 |
| N9006 | place | 15 |
Scenario: Administrative boundaries with place tags
When loading osm data
"""
n10001 Tboundary=administrative,place=city,name=A
n10002 Tboundary=natural,place=city,name=B
n10003 Tboundary=administrative,place=island,name=C
"""
Then place contains
| object | class | type | extratags!dict |
| N10001 | boundary | administrative | 'place': 'city' |
And place contains
| object | class | type |
| N10002 | boundary | natural |
| N10002 | place | city |
| N10003 | boundary | administrative |
| N10003 | place | island |
Scenario: Building fallbacks
When loading osm data
"""
n12001 Ttourism=hotel,building=yes
n12002 Tbuilding=house
n12003 Tbuilding=shed,addr:housenumber=1
n12004 Tbuilding=yes,name=Das-Haus
n12005 Tbuilding=yes,addr:postcode=12345
"""
Then place contains exactly
| object | class | type |
| N12001 | tourism | hotel |
| N12003 | building | shed |
| N12004 | building | yes |
| N12005 | place | postcode |
Scenario: Address interpolations
When loading osm data
"""
n13001 Taddr:interpolation=odd
n13002 Taddr:interpolation=even,place=city
"""
Then place contains exactly
| object | class | type | address!dict |
| N13001 | place | houses | 'interpolation': 'odd' |
| N13002 | place | houses | 'interpolation': 'even' |
Scenario: Footways
When loading osm data
"""
n1 x0.0 y0.0
n2 x0 y0.0001
w1 Thighway=footway Nn1,n2
w2 Thighway=footway,name=Road Nn1,n2
w3 Thighway=footway,name=Road,footway=sidewalk Nn1,n2
w4 Thighway=footway,name=Road,footway=crossing Nn1,n2
w5 Thighway=footway,name=Road,footway=residential Nn1,n2
"""
Then place contains exactly
| object | name+name |
| W2 | Road |
| W5 | Road |
Scenario: Tourism information
When loading osm data
"""
n100 Ttourism=information
n101 Ttourism=information,name=Generic
n102 Ttourism=information,information=guidepost
n103 Thighway=information,information=house
n104 Ttourism=information,information=yes,name=Something
n105 Ttourism=information,information=route_marker,name=3
"""
Then place contains exactly
| object | class | type |
| N100 | tourism | information |
| N101 | tourism | information |
| N102 | information | guidepost |
| N103 | highway | information |
| N104 | tourism | information |
Scenario: Water features
When loading osm data
"""
n20 Tnatural=water
n21 Tnatural=water,name=SomePond
n22 Tnatural=water,water=pond
n23 Tnatural=water,water=pond,name=Pond
n24 Tnatural=water,water=river,name=BigRiver
n25 Tnatural=water,water=yes
n26 Tnatural=water,water=yes,name=Random
"""
Then place contains exactly
| object | class | type |
| N21 | natural | water |
| N23 | water | pond |
| N26 | natural | water |
Scenario: Drop name for address fallback
When loading osm data
"""
n1 Taddr:housenumber=23,name=Foo
n2 Taddr:housenumber=23,addr:housename=Foo
n3 Taddr:housenumber=23
"""
Then place contains exactly
| object | class | type | address!dict | name!dict |
| N1 | place | house | 'housenumber': '23' | - |
| N2 | place | house | 'housenumber': '23' | 'addr:housename': 'Foo' |
| N3 | place | house | 'housenumber': '23' | - |
Scenario: Waterway locks
When loading osm data
"""
n1 Twaterway=river,lock=yes
n2 Twaterway=river,lock=yes,lock_name=LeLock
n3 Twaterway=river,lock=yes,name=LeWater
n4 Tamenity=parking,lock=yes,lock_name=Gold
"""
Then place contains exactly
| object | class | type | name!dict |
| N2 | lock | yes | 'name': 'LeLock' |
| N3 | waterway | river | 'name': 'LeWater' |
| N4 | amenity | parking | - |

View File

@@ -0,0 +1,135 @@
Feature: Updates of address interpolation objects
Test that changes to address interpolation objects are correctly
propagated.
Background:
Given the grid
| 1 | 2 |
Scenario: Adding a new interpolation
When loading osm data
"""
n1 Taddr:housenumber=3
n2 Taddr:housenumber=17
w33 Thighway=residential,name=Tao Nn1,n2
"""
Then place contains
| object | class | type |
| N1 | place | house |
| N2 | place | house |
When updating osm data
"""
w99 Taddr:interpolation=odd Nn1,n2
"""
Then place contains
| object | class | type |
| N1 | place | house |
| N2 | place | house |
| W99 | place | houses |
When indexing
Then placex contains exactly
| object | class | type |
| N1 | place | house |
| N2 | place | house |
| W33 | highway | residential |
Then location_property_osmline contains exactly
| osm_id | startnumber |
| 99 | 5 |
Scenario: Delete an existing interpolation
When loading osm data
"""
n1 Taddr:housenumber=2
n2 Taddr:housenumber=7
w99 Taddr:interpolation=odd Nn1,n2
"""
Then place contains
| object | class | type |
| N1 | place | house |
| N2 | place | house |
| W99 | place | houses |
When updating osm data
"""
w99 v2 dD
"""
Then place contains
| object | class | type |
| N1 | place | house |
| N2 | place | house |
When indexing
Then placex contains exactly
| object | class | type |
| N1 | place | house |
| N2 | place | house |
Then location_property_osmline contains exactly
| osm_id |
Scenario: Changing an object to an interpolation
When loading osm data
"""
n1 Taddr:housenumber=3
n2 Taddr:housenumber=17
w33 Thighway=residential Nn1,n2
w99 Thighway=residential Nn1,n2
"""
Then place contains
| object | class | type |
| N1 | place | house |
| N2 | place | house |
| W99 | highway | residential |
When updating osm data
"""
w99 Taddr:interpolation=odd Nn1,n2
"""
Then place contains
| object | class | type |
| N1 | place | house |
| N2 | place | house |
| W99 | place | houses |
When indexing
Then placex contains exactly
| object | class | type |
| N1 | place | house |
| N2 | place | house |
| W33 | highway | residential |
And location_property_osmline contains exactly
| osm_id | startnumber |
| 99 | 5 |
Scenario: Changing an interpolation to something else
When loading osm data
"""
n1 Taddr:housenumber=3
n2 Taddr:housenumber=17
w99 Taddr:interpolation=odd Nn1,n2
"""
Then place contains
| object | class | type |
| N1 | place | house |
| N2 | place | house |
| W99 | place | houses |
When updating osm data
"""
w99 Thighway=residential Nn1,n2
"""
Then place contains
| object | class | type |
| N1 | place | house |
| N2 | place | house |
| W99 | highway | residential |
When indexing
Then placex contains exactly
| object | class | type |
| N1 | place | house |
| N2 | place | house |
| W99 | highway | residential |
And location_property_osmline contains exactly
| osm_id |

View File

@@ -0,0 +1,167 @@
Feature: Update of postcode only objects
Tests that changes to objects containing only a postcode are
propagated correctly.
Scenario: Adding a postcode-only node
When loading osm data
"""
n1
"""
Then place contains exactly
| object |
When updating osm data
"""
n34 Tpostcode=4456
"""
Then place contains exactly
| object | class | type |
| N34 | place | postcode |
When indexing
Then placex contains exactly
| object |
Scenario: Deleting a postcode-only node
When loading osm data
"""
n34 Tpostcode=4456
"""
Then place contains exactly
| object | class | type |
| N34 | place | postcode |
When updating osm data
"""
n34 v2 dD
"""
Then place contains exactly
| object |
When indexing
Then placex contains exactly
| object |
Scenario Outline: Converting a regular object into a postcode-only node
When loading osm data
"""
n34 T<class>=<type>
"""
Then place contains exactly
| object | class | type |
| N34 | <class> | <type> |
When updating osm data
"""
n34 Tpostcode=4456
"""
Then place contains exactly
| object | class | type |
| N34 | place | postcode |
When indexing
Then placex contains exactly
| object |
Examples:
| class | type |
| amenity | restaurant |
| place | hamlet |
Scenario Outline: Converting a postcode-only node into a regular object
When loading osm data
"""
n34 Tpostcode=4456
"""
Then place contains exactly
| object | class | type |
| N34 | place | postcode |
When updating osm data
"""
n34 T<class>=<type>
"""
Then place contains exactly
| object | class | type |
| N34 | <class> | <type> |
When indexing
Then placex contains exactly
| object | class | type |
| N34 | <class> | <type> |
Examples:
| class | type |
| amenity | restaurant |
| place | hamlet |
Scenario: Converting na interpolation into a postcode-only node
Given the grid
| 1 | 2 |
When loading osm data
"""
n1 Taddr:housenumber=3
n2 Taddr:housenumber=17
w34 Taddr:interpolation=odd Nn1,n2
"""
Then place contains exactly
| object | class | type |
| N1 | place | house |
| N2 | place | house |
| W34 | place | houses |
When updating osm data
"""
w34 Tpostcode=4456 Nn1,n2
"""
Then place contains exactly
| object | class | type |
| N1 | place | house |
| N2 | place | house |
| W34 | place | postcode |
When indexing
Then location_property_osmline contains exactly
| osm_id |
And placex contains exactly
| object | class | type |
| N1 | place | house |
| N2 | place | house |
Scenario: Converting a postcode-only node into an interpolation
Given the grid
| 1 | 2 |
When loading osm data
"""
n1 Taddr:housenumber=3
n2 Taddr:housenumber=17
w33 Thighway=residential Nn1,n2
w34 Tpostcode=4456 Nn1,n2
"""
Then place contains exactly
| object | class | type |
| N1 | place | house |
| N2 | place | house |
| W33 | highway | residential |
| W34 | place | postcode |
When updating osm data
"""
w34 Taddr:interpolation=odd Nn1,n2
"""
Then place contains exactly
| object | class | type |
| N1 | place | house |
| N2 | place | house |
| W33 | highway | residential |
| W34 | place | houses |
When indexing
Then location_property_osmline contains exactly
| osm_id | startnumber | endnumber |
| 34 | 5 | 15 |
And placex contains exactly
| object | class | type |
| N1 | place | house |
| N2 | place | house |
| W33 | highway | residential |

View File

@@ -0,0 +1,140 @@
Feature: Update of relations by osm2pgsql
Testing relation update by osm2pgsql.
Scenario: Remove all members of a relation
When loading osm data
"""
n1 Tamenity=prison,name=foo
n200 x0 y0
n201 x0 y0.0001
n202 x0.0001 y0.0001
n203 x0.0001 y0
w2 Tref=45' Nn200,n201,n202,n203,n200
r1 Ttype=multipolygon,tourism=hotel,name=XZ Mw2@
"""
Then place contains
| object | class | type | name!dict |
| R1 | tourism | hotel | 'name' : 'XZ' |
When updating osm data
"""
r1 Ttype=multipolygon,tourism=hotel,name=XZ Mn1@
"""
Then place has no entry for R1
Scenario: Change type of a relation
When loading osm data
"""
n200 x0 y0
n201 x0 y0.0001
n202 x0.0001 y0.0001
n203 x0.0001 y0
w2 Tref=45 Nn200,n201,n202,n203,n200
r1 Ttype=multipolygon,tourism=hotel,name=XZ Mw2@
"""
Then place contains
| object | class | type | name!dict |
| R1 | tourism | hotel | 'name' : 'XZ' |
When updating osm data
"""
r1 Ttype=multipolygon,amenity=prison,name=XZ Mw2@
"""
Then place has no entry for R1:tourism
And place contains
| object | class | type | name!dict |
| R1 | amenity | prison | 'name' : 'XZ' |
Scenario: Change name of a relation
When loading osm data
"""
n200 x0 y0
n201 x0 y0.0001
n202 x0.0001 y0.0001
n203 x0.0001 y0
w2 Tref=45 Nn200,n201,n202,n203,n200
r1 Ttype=multipolygon,tourism=hotel,name=AB Mw2@
"""
Then place contains
| object | class | type | name!dict |
| R1 | tourism | hotel | 'name' : 'AB' |
When updating osm data
"""
r1 Ttype=multipolygon,tourism=hotel,name=XY Mw2@
"""
Then place contains
| object | class | type | name!dict |
| R1 | tourism | hotel | 'name' : 'XY' |
Scenario: Change type of a relation into something unknown
When loading osm data
"""
n200 x0 y0
n201 x0 y0.0001
n202 x0.0001 y0.0001
n203 x0.0001 y0
w2 Tref=45 Nn200,n201,n202,n203,n200
r1 Ttype=multipolygon,tourism=hotel,name=XY Mw2@
"""
Then place contains
| object | class | type | name!dict |
| R1 | tourism | hotel | 'name' : 'XY' |
When updating osm data
"""
r1 Ttype=multipolygon,amenities=prison,name=XY Mw2@
"""
Then place has no entry for R1
Scenario: Type tag is removed
When loading osm data
"""
n200 x0 y0
n201 x0 y0.0001
n202 x0.0001 y0.0001
n203 x0.0001 y0
w2 Tref=45 Nn200,n201,n202,n203,n200
r1 Ttype=multipolygon,tourism=hotel,name=XY Mw2@
"""
Then place contains
| object | class | type | name!dict |
| R1 | tourism | hotel | 'name' : 'XY' |
When updating osm data
"""
r1 Ttourism=hotel,name=XY Mw2@
"""
Then place has no entry for R1
Scenario: Type tag is renamed to something unknown
When loading osm data
"""
n200 x0 y0
n201 x0 y0.0001
n202 x0.0001 y0.0001
n203 x0.0001 y0
w2 Tref=45 Nn200,n201,n202,n203,n200
r1 Ttype=multipolygon,tourism=hotel,name=XY Mw2@
"""
Then place contains
| object | class | type | name!dict |
| R1 | tourism | hotel | 'name' : 'XY' |
When updating osm data
"""
r1 Ttype=multipolygonn,tourism=hotel,name=XY Mw2@
"""
Then place has no entry for R1
Scenario: Country boundary names are left untouched when country_code unknown
When loading osm data
"""
n200 Tamenity=prison x0 y0
n201 x0 y0.0001
n202 x0.0001 y0.0001
n203 x0.0001 y0
"""
And updating osm data
"""
w1 Nn200,n201,n202,n203,n200
r1 Ttype=boundary,boundary=administrative,name=Foo,country_code=XX,admin_level=2 Mw1@
"""
Then place contains
| object | address+country | name!dict |
| R1 | XX | 'name' : 'Foo' |

View File

@@ -0,0 +1,48 @@
Feature: Update of simple objects by osm2pgsql
Testing basic update functions of osm2pgsql.
Scenario: Adding a new object
When loading osm data
"""
n1 Tplace=town,name=Middletown
"""
Then place contains exactly
| object | class | type | name+name |
| N1 | place | town | Middletown |
When updating osm data
"""
n2 Tamenity=hotel,name=Posthotel
"""
Then place contains exactly
| object | class | type | name+name |
| N1 | place | town | Middletown |
| N2 | amenity | hotel | Posthotel |
And placex contains exactly
| object | class | type | name+name | indexed_status |
| N1 | place | town | Middletown | 0 |
| N2 | amenity | hotel | Posthotel | 1 |
Scenario: Deleting an existing object
When loading osm data
"""
n1 Tplace=town,name=Middletown
n2 Tamenity=hotel,name=Posthotel
"""
Then place contains exactly
| object | class | type | name+name |
| N1 | place | town | Middletown |
| N2 | amenity | hotel | Posthotel |
When updating osm data
"""
n2 dD
"""
Then place contains exactly
| object | class | type | name+name |
| N1 | place | town | Middletown |
And placex contains exactly
| object | class | type | name+name | indexed_status |
| N1 | place | town | Middletown | 0 |
| N2 | amenity | hotel | Posthotel | 100 |

View File

@@ -0,0 +1,512 @@
Feature: Tag evaluation
Tests if tags are correctly updated in the place table
Background:
Given the grid
| 1 | 2 | 3 |
| 10 | 11 | |
| 45 | 46 | |
Scenario: Main tag deleted
When loading osm data
"""
n1 Tamenity=restaurant
n2 Thighway=bus_stop,railway=stop,name=X
n3 Tamenity=prison
"""
Then place contains exactly
| object | class | type |
| N1 | amenity | restaurant |
| N2 | highway | bus_stop |
| N2 | railway | stop |
| N3 | amenity | prison |
When updating osm data
"""
n1 Tnot_a=restaurant
n2 Thighway=bus_stop,name=X
"""
Then place contains exactly
| object | class | type |
| N2 | highway | bus_stop |
| N3 | amenity | prison |
And placex contains
| object | class | indexed_status |
| N3 | amenity | 0 |
When indexing
Then placex contains exactly
| object | class | type | name!dict |
| N2 | highway | bus_stop | 'name': 'X' |
| N3 | amenity | prison | - |
Scenario: Main tag added
When loading osm data
"""
n1 Tatity=restaurant
n2 Thighway=bus_stop,name=X
"""
Then place contains exactly
| object | class | type |
| N2 | highway | bus_stop |
When updating osm data
"""
n1 Tamenity=restaurant
n2 Thighway=bus_stop,railway=stop,name=X
"""
Then place contains exactly
| object | class | type |
| N1 | amenity | restaurant |
| N2 | highway | bus_stop |
| N2 | railway | stop |
When indexing
Then placex contains exactly
| object | class | type | name!dict |
| N1 | amenity | restaurant | - |
| N2 | highway | bus_stop | 'name': 'X' |
| N2 | railway | stop | 'name': 'X' |
Scenario: Main tag modified
When loading osm data
"""
n10 Thighway=footway,name=X
n11 Tamenity=atm
"""
Then place contains exactly
| object | class | type |
| N10 | highway | footway |
| N11 | amenity | atm |
When updating osm data
"""
n10 Thighway=path,name=X
n11 Thighway=primary
"""
Then place contains exactly
| object | class | type |
| N10 | highway | path |
| N11 | highway | primary |
When indexing
Then placex contains exactly
| object | class | type | name!dict |
| N10 | highway | path | 'name': 'X' |
| N11 | highway | primary | - |
Scenario: Main tags with name, name added
When loading osm data
"""
n45 Tlanduse=cemetry
n46 Tbuilding=yes
"""
Then place contains exactly
| object | class | type |
When updating osm data
"""
n45 Tlanduse=cemetry,name=TODO
n46 Tbuilding=yes,addr:housenumber=1
"""
Then place contains exactly
| object | class | type |
| N45 | landuse | cemetry |
| N46 | building| yes |
When indexing
Then placex contains exactly
| object | class | type | name!dict | address!dict |
| N45 | landuse | cemetry | 'name': 'TODO' | - |
| N46 | building| yes | - | 'housenumber': '1' |
Scenario: Main tags with name, name removed
When loading osm data
"""
n45 Tlanduse=cemetry,name=TODO
n46 Tbuilding=yes,addr:housenumber=1
"""
Then place contains exactly
| object | class | type |
| N45 | landuse | cemetry |
| N46 | building| yes |
When updating osm data
"""
n45 Tlanduse=cemetry
n46 Tbuilding=yes
"""
Then place contains exactly
| object | class | type |
When indexing
Then placex contains exactly
| object |
Scenario: Main tags with name, name modified
When loading osm data
"""
n45 Tlanduse=cemetry,name=TODO
n46 Tbuilding=yes,addr:housenumber=1
"""
Then place contains exactly
| object | class | type | name!dict | address!dict |
| N45 | landuse | cemetry | 'name' : 'TODO' | - |
| N46 | building| yes | - | 'housenumber': '1'|
When updating osm data
"""
n45 Tlanduse=cemetry,name=DONE
n46 Tbuilding=yes,addr:housenumber=10
"""
Then place contains exactly
| object | class | type | name!dict | address!dict |
| N45 | landuse | cemetry | 'name' : 'DONE' | - |
| N46 | building| yes | - | 'housenumber': '10'|
When indexing
Then placex contains exactly
| object | class | type | name!dict | address!dict |
| N45 | landuse | cemetry | 'name' : 'DONE' | - |
| N46 | building| yes | - | 'housenumber': '10'|
Scenario: Main tag added to address only node
When loading osm data
"""
n1 Taddr:housenumber=345
"""
Then place contains exactly
| object | class | type | address!dict |
| N1 | place | house | 'housenumber': '345'|
When updating osm data
"""
n1 Taddr:housenumber=345,building=yes
"""
Then place contains exactly
| object | class | type | address!dict |
| N1 | building | yes | 'housenumber': '345'|
When indexing
Then placex contains exactly
| object | class | type | address!dict |
| N1 | building | yes | 'housenumber': '345'|
Scenario: Main tag removed from address only node
When loading osm data
"""
n1 Taddr:housenumber=345,building=yes
"""
Then place contains exactly
| object | class | type | address!dict |
| N1 | building | yes | 'housenumber': '345'|
When updating osm data
"""
n1 Taddr:housenumber=345
"""
Then place contains exactly
| object | class | type | address!dict |
| N1 | place | house | 'housenumber': '345'|
When indexing
Then placex contains exactly
| object | class | type | address!dict |
| N1 | place | house | 'housenumber': '345'|
Scenario: Main tags with name key, adding key name
When loading osm data
"""
n2 Tbridge=yes
"""
Then place contains exactly
| object | class | type |
When updating osm data
"""
n2 Tbridge=yes,bridge:name=high
"""
Then place contains exactly
| object | class | type | name!dict |
| N2 | bridge | yes | 'name': 'high' |
When indexing
Then placex contains exactly
| object | class | type | name!dict |
| N2 | bridge | yes | 'name': 'high' |
Scenario: Main tags with name key, deleting key name
When loading osm data
"""
n2 Tbridge=yes,bridge:name=high
"""
Then place contains exactly
| object | class | type | name!dict |
| N2 | bridge | yes | 'name': 'high' |
When updating osm data
"""
n2 Tbridge=yes
"""
Then place contains exactly
| object |
When indexing
Then placex contains exactly
| object |
Scenario: Main tags with name key, changing key name
When loading osm data
"""
n2 Tbridge=yes,bridge:name=high
"""
Then place contains exactly
| object | class | type | name!dict |
| N2 | bridge | yes | 'name': 'high' |
When updating osm data
"""
n2 Tbridge=yes,bridge:name:en=high
"""
Then place contains exactly
| object | class | type | name!dict |
| N2 | bridge | yes | 'name:en': 'high' |
When indexing
Then placex contains exactly
| object | class | type | name!dict |
| N2 | bridge | yes | 'name:en': 'high' |
Scenario: Downgrading a highway to one that is dropped without name
When loading osm data
"""
n100 x0 y0
n101 x0.0001 y0.0001
w1 Thighway=residential Nn100,n101
"""
Then place contains exactly
| object | class |
| W1 | highway |
When updating osm data
"""
w1 Thighway=service Nn100,n101
"""
Then place contains exactly
| object |
When indexing
Then placex contains exactly
| object |
Scenario: Upgrading a highway to one that is not dropped without name
When loading osm data
"""
n100 x0 y0
n101 x0.0001 y0.0001
w1 Thighway=service Nn100,n101
"""
Then place contains exactly
| object |
When updating osm data
"""
w1 Thighway=unclassified Nn100,n101
"""
Then place contains exactly
| object | class |
| W1 | highway |
When indexing
Then placex contains exactly
| object | class |
| W1 | highway |
Scenario: Downgrading a highway when a second tag is present
When loading osm data
"""
n100 x0 y0
n101 x0.0001 y0.0001
w1 Thighway=residential,tourism=hotel Nn100,n101
"""
Then place contains exactly
| object | class | type |
| W1 | highway | residential |
| W1 | tourism | hotel |
When updating osm data
"""
w1 Thighway=service,tourism=hotel Nn100,n101
"""
Then place contains exactly
| object | class | type |
| W1 | tourism | hotel |
When indexing
Then placex contains exactly
| object | class | type |
| W1 | tourism | hotel |
Scenario: Upgrading a highway when a second tag is present
When loading osm data
"""
n100 x0 y0
n101 x0.0001 y0.0001
w1 Thighway=service,tourism=hotel Nn100,n101
"""
Then place contains exactly
| object | class | type |
| W1 | tourism | hotel |
When updating osm data
"""
w1 Thighway=residential,tourism=hotel Nn100,n101
"""
Then place contains exactly
| object | class | type |
| W1 | highway | residential |
| W1 | tourism | hotel |
When indexing
Then placex contains exactly
| object | class | type |
| W1 | highway | residential |
| W1 | tourism | hotel |
Scenario: Replay on administrative boundary
When loading osm data
"""
n10 x34.0 y-4.23
n11 x34.1 y-4.23
n12 x34.2 y-4.13
w10 Tboundary=administrative,waterway=river,name=Border,admin_level=2 Nn12,n11,n10
"""
Then place contains exactly
| object | class | type | admin_level | name!dict |
| W10 | waterway | river | 2 | 'name': 'Border' |
| W10 | boundary | administrative | 2 | 'name': 'Border' |
When updating osm data
"""
w10 Tboundary=administrative,waterway=river,name=Border,admin_level=2 Nn12,n11,n10
"""
Then place contains exactly
| object | class | type | admin_level | name!dict |
| W10 | waterway | river | 2 | 'name': 'Border' |
| W10 | boundary | administrative | 2 | 'name': 'Border' |
When indexing
Then placex contains exactly
| object | class | type | admin_level | name!dict |
| W10 | waterway | river | 2 | 'name': 'Border' |
Scenario: Change admin_level on administrative boundary
Given the grid
| 10 | 11 |
| 13 | 12 |
When loading osm data
"""
n10
n11
n12
n13
w10 Nn10,n11,n12,n13,n10
r10 Ttype=multipolygon,boundary=administrative,name=Border,admin_level=2 Mw10@
"""
Then place contains exactly
| object | class | admin_level |
| R10 | boundary | 2 |
When updating osm data
"""
r10 Ttype=multipolygon,boundary=administrative,name=Border,admin_level=4 Mw10@
"""
Then place contains exactly
| object | class | type | admin_level |
| R10 | boundary | administrative | 4 |
When indexing
Then placex contains exactly
| object | class | type | admin_level |
| R10 | boundary | administrative | 4 |
Scenario: Change boundary to administrative
Given the grid
| 10 | 11 |
| 13 | 12 |
When loading osm data
"""
n10
n11
n12
n13
w10 Nn10,n11,n12,n13,n10
r10 Ttype=multipolygon,boundary=informal,name=Border,admin_level=4 Mw10@
"""
Then place contains exactly
| object | class | type | admin_level |
| R10 | boundary | informal | 4 |
When updating osm data
"""
r10 Ttype=multipolygon,boundary=administrative,name=Border,admin_level=4 Mw10@
"""
Then place contains exactly
| object | class | type | admin_level |
| R10 | boundary | administrative | 4 |
When indexing
Then placex contains exactly
| object | class | type | admin_level |
| R10 | boundary | administrative | 4 |
Scenario: Change boundary away from administrative
Given the grid
| 10 | 11 |
| 13 | 12 |
When loading osm data
"""
n10
n11
n12
n13
w10 Nn10,n11,n12,n13,n10
r10 Ttype=multipolygon,boundary=administrative,name=Border,admin_level=4 Mw10@
"""
Then place contains exactly
| object | class | type | admin_level |
| R10 | boundary | administrative | 4 |
When updating osm data
"""
r10 Ttype=multipolygon,boundary=informal,name=Border,admin_level=4 Mw10@
"""
Then place contains exactly
| object | class | type | admin_level |
| R10 | boundary | informal | 4 |
When indexing
Then placex contains exactly
| object | class | type | admin_level |
| R10 | boundary | informal | 4 |
Scenario: Main tag and geometry is changed
When loading osm data
"""
n1 x40 y40
n2 x40.0001 y40
n3 x40.0001 y40.0001
n4 x40 y40.0001
w5 Tbuilding=house,name=Foo Nn1,n2,n3,n4,n1
"""
Then place contains exactly
| object | class | type |
| W5 | building | house |
When updating osm data
"""
n1 x39.999 y40
w5 Tbuilding=terrace,name=Bar Nn1,n2,n3,n4,n1
"""
Then place contains exactly
| object | class | type |
| W5 | building | terrace |

152
test/bdd/test_osm2pgsql.py Normal file
View File

@@ -0,0 +1,152 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# This file is part of Nominatim. (https://nominatim.org)
#
# Copyright (C) 2025 by the Nominatim developer community.
# For a full list of authors see the git log.
"""
Collector for BDD osm2pgsql import style tests.
"""
import asyncio
import random
import psycopg
import pytest
from pytest_bdd.parsers import re as step_parse
from pytest_bdd import scenarios, when, given, then
from nominatim_db import cli
from nominatim_db.config import Configuration
from nominatim_db.tools.exec_utils import run_osm2pgsql
from nominatim_db.tools.database_import import load_data, create_table_triggers
from nominatim_db.tools.replication import run_osm2pgsql_updates
from utils.db import DBManager
from utils.checks import check_table_content, check_table_has_lines
@pytest.fixture
def def_config(pytestconfig):
dbname = pytestconfig.getini('nominatim_test_db')
return Configuration(None,
environ={'NOMINATIM_DATABASE_DSN': f"pgsql:dbname={dbname}"})
@pytest.fixture
def db(template_db, pytestconfig):
""" Set up an empty database for use with osm2pgsql.
"""
dbm = DBManager(purge=pytestconfig.option.NOMINATIM_PURGE)
dbname = pytestconfig.getini('nominatim_test_db')
dbm.create_db_from_template(dbname, template_db)
yield dbname
if not pytestconfig.option.NOMINATIM_KEEP_DB:
dbm.drop_db(dbname)
@pytest.fixture
def db_conn(def_config):
with psycopg.connect(def_config.get_libpq_dsn()) as conn:
info = psycopg.types.TypeInfo.fetch(conn, "hstore")
psycopg.types.hstore.register_hstore(info, conn)
yield conn
@pytest.fixture
def osm2pgsql_options(def_config):
return dict(osm2pgsql='osm2pgsql',
osm2pgsql_cache=50,
osm2pgsql_style=str(def_config.get_import_style_file()),
osm2pgsql_style_path=def_config.lib_dir.lua,
threads=1,
dsn=def_config.get_libpq_dsn(),
flatnode_file='',
tablespaces=dict(slim_data='', slim_index='',
main_data='', main_index=''),
append=False)
@pytest.fixture
def opl_writer(tmp_path, node_grid):
nr = [0]
def _write(data):
fname = tmp_path / f"test_osm_{nr[0]}.opl"
nr[0] += 1
with fname.open('wt') as fd:
for line in data.split('\n'):
if line.startswith('n') and ' x' not in line:
coord = node_grid.get(line[1:].split(' ')[0]) \
or (random.uniform(-180, 180), random.uniform(-90, 90))
line = f"{line} x{coord[0]:.7f} y{coord[1]:.7f}"
fd.write(line)
fd.write('\n')
return fname
return _write
@given('the lua style file', target_fixture='osm2pgsql_options')
def set_lua_style_file(osm2pgsql_options, docstring, tmp_path):
style = tmp_path / 'custom.lua'
style.write_text(docstring)
osm2pgsql_options['osm2pgsql_style'] = str(style)
return osm2pgsql_options
@when('loading osm data')
def load_from_osm_file(db, osm2pgsql_options, opl_writer, docstring):
""" 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.
"""
osm2pgsql_options['import_file'] = opl_writer(docstring.replace(r'//', r'/'))
osm2pgsql_options['append'] = False
run_osm2pgsql(osm2pgsql_options)
@when('updating osm data')
def update_from_osm_file(db_conn, def_config, osm2pgsql_options, opl_writer, docstring):
""" Update a database previously populated with 'loading osm data'.
Needs to run indexing on the existing data first to yield the correct
result.
The data is expected as attached text in OPL format.
"""
create_table_triggers(db_conn, def_config)
asyncio.run(load_data(def_config.get_libpq_dsn(), 1))
cli.nominatim(['index'], def_config.environ)
cli.nominatim(['refresh', '--functions'], def_config.environ)
osm2pgsql_options['import_file'] = opl_writer(docstring.replace(r'//', r'/'))
run_osm2pgsql_updates(db_conn, osm2pgsql_options)
@when('indexing')
def do_index(def_config):
""" Run Nominatim's indexing step.
"""
cli.nominatim(['index'], def_config.environ)
@then(step_parse(r'(?P<table>\w+) contains(?P<exact> exactly)?'))
def check_place_content(db_conn, datatable, node_grid, table, exact):
check_table_content(db_conn, table, datatable, grid=node_grid, exact=bool(exact))
@then(step_parse('(?P<table>placex?) has no entry for '
r'(?P<osm_type>[NRW])(?P<osm_id>\d+)(?::(?P<osm_class>\S+))?'),
converters={'osm_id': int})
def check_place_missing_lines(db_conn, table, osm_type, osm_id, osm_class):
check_table_has_lines(db_conn, table, osm_type, osm_id, osm_class)
scenarios('features/osm2pgsql')

View File

@@ -9,6 +9,12 @@ Helper functions to compare expected values.
"""
import json
import re
import math
import itertools
from psycopg import sql as pysql
from psycopg.rows import dict_row, tuple_row
from .geometry_alias import ALIASES
COMPARATOR_TERMS = {
'exactly': lambda exp, act: exp == act,
@@ -43,10 +49,12 @@ COMPARISON_FUNCS = {
None: lambda val, exp: str(val) == exp,
'i': lambda val, exp: str(val).lower() == exp.lower(),
'fm': lambda val, exp: re.fullmatch(exp, val) is not None,
'dict': lambda val, exp: val is None if exp == '-' else (val == eval('{' + exp + '}')),
'in_box': within_box
}
OSM_TYPE = {'node': 'n', 'way': 'w', 'relation': 'r'}
OSM_TYPE = {'node': 'n', 'way': 'w', 'relation': 'r',
'N': 'n', 'W': 'w', 'R': 'r'}
class ResultAttr:
@@ -60,12 +68,15 @@ class ResultAttr:
Available formatters:
!:... - use a formatting expression according to Python Mini Format Spec
!i - make case-insensitive comparison
!fm - consider comparison string a regular expression and match full value
!:... - use a formatting expression according to Python Mini Format Spec
!i - make case-insensitive comparison
!fm - consider comparison string a regular expression and match full value
!wkt - convert the expected value to a WKT string before comparing
!in_box - the expected value is a comma-separated bbox description
"""
def __init__(self, obj, key):
def __init__(self, obj, key, grid=None):
self.grid = grid
self.obj = obj
if '!' in key:
self.key, self.fmt = key.rsplit('!', 1)
@@ -100,6 +111,9 @@ class ResultAttr:
if self.fmt.startswith(':'):
return other == f"{{{self.fmt}}}".format(self.subobj)
if self.fmt == 'wkt':
return self.compare_wkt(self.subobj, other)
raise RuntimeError(f"Unknown format string '{self.fmt}'.")
def __repr__(self):
@@ -107,3 +121,125 @@ class ResultAttr:
if self.fmt:
k += '!' + self.fmt
return f"result[{k}]({self.subobj})"
def compare_wkt(self, value, expected):
""" Compare a WKT value against a compact geometry format.
The function understands the following formats:
country:<country code>
Point geometry guaranteed to be in the given country
<P>
Point geometry
<P>,...,<P>
Line geometry
(<P>,...,<P>)
Polygon geometry
<P> may either be a coordinate of the form '<x> <y>' or a single
number. In the latter case it must refer to a point in
a previously defined grid.
"""
m = re.fullmatch(r'(POINT)\(([0-9. -]*)\)', value) \
or re.fullmatch(r'(LINESTRING)\(([0-9,. -]*)\)', value) \
or re.fullmatch(r'(POLYGON)\(\(([0-9,. -]*)\)\)', value)
if not m:
return False
converted = [list(map(float, pt.split(' ', 1)))
for pt in map(str.strip, m[2].split(','))]
if expected.startswith('country:'):
ccode = geom[8:].upper()
assert ccode in ALIASES, f"Geometry error: unknown country {ccode}"
return m[1] == 'POINT' and \
all(math.isclose(p1, p2) for p1, p2 in
zip(converted[0], ALIASES[ccode]))
if ',' not in expected:
return m[1] == 'POINT' and \
all(math.isclose(p1, p2) for p1, p2 in
zip(converted[0], self.get_point(expected)))
if '(' not in expected:
return m[1] == 'LINESTRING' and \
all(math.isclose(p1[0], p2[0]) and math.isclose(p1[1], p2[1]) for p1, p2 in
zip(converted, (self.get_point(p) for p in expected.split(','))))
if m[1] != 'POLYGON':
return False
# Polygon comparison is tricky because the polygons don't necessarily
# end at the same point or have the same winding order.
# Brute force all possible variants of the expected polygon
exp_coords = [self.get_point(p) for p in expected[1:-1].split(',')]
if exp_coords[0] != exp_coords[-1]:
raise RuntimeError(f"Invalid polygon {expected}. "
"First and last point need to be the same")
for line in (exp_coords[:-1], exp_coords[-1:0:-1]):
for i in range(len(line)):
if all(math.isclose(p1[0], p2[0]) and math.isclose(p1[1], p2[1]) for p1, p2 in
zip(converted, line[i:] + line[:i])):
return True
return False
def get_point(self, pt):
pt = pt.strip()
if ' ' in pt:
return list(map(float, pt.split(' ', 1)))
assert self.grid
return self.grid.get(pt)
def check_table_content(conn, tablename, data, grid=None, exact=False):
lines = set(range(1, len(data)))
cols = []
for col in data[0]:
if col == 'object':
cols.extend(('osm_id', 'osm_type'))
elif '!' in col:
name, fmt = col.rsplit('!', 1)
if fmt == 'wkt':
cols.append(f"ST_AsText({name}) as {name}")
else:
cols.append(name.split('+')[0])
else:
cols.append(col.split('+')[0])
with conn.cursor(row_factory=dict_row) as cur:
cur.execute(pysql.SQL(f"SELECT {','.join(cols)} FROM")
+ pysql.Identifier(tablename))
table_content = ''
for row in cur:
table_content += '\n' + str(row)
for i in lines:
for col, value in zip(data[0], data[i]):
if ResultAttr(row, col, grid=grid) != value:
break
else:
lines.remove(i)
break
else:
assert not exact, f"Unexpected row in table {tablename}: {row}"
assert not lines, \
"Rows not found:\n" \
+ '\n'.join(str(data[i]) for i in lines) \
+ "\nTable content:\n" \
+ table_content
def check_table_has_lines(conn, tablename, osm_type, osm_id, osm_class):
sql = pysql.SQL("""SELECT count(*) FROM {}
WHERE osm_type = %s and osm_id = %s""").format(pysql.Identifier(tablename))
params = [osm_type, int(osm_id)]
if osm_class:
sql += pysql.SQL(' AND class = %s')
params.append(osm_class)
with conn.cursor(row_factory=tuple_row) as cur:
assert cur.execute(sql, params).fetchone()[0] == 0

View File

@@ -7,9 +7,16 @@
"""
Helper functions for managing test databases.
"""
import asyncio
import psycopg
from psycopg import sql as pysql
from nominatim_db.tools.database_import import setup_database_skeleton, create_tables, \
create_partition_tables, create_search_indices
from nominatim_db.data.country_info import setup_country_tables
from nominatim_db.tools.refresh import create_functions, load_address_levels_from_config
from nominatim_db.tools.exec_utils import run_osm2pgsql
from nominatim_db.tokenizer import factory as tokenizer_factory
class DBManager:
@@ -42,3 +49,53 @@ class DBManager:
cur = conn.execute('select count(*) from pg_database where datname = %s',
(dbname,))
return cur.fetchone()[0] == 1
def create_db_from_template(self, dbname, template):
""" Create a new database from the given template database.
Any existing database with the same name will be dropped.
"""
with psycopg.connect(dbname='postgres') as conn:
conn.autocommit = True
conn.execute(pysql.SQL('DROP DATABASE IF EXISTS')
+ pysql.Identifier(dbname))
conn.execute(pysql.SQL('CREATE DATABASE {} WITH TEMPLATE {}')
.format(pysql.Identifier(dbname),
pysql.Identifier(template)))
def setup_template_db(self, config):
""" Create a template DB which contains the necessary extensions
and basic static tables.
The template will only be created if the database does not yet
exist or 'purge' is set.
"""
dsn = config.get_libpq_dsn()
if self.check_for_db(config.get_database_params()['dbname']):
return
setup_database_skeleton(dsn)
run_osm2pgsql(dict(osm2pgsql='osm2pgsql',
osm2pgsql_cache=1,
osm2pgsql_style=str(config.get_import_style_file()),
osm2pgsql_style_path=config.lib_dir.lua,
threads=1,
dsn=dsn,
flatnode_file='',
tablespaces=dict(slim_data='', slim_index='',
main_data='', main_index=''),
append=False,
import_data=b'<osm version="0.6"></osm>'))
setup_country_tables(dsn, config.lib_dir.data)
with psycopg.connect(dsn) as conn:
create_tables(conn, config)
load_address_levels_from_config(conn, config)
create_partition_tables(conn, config)
create_functions(conn, config, enable_diff_updates=False)
asyncio.run(create_search_indices(conn, config))
tokenizer_factory.create_tokenizer(config)

View File

@@ -0,0 +1,262 @@
# SPDX-License-Identifier: GPL-2.0-only
#
# This file is part of Nominatim. (https://nominatim.org)
#
# 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)
}

34
test/bdd/utils/grid.py Normal file
View File

@@ -0,0 +1,34 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# This file is part of Nominatim. (https://nominatim.org)
#
# Copyright (C) 2025 by the Nominatim developer community.
# For a full list of authors see the git log.
"""
A grid describing node placement in an area.
Useful for visually describing geometries.
"""
class Grid:
def __init__(self, table, step, origin):
if step is None:
step = 0.00001
if origin is None:
origin = (0.0, 0.0)
self.grid = {}
y = origin[1]
for line in table:
x = origin[0]
for pt_id in line:
if pt_id:
self.grid[pt_id] = (x, y)
x += step
y += step
def get(self, nodeid):
""" Get the coordinates for the given grid node.
"""
return self.grid.get(nodeid)