]> git.openstreetmap.org Git - nominatim.git/blob - test/bdd/steps/nominatim_environment.py
345e134975db02b77425634d4ecb4a08b8242661
[nominatim.git] / test / bdd / steps / nominatim_environment.py
1 from pathlib import Path
2 import sys
3 import tempfile
4
5 import psycopg2
6 import psycopg2.extras
7
8 sys.path.insert(1, str((Path(__file__) / '..' / '..' / '..' / '..').resolve()))
9
10 from nominatim import cli
11 from nominatim.config import Configuration
12 from nominatim.tools import refresh
13 from steps.utils import run_script
14
15 class NominatimEnvironment:
16     """ Collects all functions for the execution of Nominatim functions.
17     """
18
19     def __init__(self, config):
20         self.build_dir = Path(config['BUILDDIR']).resolve()
21         self.src_dir = (Path(__file__) / '..' / '..' / '..' / '..').resolve()
22         self.db_host = config['DB_HOST']
23         self.db_port = config['DB_PORT']
24         self.db_user = config['DB_USER']
25         self.db_pass = config['DB_PASS']
26         self.template_db = config['TEMPLATE_DB']
27         self.test_db = config['TEST_DB']
28         self.api_test_db = config['API_TEST_DB']
29         self.api_test_file = config['API_TEST_FILE']
30         self.server_module_path = config['SERVER_MODULE_PATH']
31         self.reuse_template = not config['REMOVE_TEMPLATE']
32         self.keep_scenario_db = config['KEEP_TEST_DB']
33         self.code_coverage_path = config['PHPCOV']
34         self.code_coverage_id = 1
35
36         self.default_config = Configuration(None, self.src_dir / 'settings').get_os_env()
37         self.test_env = None
38         self.template_db_done = False
39         self.api_db_done = False
40         self.website_dir = None
41
42     def connect_database(self, dbname):
43         """ Return a connection to the database with the given name.
44             Uses configured host, user and port.
45         """
46         dbargs = {'database': dbname}
47         if self.db_host:
48             dbargs['host'] = self.db_host
49         if self.db_port:
50             dbargs['port'] = self.db_port
51         if self.db_user:
52             dbargs['user'] = self.db_user
53         if self.db_pass:
54             dbargs['password'] = self.db_pass
55         conn = psycopg2.connect(**dbargs)
56         return conn
57
58     def next_code_coverage_file(self):
59         """ Generate the next name for a coverage file.
60         """
61         fn = Path(self.code_coverage_path) / "{:06d}.cov".format(self.code_coverage_id)
62         self.code_coverage_id += 1
63
64         return fn.resolve()
65
66     def write_nominatim_config(self, dbname):
67         """ Set up a custom test configuration that connects to the given
68             database. This sets up the environment variables so that they can
69             be picked up by dotenv and creates a project directory with the
70             appropriate website scripts.
71         """
72         dsn = 'pgsql:dbname={}'.format(dbname)
73         if self.db_host:
74             dsn += ';host=' + self.db_host
75         if self.db_port:
76             dsn += ';port=' + self.db_port
77         if self.db_user:
78             dsn += ';user=' + self.db_user
79         if self.db_pass:
80             dsn += ';password=' + self.db_pass
81
82         if self.website_dir is not None \
83            and self.test_env is not None \
84            and dsn == self.test_env['NOMINATIM_DATABASE_DSN']:
85             return # environment already set uo
86
87         self.test_env = dict(self.default_config)
88         self.test_env['NOMINATIM_DATABASE_DSN'] = dsn
89         self.test_env['NOMINATIM_FLATNODE_FILE'] = ''
90         self.test_env['NOMINATIM_IMPORT_STYLE'] = 'full'
91         self.test_env['NOMINATIM_USE_US_TIGER_DATA'] = 'yes'
92         self.test_env['NOMINATIM_DATADIR'] = str((self.src_dir / 'data').resolve())
93         self.test_env['NOMINATIM_SQLDIR'] = str((self.src_dir / 'lib-sql').resolve())
94         self.test_env['NOMINATIM_CONFIGDIR'] = str((self.src_dir / 'settings').resolve())
95         self.test_env['NOMINATIM_DATABASE_MODULE_SRC_PATH'] = str((self.build_dir / 'module').resolve())
96         self.test_env['NOMINATIM_OSM2PGSQL_BINARY'] = str((self.build_dir / 'osm2pgsql' / 'osm2pgsql').resolve())
97         self.test_env['NOMINATIM_NOMINATIM_TOOL'] = str((self.build_dir / 'nominatim').resolve())
98
99         if self.server_module_path:
100             self.test_env['NOMINATIM_DATABASE_MODULE_PATH'] = self.server_module_path
101         else:
102             # avoid module being copied into the temporary environment
103             self.test_env['NOMINATIM_DATABASE_MODULE_PATH'] = str((self.build_dir / 'module').resolve())
104
105         if self.website_dir is not None:
106             self.website_dir.cleanup()
107
108         self.website_dir = tempfile.TemporaryDirectory()
109         refresh.setup_website(Path(self.website_dir.name) / 'website',
110                               self.get_test_config())
111
112
113     def get_test_config(self):
114         cfg = Configuration(Path(self.website_dir.name), self.src_dir / 'settings',
115                             environ=self.test_env)
116         cfg.set_libdirs(module=self.build_dir / 'module',
117                         osm2pgsql=self.build_dir / 'osm2pgsql' / 'osm2pgsql',
118                         php=self.src_dir / 'lib-php',
119                         sql=self.src_dir / 'lib-sql',
120                         data=self.src_dir / 'data')
121         return cfg
122
123     def get_libpq_dsn(self):
124         dsn = self.test_env['NOMINATIM_DATABASE_DSN']
125
126         def quote_param(param):
127             key, val = param.split('=')
128             val = val.replace('\\', '\\\\').replace("'", "\\'")
129             if ' ' in val:
130                 val = "'" + val + "'"
131             return key + '=' + val
132
133         if dsn.startswith('pgsql:'):
134             # Old PHP DSN format. Convert before returning.
135             return ' '.join([quote_param(p) for p in dsn[6:].split(';')])
136
137         return dsn
138
139
140     def db_drop_database(self, name):
141         """ Drop the database with the given name.
142         """
143         conn = self.connect_database('postgres')
144         conn.set_isolation_level(0)
145         cur = conn.cursor()
146         cur.execute('DROP DATABASE IF EXISTS {}'.format(name))
147         conn.close()
148
149     def setup_template_db(self):
150         """ Setup a template database that already contains common test data.
151             Having a template database speeds up tests considerably but at
152             the price that the tests sometimes run with stale data.
153         """
154         if self.template_db_done:
155             return
156
157         self.template_db_done = True
158
159         if self._reuse_or_drop_db(self.template_db):
160             return
161
162         self.write_nominatim_config(self.template_db)
163
164         try:
165             # execute nominatim import on an empty file to get the right tables
166             with tempfile.NamedTemporaryFile(dir='/tmp', suffix='.xml') as fd:
167                 fd.write(b'<osm version="0.6"></osm>')
168                 fd.flush()
169                 self.run_nominatim('import', '--osm-file', fd.name,
170                                              '--osm2pgsql-cache', '1',
171                                              '--ignore-errors')
172         except:
173             self.db_drop_database(self.template_db)
174             raise
175
176
177     def setup_api_db(self):
178         """ Setup a test against the API test database.
179         """
180         self.write_nominatim_config(self.api_test_db)
181
182         if self.api_db_done:
183             return
184
185         self.api_db_done = True
186
187         if self._reuse_or_drop_db(self.api_test_db):
188             return
189
190         testdata = Path('__file__') / '..' / '..' / 'testdb'
191         self.test_env['NOMINATIM_WIKIPEDIA_DATA_PATH'] = str(testdata.resolve())
192
193         try:
194             self.run_nominatim('import', '--osm-file', str(self.api_test_file))
195             self.run_nominatim('add-data', '--tiger-data', str((testdata / 'tiger').resolve()))
196             self.run_nominatim('freeze')
197
198             phrase_file = str((testdata / 'specialphrases_testdb.sql').resolve())
199             run_script(['psql', '-d', self.api_test_db, '-f', phrase_file])
200         except:
201             self.db_drop_database(self.api_test_db)
202             raise
203
204
205     def setup_unknown_db(self):
206         """ Setup a test against a non-existing database.
207         """
208         self.write_nominatim_config('UNKNOWN_DATABASE_NAME')
209
210     def setup_db(self, context):
211         """ Setup a test against a fresh, empty test database.
212         """
213         self.setup_template_db()
214         self.write_nominatim_config(self.test_db)
215         conn = self.connect_database(self.template_db)
216         conn.set_isolation_level(0)
217         cur = conn.cursor()
218         cur.execute('DROP DATABASE IF EXISTS {}'.format(self.test_db))
219         cur.execute('CREATE DATABASE {} TEMPLATE = {}'.format(self.test_db, self.template_db))
220         conn.close()
221         context.db = self.connect_database(self.test_db)
222         context.db.autocommit = True
223         psycopg2.extras.register_hstore(context.db, globally=False)
224
225     def teardown_db(self, context):
226         """ Remove the test database, if it exists.
227         """
228         if 'db' in context:
229             context.db.close()
230
231         if not self.keep_scenario_db:
232             self.db_drop_database(self.test_db)
233
234     def _reuse_or_drop_db(self, name):
235         """ Check for the existance of the given DB. If reuse is enabled,
236             then the function checks for existance and returns True if the
237             database is already there. Otherwise an existing database is
238             dropped and always false returned.
239         """
240         if self.reuse_template:
241             conn = self.connect_database('postgres')
242             with conn.cursor() as cur:
243                 cur.execute('select count(*) from pg_database where datname = %s',
244                             (name,))
245                 if cur.fetchone()[0] == 1:
246                     return True
247             conn.close()
248         else:
249             self.db_drop_database(name)
250
251         return False
252
253     def reindex_placex(self, db):
254         """ Run the indexing step until all data in the placex has
255             been processed. Indexing during updates can produce more data
256             to index under some circumstances. That is why indexing may have
257             to be run multiple times.
258         """
259         with db.cursor() as cur:
260             while True:
261                 self.run_nominatim('index')
262
263                 cur.execute("SELECT 'a' FROM placex WHERE indexed_status != 0 LIMIT 1")
264                 if cur.rowcount == 0:
265                     return
266
267     def run_nominatim(self, *cmdline):
268         """ Run the nominatim command-line tool via the library.
269         """
270         if self.website_dir is not None:
271             cmdline = list(cmdline) + ['--project-dir', self.website_dir.name]
272
273         cli.nominatim(module_dir='',
274                       osm2pgsql_path=str(self.build_dir / 'osm2pgsql' / 'osm2pgsql'),
275                       phplib_dir=str(self.src_dir / 'lib-php'),
276                       sqllib_dir=str(self.src_dir / 'lib-sql'),
277                       data_dir=str(self.src_dir / 'data'),
278                       config_dir=str(self.src_dir / 'settings'),
279                       cli_args=cmdline,
280                       phpcgi_path='',
281                       environ=self.test_env)
282
283
284     def copy_from_place(self, db):
285         """ Copy data from place to the placex and location_property_osmline
286             tables invoking the appropriate triggers.
287         """
288         self.run_nominatim('refresh', '--functions', '--no-diff-updates')
289
290         with db.cursor() as cur:
291             cur.execute("""INSERT INTO placex (osm_type, osm_id, class, type,
292                                                name, admin_level, address,
293                                                extratags, geometry)
294                              SELECT osm_type, osm_id, class, type,
295                                     name, admin_level, address,
296                                     extratags, geometry
297                                FROM place
298                                WHERE not (class='place' and type='houses' and osm_type='W')""")
299             cur.execute("""INSERT INTO location_property_osmline (osm_id, address, linegeo)
300                              SELECT osm_id, address, geometry
301                                FROM place
302                               WHERE class='place' and type='houses'
303                                     and osm_type='W'
304                                     and ST_GeometryType(geometry) = 'ST_LineString'""")