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