]> git.openstreetmap.org Git - nominatim.git/blob - tests/steps/db_setup.py
Merge pull request #381 from lonvia/cmake-port
[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     for k in cols:
99         if not cols[k]:
100             cols[k] = None
101
102
103 def _insert_place_table_nodes(places, force_name):
104     cur = world.conn.cursor()
105     for line in places:
106         cols = dict(line)
107         cols['osm_type'] = 'N'
108         _format_placex_cols(cols, 'POINT', force_name)
109         if 'geometry' in cols:
110             coords = cols.pop('geometry')
111         else:
112             coords = "ST_Point(%f, %f)" % (random.random()*360 - 180, random.random()*180 - 90)
113
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))]),
117               coords
118              )
119         cur.execute(query, cols.values())
120     world.conn.commit()
121
122
123 def _insert_place_table_objects(places, geomtype, force_name):
124     cur = world.conn.cursor()
125     for line in places:
126         cols = dict(line)
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')
131
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))]),
135               coords
136              )
137         cur.execute(query, cols.values())
138     world.conn.commit()
139
140 @step(u'the scene (.*)')
141 def import_set_scene(step, scene):
142     world.load_scene(scene)
143
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.
148     """
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')
158     cur.close()
159     world.conn.commit()
160
161
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.
166     """
167     cur = world.conn.cursor()
168     for line in step.hashes:
169         members = []
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])
179                 else:
180                     members.append('')
181         tags = []
182         for k,v in world.make_hash(line['tags']).iteritems():
183             tags.extend((k,v))
184         if not members:
185             members = None
186
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))
192     world.conn.commit()
193         
194
195 @step(u'the ways')
196 def import_fill_planet_osm_ways(step):
197     cur = world.conn.cursor()
198     for line in step.hashes:
199         if 'tags' in line:
200             tags = world.make_hash(line['tags'])
201         else:
202             tags = None
203         nodes = [int(x.strip()) for x in line['nodes'].split(',')]
204
205         cur.execute("""INSERT INTO planet_osm_ways
206                        (id, nodes, tags, pending)
207                        VALUES (%s, %s, %s, false)""",
208                     (line['id'], nodes, tags))
209     world.conn.commit()
210
211 ############### import and update steps #######################################
212
213 @step(u'importing')
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""")
221     world.conn.commit()
222     world.run_nominatim_script('setup', 'index', 'index-noanalyse')
223     #world.db_dump_table('placex')
224
225
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.
229     """
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')
238
239 @step(u'marking for delete (.*)')
240 def update_delete_places(step, places):
241     """ Remove an entry from place and reindex database.
242     """
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)
247         if cls is None:
248             q = "delete from place where osm_type = %s and osm_id = %s"
249             params = (osmtype, osmid)
250         else:
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)
254     world.conn.commit()
255     #world.db_dump_table('placex')
256     world.run_nominatim_script('update', 'index')
257
258
259
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
263         can be used.
264     """
265     cmd = [os.path.join(world.config.source_dir, 'utils', 'query.php'),
266            '--search', query]
267     if with_dups is not None:
268         cmd.append('--nodedupe')
269     proc = subprocess.Popen(cmd, cwd=world.config.source_dir,
270                             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
271     (outp, err) = proc.communicate()
272     assert (proc.returncode == 0), "query.php failed with message: %s" % err
273     world.page = outp
274     world.response_format = 'json'   
275     world.returncode = 200
276