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
100 def _insert_place_table_nodes(places, force_name):
101 cur = world.conn.cursor()
104 cols['osm_type'] = 'N'
105 _format_placex_cols(cols, 'POINT', force_name)
106 if 'geometry' in cols:
107 coords = cols.pop('geometry')
109 coords = "ST_Point(%f, %f)" % (random.random()*360 - 180, random.random()*180 - 90)
111 query = 'INSERT INTO place (%s,geometry) values(%s, ST_SetSRID(%s, 4326))' % (
112 ','.join(cols.iterkeys()),
113 ','.join(['%s' for x in range(len(cols))]),
116 cur.execute(query, cols.values())
120 def _insert_place_table_objects(places, geomtype, force_name):
121 cur = world.conn.cursor()
124 if 'osm_type' not in cols:
125 cols['osm_type'] = 'W'
126 _format_placex_cols(cols, geomtype, force_name)
127 coords = cols.pop('geometry')
129 query = 'INSERT INTO place (%s, geometry) values(%s, ST_SetSRID(%s, 4326))' % (
130 ','.join(cols.iterkeys()),
131 ','.join(['%s' for x in range(len(cols))]),
134 cur.execute(query, cols.values())
137 @step(u'the scene (.*)')
138 def import_set_scene(step, scene):
139 world.load_scene(scene)
141 @step(u'the (named )?place (node|way|area)s')
142 def import_place_table_nodes(step, named, osmtype):
143 """Insert a list of nodes into the placex table.
144 Expects a table where columns are named in the same way as placex.
146 cur = world.conn.cursor()
147 cur.execute('ALTER TABLE place DISABLE TRIGGER place_before_insert')
148 if osmtype == 'node':
149 _insert_place_table_nodes(step.hashes, named is not None)
150 elif osmtype == 'way' :
151 _insert_place_table_objects(step.hashes, 'LINESTRING', named is not None)
152 elif osmtype == 'area' :
153 _insert_place_table_objects(step.hashes, 'POLYGON', named is not None)
154 cur.execute('ALTER TABLE place ENABLE TRIGGER place_before_insert')
159 @step(u'the relations')
160 def import_fill_planet_osm_rels(step):
161 """Adds a raw relation to the osm2pgsql table.
162 Three columns need to be suplied: id, tags, members.
164 cur = world.conn.cursor()
165 for line in step.hashes:
167 parts = { 'n' : [], 'w' : [], 'r' : [] }
168 if line['members'].strip():
169 for mem in line['members'].split(','):
170 memparts = mem.strip().split(':', 2)
171 memid = memparts[0].lower()
172 parts[memid[0]].append(int(memid[1:]))
173 members.append(memid)
174 if len(memparts) == 2:
175 members.append(memparts[1])
179 for k,v in world.make_hash(line['tags']).iteritems():
184 cur.execute("""INSERT INTO planet_osm_rels
185 (id, way_off, rel_off, parts, members, tags, pending)
186 VALUES (%s, %s, %s, %s, %s, %s, false)""",
187 (line['id'], len(parts['n']), len(parts['n']) + len(parts['w']),
188 parts['n'] + parts['w'] + parts['r'], members, tags))
193 def import_fill_planet_osm_ways(step):
194 cur = world.conn.cursor()
195 for line in step.hashes:
197 tags = world.make_hash(line['tags'])
200 nodes = [int(x.strip()) for x in line['nodes'].split(',')]
202 cur.execute("""INSERT INTO planet_osm_ways
203 (id, nodes, tags, pending)
204 VALUES (%s, %s, %s, false)""",
205 (line['id'], nodes, tags))
208 ############### import and update steps #######################################
211 def import_database(step):
212 """ Runs the actual indexing. """
213 world.run_nominatim_script('setup', 'create-functions', 'create-partition-functions')
214 cur = world.conn.cursor()
215 cur.execute("""insert into placex (osm_type, osm_id, class, type, name, admin_level,
216 housenumber, street, addr_place, isin, postcode, country_code, extratags,
217 geometry) select * from place""")
219 world.run_nominatim_script('setup', 'index', 'index-noanalyse')
220 #world.db_dump_table('placex')
223 @step(u'updating place (node|way|area)s')
224 def update_place_table_nodes(step, osmtype):
225 """ Replace a geometry in place by reinsertion and reindex database.
227 world.run_nominatim_script('setup', 'create-functions', 'create-partition-functions', 'enable-diff-updates')
228 if osmtype == 'node':
229 _insert_place_table_nodes(step.hashes, False)
230 elif osmtype == 'way':
231 _insert_place_table_objects(step.hashes, 'LINESTRING', False)
232 elif osmtype == 'area':
233 _insert_place_table_objects(step.hashes, 'POLYGON', False)
234 world.run_nominatim_script('update', 'index')
236 @step(u'marking for delete (.*)')
237 def update_delete_places(step, places):
238 """ Remove an entry from place and reindex database.
240 world.run_nominatim_script('setup', 'create-functions', 'create-partition-functions', 'enable-diff-updates')
241 cur = world.conn.cursor()
242 for place in places.split(','):
243 osmtype, osmid, cls = world.split_id(place)
245 q = "delete from place where osm_type = %s and osm_id = %s"
246 params = (osmtype, osmid)
248 q = "delete from place where osm_type = %s and osm_id = %s and class = %s"
249 params = (osmtype, osmid, cls)
250 cur.execute(q, params)
252 #world.db_dump_table('placex')
253 world.run_nominatim_script('update', 'index')
257 @step(u'sending query "(.*)"( with dups)?$')
258 def query_cmd(step, query, with_dups):
259 """ Results in standard query output. The same tests as for API queries
262 cmd = [os.path.join(world.config.source_dir, 'utils', 'query.php'),
264 if with_dups is not None:
265 cmd.append('--nodedupe')
266 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
267 (outp, err) = proc.communicate()
268 assert (proc.returncode == 0), "query.php failed with message: %s" % err
270 world.response_format = 'json'
271 world.returncode = 200