1 from pathlib import Path
8 sys.path.insert(1, str((Path(__file__) / '..' / '..' / '..' / '..').resolve()))
10 from nominatim.config import Configuration
11 from steps.utils import run_script
13 class NominatimEnvironment:
14 """ Collects all functions for the execution of Nominatim functions.
17 def __init__(self, config):
18 self.build_dir = Path(config['BUILDDIR']).resolve()
19 self.src_dir = (Path(__file__) / '..' / '..' / '..' / '..').resolve()
20 self.db_host = config['DB_HOST']
21 self.db_port = config['DB_PORT']
22 self.db_user = config['DB_USER']
23 self.db_pass = config['DB_PASS']
24 self.template_db = config['TEMPLATE_DB']
25 self.test_db = config['TEST_DB']
26 self.api_test_db = config['API_TEST_DB']
27 self.api_test_file = config['API_TEST_FILE']
28 self.server_module_path = config['SERVER_MODULE_PATH']
29 self.reuse_template = not config['REMOVE_TEMPLATE']
30 self.keep_scenario_db = config['KEEP_TEST_DB']
31 self.code_coverage_path = config['PHPCOV']
32 self.code_coverage_id = 1
34 self.default_config = Configuration(None, self.src_dir / 'settings').get_os_env()
36 self.template_db_done = False
37 self.api_db_done = False
38 self.website_dir = None
40 def connect_database(self, dbname):
41 """ Return a connection to the database with the given name.
42 Uses configured host, user and port.
44 dbargs = {'database': dbname}
46 dbargs['host'] = self.db_host
48 dbargs['port'] = self.db_port
50 dbargs['user'] = self.db_user
52 dbargs['password'] = self.db_pass
53 conn = psycopg2.connect(**dbargs)
56 def next_code_coverage_file(self):
57 """ Generate the next name for a coverage file.
59 fn = Path(self.code_coverage_path) / "{:06d}.cov".format(self.code_coverage_id)
60 self.code_coverage_id += 1
64 def write_nominatim_config(self, dbname):
65 """ Set up a custom test configuration that connects to the given
66 database. This sets up the environment variables so that they can
67 be picked up by dotenv and creates a project directory with the
68 appropriate website scripts.
70 dsn = 'pgsql:dbname={}'.format(dbname)
72 dsn += ';host=' + self.db_host
74 dsn += ';port=' + self.db_port
76 dsn += ';user=' + self.db_user
78 dsn += ';password=' + self.db_pass
80 if self.website_dir is not None \
81 and self.test_env is not None \
82 and dsn == self.test_env['NOMINATIM_DATABASE_DSN']:
83 return # environment already set uo
85 self.test_env = dict(self.default_config)
86 self.test_env['NOMINATIM_DATABASE_DSN'] = dsn
87 self.test_env['NOMINATIM_FLATNODE_FILE'] = ''
88 self.test_env['NOMINATIM_IMPORT_STYLE'] = 'full'
89 self.test_env['NOMINATIM_USE_US_TIGER_DATA'] = 'yes'
90 self.test_env['NOMINATIM_DATADIR'] = self.src_dir
91 self.test_env['NOMINATIM_BINDIR'] = self.src_dir / 'utils'
92 self.test_env['NOMINATIM_DATABASE_MODULE_PATH'] = self.build_dir / 'module'
93 self.test_env['NOMINATIM_OSM2PGSQL_BINARY'] = self.build_dir / 'osm2pgsql' / 'osm2pgsql'
95 if self.server_module_path:
96 self.test_env['NOMINATIM_DATABASE_MODULE_PATH'] = self.server_module_path
98 if self.website_dir is not None:
99 self.website_dir.cleanup()
101 self.website_dir = tempfile.TemporaryDirectory()
102 self.run_setup_script('setup-website')
105 def db_drop_database(self, name):
106 """ Drop the database with the given name.
108 conn = self.connect_database('postgres')
109 conn.set_isolation_level(0)
111 cur.execute('DROP DATABASE IF EXISTS {}'.format(name))
114 def setup_template_db(self):
115 """ Setup a template database that already contains common test data.
116 Having a template database speeds up tests considerably but at
117 the price that the tests sometimes run with stale data.
119 if self.template_db_done:
122 self.template_db_done = True
124 if self._reuse_or_drop_db(self.template_db):
128 # call the first part of database setup
129 self.write_nominatim_config(self.template_db)
130 self.run_setup_script('create-db', 'setup-db')
131 # remove external data to speed up indexing for tests
132 conn = self.connect_database(self.template_db)
134 cur.execute("""select tablename from pg_tables
135 where tablename in ('gb_postcode', 'us_postcode')""")
137 conn.cursor().execute('TRUNCATE TABLE {}'.format(t[0]))
141 # execute osm2pgsql import on an empty file to get the right tables
142 with tempfile.NamedTemporaryFile(dir='/tmp', suffix='.xml') as fd:
143 fd.write(b'<osm version="0.6"></osm>')
145 self.run_setup_script('import-data',
149 'create-partition-tables',
150 'create-partition-functions',
152 'create-search-indices',
154 osm2pgsql_cache='200')
156 self.db_drop_database(self.template_db)
160 def setup_api_db(self):
161 """ Setup a test against the API test database.
163 self.write_nominatim_config(self.api_test_db)
168 self.api_db_done = True
170 if self._reuse_or_drop_db(self.api_test_db):
173 testdata = Path('__file__') / '..' / '..' / 'testdb'
174 self.test_env['NOMINATIM_TIGER_DATA_PATH'] = str((testdata / 'tiger').resolve())
175 self.test_env['NOMINATIM_WIKIPEDIA_DATA_PATH'] = str(testdata.resolve())
178 self.run_setup_script('all', osm_file=self.api_test_file)
179 self.run_setup_script('import-tiger-data')
181 phrase_file = str((testdata / 'specialphrases_testdb.sql').resolve())
182 run_script(['psql', '-d', self.api_test_db, '-f', phrase_file])
184 self.db_drop_database(self.api_test_db)
188 def setup_unknown_db(self):
189 """ Setup a test against a non-existing database.
191 self.write_nominatim_config('UNKNOWN_DATABASE_NAME')
193 def setup_db(self, context):
194 """ Setup a test against a fresh, empty test database.
196 self.setup_template_db()
197 self.write_nominatim_config(self.test_db)
198 conn = self.connect_database(self.template_db)
199 conn.set_isolation_level(0)
201 cur.execute('DROP DATABASE IF EXISTS {}'.format(self.test_db))
202 cur.execute('CREATE DATABASE {} TEMPLATE = {}'.format(self.test_db, self.template_db))
204 context.db = self.connect_database(self.test_db)
205 context.db.autocommit = True
206 psycopg2.extras.register_hstore(context.db, globally=False)
208 def teardown_db(self, context):
209 """ Remove the test database, if it exists.
214 if not self.keep_scenario_db:
215 self.db_drop_database(self.test_db)
217 def _reuse_or_drop_db(self, name):
218 """ Check for the existance of the given DB. If reuse is enabled,
219 then the function checks for existance and returns True if the
220 database is already there. Otherwise an existing database is
221 dropped and always false returned.
223 if self.reuse_template:
224 conn = self.connect_database('postgres')
225 with conn.cursor() as cur:
226 cur.execute('select count(*) from pg_database where datname = %s',
228 if cur.fetchone()[0] == 1:
232 self.db_drop_database(name)
236 def reindex_placex(self, db):
237 """ Run the indexing step until all data in the placex has
238 been processed. Indexing during updates can produce more data
239 to index under some circumstances. That is why indexing may have
240 to be run multiple times.
242 with db.cursor() as cur:
244 self.run_update_script('index')
246 cur.execute("SELECT 'a' FROM placex WHERE indexed_status != 0 LIMIT 1")
247 if cur.rowcount == 0:
250 def run_setup_script(self, *args, **kwargs):
251 """ Run the Nominatim setup script with the given arguments.
253 self.run_nominatim_script('setup', *args, **kwargs)
255 def run_update_script(self, *args, **kwargs):
256 """ Run the Nominatim update script with the given arguments.
258 self.run_nominatim_script('update', *args, **kwargs)
260 def run_nominatim_script(self, script, *args, **kwargs):
261 """ Run one of the Nominatim utility scripts with the given arguments.
263 cmd = ['/usr/bin/env', 'php', '-Cq']
264 cmd.append((Path(self.src_dir) / 'lib' / 'admin' / '{}.php'.format(script)).resolve())
265 cmd.extend(['--' + x for x in args])
266 for k, v in kwargs.items():
267 cmd.extend(('--' + k.replace('_', '-'), str(v)))
269 if self.website_dir is not None:
270 cwd = self.website_dir.name
274 run_script(cmd, cwd=cwd, env=self.test_env)
276 def copy_from_place(self, db):
277 """ Copy data from place to the placex and location_property_osmline
278 tables invoking the appropriate triggers.
280 self.run_setup_script('create-functions', 'create-partition-functions')
282 with db.cursor() as cur:
283 cur.execute("""INSERT INTO placex (osm_type, osm_id, class, type,
284 name, admin_level, address,
286 SELECT osm_type, osm_id, class, type,
287 name, admin_level, address,
290 WHERE not (class='place' and type='houses' and osm_type='W')""")
291 cur.execute("""INSERT INTO location_property_osmline (osm_id, address, linegeo)
292 SELECT osm_id, address, geometry
294 WHERE class='place' and type='houses'
296 and ST_GeometryType(geometry) = 'ST_LineString'""")