]> git.openstreetmap.org Git - nominatim.git/blob - tests/steps/db_setup.py
Merge pull request #420 from lonvia/remove-explicit-software-versions
[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)
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))
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 (id, nodes, tags)
206                        VALUES (%s, %s, %s)""",
207                     (line['id'], nodes, tags))
208     world.conn.commit()
209
210 ############### import and update steps #######################################
211
212 @step(u'importing')
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""")
220     world.conn.commit()
221     world.run_nominatim_script('setup', 'index', 'index-noanalyse')
222     #world.db_dump_table('placex')
223
224
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.
228     """
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')
237
238 @step(u'marking for delete (.*)')
239 def update_delete_places(step, places):
240     """ Remove an entry from place and reindex database.
241     """
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)
246         if cls is None:
247             q = "delete from place where osm_type = %s and osm_id = %s"
248             params = (osmtype, osmid)
249         else:
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)
253     world.conn.commit()
254     #world.db_dump_table('placex')
255     world.run_nominatim_script('update', 'index')
256
257
258
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
262         can be used.
263     """
264     cmd = [os.path.join(world.config.source_dir, 'utils', 'query.php'),
265            '--search', query]
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
272     world.page = outp
273     world.response_format = 'json'
274     world.request_type = 'search'
275     world.returncode = 200
276