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
27 psycopg2.extensions.register_type(psycopg2.extensions.UNICODE)
30 def setup_test_database(scenario):
31 """ Creates a new test database from the template database
32 that was set up earlier in terrain.py. Will be done only
33 for scenarios whose feature is tagged with 'DB'.
35 if scenario.feature.tags is not None and 'DB' in scenario.feature.tags:
36 world.db_template_setup()
37 world.write_nominatim_config(world.config.test_db)
38 conn = psycopg2.connect(database=world.config.template_db)
39 conn.set_isolation_level(0)
41 cur.execute('DROP DATABASE IF EXISTS %s' % (world.config.test_db, ))
42 cur.execute('CREATE DATABASE %s TEMPLATE = %s' % (world.config.test_db, world.config.template_db))
44 world.conn = psycopg2.connect(database=world.config.test_db)
45 psycopg2.extras.register_hstore(world.conn, globally=False, unicode=True)
47 @step('a wiped database')
48 def db_setup_wipe_db(step):
49 """Explicit DB scenario setup only needed
50 to work around a bug where scenario outlines don't call
51 before_each_scenario correctly.
53 if hasattr(world, 'conn'):
55 conn = psycopg2.connect(database=world.config.template_db)
56 conn.set_isolation_level(0)
58 cur.execute('DROP DATABASE IF EXISTS %s' % (world.config.test_db, ))
59 cur.execute('CREATE DATABASE %s TEMPLATE = %s' % (world.config.test_db, world.config.template_db))
61 world.conn = psycopg2.connect(database=world.config.test_db)
62 psycopg2.extras.register_hstore(world.conn, globally=False, unicode=True)
66 def tear_down_test_database(scenario):
67 """ Drops any previously created test database.
69 if hasattr(world, 'conn'):
71 if scenario.feature.tags is not None and 'DB' in scenario.feature.tags and not world.config.keep_scenario_db:
72 conn = psycopg2.connect(database=world.config.template_db)
73 conn.set_isolation_level(0)
75 cur.execute('DROP DATABASE %s' % (world.config.test_db,))
79 def _format_placex_cols(cols, geomtype, force_name):
81 if cols['name'].startswith("'"):
82 cols['name'] = world.make_hash(cols['name'])
84 cols['name'] = { 'name' : cols['name'] }
86 cols['name'] = { 'name' : base64.urlsafe_b64encode(os.urandom(int(random.random()*30))) }
87 if 'extratags' in cols:
88 cols['extratags'] = world.make_hash(cols['extratags'])
89 if 'admin_level' not in cols:
90 cols['admin_level'] = 100
91 if 'geometry' in cols:
92 coords = world.get_scene_geometry(cols['geometry'])
94 coords = "'%s(%s)'::geometry" % (geomtype, cols['geometry'])
96 coords = "'%s'::geometry" % coords.wkt
97 cols['geometry'] = coords
103 def _insert_place_table_nodes(places, force_name):
104 cur = world.conn.cursor()
107 cols['osm_type'] = 'N'
108 _format_placex_cols(cols, 'POINT', force_name)
109 if 'geometry' in cols:
110 coords = cols.pop('geometry')
112 coords = "ST_Point(%f, %f)" % (random.random()*360 - 180, random.random()*180 - 90)
114 query = 'INSERT INTO place (%s,geometry) values(%s, ST_SetSRID(%s, 4326))' % (
115 ','.join(cols.iterkeys()),
116 ','.join(['%s' for x in range(len(cols))]),
119 cur.execute(query, cols.values())
123 def _insert_place_table_objects(places, geomtype, force_name):
124 cur = world.conn.cursor()
127 if 'osm_type' not in cols:
128 cols['osm_type'] = 'W'
129 _format_placex_cols(cols, geomtype, force_name)
130 coords = cols.pop('geometry')
132 query = 'INSERT INTO place (%s, geometry) values(%s, ST_SetSRID(%s, 4326))' % (
133 ','.join(cols.iterkeys()),
134 ','.join(['%s' for x in range(len(cols))]),
137 cur.execute(query, cols.values())
140 @step(u'the scene (.*)')
141 def import_set_scene(step, scene):
142 world.load_scene(scene)
144 @step(u'the (named )?place (node|way|area)s')
145 def import_place_table_nodes(step, named, osmtype):
146 """Insert a list of nodes into the placex table.
147 Expects a table where columns are named in the same way as placex.
149 cur = world.conn.cursor()
150 cur.execute('ALTER TABLE place DISABLE TRIGGER place_before_insert')
151 if osmtype == 'node':
152 _insert_place_table_nodes(step.hashes, named is not None)
153 elif osmtype == 'way' :
154 _insert_place_table_objects(step.hashes, 'LINESTRING', named is not None)
155 elif osmtype == 'area' :
156 _insert_place_table_objects(step.hashes, 'POLYGON', named is not None)
157 cur.execute('ALTER TABLE place ENABLE TRIGGER place_before_insert')
162 @step(u'the relations')
163 def import_fill_planet_osm_rels(step):
164 """Adds a raw relation to the osm2pgsql table.
165 Three columns need to be suplied: id, tags, members.
167 cur = world.conn.cursor()
168 for line in step.hashes:
170 parts = { 'n' : [], 'w' : [], 'r' : [] }
171 if line['members'].strip():
172 for mem in line['members'].split(','):
173 memparts = mem.strip().split(':', 2)
174 memid = memparts[0].lower()
175 parts[memid[0]].append(int(memid[1:]))
176 members.append(memid)
177 if len(memparts) == 2:
178 members.append(memparts[1])
182 for k,v in world.make_hash(line['tags']).iteritems():
187 cur.execute("""INSERT INTO planet_osm_rels
188 (id, way_off, rel_off, parts, members, tags)
189 VALUES (%s, %s, %s, %s, %s, %s)""",
190 (line['id'], len(parts['n']), len(parts['n']) + len(parts['w']),
191 parts['n'] + parts['w'] + parts['r'], members, tags))
196 def import_fill_planet_osm_ways(step):
197 cur = world.conn.cursor()
198 for line in step.hashes:
200 tags = world.make_hash(line['tags'])
203 nodes = [int(x.strip()) for x in line['nodes'].split(',')]
205 cur.execute("""INSERT INTO planet_osm_ways (id, nodes, tags)
206 VALUES (%s, %s, %s)""",
207 (line['id'], nodes, tags))
210 ############### import and update steps #######################################
213 def import_database(step):
214 """ Runs the actual indexing. """
215 world.run_nominatim_script('setup', 'create-functions', 'create-partition-functions')
216 cur = world.conn.cursor()
217 cur.execute("""insert into placex (osm_type, osm_id, class, type, name, admin_level,
218 housenumber, street, addr_place, isin, postcode, country_code, extratags,
219 geometry) select * from place""")
221 world.run_nominatim_script('setup', 'index', 'index-noanalyse')
222 #world.db_dump_table('placex')
225 @step(u'updating place (node|way|area)s')
226 def update_place_table_nodes(step, osmtype):
227 """ Replace a geometry in place by reinsertion and reindex database.
229 world.run_nominatim_script('setup', 'create-functions', 'create-partition-functions', 'enable-diff-updates')
230 if osmtype == 'node':
231 _insert_place_table_nodes(step.hashes, False)
232 elif osmtype == 'way':
233 _insert_place_table_objects(step.hashes, 'LINESTRING', False)
234 elif osmtype == 'area':
235 _insert_place_table_objects(step.hashes, 'POLYGON', False)
236 world.run_nominatim_script('update', 'index')
238 @step(u'marking for delete (.*)')
239 def update_delete_places(step, places):
240 """ Remove an entry from place and reindex database.
242 world.run_nominatim_script('setup', 'create-functions', 'create-partition-functions', 'enable-diff-updates')
243 cur = world.conn.cursor()
244 for place in places.split(','):
245 osmtype, osmid, cls = world.split_id(place)
247 q = "delete from place where osm_type = %s and osm_id = %s"
248 params = (osmtype, osmid)
250 q = "delete from place where osm_type = %s and osm_id = %s and class = %s"
251 params = (osmtype, osmid, cls)
252 cur.execute(q, params)
254 #world.db_dump_table('placex')
255 world.run_nominatim_script('update', 'index')
259 @step(u'sending query "(.*)"( with dups)?$')
260 def query_cmd(step, query, with_dups):
261 """ Results in standard query output. The same tests as for API queries
264 cmd = [os.path.join(world.config.source_dir, 'utils', 'query.php'),
266 if with_dups is not None:
267 cmd.append('--nodedupe')
268 proc = subprocess.Popen(cmd, cwd=world.config.source_dir,
269 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
270 (outp, err) = proc.communicate()
271 assert (proc.returncode == 0), "query.php failed with message: %s" % err
273 world.response_format = 'json'
274 world.request_type = 'search'
275 world.returncode = 200