]> git.openstreetmap.org Git - nominatim.git/blob - tests/steps/db_setup.py
add functional tests
[nominatim.git] / tests / steps / db_setup.py
1 """ Steps for setting up a test database with imports and updates.
2
3     There are two ways to state geometries for test data: with coordinates
4     and via scenes.
5
6     Coordinates should be given as a wkt without the enclosing type name.
7
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>`.
15 """
16
17 from nose.tools import *
18 from lettuce import *
19 import psycopg2
20 import psycopg2.extensions
21 import psycopg2.extras
22 import os
23 import subprocess
24 import random
25 import base64
26
27 psycopg2.extensions.register_type(psycopg2.extensions.UNICODE)
28
29 @before.each_scenario
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'.
34     """
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)
40         cur = conn.cursor()
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))
43         conn.close()
44         world.conn = psycopg2.connect(database=world.config.test_db)
45         psycopg2.extras.register_hstore(world.conn, globally=False, unicode=True)
46
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.
52     """
53     if hasattr(world, 'conn'):
54         world.conn.close()
55     conn = psycopg2.connect(database=world.config.template_db)
56     conn.set_isolation_level(0)
57     cur = conn.cursor()
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))
60     conn.close()
61     world.conn = psycopg2.connect(database=world.config.test_db)
62     psycopg2.extras.register_hstore(world.conn, globally=False, unicode=True)
63
64
65 @after.each_scenario
66 def tear_down_test_database(scenario):
67     """ Drops any previously created test database.
68     """
69     if hasattr(world, 'conn'):
70         world.conn.close()
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)
74         cur = conn.cursor()
75         cur.execute('DROP DATABASE %s' % (world.config.test_db,))
76         conn.close()
77
78
79 def _format_placex_cols(cols, geomtype, force_name):
80     if 'name' in cols:
81         if cols['name'].startswith("'"):
82             cols['name'] = world.make_hash(cols['name'])
83         else:
84             cols['name'] = { 'name' : cols['name'] }
85     elif force_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'])
93         if coords is None:
94             coords = "'%s(%s)'::geometry" % (geomtype, cols['geometry'])
95         else:
96             coords = "'%s'::geometry" % coords.wkt
97         cols['geometry'] = coords
98
99
100 def _insert_place_table_nodes(places, force_name):
101     cur = world.conn.cursor()
102     for line in places:
103         cols = dict(line)
104         cols['osm_type'] = 'N'
105         _format_placex_cols(cols, 'POINT', force_name)
106         if 'geometry' in cols:
107             coords = cols.pop('geometry')
108         else:
109             coords = "ST_Point(%f, %f)" % (random.random()*360 - 180, random.random()*180 - 90)
110
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))]),
114               coords
115              )
116         cur.execute(query, cols.values())
117     world.conn.commit()
118
119
120 def _insert_place_table_objects(places, geomtype, force_name):
121     cur = world.conn.cursor()
122     for line in places:
123         cols = dict(line)
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')
128
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))]),
132               coords
133              )
134         cur.execute(query, cols.values())
135     world.conn.commit()
136
137 @step(u'the scene (.*)')
138 def import_set_scene(step, scene):
139     world.load_scene(scene)
140
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.
145     """
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')
155     cur.close()
156     world.conn.commit()
157
158
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.
163     """
164     cur = world.conn.cursor()
165     for line in step.hashes:
166         members = []
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])
176                 else:
177                     members.append('')
178         tags = []
179         for k,v in world.make_hash(line['tags']).iteritems():
180             tags.extend((k,v))
181         if not members:
182             members = None
183
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))
189     world.conn.commit()
190         
191
192 @step(u'the ways')
193 def import_fill_planet_osm_ways(step):
194     cur = world.conn.cursor()
195     for line in step.hashes:
196         if 'tags' in line:
197             tags = world.make_hash(line['tags'])
198         else:
199             tags = None
200         nodes = [int(x.strip()) for x in line['nodes'].split(',')]
201
202         cur.execute("""INSERT INTO planet_osm_ways
203                        (id, nodes, tags, pending)
204                        VALUES (%s, %s, %s, false)""",
205                     (line['id'], nodes, tags))
206     world.conn.commit()
207
208 ############### import and update steps #######################################
209
210 @step(u'importing')
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""")
218     world.conn.commit()
219     world.run_nominatim_script('setup', 'index', 'index-noanalyse')
220     #world.db_dump_table('placex')
221
222
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.
226     """
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')
235
236 @step(u'marking for delete (.*)')
237 def update_delete_places(step, places):
238     """ Remove an entry from place and reindex database.
239     """
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)
244         if cls is None:
245             q = "delete from place where osm_type = %s and osm_id = %s"
246             params = (osmtype, osmid)
247         else:
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)
251     world.conn.commit()
252     #world.db_dump_table('placex')
253     world.run_nominatim_script('update', 'index')
254
255
256
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
260         can be used.
261     """
262     cmd = [os.path.join(world.config.source_dir, 'utils', 'query.php'),
263            '--search', query]
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
269     world.page = outp
270     world.response_format = 'json'   
271     world.returncode = 200
272