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
 
  28 psycopg2.extensions.register_type(psycopg2.extensions.UNICODE)
 
  31 def setup_test_database(scenario):
 
  32     """ Creates a new test database from the template database
 
  33         that was set up earlier in terrain.py. Will be done only
 
  34         for scenarios whose feature is tagged with 'DB'.
 
  36     if scenario.feature.tags is not None and 'DB' in scenario.feature.tags:
 
  37         world.db_template_setup()
 
  38         world.write_nominatim_config(world.config.test_db)
 
  39         conn = psycopg2.connect(database=world.config.template_db)
 
  40         conn.set_isolation_level(0)
 
  42         cur.execute('DROP DATABASE IF EXISTS %s' % (world.config.test_db, ))
 
  43         cur.execute('CREATE DATABASE %s TEMPLATE = %s' % (world.config.test_db, world.config.template_db))
 
  45         world.conn = psycopg2.connect(database=world.config.test_db)
 
  46         psycopg2.extras.register_hstore(world.conn, globally=False, unicode=True)
 
  48 @step('a wiped database')
 
  49 def db_setup_wipe_db(step):
 
  50     """Explicit DB scenario setup only needed
 
  51        to work around a bug where scenario outlines don't call
 
  52        before_each_scenario correctly.
 
  54     if hasattr(world, 'conn'):
 
  56     conn = psycopg2.connect(database=world.config.template_db)
 
  57     conn.set_isolation_level(0)
 
  59     cur.execute('DROP DATABASE IF EXISTS %s' % (world.config.test_db, ))
 
  60     cur.execute('CREATE DATABASE %s TEMPLATE = %s' % (world.config.test_db, world.config.template_db))
 
  62     world.conn = psycopg2.connect(database=world.config.test_db)
 
  63     psycopg2.extras.register_hstore(world.conn, globally=False, unicode=True)
 
  67 def tear_down_test_database(scenario):
 
  68     """ Drops any previously created test database.
 
  70     if hasattr(world, 'conn'):
 
  72     if scenario.feature.tags is not None and 'DB' in scenario.feature.tags and not world.config.keep_scenario_db:
 
  73         conn = psycopg2.connect(database=world.config.template_db)
 
  74         conn.set_isolation_level(0)
 
  76         cur.execute('DROP DATABASE %s' % (world.config.test_db,))
 
  80 def _format_placex_cols(cols, geomtype, force_name):
 
  82         if cols['name'].startswith("'"):
 
  83             cols['name'] = world.make_hash(cols['name'])
 
  85             cols['name'] = { 'name' : cols['name'] }
 
  87         cols['name'] = { 'name' : base64.urlsafe_b64encode(os.urandom(int(random.random()*30))) }
 
  88     if 'extratags' in cols:
 
  89         cols['extratags'] = world.make_hash(cols['extratags'])
 
  90     if 'admin_level' not in cols:
 
  91         cols['admin_level'] = 100
 
  92     if 'geometry' in cols:
 
  93         coords = world.get_scene_geometry(cols['geometry'])
 
  95             coords = "'%s(%s)'::geometry" % (geomtype, cols['geometry'])
 
  97             coords = "'%s'::geometry" % coords.wkt
 
  98         cols['geometry'] = coords
 
 104 def _insert_place_table_nodes(places, force_name):
 
 105     cur = world.conn.cursor()
 
 108         cols['osm_type'] = 'N'
 
 109         _format_placex_cols(cols, 'POINT', force_name)
 
 110         if 'geometry' in cols:
 
 111             coords = cols.pop('geometry')
 
 113             coords = "ST_Point(%f, %f)" % (random.random()*360 - 180, random.random()*180 - 90)
 
 115         query = 'INSERT INTO place (%s,geometry) values(%s, ST_SetSRID(%s, 4326))' % (
 
 116               ','.join(cols.iterkeys()),
 
 117               ','.join(['%s' for x in range(len(cols))]),
 
 120         cur.execute(query, cols.values())
 
 124 def _insert_place_table_objects(places, geomtype, force_name):
 
 125     cur = world.conn.cursor()
 
 128         if 'osm_type' not in cols:
 
 129             cols['osm_type'] = 'W'
 
 130         _format_placex_cols(cols, geomtype, force_name)
 
 131         coords = cols.pop('geometry')
 
 133         query = 'INSERT INTO place (%s, geometry) values(%s, ST_SetSRID(%s, 4326))' % (
 
 134               ','.join(cols.iterkeys()),
 
 135               ','.join(['%s' for x in range(len(cols))]),
 
 138         cur.execute(query, cols.values())
 
 141 @step(u'the scene (.*)')
 
 142 def import_set_scene(step, scene):
 
 143     world.load_scene(scene)
 
 145 @step(u'the (named )?place (node|way|area)s')
 
 146 def import_place_table_nodes(step, named, osmtype):
 
 147     """Insert a list of nodes into the place table.
 
 148        Expects a table where columns are named in the same way as place.
 
 150     cur = world.conn.cursor()
 
 151     cur.execute('ALTER TABLE place DISABLE TRIGGER place_before_insert')
 
 152     if osmtype == 'node':
 
 153         _insert_place_table_nodes(step.hashes, named is not None)
 
 154     elif osmtype == 'way' :
 
 155         _insert_place_table_objects(step.hashes, 'LINESTRING', named is not None)
 
 156     elif osmtype == 'area' :
 
 157         _insert_place_table_objects(step.hashes, 'POLYGON', named is not None)
 
 158     cur.execute('ALTER TABLE place ENABLE TRIGGER place_before_insert')
 
 163 @step(u'the relations')
 
 164 def import_fill_planet_osm_rels(step):
 
 165     """Adds a raw relation to the osm2pgsql table.
 
 166        Three columns need to be suplied: id, tags, members.
 
 168     cur = world.conn.cursor()
 
 169     for line in step.hashes:
 
 171         parts = { 'n' : [], 'w' : [], 'r' : [] }
 
 172         if line['members'].strip():
 
 173             for mem in line['members'].split(','):
 
 174                 memparts = mem.strip().split(':', 2)
 
 175                 memid = memparts[0].lower()
 
 176                 parts[memid[0]].append(int(memid[1:]))
 
 177                 members.append(memid)
 
 178                 if len(memparts) == 2:
 
 179                     members.append(memparts[1])
 
 183         for k,v in world.make_hash(line['tags']).iteritems():
 
 188         cur.execute("""INSERT INTO planet_osm_rels
 
 189                       (id, way_off, rel_off, parts, members, tags)
 
 190                       VALUES (%s, %s, %s, %s, %s, %s)""",
 
 191                    (line['id'], len(parts['n']), len(parts['n']) + len(parts['w']),
 
 192                    parts['n'] + parts['w'] + parts['r'], members, tags))
 
 197 def import_fill_planet_osm_ways(step):
 
 198     cur = world.conn.cursor()
 
 199     for line in step.hashes:
 
 201             tags = world.make_hash(line['tags'])
 
 204         nodes = [int(x.strip()) for x in line['nodes'].split(',')]
 
 206         cur.execute("""INSERT INTO planet_osm_ways (id, nodes, tags)
 
 207                        VALUES (%s, %s, %s)""",
 
 208                     (line['id'], nodes, tags))
 
 211 ############### import and update steps #######################################
 
 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     #world.db_dump_table('place')
 
 219     cur.execute("""insert into placex (osm_type, osm_id, class, type, name, admin_level,
 
 220                    housenumber, street, addr_place, isin, postcode, country_code, extratags,
 
 221                    geometry) select * from place where not (class='place' and type='houses' and osm_type='W')""")
 
 222     cur.execute("""select insert_osmline (osm_id, housenumber, street, addr_place, postcode, country_code, geometry) from place where class='place' and type='houses' and osm_type='W'""")
 
 224     world.run_nominatim_script('setup', 'index', 'index-noanalyse')
 
 225     #world.db_dump_table('placex')
 
 226     #world.db_dump_table('location_property_osmline')
 
 228 @step(u'updating place (node|way|area)s')
 
 229 def update_place_table_nodes(step, osmtype):
 
 230     """ Replace a geometry in place by reinsertion and reindex database."""
 
 231     world.run_nominatim_script('setup', 'create-functions', 'create-partition-functions', 'enable-diff-updates')
 
 232     if osmtype == 'node':
 
 233         _insert_place_table_nodes(step.hashes, False)
 
 234     elif osmtype == 'way':
 
 235         _insert_place_table_objects(step.hashes, 'LINESTRING', False)
 
 236     elif osmtype == 'area':
 
 237         _insert_place_table_objects(step.hashes, 'POLYGON', False)
 
 238     world.run_nominatim_script('update', 'index')
 
 240 @step(u'marking for delete (.*)')
 
 241 def update_delete_places(step, places):
 
 242     """ Remove an entry from place and reindex database.
 
 244     world.run_nominatim_script('setup', 'create-functions', 'create-partition-functions', 'enable-diff-updates')
 
 245     cur = world.conn.cursor()
 
 246     for place in places.split(','):
 
 247         osmtype, osmid, cls = world.split_id(place)
 
 249             q = "delete from place where osm_type = %s and osm_id = %s"
 
 250             params = (osmtype, osmid)
 
 252             q = "delete from place where osm_type = %s and osm_id = %s and class = %s"
 
 253             params = (osmtype, osmid, cls)
 
 254         cur.execute(q, params)
 
 256     #world.db_dump_table('placex')
 
 257     world.run_nominatim_script('update', 'index')
 
 261 @step(u'sending query "(.*)"( with dups)?$')
 
 262 def query_cmd(step, query, with_dups):
 
 263     """ Results in standard query output. The same tests as for API queries
 
 266     cmd = [os.path.join(world.config.source_dir, 'utils', 'query.php'),
 
 268     if with_dups is not None:
 
 269         cmd.append('--nodedupe')
 
 270     proc = subprocess.Popen(cmd, cwd=world.config.source_dir,
 
 271                             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
 
 272     (outp, err) = proc.communicate()
 
 273     assert (proc.returncode == 0), "query.php failed with message: %s" % err
 
 275     world.response_format = 'json'
 
 276     world.request_type = 'search'
 
 277     world.returncode = 200