1 """ Steps for setting up a test database with imports and updates.
3 There are two ways to state geometries for test data: with coordinates
6 Coordinates should be given as a wkt without the enclosing type name.
8 Scenes are prepared geometries which can be found in the scenes/data/
9 directory. Each scene is saved in a .wkt file with its name, which
10 contains a list of id/wkt pairs. A scene can be set globally
11 for a scene by using the step `the scene <scene name>`. Then each
12 object should be refered to as `:<object id>`. A geometry can also
13 be referred to without loading the scene by explicitly stating the
14 scene: `<scene name>:<object id>`.
17 from nose.tools import *
20 import psycopg2.extensions
21 import psycopg2.extras
28 psycopg2.extensions.register_type(psycopg2.extensions.UNICODE)
31 def setup_test_database(scenario):
32 """ Creates a new test database from the template database
33 that was set up earlier in terrain.py. Will be done only
34 for scenarios whose feature is tagged with 'DB'.
36 if scenario.feature.tags is not None and 'DB' in scenario.feature.tags:
37 world.db_template_setup()
38 world.write_nominatim_config(world.config.test_db)
39 conn = psycopg2.connect(database=world.config.template_db)
40 conn.set_isolation_level(0)
42 cur.execute('DROP DATABASE IF EXISTS %s' % (world.config.test_db, ))
43 cur.execute('CREATE DATABASE %s TEMPLATE = %s' % (world.config.test_db, world.config.template_db))
45 world.conn = psycopg2.connect(database=world.config.test_db)
46 psycopg2.extras.register_hstore(world.conn, globally=False, unicode=True)
48 @step('a wiped database')
49 def db_setup_wipe_db(step):
50 """Explicit DB scenario setup only needed
51 to work around a bug where scenario outlines don't call
52 before_each_scenario correctly.
54 if hasattr(world, 'conn'):
56 conn = psycopg2.connect(database=world.config.template_db)
57 conn.set_isolation_level(0)
59 cur.execute('DROP DATABASE IF EXISTS %s' % (world.config.test_db, ))
60 cur.execute('CREATE DATABASE %s TEMPLATE = %s' % (world.config.test_db, world.config.template_db))
62 world.conn = psycopg2.connect(database=world.config.test_db)
63 psycopg2.extras.register_hstore(world.conn, globally=False, unicode=True)
67 def tear_down_test_database(scenario):
68 """ Drops any previously created test database.
70 if hasattr(world, 'conn'):
72 if scenario.feature.tags is not None and 'DB' in scenario.feature.tags and not world.config.keep_scenario_db:
73 conn = psycopg2.connect(database=world.config.template_db)
74 conn.set_isolation_level(0)
76 cur.execute('DROP DATABASE %s' % (world.config.test_db,))
80 def _format_placex_cols(cols, geomtype, force_name):
82 if cols['name'].startswith("'"):
83 cols['name'] = world.make_hash(cols['name'])
85 cols['name'] = { 'name' : cols['name'] }
87 cols['name'] = { 'name' : base64.urlsafe_b64encode(os.urandom(int(random.random()*30))) }
88 if 'extratags' in cols:
89 cols['extratags'] = world.make_hash(cols['extratags'])
90 if 'admin_level' not in cols:
91 cols['admin_level'] = 100
92 if 'geometry' in cols:
93 coords = world.get_scene_geometry(cols['geometry'])
95 coords = "'%s(%s)'::geometry" % (geomtype, cols['geometry'])
97 coords = "'%s'::geometry" % coords.wkt
98 cols['geometry'] = coords
104 def _insert_place_table_nodes(places, force_name):
105 cur = world.conn.cursor()
108 cols['osm_type'] = 'N'
109 _format_placex_cols(cols, 'POINT', force_name)
110 if 'geometry' in cols:
111 coords = cols.pop('geometry')
113 coords = "ST_Point(%f, %f)" % (random.random()*360 - 180, random.random()*180 - 90)
115 query = 'INSERT INTO place (%s,geometry) values(%s, ST_SetSRID(%s, 4326))' % (
116 ','.join(cols.iterkeys()),
117 ','.join(['%s' for x in range(len(cols))]),
120 cur.execute(query, cols.values())
124 def _insert_place_table_objects(places, geomtype, force_name):
125 cur = world.conn.cursor()
128 if 'osm_type' not in cols:
129 cols['osm_type'] = 'W'
130 _format_placex_cols(cols, geomtype, force_name)
131 coords = cols.pop('geometry')
133 query = 'INSERT INTO place (%s, geometry) values(%s, ST_SetSRID(%s, 4326))' % (
134 ','.join(cols.iterkeys()),
135 ','.join(['%s' for x in range(len(cols))]),
138 cur.execute(query, cols.values())
141 @step(u'the scene (.*)')
142 def import_set_scene(step, scene):
143 world.load_scene(scene)
145 @step(u'the (named )?place (node|way|area)s')
146 def import_place_table_nodes(step, named, osmtype):
147 """Insert a list of nodes into the place table.
148 Expects a table where columns are named in the same way as place.
150 cur = world.conn.cursor()
151 cur.execute('ALTER TABLE place DISABLE TRIGGER place_before_insert')
152 if osmtype == 'node':
153 _insert_place_table_nodes(step.hashes, named is not None)
154 elif osmtype == 'way' :
155 _insert_place_table_objects(step.hashes, 'LINESTRING', named is not None)
156 elif osmtype == 'area' :
157 _insert_place_table_objects(step.hashes, 'POLYGON', named is not None)
158 cur.execute('ALTER TABLE place ENABLE TRIGGER place_before_insert')
163 @step(u'the relations')
164 def import_fill_planet_osm_rels(step):
165 """Adds a raw relation to the osm2pgsql table.
166 Three columns need to be suplied: id, tags, members.
168 cur = world.conn.cursor()
169 for line in step.hashes:
171 parts = { 'n' : [], 'w' : [], 'r' : [] }
172 if line['members'].strip():
173 for mem in line['members'].split(','):
174 memparts = mem.strip().split(':', 2)
175 memid = memparts[0].lower()
176 parts[memid[0]].append(int(memid[1:]))
177 members.append(memid)
178 if len(memparts) == 2:
179 members.append(memparts[1])
183 for k,v in world.make_hash(line['tags']).iteritems():
188 cur.execute("""INSERT INTO planet_osm_rels
189 (id, way_off, rel_off, parts, members, tags)
190 VALUES (%s, %s, %s, %s, %s, %s)""",
191 (line['id'], len(parts['n']), len(parts['n']) + len(parts['w']),
192 parts['n'] + parts['w'] + parts['r'], members, tags))
197 def import_fill_planet_osm_ways(step):
198 cur = world.conn.cursor()
199 for line in step.hashes:
201 tags = world.make_hash(line['tags'])
204 nodes = [int(x.strip()) for x in line['nodes'].split(',')]
206 cur.execute("""INSERT INTO planet_osm_ways (id, nodes, tags)
207 VALUES (%s, %s, %s)""",
208 (line['id'], nodes, tags))
211 ############### import and update steps #######################################
214 def import_database(step):
215 """ Runs the actual indexing. """
216 world.run_nominatim_script('setup', 'create-functions', 'create-partition-functions')
217 cur = world.conn.cursor()
218 #world.db_dump_table('place')
219 cur.execute("""insert into placex (osm_type, osm_id, class, type, name, admin_level,
220 housenumber, street, addr_place, isin, postcode, country_code, extratags,
221 geometry) select * from place where not (class='place' and type='houses' and osm_type='W')""")
222 cur.execute("""select insert_osmline (osm_id, housenumber, street, addr_place, postcode, country_code, geometry) from place where class='place' and type='houses' and osm_type='W'""")
224 world.run_nominatim_script('setup', 'index', 'index-noanalyse')
225 #world.db_dump_table('placex')
226 #world.db_dump_table('location_property_osmline')
228 @step(u'updating place (node|way|area)s')
229 def update_place_table_nodes(step, osmtype):
230 """ Replace a geometry in place by reinsertion and reindex database."""
231 world.run_nominatim_script('setup', 'create-functions', 'create-partition-functions', 'enable-diff-updates')
232 if osmtype == 'node':
233 _insert_place_table_nodes(step.hashes, False)
234 elif osmtype == 'way':
235 _insert_place_table_objects(step.hashes, 'LINESTRING', False)
236 elif osmtype == 'area':
237 _insert_place_table_objects(step.hashes, 'POLYGON', False)
238 world.run_nominatim_script('update', 'index')
240 @step(u'marking for delete (.*)')
241 def update_delete_places(step, places):
242 """ Remove an entry from place and reindex database.
244 world.run_nominatim_script('setup', 'create-functions', 'create-partition-functions', 'enable-diff-updates')
245 cur = world.conn.cursor()
246 for place in places.split(','):
247 osmtype, osmid, cls = world.split_id(place)
249 q = "delete from place where osm_type = %s and osm_id = %s"
250 params = (osmtype, osmid)
252 q = "delete from place where osm_type = %s and osm_id = %s and class = %s"
253 params = (osmtype, osmid, cls)
254 cur.execute(q, params)
256 #world.db_dump_table('placex')
257 world.run_nominatim_script('update', 'index')
261 @step(u'sending query "(.*)"( with dups)?$')
262 def query_cmd(step, query, with_dups):
263 """ Results in standard query output. The same tests as for API queries
266 cmd = [os.path.join(world.config.source_dir, 'utils', 'query.php'),
268 if with_dups is not None:
269 cmd.append('--nodedupe')
270 proc = subprocess.Popen(cmd, cwd=world.config.source_dir,
271 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
272 (outp, err) = proc.communicate()
273 assert (proc.returncode == 0), "query.php failed with message: %s" % err
275 world.response_format = 'json'
276 world.request_type = 'search'
277 world.returncode = 200