1# -*- coding: utf-8 -*-
2"""Convenience interface to a locally spawned QGIS Server, e.g. for unit tests
3
4.. note:: This program is free software; you can redistribute it and/or modify
5it under the terms of the GNU General Public License as published by
6the Free Software Foundation; either version 2 of the License, or
7(at your option) any later version.
8"""
9
10__author__ = 'Larry Shaffer'
11__date__ = '2014/02/11'
12__copyright__ = 'Copyright 2014, The QGIS Project'
13
14import sys
15import os
16import shutil
17import platform
18import subprocess
19import time
20import urllib.request
21import urllib.parse
22import urllib.error
23import urllib.request
24import urllib.error
25import urllib.parse
26import tempfile
27
28from utilities import (
29    unitTestDataPath,
30    getExecutablePath,
31    openInBrowserTab,
32    getTempfilePath
33)
34
35# allow import error to be raised if qgis is not on sys.path
36try:
37    # noinspection PyUnresolvedReferences
38    from qgis.core import QgsRectangle, QgsCoordinateReferenceSystem
39except ImportError as e:
40    raise ImportError(str(e) + '\n\nPlace path to pyqgis modules on sys.path,'
41                               ' or assign to PYTHONPATH')
42
43FCGIBIN = None
44MAPSERV = None
45SERVRUN = False
46
47
48class ServerProcessError(Exception):
49
50    def __init__(self, title, msg, err=''):
51        msg += '\n' + ('\n' + str(err).strip() + '\n' if err else '')
52        self.msg = """
53#----------------------------------------------------------------#
54{0}
55{1}
56#----------------------------------------------------------------#
57    """.format(title, msg)
58
59    def __str__(self):
60        return self.msg
61
62
63class ServerProcess(object):
64
65    def __init__(self):
66        self._startenv = None
67        self._startcmd = []
68        self._stopcmd = []
69        self._restartcmd = []
70        self._statuscmd = []
71        self._process = None
72        """:type : subprocess.Popen"""
73        self._win = self._mac = self._linux = self._unix = False
74        self._dist = ()
75        self._resolve_platform()
76
77    # noinspection PyMethodMayBeStatic
78    def _run(self, cmd, env=None):
79        # print repr(cmd)
80        p = subprocess.Popen(cmd,
81                             stderr=subprocess.PIPE,
82                             stdout=subprocess.PIPE,
83                             env=env,
84                             close_fds=True)
85        err = p.communicate()[1]
86        if err:
87            if p:
88                p.kill()
89                # noinspection PyUnusedLocal
90                p = None
91            cmd_s = repr(cmd).strip() + ('\n' + 'ENV: ' + repr(env).strip() +
92                                         '\n' if env is not None else '')
93            raise ServerProcessError('Server process command failed',
94                                     cmd_s, err)
95        return p
96
97    def start(self):
98        if self.running():
99            return
100        self._process = self._run(self._startcmd, env=self._startenv)
101
102    def stop(self):
103        if not self.running():
104            return False
105        if self._stopcmd:
106            self._run(self._stopcmd)
107        else:
108            self._process.terminate()
109        self._process = None
110        return True
111
112    def restart(self):
113        if self._restartcmd:
114            self._run(self._restartcmd)
115        else:
116            self.stop()
117            self.start()
118
119    def running(self):
120        running = False
121        if self._statuscmd:
122            try:
123                subprocess.check_call(self._statuscmd,
124                                      stdout=subprocess.PIPE,
125                                      stderr=subprocess.PIPE)
126                running = True
127            except subprocess.CalledProcessError:
128                running = False
129        elif self._process:
130            running = self._process.poll() is None
131        return running
132
133    def set_startenv(self, env):
134        self._startenv = env
135
136    def set_startcmd(self, cmd):
137        self._startcmd = cmd
138
139    def set_stopcmd(self, cmd):
140        self._stopcmd = cmd
141
142    def set_restartcmd(self, cmd):
143        self._restartcmd = cmd
144
145    def set_statuscmd(self, cmd):
146        self._statuscmd = cmd
147
148    def process(self):
149        return self._process
150
151    def pid(self):
152        pid = 0
153        if self._process:
154            pid = self._process.pid
155        return pid
156
157    def _resolve_platform(self):
158        s = platform.system().lower()
159        self._linux = s.startswith('lin')
160        self._mac = s.startswith('dar')
161        self._unix = self._linux or self._mac
162        self._win = s.startswith('win')
163        self._dist = platform.dist()
164
165
166class WebServerProcess(ServerProcess):
167
168    def __init__(self, kind, exe, conf_dir, temp_dir):
169        ServerProcess.__init__(self)
170        sufx = 'unix' if self._unix else 'win'
171        if kind == 'lighttpd':
172            conf = os.path.join(conf_dir, 'lighttpd', 'config',
173                                'lighttpd_{0}.conf'.format(sufx))
174            self.set_startenv({'QGIS_SERVER_TEMP_DIR': temp_dir})
175            init_scr_dir = os.path.join(conf_dir, 'lighttpd', 'scripts')
176            if self._mac:
177                init_scr = os.path.join(init_scr_dir, 'lighttpd_mac.sh')
178                self.set_startcmd([init_scr, 'start', exe, conf, temp_dir])
179                self.set_stopcmd([init_scr, 'stop'])
180                self.set_restartcmd([init_scr, 'restart', exe, conf, temp_dir])
181                self.set_statuscmd([init_scr, 'status'])
182            elif self._linux:
183                dist = self._dist[0].lower()
184                if dist == 'debian' or dist == 'ubuntu':
185                    init_scr = os.path.join(init_scr_dir, 'lighttpd_debian.sh')
186                    self.set_startcmd([
187                        init_scr, 'start', exe, temp_dir, conf])
188                    self.set_stopcmd([init_scr, 'stop', exe, temp_dir])
189                    self.set_restartcmd([
190                        init_scr, 'restart', exe, temp_dir, conf])
191                    self.set_statuscmd([init_scr, 'status', exe, temp_dir])
192                elif dist == 'fedora' or dist == 'rhel':  # are these correct?
193                    pass
194            else:  # win
195                pass
196
197
198class FcgiServerProcess(ServerProcess):
199
200    def __init__(self, kind, exe, fcgi_bin, conf_dir, temp_dir):
201        ServerProcess.__init__(self)
202        if kind == 'spawn-fcgi':
203            if self._unix:
204                fcgi_sock = os.path.join(temp_dir, 'var', 'run',
205                                         'qgs_mapserv.sock')
206                init_scr_dir = os.path.join(conf_dir, 'fcgi', 'scripts')
207                self.set_startenv({
208                    'QGIS_LOG_FILE':
209                    os.path.join(temp_dir, 'log', 'qgis_server.log')})
210                if self._mac:
211                    init_scr = os.path.join(init_scr_dir, 'spawn_fcgi_mac.sh')
212                    self.set_startcmd([init_scr, 'start', exe, fcgi_sock,
213                                       temp_dir + fcgi_bin, temp_dir])
214                    self.set_stopcmd([init_scr, 'stop'])
215                    self.set_restartcmd([init_scr, 'restart', exe, fcgi_sock,
216                                         temp_dir + fcgi_bin, temp_dir])
217                    self.set_statuscmd([init_scr, 'status'])
218                elif self._linux:
219                    dist = self._dist[0].lower()
220                    if dist == 'debian' or dist == 'ubuntu':
221                        init_scr = os.path.join(init_scr_dir,
222                                                'spawn_fcgi_debian.sh')
223                        self.set_startcmd([
224                            init_scr, 'start', exe, fcgi_sock,
225                            temp_dir + fcgi_bin, temp_dir])
226                        self.set_stopcmd([
227                            init_scr, 'stop', exe, fcgi_sock,
228                            temp_dir + fcgi_bin, temp_dir])
229                        self.set_restartcmd([
230                            init_scr, 'restart', exe, fcgi_sock,
231                            temp_dir + fcgi_bin, temp_dir])
232                        self.set_statuscmd([
233                            init_scr, 'status', exe, fcgi_sock,
234                            temp_dir + fcgi_bin, temp_dir])
235                    elif dist == 'fedora' or dist == 'rhel':
236                        pass
237            else:  # win
238                pass
239
240
241# noinspection PyPep8Naming,PyShadowingNames
242class QgisLocalServer(object):
243
244    def __init__(self, fcgi_bin):
245        msg = 'FCGI binary not found at:\n{0}'.format(fcgi_bin)
246        assert os.path.exists(fcgi_bin), msg
247
248        msg = "FCGI binary not 'qgis_mapserv.fcgi':"
249        assert fcgi_bin.endswith('qgis_mapserv.fcgi'), msg
250
251        # hardcoded url, makes all this automated
252        self._ip = '127.0.0.1'
253        self._port = '8448'
254        self._web_url = 'http://{0}:{1}'.format(self._ip, self._port)
255        self._fcgibin_path = '/cgi-bin/qgis_mapserv.fcgi'
256        self._fcgi_url = '{0}{1}'.format(self._web_url, self._fcgibin_path)
257        self._conf_dir = unitTestDataPath('qgis_local_server')
258
259        self._fcgiserv_process = self._webserv_process = None
260        self._fcgiserv_bin = fcgi_bin
261        self._fcgiserv_path = self._webserv_path = ''
262        self._fcgiserv_kind = self._webserv_kind = ''
263        self._temp_dir = ''
264        self._web_dir = ''
265
266        servers = [
267            ('spawn-fcgi', 'lighttpd')
268            # ('fcgiwrap', 'nginx'),
269            # ('uwsgi', 'nginx'),
270        ]
271
272        chkd = ''
273        for fcgi, web in servers:
274            fcgi_path = getExecutablePath(fcgi)
275            web_path = getExecutablePath(web)
276            if fcgi_path and web_path:
277                self._fcgiserv_path = fcgi_path
278                self._webserv_path = web_path
279                self._fcgiserv_kind = fcgi
280                self._webserv_kind = web
281                break
282            else:
283                chkd += "Find '{0}': {1}\n".format(fcgi, fcgi_path)
284                chkd += "Find '{0}': {1}\n\n".format(web, web_path)
285
286        if not (self._fcgiserv_path and self._webserv_path):
287            raise ServerProcessError(
288                'Could not locate server binaries',
289                chkd,
290                'Make sure one of the sets of servers is available on PATH'
291            )
292
293        self._temp_dir = tempfile.mkdtemp()
294        self._setup_temp_dir()
295
296        # initialize the servers
297        self._fcgiserv_process = FcgiServerProcess(
298            self._fcgiserv_kind, self._fcgiserv_path,
299            self._fcgibin_path, self._conf_dir, self._temp_dir)
300        self._webserv_process = WebServerProcess(
301            self._webserv_kind, self._webserv_path,
302            self._conf_dir, self._temp_dir)
303
304        # stop any leftover processes, if possible
305        self.stop_processes()
306
307    def startup(self, chkcapa=False):
308        if not os.path.exists(self._temp_dir):
309            self._setup_temp_dir()
310        self.start_processes()
311        if chkcapa:
312            self.check_server_capabilities()
313
314    def shutdown(self):
315        self.stop_processes()
316        self.remove_temp_dir()
317
318    def start_processes(self):
319        self._fcgiserv_process.start()
320        self._webserv_process.start()
321
322    def stop_processes(self):
323        self._fcgiserv_process.stop()
324        self._webserv_process.stop()
325
326    def restart_processes(self):
327        self._fcgiserv_process.restart()
328        self._webserv_process.restart()
329
330    def fcgi_server_process(self):
331        return self._fcgiserv_process
332
333    def web_server_process(self):
334        return self._webserv_process
335
336    def processes_running(self):
337        return (self._fcgiserv_process.running() and
338                self._webserv_process.running())
339
340    def config_dir(self):
341        return self._conf_dir
342
343    def web_dir(self):
344        return self._web_dir
345
346    def open_web_dir(self):
347        self._open_fs_item(self._web_dir)
348
349    def web_dir_install(self, items, src_dir=''):
350        msg = 'Items parameter should be passed in as a list'
351        assert isinstance(items, list), msg
352        for item in items:
353            if item.startswith('.') or item.endswith('~'):
354                continue
355            path = item
356            if src_dir:
357                path = os.path.join(src_dir, item)
358            try:
359                if os.path.isfile(path):
360                    shutil.copy2(path, self._web_dir)
361                elif os.path.isdir(path):
362                    shutil.copytree(path, self._web_dir)
363            except Exception as err:
364                raise ServerProcessError('Failed to copy to web directory:',
365                                         item,
366                                         str(err))
367
368    def clear_web_dir(self):
369        for f in os.listdir(self._web_dir):
370            path = os.path.join(self._web_dir, f)
371            try:
372                if os.path.isfile(path):
373                    os.unlink(path)
374                else:
375                    shutil.rmtree(path)
376            except Exception as err:
377                raise ServerProcessError('Failed to clear web directory', err)
378
379    def temp_dir(self):
380        return self._temp_dir
381
382    def open_temp_dir(self):
383        self._open_fs_item(self._temp_dir)
384
385    def remove_temp_dir(self):
386        if os.path.exists(self._temp_dir):
387            shutil.rmtree(self._temp_dir)
388
389    def ip(self):
390        return self._ip
391
392    def port(self):
393        return self._port
394
395    def web_url(self):
396        return self._web_url
397
398    def fcgi_url(self):
399        return self._fcgi_url
400
401    def check_server_capabilities(self):
402        params = {
403            'SERVICE': 'WMS',
404            'VERSION': '1.3.0',
405            'REQUEST': 'GetCapabilities'
406        }
407        if not self.get_capabilities(params, False)[0]:
408            self.shutdown()
409            raise ServerProcessError(
410                'Local QGIS Server shutdown',
411                'Test QGIS Server is not accessible at:\n' + self._fcgi_url,
412                'Error: failed to retrieve server capabilities'
413            )
414
415    def get_capabilities(self, params, browser=False):
416        assert self.processes_running(), 'Server processes not running'
417
418        params = self._params_to_upper(params)
419        if (('REQUEST' in params and params['REQUEST'] != 'GetCapabilities') or
420                'REQUEST' not in params):
421            params['REQUEST'] = 'GetCapabilities'
422
423        url = self._fcgi_url + '?' + self.process_params(params)
424
425        res = urllib.request.urlopen(url)
426        xml = res.read().decode('utf-8')
427        if browser:
428            tmp_name = getTempfilePath('html')
429            with open(tmp_name, 'wt') as temp_html:
430                temp_html.write(xml)
431            url = tmp_name
432            openInBrowserTab(url)
433            return False, ''
434
435        success = ('error reading the project file' in xml or
436                   'WMS_Capabilities' in xml)
437        return success, xml
438
439    def get_map(self, params, browser=False):
440        assert self.processes_running(), 'Server processes not running'
441
442        msg = ('Map request parameters should be passed in as a dict '
443               '(key case can be mixed)')
444        assert isinstance(params, dict), msg
445
446        params = self._params_to_upper(params)
447        try:
448            proj = params['MAP']
449        except KeyError as err:
450            raise KeyError(str(err) + '\nMAP not found in parameters dict')
451
452        if not os.path.exists(proj):
453            msg = '{0}'.format(proj)
454            w_proj = os.path.join(self._web_dir, proj)
455            if os.path.exists(w_proj):
456                params['MAP'] = w_proj
457            else:
458                msg += '\n  or\n' + w_proj
459                raise ServerProcessError(
460                    'GetMap Request Error',
461                    'Project not found at:\n{0}'.format(msg)
462                )
463
464        if (('REQUEST' in params and params['REQUEST'] != 'GetMap') or
465                'REQUEST' not in params):
466            params['REQUEST'] = 'GetMap'
467
468        url = self._fcgi_url + '?' + self.process_params(params)
469
470        if browser:
471            openInBrowserTab(url)
472            return False, ''
473
474        # try until qgis_mapserv.fcgi process is available (for 20 seconds)
475        # on some platforms the fcgi_server_process is a daemon handling the
476        # launch of the fcgi-spawner, which may be running quickly, but the
477        # qgis_mapserv.fcgi spawned process is not yet accepting connections
478        resp = None
479        tmp_png = None
480        # noinspection PyUnusedLocal
481        filepath = ''
482        # noinspection PyUnusedLocal
483        success = False
484        start_time = time.time()
485        while time.time() - start_time < 20:
486            resp = None
487            try:
488                tmp_png = urllib.request.urlopen(url)
489            except urllib.error.HTTPError as resp:
490                if resp.code == 503 or resp.code == 500:
491                    time.sleep(1)
492                else:
493                    raise ServerProcessError(
494                        'Web/FCGI Process Request HTTPError',
495                        'Could not connect to process: ' + str(resp.code),
496                        resp.message
497                    )
498            except urllib.error.URLError as resp:
499                raise ServerProcessError(
500                    'Web/FCGI Process Request URLError',
501                    'Could not connect to process',
502                    resp.reason
503                )
504            else:
505                delta = time.time() - start_time
506                print(('Seconds elapsed for server GetMap: ' + str(delta)))
507                break
508
509        if resp is not None:
510            raise ServerProcessError(
511                'Web/FCGI Process Request Error',
512                'Could not connect to process: ' + str(resp.code)
513            )
514
515        if (tmp_png is not None and
516                tmp_png.info().getmaintype() == 'image' and
517                tmp_png.info().getheader('Content-Type') == 'image/png'):
518
519            filepath = getTempfilePath('png')
520            with open(filepath, 'wb') as temp_image:
521                temp_image.write(tmp_png.read())
522            success = True
523        else:
524            raise ServerProcessError(
525                'FCGI Process Request Error',
526                'No valid PNG output'
527            )
528
529        return success, filepath, url
530
531    def process_params(self, params):
532        # set all keys to uppercase
533        params = self._params_to_upper(params)
534        # convert all convenience objects to compatible strings
535        self._convert_instances(params)
536        # encode params
537        return urllib.parse.urlencode(params, True)
538
539    @staticmethod
540    def _params_to_upper(params):
541        return dict((k.upper(), v) for k, v in list(params.items()))
542
543    @staticmethod
544    def _convert_instances(params):
545        if not params:
546            return
547        if ('LAYERS' in params and
548                isinstance(params['LAYERS'], list)):
549            params['LAYERS'] = ','.join(params['LAYERS'])
550        if ('BBOX' in params and
551                isinstance(params['BBOX'], QgsRectangle)):
552            # not needed for QGIS's 1.3.0 server?
553            # # invert x, y of rect and set precision to 16
554            # rect = self.params['BBOX']
555            # bbox = ','.join(map(lambda x: '{0:0.16f}'.format(x),
556            #                     [rect.yMinimum(), rect.xMinimum(),
557            #                      rect.yMaximum(), rect.xMaximum()]))
558            params['BBOX'] = \
559                params['BBOX'].toString(1).replace(' : ', ',')
560
561        if ('CRS' in params and
562                isinstance(params['CRS'], QgsCoordinateReferenceSystem)):
563            params['CRS'] = params['CRS'].authid()
564
565    def _setup_temp_dir(self):
566        self._web_dir = os.path.join(self._temp_dir, 'www', 'htdocs')
567        cgi_bin = os.path.join(self._temp_dir, 'cgi-bin')
568
569        os.makedirs(cgi_bin, mode=0o755)
570        os.makedirs(os.path.join(self._temp_dir, 'log'), mode=0o755)
571        os.makedirs(os.path.join(self._temp_dir, 'var', 'run'), mode=0o755)
572        os.makedirs(self._web_dir, mode=0o755)
573
574        # symlink or copy in components
575        shutil.copy2(os.path.join(self._conf_dir, 'index.html'), self._web_dir)
576        if not platform.system().lower().startswith('win'):
577            # symlink allow valid runningFromBuildDir results
578            os.symlink(self._fcgiserv_bin,
579                       os.path.join(cgi_bin,
580                                    os.path.basename(self._fcgiserv_bin)))
581        else:
582            # TODO: what to do here for Win runningFromBuildDir?
583            #       copy qgisbuildpath.txt from output/bin directory, too?
584            shutil.copy2(self._fcgiserv_bin, cgi_bin)
585
586    @staticmethod
587    def _exe_path(exe):
588        exe_exts = []
589        if (platform.system().lower().startswith('win') and
590                "PATHEXT" in os.environ):
591            exe_exts = os.environ["PATHEXT"].split(os.pathsep)
592
593        for path in os.environ["PATH"].split(os.pathsep):
594            exe_path = os.path.join(path, exe)
595            if os.path.exists(exe_path):
596                return exe_path
597            for ext in exe_exts:
598                if os.path.exists(exe_path + ext):
599                    return exe_path
600        return ''
601
602    @staticmethod
603    def _open_fs_item(item):
604        if not os.path.exists(item):
605            return
606        s = platform.system().lower()
607        if s.startswith('dar'):
608            subprocess.call(['open', item])
609        elif s.startswith('lin'):
610            # xdg-open "$1" &> /dev/null &
611            subprocess.call(['xdg-open', item])
612        elif s.startswith('win'):
613            subprocess.call([item])
614        else:  # ?
615            pass
616
617
618# noinspection PyPep8Naming
619def getLocalServer():
620    """ Start a local test server controller that independently manages Web and
621    FCGI-spawn processes.
622
623    Input
624        NIL
625
626    Output
627        handle to QgsLocalServer, that's been tested to be valid, then shutdown
628
629    If MAPSERV is already running the handle to it will be returned.
630
631    Before unit test class add:
632
633        MAPSERV = getLocalServer()
634
635    IMPORTANT: When using MAPSERV in a test class, ensure to set these:
636
637        @classmethod
638        def setUpClass(cls):
639            MAPSERV.startup()
640
641    This ensures the subprocesses are started and the temp directory is created.
642
643        @classmethod
644        def tearDownClass(cls):
645            MAPSERV.shutdown()
646            # or, when testing, instead of shutdown...
647            #   MAPSERV.stop_processes()
648            #   MAPSERV.open_temp_dir()
649
650    This ensures the subprocesses are stopped and the temp directory is removed.
651    If this is not used, the server processes may continue to run after tests.
652
653    If you need to restart the qgis_mapserv.fcgi spawning process to show
654    changes to project settings, consider adding:
655
656        def setUp(self):
657            '''Run before each test.'''
658            # web server stays up across all tests
659            MAPSERV.fcgi_server_process().start()
660
661        def tearDown(self):
662            '''Run after each test.'''
663            # web server stays up across all tests
664            MAPSERV.fcgi_server_process().stop()
665
666    :rtype: QgisLocalServer
667    """
668    global SERVRUN  # pylint: disable=W0603
669    global MAPSERV  # pylint: disable=W0603
670    if SERVRUN:
671        msg = 'Local server has already failed to launch or run'
672        assert MAPSERV is not None, msg
673    else:
674        SERVRUN = True
675
676    global FCGIBIN  # pylint: disable=W0603
677    if FCGIBIN is None:
678        msg = 'Could not find QGIS_PREFIX_PATH (build directory) in environ'
679        assert 'QGIS_PREFIX_PATH' in os.environ, msg
680
681        fcgi_path = os.path.join(os.environ['QGIS_PREFIX_PATH'], 'bin',
682                                 'qgis_mapserv.fcgi')
683        msg = 'Could not locate qgis_mapserv.fcgi in build/bin directory'
684        assert os.path.exists(fcgi_path), msg
685
686        FCGIBIN = fcgi_path
687
688    if MAPSERV is None:
689        # On QgisLocalServer init, Web and FCGI-spawn executables are located,
690        # configurations to start/stop/restart those processes (relative to
691        # host platform) are loaded into controller, a temporary web
692        # directory is created, and the FCGI binary copied to its cgi-bin.
693        srv = QgisLocalServer(FCGIBIN)
694        # noinspection PyStatementEffect
695        """:type : QgisLocalServer"""
696
697        try:
698            msg = 'Temp web directory could not be created'
699            assert os.path.exists(srv.temp_dir()), msg
700
701            # install test project components to temporary web directory
702            test_proj_dir = os.path.join(srv.config_dir(), 'test-project')
703            srv.web_dir_install(os.listdir(test_proj_dir), test_proj_dir)
704            # verify they were copied
705            msg = 'Test project could not be copied to temp web directory'
706            res = os.path.exists(os.path.join(srv.web_dir(), 'test-server.qgs'))
707            assert res, msg
708
709            # verify subprocess' status can be checked
710            msg = 'Server processes status could not be checked'
711            assert not srv.processes_running(), msg
712
713            # startup server subprocesses, and check capabilities
714            srv.startup()
715            msg = 'Server processes could not be started'
716            assert srv.processes_running(), msg
717
718            # verify web server (try for 30 seconds)
719            start_time = time.time()
720            res = None
721            while time.time() - start_time < 30:
722                time.sleep(1)
723                try:
724                    res = urllib.request.urlopen(srv.web_url())
725                    if res.getcode() == 200:
726                        break
727                except urllib.error.URLError:
728                    pass
729            msg = 'Web server basic access to root index.html failed'
730            # print repr(res)
731            assert (res is not None and
732                    res.getcode() == 200 and
733                    'Web Server Working' in res.read().decode('utf-8')), msg
734
735            # verify basic wms service
736            params = {
737                'SERVICE': 'WMS',
738                'VERSION': '1.3.0',
739                'REQUEST': 'GetCapabilities'
740            }
741            msg = '\nFCGI server failed to return capabilities'
742            assert srv.get_capabilities(params, False)[0], msg
743
744            params = {
745                'SERVICE': 'WMS',
746                'VERSION': '1.3.0',
747                'REQUEST': 'GetCapabilities',
748                'MAP': 'test-server.qgs'
749            }
750            msg = '\nFCGI server failed to return capabilities for project'
751            assert srv.get_capabilities(params, False)[0], msg
752
753            # verify the subprocesses can be stopped and controller shutdown
754            srv.shutdown()  # should remove temp directory (and test project)
755            msg = 'Server processes could not be stopped'
756            assert not srv.processes_running(), msg
757            msg = 'Temp web directory could not be removed'
758            assert not os.path.exists(srv.temp_dir()), msg
759
760            MAPSERV = srv
761        except AssertionError as err:
762            srv.shutdown()
763            raise AssertionError(err)
764
765    return MAPSERV
766
767
768if __name__ == '__main__':
769    # NOTE: see test_qgis_local_server.py for CTest suite
770
771    import argparse
772    parser = argparse.ArgumentParser()
773    parser.add_argument(
774        'fcgi', metavar='fcgi-bin-path',
775        help='Path to qgis_mapserv.fcgi'
776    )
777    args = parser.parse_args()
778
779    fcgi = os.path.realpath(args.fcgi)
780    if not os.path.isabs(fcgi) or not os.path.exists(fcgi):
781        print('qgis_mapserv.fcgi not resolved to existing absolute path.')
782        sys.exit(1)
783
784    local_srv = QgisLocalServer(fcgi)
785    proj_dir = os.path.join(local_srv.config_dir(), 'test-project')
786    local_srv.web_dir_install(os.listdir(proj_dir), proj_dir)
787    # local_srv.open_temp_dir()
788    # sys.exit()
789    # creating crs needs app instance to access /resources/srs.db
790    #   crs = QgsCoordinateReferenceSystem('EPSG:32613')
791    # default for labeling test data sources: WGS 84 / UTM zone 13N
792    req_params = {
793        'SERVICE': 'WMS',
794        'VERSION': '1.3.0',
795        'REQUEST': 'GetMap',
796        # 'MAP': os.path.join(local_srv.web_dir(), 'test-server.qgs'),
797        'MAP': 'test-server.qgs',
798        # layer stacking order for rendering: bottom,to,top
799        'LAYERS': ['background', 'aoi'],  # or 'background,aoi'
800        'STYLES': ',',
801        # 'CRS': QgsCoordinateReferenceSystem obj
802        'CRS': 'EPSG:32613',
803        # 'BBOX': QgsRectangle(606510, 4823130, 612510, 4827130)
804        'BBOX': '606510,4823130,612510,4827130',
805        'FORMAT': 'image/png',  # or: 'image/png; mode=8bit'
806        'WIDTH': '600',
807        'HEIGHT': '400',
808        'DPI': '72',
809        'MAP_RESOLUTION': '72',
810        'FORMAT_OPTIONS': 'dpi:72',
811        'TRANSPARENT': 'FALSE',
812        'IgnoreGetMapUrl': '1'
813    }
814
815    # local_srv.web_server_process().start()
816    # openInBrowserTab('http://127.0.0.1:8448')
817    # local_srv.web_server_process().stop()
818    # sys.exit()
819    local_srv.startup(False)
820    openInBrowserTab('http://127.0.0.1:8448')
821    try:
822        local_srv.check_server_capabilities()
823        # open resultant png with system
824        result, png, url = local_srv.get_map(req_params)
825    finally:
826        local_srv.shutdown()
827
828    if result:
829        # print png
830        openInBrowserTab('file://' + png)
831    else:
832        raise ServerProcessError('GetMap Test', 'Failed to generate PNG')
833