extend BDD API tests to query via Python frameworks

A new config option ENGINE allows to choose between php and any of the
supported Python engines.
This commit is contained in:
Sarah Hoffmann
2022-12-06 11:20:50 +01:00
parent d7bc846c3c
commit 7219ee6532
6 changed files with 83 additions and 11 deletions

View File

@@ -7,7 +7,7 @@
""" """
Server implementation using the falcon webserver framework. Server implementation using the falcon webserver framework.
""" """
from typing import Type, Any from typing import Type, Any, Optional, Mapping
from pathlib import Path from pathlib import Path
import falcon import falcon
@@ -26,8 +26,8 @@ class NominatimV1:
""" Implementation of V1 version of the Nominatim API. """ Implementation of V1 version of the Nominatim API.
""" """
def __init__(self, project_dir: Path) -> None: def __init__(self, project_dir: Path, environ: Optional[Mapping[str, str]]) -> None:
self.api = NominatimAPIAsync(project_dir) self.api = NominatimAPIAsync(project_dir, environ)
self.formatters = {} self.formatters = {}
for rtype in (StatusResult, ): for rtype in (StatusResult, ):
@@ -67,12 +67,13 @@ class NominatimV1:
self.format_response(req, resp, result) self.format_response(req, resp, result)
def get_application(project_dir: Path) -> falcon.asgi.App: def get_application(project_dir: Path,
environ: Optional[Mapping[str, str]] = None) -> falcon.asgi.App:
""" Create a Nominatim falcon ASGI application. """ Create a Nominatim falcon ASGI application.
""" """
app = falcon.asgi.App() app = falcon.asgi.App()
api = NominatimV1(project_dir) api = NominatimV1(project_dir, environ)
app.add_route('/status', api, suffix='status') app.add_route('/status', api, suffix='status')

View File

@@ -7,7 +7,7 @@
""" """
Server implementation using the sanic webserver framework. Server implementation using the sanic webserver framework.
""" """
from typing import Any, Optional from typing import Any, Optional, Mapping
from pathlib import Path from pathlib import Path
import sanic import sanic
@@ -64,12 +64,13 @@ async def status(request: sanic.Request) -> sanic.HTTPResponse:
return api_response(request,await request.app.ctx.api.status()) return api_response(request,await request.app.ctx.api.status())
def get_application(project_dir: Path) -> sanic.Sanic: def get_application(project_dir: Path,
environ: Optional[Mapping[str, str]] = None) -> sanic.Sanic:
""" Create a Nominatim sanic ASGI application. """ Create a Nominatim sanic ASGI application.
""" """
app = sanic.Sanic("NominatimInstance") app = sanic.Sanic("NominatimInstance")
app.ctx.api = NominatimAPIAsync(project_dir) app.ctx.api = NominatimAPIAsync(project_dir, environ)
app.ctx.formatters = {} app.ctx.formatters = {}
for rtype in (StatusResult, ): for rtype in (StatusResult, ):
app.ctx.formatters[rtype] = formatting.create(rtype) app.ctx.formatters[rtype] = formatting.create(rtype)

View File

@@ -7,7 +7,7 @@
""" """
Server implementation using the starlette webserver framework. Server implementation using the starlette webserver framework.
""" """
from typing import Any, Type from typing import Any, Type, Optional, Mapping
from pathlib import Path from pathlib import Path
from starlette.applications import Starlette from starlette.applications import Starlette
@@ -67,11 +67,12 @@ V1_ROUTES = [
Route('/status', endpoint=on_status) Route('/status', endpoint=on_status)
] ]
def get_application(project_dir: Path) -> Starlette: def get_application(project_dir: Path,
environ: Optional[Mapping[str, str]] = None) -> Starlette:
""" Create a Nominatim falcon ASGI application. """ Create a Nominatim falcon ASGI application.
""" """
app = Starlette(debug=True, routes=V1_ROUTES) app = Starlette(debug=True, routes=V1_ROUTES)
app.state.API = NominatimAPIAsync(project_dir) app.state.API = NominatimAPIAsync(project_dir, environ)
return app return app

View File

@@ -28,6 +28,7 @@ userconfig = {
'SERVER_MODULE_PATH' : None, 'SERVER_MODULE_PATH' : None,
'TOKENIZER' : None, # Test with a custom tokenizer 'TOKENIZER' : None, # Test with a custom tokenizer
'STYLE' : 'extratags', 'STYLE' : 'extratags',
'API_ENGINE': 'php',
'PHPCOV' : False, # set to output directory to enable code coverage 'PHPCOV' : False, # set to output directory to enable code coverage
} }

View File

@@ -5,9 +5,13 @@
# Copyright (C) 2022 by the Nominatim developer community. # Copyright (C) 2022 by the Nominatim developer community.
# For a full list of authors see the git log. # For a full list of authors see the git log.
from pathlib import Path from pathlib import Path
import importlib
import sys import sys
import tempfile import tempfile
from asgi_lifespan import LifespanManager
import httpx
import psycopg2 import psycopg2
import psycopg2.extras import psycopg2.extras
@@ -49,6 +53,12 @@ class NominatimEnvironment:
self.api_db_done = False self.api_db_done = False
self.website_dir = None self.website_dir = None
self.api_engine = None
if config['API_ENGINE'] != 'php':
if not hasattr(self, f"create_api_request_func_{config['API_ENGINE']}"):
raise RuntimeError(f"Unknown API engine '{config['API_ENGINE']}'")
self.api_engine = getattr(self, f"create_api_request_func_{config['API_ENGINE']}")()
def connect_database(self, dbname): def connect_database(self, dbname):
""" Return a connection to the database with the given name. """ Return a connection to the database with the given name.
Uses configured host, user and port. Uses configured host, user and port.
@@ -323,3 +333,49 @@ class NominatimEnvironment:
WHERE class='place' and type='houses' WHERE class='place' and type='houses'
and osm_type='W' and osm_type='W'
and ST_GeometryType(geometry) = 'ST_LineString'""") and ST_GeometryType(geometry) = 'ST_LineString'""")
def create_api_request_func_starlette(self):
import nominatim.server.starlette.server
async def _request(endpoint, params, project_dir, environ):
app = nominatim.server.starlette.server.get_application(project_dir, environ)
async with LifespanManager(app):
async with httpx.AsyncClient(app=app, base_url="http://nominatim.test") as client:
response = await client.get(f"/{endpoint}", params=params)
return response.text, response.status_code
return _request
def create_api_request_func_sanic(self):
import nominatim.server.sanic.server
async def _request(endpoint, params, project_dir, environ):
app = nominatim.server.sanic.server.get_application(project_dir, environ)
_, response = await app.asgi_client.get(f"/{endpoint}", params=params)
return response.text, response.status_code
return _request
def create_api_request_func_falcon(self):
import nominatim.server.falcon.server
import falcon.testing
async def _request(endpoint, params, project_dir, environ):
app = nominatim.server.falcon.server.get_application(project_dir, environ)
async with falcon.testing.ASGIConductor(app) as conductor:
response = await conductor.get(f"/{endpoint}", params=params)
return response.text, response.status_code
return _request

View File

@@ -9,10 +9,12 @@
Queries may either be run directly via PHP using the query script Queries may either be run directly via PHP using the query script
or via the HTTP interface using php-cgi. or via the HTTP interface using php-cgi.
""" """
from pathlib import Path
import json import json
import os import os
import re import re
import logging import logging
import asyncio
from urllib.parse import urlencode from urllib.parse import urlencode
from utils import run_script from utils import run_script
@@ -72,6 +74,16 @@ def send_api_query(endpoint, params, fmt, context):
for h in context.table.headings: for h in context.table.headings:
params[h] = context.table[0][h] params[h] = context.table[0][h]
if context.nominatim.api_engine is None:
return send_api_query_php(endpoint, params, context)
return asyncio.run(context.nominatim.api_engine(endpoint, params,
Path(context.nominatim.website_dir.name),
context.nominatim.test_env))
def send_api_query_php(endpoint, params, context):
env = dict(BASE_SERVER_ENV) env = dict(BASE_SERVER_ENV)
env['QUERY_STRING'] = urlencode(params) env['QUERY_STRING'] = urlencode(params)