]> git.openstreetmap.org Git - nominatim.git/blob - tests/steps/terrain.py
39fc662593c8d9f2fe78d34efc76300d1d418aca
[nominatim.git] / tests / steps / terrain.py
1 from lettuce import *
2 from nose.tools import *
3 import logging
4 import os
5 import subprocess
6 import psycopg2
7 import re
8 from haversine import haversine
9 from shapely.wkt import loads as wkt_load
10 from shapely.ops import linemerge
11
12 logger = logging.getLogger(__name__)
13
14 class NominatimConfig:
15
16     def __init__(self):
17         # logging setup
18         loglevel = getattr(logging, os.environ.get('LOGLEVEL','info').upper())
19         if 'LOGFILE' in os.environ:
20             logging.basicConfig(filename=os.environ.get('LOGFILE','run.log'),
21                                 level=loglevel)
22         else:
23             logging.basicConfig(level=loglevel)
24         # Nominatim test setup
25         self.base_url = os.environ.get('NOMINATIM_SERVER', 'http://localhost/nominatim')
26         self.source_dir = os.path.abspath(os.environ.get('NOMINATIM_DIR', '../build'))
27         self.template_db = os.environ.get('TEMPLATE_DB', 'test_template_nominatim')
28         self.test_db = os.environ.get('TEST_DB', 'test_nominatim')
29         self.local_settings_file = os.environ.get('NOMINATIM_SETTINGS', '/tmp/nominatim_settings.php')
30         self.reuse_template = 'NOMINATIM_REMOVE_TEMPLATE' not in os.environ
31         self.keep_scenario_db = 'NOMINATIM_KEEP_SCENARIO_DB' in os.environ
32         os.environ['NOMINATIM_SETTINGS'] = '/tmp/nominatim_settings.php'
33
34         scriptpath = os.path.dirname(os.path.abspath(__file__))
35         self.scene_path = os.environ.get('SCENE_PATH', 
36                 os.path.join(scriptpath, '..', 'scenes', 'data'))
37
38
39     def __str__(self):
40         return 'Server URL: %s\nSource dir: %s\n' % (self.base_url, self.source_dir)
41
42 world.config = NominatimConfig()
43
44 @world.absorb
45 def write_nominatim_config(dbname):
46     f = open(world.config.local_settings_file, 'w')
47     f.write("<?php\n  @define('CONST_Database_DSN', 'pgsql://@/%s');\n" % dbname)
48     f.close()
49
50
51 @world.absorb
52 def run_nominatim_script(script, *args):
53     cmd = [os.path.join(world.config.source_dir, 'utils', '%s.php' % script)]
54     cmd.extend(['--%s' % x for x in args])
55     proc = subprocess.Popen(cmd, cwd=world.config.source_dir,
56                             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
57     (outp, outerr) = proc.communicate()
58     logger.debug("run_nominatim_script: %s\n%s\n%s" % (cmd, outp, outerr))
59     assert (proc.returncode == 0), "Script '%s' failed:\n%s\n%s\n" % (script, outp, outerr)
60
61 @world.absorb
62 def make_hash(inp):
63     return eval('{' + inp + '}')
64
65 @world.absorb
66 def split_id(oid):
67     """ Splits a unique identifier for places into its components.
68         As place_ids cannot be used for testing, we use a unique
69         identifier instead that is of the form <osmtype><osmid>[:class].
70     """
71     oid = oid.strip()
72     if oid == 'None':
73         return None, None, None
74     osmtype = oid[0]
75     assert_in(osmtype, ('R','N','W'))
76     if ':' in oid:
77         osmid, cls = oid[1:].split(':')
78         return (osmtype, int(osmid), cls)
79     else:
80         return (osmtype, int(oid[1:]), None)
81
82 @world.absorb
83 def get_placeid(oid):
84     """ Tries to retrive the place_id for a unique identifier. """
85     if oid[0].isdigit():
86         return int(oid)
87
88     osmtype, osmid, cls = world.split_id(oid)
89     if osmtype is None:
90         return None
91     cur = world.conn.cursor()
92     if cls is None:
93         q = 'SELECT place_id FROM placex where osm_type = %s and osm_id = %s'
94         params = (osmtype, osmid)
95     else:
96         q = 'SELECT place_id FROM placex where osm_type = %s and osm_id = %s and class = %s'
97         params = (osmtype, osmid, cls)
98     cur.execute(q, params)
99     assert_equals(cur.rowcount, 1, "%d rows found for place %s" % (cur.rowcount, oid))
100     return cur.fetchone()[0]
101
102
103 @world.absorb
104 def match_geometry(coord, matchstring):
105     m = re.match(r'([-0-9.]+),\s*([-0-9.]+)\s*(?:\+-([0-9.]+)([a-z]+)?)?', matchstring)
106     assert_is_not_none(m, "Invalid match string")
107
108     logger.debug("Distmatch: %s/%s %s %s" % (m.group(1), m.group(2), m.group(3), m.group(4) ))
109     dist = haversine(coord, (float(m.group(1)), float(m.group(2))))
110
111     if m.group(3) is not None:
112         expdist = float(m.group(3))
113         if m.group(4) is not None:
114             if m.group(4) == 'm':
115                 expdist = expdist/1000
116             elif m.group(4) == 'km':
117                 pass
118             else:
119                 raise Exception("Unknown unit '%s' in geometry match" % (m.group(4), ))
120     else:
121         expdist = 0
122
123     logger.debug("Distances expected: %f, got: %f" % (expdist, dist))
124     assert dist <= expdist, "Geometry too far away, expected: %f, got: %f" % (expdist, dist)
125
126
127
128 @world.absorb
129 def db_dump_table(table):
130     cur = world.conn.cursor()
131     cur.execute('SELECT * FROM %s' % table)
132     print '<<<<<<< BEGIN OF TABLE DUMP %s' % table
133     for res in cur:
134             print res
135     print '<<<<<<< END OF TABLE DUMP %s' % table
136
137 @world.absorb
138 def db_drop_database(name):
139     conn = psycopg2.connect(database='postgres')
140     conn.set_isolation_level(0)
141     cur = conn.cursor()
142     cur.execute('DROP DATABASE IF EXISTS %s' % (name, ))
143     conn.close()
144
145
146 world.is_template_set_up = False
147
148 @world.absorb
149 def db_template_setup():
150     """ Set up a template database, containing all tables
151         but not yet any functions.
152     """
153     if world.is_template_set_up:
154         return
155
156     world.is_template_set_up = True
157     world.write_nominatim_config(world.config.template_db)
158     if world.config.reuse_template:
159         # check that the template is there
160         conn = psycopg2.connect(database='postgres')
161         cur = conn.cursor()
162         cur.execute('select count(*) from pg_database where datname = %s', 
163                      (world.config.template_db,))
164         if cur.fetchone()[0] == 1:
165             return
166     else:
167         # just in case... make sure a previous table has been dropped
168         world.db_drop_database(world.config.template_db)
169     # call the first part of database setup
170     world.run_nominatim_script('setup', 'create-db', 'setup-db')
171     # remove external data to speed up indexing for tests
172     conn = psycopg2.connect(database=world.config.template_db)
173     psycopg2.extras.register_hstore(conn, globally=False, unicode=True)
174     cur = conn.cursor()
175     for table in ('gb_postcode', 'us_postcode'):
176         cur.execute('TRUNCATE TABLE %s' % (table,))
177     conn.commit()
178     conn.close()
179     # execute osm2pgsql on an empty file to get the right tables
180     osm2pgsql = os.path.join(world.config.source_dir, 'osm2pgsql', 'osm2pgsql')
181     proc = subprocess.Popen([osm2pgsql, '-lsc', '-r', 'xml', '-O', 'gazetteer', '-d', world.config.template_db, '-'],
182                             cwd=world.config.source_dir, stdin=subprocess.PIPE,
183                             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
184     [outstr, errstr] = proc.communicate(input='<osm version="0.6"></osm>')
185     logger.debug("running osm2pgsql for template: %s\n%s\n%s" % (osm2pgsql, outstr, errstr))
186     world.run_nominatim_script('setup', 'create-functions', 'create-tables', 'create-partition-tables', 'create-partition-functions', 'load-data', 'create-search-indices')
187
188
189 # Leave the table around so it can be reused again after a non-reuse test round.
190 #@after.all
191 def db_template_teardown(total):
192     """ Set up a template database, containing all tables
193         but not yet any functions.
194     """
195     if world.is_template_set_up:
196         # remove template DB
197         if not world.config.reuse_template:
198             world.db_drop_database(world.config.template_db)
199         try:
200             os.remove(world.config.local_settings_file)
201         except OSError:
202             pass # ignore missing file
203
204
205 ##########################################################################
206 #
207 # Data scene handling
208 #
209
210 world.scenes = {}
211 world.current_scene = None
212
213 @world.absorb
214 def load_scene(name):
215     if name in world.scenes:
216         world.current_scene = world.scenes[name]
217     else:
218         with open(os.path.join(world.config.scene_path, "%s.wkt" % name), 'r') as fd:
219             scene = {}
220             for line in fd:
221                 if line.strip():
222                     obj, wkt = line.split('|', 2)
223                     wkt = wkt.strip()
224                     scene[obj.strip()] = wkt_load(wkt)
225             world.scenes[name] = scene
226             world.current_scene = scene
227
228 @world.absorb
229 def get_scene_geometry(name):
230     if not ':' in name:
231         # Not a scene description
232         return None
233     
234     geoms = []
235     for obj in name.split('+'):
236         oname = obj.strip()
237         if oname.startswith(':'):
238             geoms.append(world.current_scene[oname[1:]])
239         else:
240             scene, obj = oname.split(':', 2)
241             oldscene = world.current_scene
242             world.load_scene(scene)
243             wkt = world.current_scene[obj]
244             world.current_scene = oldscene
245             geoms.append(wkt)
246
247     if len(geoms) == 1:
248         return geoms[0]
249     else:
250         return linemerge(geoms)