Skip to content

Commit 9d3adf7

Browse files
committed
add tests on the server side
1 parent dafb8d8 commit 9d3adf7

File tree

8 files changed

+328
-1
lines changed

8 files changed

+328
-1
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ lizmap.cfg
1515
build/
1616
dist/
1717

18+
.local
19+
.cache
20+
.pytest_cache
21+
__output__
22+
1823
lizmap.zip
1924
unittest.qgs
2025
test/data/points_lines.gpkg-shm

Makefile

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,3 +235,32 @@ i18n_3_pull:
235235
i18n_4_compile:
236236
@echo Compile TS files to QM 4/4
237237
@./scripts/update_compiled_strings.sh $(LOCALES)
238+
239+
SHELL:=bash
240+
241+
COMMITID=$(shell git rev-parse --short HEAD)
242+
243+
ifdef REGISTRY_URL
244+
REGISTRY_PREFIX=$(REGISTRY_URL)/
245+
endif
246+
247+
FLAVOR:=3.4
248+
249+
BECOME_USER:=$(shell id -u)
250+
251+
QGIS_IMAGE=$(REGISTRY_PREFIX)qgis-platform:$(FLAVOR)
252+
253+
LOCAL_HOME ?= $(shell pwd)
254+
255+
SRCDIR=$(shell realpath .)
256+
257+
test_server:
258+
mkdir -p $$(pwd)/.local $(LOCAL_HOME)/.cache
259+
docker run --rm --name qgis-server-lizmap-test-$(FLAVOR)-$(COMMITID) -w /src/test/server \
260+
-u $(BECOME_USER) \
261+
-v $(SRCDIR):/src \
262+
-v $$(pwd)/.local:/.local \
263+
-v $(LOCAL_HOME)/.cache:/.cache \
264+
-e PIP_CACHE_DIR=/.cache \
265+
-e PYTEST_ADDOPTS="$(TEST_OPTS)" \
266+
$(QGIS_IMAGE) ./run-tests.sh

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ From QGIS application:
4949
or from GitHub repository:
5050

5151
1. Clone the repo: `git clone --recursive git@github.com:3liz/lizmap-plugin.git lizmap`
52-
1. `mv lizmap/lizmap ~/.local/share/QGIS/QGIS3/profiles/default/python/plugins`
52+
1. `mv lizmap ~/.local/share/QGIS/QGIS3/profiles/default/python/plugins`
5353

5454
If it's from a previous GitHub repository:
5555
1. `git submodule update` to update the submodule.

test/server/conftest.py

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
import sys
2+
import os
3+
import pytest
4+
import lxml.etree
5+
import glob
6+
import configparser
7+
import logging
8+
9+
logging.basicConfig( stream=sys.stderr )
10+
logging.disable(logging.NOTSET)
11+
12+
LOGGER = logging.getLogger('server')
13+
LOGGER.setLevel(logging.DEBUG)
14+
15+
from typing import Any, Mapping, Dict, Generator
16+
17+
from qgis.core import Qgis, QgsApplication, QgsProject,QgsFontUtils
18+
from qgis.server import (QgsServer,
19+
QgsServerRequest,
20+
QgsBufferServerRequest,
21+
QgsBufferServerResponse)
22+
23+
qgis_application = None
24+
25+
def pytest_addoption(parser):
26+
parser.addoption("--qgis-plugins", metavar="PATH", help="Plugin path", default=None)
27+
28+
29+
plugin_path = None
30+
31+
32+
def pytest_configure(config):
33+
global plugin_path
34+
plugin_path = config.getoption('qgis_plugins')
35+
36+
37+
def pytest_sessionstart(session):
38+
""" Start qgis application
39+
"""
40+
global qgis_application
41+
os.environ['QT_QPA_PLATFORM'] = 'offscreen'
42+
43+
# Define this in global environment
44+
#os.environ['QGIS_DISABLE_MESSAGE_HOOKS'] = 1
45+
#os.environ['QGIS_NO_OVERRIDE_IMPORT'] = 1
46+
qgis_application = QgsApplication([], False)
47+
qgis_application.initQgis()
48+
49+
# Install logger hook
50+
install_logger_hook(verbose=True)
51+
52+
53+
def pytest_sessionfinish(session, exitstatus):
54+
""" End qgis session
55+
"""
56+
global qgis_application
57+
qgis_application.exitQgis()
58+
del qgis_application
59+
60+
61+
NAMESPACES = {
62+
'xlink': "http://www.w3.org/1999/xlink",
63+
'wms': "http://www.opengis.net/wms",
64+
'wfs': "http://www.opengis.net/wfs",
65+
'wcs': "http://www.opengis.net/wcs",
66+
'ows': "http://www.opengis.net/ows/1.1",
67+
'gml': "http://www.opengis.net/gml",
68+
'xsi': "http://www.w3.org/2001/XMLSchema-instance"
69+
}
70+
71+
class OWSResponse:
72+
73+
def __init__(self, resp: QgsBufferServerResponse) -> None:
74+
self._resp = resp
75+
self._xml = None
76+
77+
@property
78+
def xml(self) -> 'xml':
79+
if self._xml is None and self._resp.headers().get('Content-Type','').find('text/xml')==0:
80+
self._xml = lxml.etree.fromstring(self.content.decode('utf-8'))
81+
return self._xml
82+
83+
@property
84+
def content(self) -> bytes:
85+
return bytes(self._resp.body())
86+
87+
@property
88+
def status_code(self) -> int:
89+
return self._resp.statusCode()
90+
91+
@property
92+
def headers(self) -> Dict[str,str]:
93+
return self._resp.headers()
94+
95+
def xpath(self, path: str) -> lxml.etree.Element:
96+
assert self.xml is not None
97+
return self.xml.xpath(path, namespaces=NAMESPACES)
98+
99+
def xpath_text(self, path: str) -> str:
100+
assert self.xml is not None
101+
return ' '.join(e.text for e in self.xpath(path))
102+
103+
104+
@pytest.fixture(scope='session')
105+
def client(request):
106+
""" Return a qgis server instance
107+
"""
108+
class _Client:
109+
110+
def __init__(self) -> None:
111+
self.fontFamily = QgsFontUtils.standardTestFontFamily()
112+
QgsFontUtils.loadStandardTestFonts(['All'])
113+
114+
# Activate debug headers
115+
os.environ['QGIS_WMTS_CACHE_DEBUG_HEADERS'] = 'true'
116+
117+
self.datapath = request.config.rootdir.join('data')
118+
self.server = QgsServer()
119+
120+
121+
# Load plugins
122+
load_plugins(self.server.serverInterface())
123+
124+
def getplugin(self, name) -> Any:
125+
""" retourne l'instance du plugin
126+
"""
127+
return server_plugins.get(name)
128+
129+
def getprojectpath(self, name: str) -> str:
130+
return self.datapath.join(name)
131+
132+
def get(self, query: str, project: str=None) -> OWSResponse:
133+
""" Return server response from query
134+
"""
135+
request = QgsBufferServerRequest(query, QgsServerRequest.GetMethod, {}, None)
136+
response = QgsBufferServerResponse()
137+
if project is not None and not os.path.isabs(project):
138+
projectpath = self.datapath.join(project)
139+
qgsproject = QgsProject()
140+
if not qgsproject.read(projectpath.strpath):
141+
raise ValueError("Error reading project '%s':" % projectpath.strpath)
142+
else:
143+
qgsproject = None
144+
self.server.handleRequest(request, response, project=qgsproject)
145+
return OWSResponse(response)
146+
147+
return _Client()
148+
149+
150+
##
151+
## Plugins
152+
##
153+
154+
def checkQgisVersion(minver: str, maxver: str) -> bool:
155+
156+
def to_int(ver):
157+
major, *ver = ver.split('.')
158+
major = int(major)
159+
minor = int(ver[0]) if len(ver) > 0 else 0
160+
rev = int(ver[1]) if len(ver) > 1 else 0
161+
if minor >= 99:
162+
minor = rev = 0
163+
major += 1
164+
if rev > 99:
165+
rev = 99
166+
return int("{:d}{:02d}{:02d}".format(major,minor,rev))
167+
168+
169+
version = to_int(Qgis.QGIS_VERSION.split('-')[0])
170+
minver = to_int(minver) if minver else version
171+
maxver = to_int(maxver) if maxver else version
172+
return minver <= version <= maxver
173+
174+
175+
def find_plugins(pluginpath: str) -> Generator[str,None,None]:
176+
""" Load plugins
177+
"""
178+
for plugin in glob.glob(os.path.join(plugin_path + "/*")):
179+
if not os.path.exists(os.path.join(plugin, '__init__.py')):
180+
continue
181+
182+
metadatafile = os.path.join(plugin, 'metadata.txt')
183+
if not os.path.exists(metadatafile):
184+
continue
185+
186+
cp = configparser.ConfigParser()
187+
try:
188+
with open(metadatafile, mode='rt') as f:
189+
cp.read_file(f)
190+
if not cp['general'].getboolean('server'):
191+
logging.critical("%s is not a server plugin", plugin)
192+
continue
193+
194+
minver = cp['general'].get('qgisMinimumVersion')
195+
maxver = cp['general'].get('qgisMaximumVersion')
196+
except Exception as exc:
197+
LOGGER.critical("Error reading plugin metadata '%s': %s",metadatafile,exc)
198+
continue
199+
200+
if not checkQgisVersion(minver,maxver):
201+
LOGGER.critical(("Unsupported version for %s:"
202+
"\n MinimumVersion: %s"
203+
"\n MaximumVersion: %s"
204+
"\n Qgis version: %s"
205+
"\n Discarding") % (plugin,minver,maxver,
206+
Qgis.QGIS_VERSION.split('-')[0]))
207+
continue
208+
209+
yield os.path.basename(plugin)
210+
211+
212+
server_plugins = {}
213+
214+
215+
def load_plugins(serverIface: 'QgsServerInterface') -> None:
216+
""" Start all plugins
217+
"""
218+
if not plugin_path:
219+
return
220+
221+
LOGGER.info("Initializing plugins from %s", plugin_path)
222+
sys.path.append(plugin_path)
223+
224+
for plugin in find_plugins(plugin_path):
225+
try:
226+
__import__(plugin)
227+
228+
package = sys.modules[plugin]
229+
230+
# Initialize the plugin
231+
server_plugins[plugin] = package.serverClassFactory(serverIface)
232+
LOGGER.info("Loaded plugin %s",plugin)
233+
except:
234+
LOGGER.error("Error loading plugin %s",plugin)
235+
raise
236+
237+
238+
#
239+
# Logger hook
240+
#
241+
242+
def install_logger_hook( verbose: bool=False ) -> None:
243+
""" Install message log hook
244+
"""
245+
from qgis.core import Qgis, QgsApplication, QgsMessageLog
246+
# Add a hook to qgis message log
247+
def writelogmessage(message, tag, level):
248+
arg = '{}: {}'.format( tag, message )
249+
if level == Qgis.Warning:
250+
LOGGER.warning(arg)
251+
elif level == Qgis.Critical:
252+
LOGGER.error(arg)
253+
elif verbose:
254+
# Qgis is somehow very noisy
255+
# log only if verbose is set
256+
LOGGER.info(arg)
257+
258+
messageLog = QgsApplication.messageLog()
259+
messageLog.messageReceived.connect( writelogmessage )

test/server/pytest.ini

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[pytest]
2+
addopts= --junit-xml=__output__/junit.xml
3+
junit_family=xunit2
4+
norecursedirs=
5+
tests/data
6+
.local
7+
.cache
8+
__output__
9+
__pycache__
10+

test/server/requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pytest
2+
lxml
3+

test/server/run-tests.sh

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
# Add /.local to path
6+
export PATH=$PATH:/.local/bin
7+
8+
echo "Installing required packages..."
9+
pip3 install -q -U --prefer-binary --user -r requirements.txt
10+
11+
# Disable qDebug stuff that bloats test outputs
12+
export QT_LOGGING_RULES="*.debug=false;*.warning=false"
13+
14+
# Disable python hooks/overrides
15+
export QGIS_DISABLE_MESSAGE_HOOKS=1
16+
export QGIS_NO_OVERRIDE_IMPORT=1
17+
18+
pytest -v --qgis-plugins=/src $@
19+

test/server/test_sample.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def test_assert(client):
2+
assert True

0 commit comments

Comments
 (0)