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, pending)
189 VALUES (%s, %s, %s, %s, %s, %s, false)""",
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
206 (id, nodes, tags, pending)
207 VALUES (%s, %s, %s, false)""",
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 cur.execute("""insert into placex (osm_type, osm_id, class, type, name, admin_level,
219 housenumber, street, addr_place, isin, postcode, country_code, extratags,
220 geometry) select * from place""")
222 world.run_nominatim_script('setup', 'index', 'index-noanalyse')
223 #world.db_dump_table('placex')
226 @step(u'updating place (node|way|area)s')
227 def update_place_table_nodes(step, osmtype):
228 """ Replace a geometry in place by reinsertion and reindex database.
230 world.run_nominatim_script('setup', 'create-functions', 'create-partition-functions', 'enable-diff-updates')
231 if osmtype == 'node':
232 _insert_place_table_nodes(step.hashes, False)
233 elif osmtype == 'way':
234 _insert_place_table_objects(step.hashes, 'LINESTRING', False)
235 elif osmtype == 'area':
236 _insert_place_table_objects(step.hashes, 'POLYGON', False)
237 world.run_nominatim_script('update', 'index')
239 @step(u'marking for delete (.*)')
240 def update_delete_places(step, places):
241 """ Remove an entry from place and reindex database.
243 world.run_nominatim_script('setup', 'create-functions', 'create-partition-functions', 'enable-diff-updates')
244 cur = world.conn.cursor()
245 for place in places.split(','):
246 osmtype, osmid, cls = world.split_id(place)
248 q = "delete from place where osm_type = %s and osm_id = %s"
249 params = (osmtype, osmid)
251 q = "delete from place where osm_type = %s and osm_id = %s and class = %s"
252 params = (osmtype, osmid, cls)
253 cur.execute(q, params)
255 #world.db_dump_table('placex')
256 world.run_nominatim_script('update', 'index')
260 @step(u'sending query "(.*)"( with dups)?$')
261 def query_cmd(step, query, with_dups):
262 """ Results in standard query output. The same tests as for API queries
265 cmd = [os.path.join(world.config.source_dir, 'utils', 'query.php'),
267 if with_dups is not None:
268 cmd.append('--nodedupe')
269 proc = subprocess.Popen(cmd, 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.returncode = 200