6

UPDATE: This issue may be isolated to my MacBook. The seg fault did not occur in testing on Windows.

I have put together a sample unit test for a QGIS python plugin. It is causing a Segmentation Fault 11 when the test is run after the tests complete and the XML output is written.

I based the unit tests on code in this excellent discussion.

I seem to have isolated the issue to three offending lines of code.

    app = QgsApplication([], True)
    # create the plugin
    iface = DummyInterface()
    plugin = gupsapp.classFactory(iface)

If these lines are run upon entry of each test, everything is good, no seg faults. However, if I move these lines to the setUp method (in the hopes of creating a custom unittest base class) the seg fault pops up.

I have tried including a tearDown() and a tearDownClass() with various combinations of QgsApplication.exit() and QgsApplication.exitQgis() which either does not help or seg faults between tests.

Unit test code:

from qgis.core import *
from qgis.gui import *

from PyQt4 import QtCore
# from PyQt4 import QtGui
# from PyQt4 QtTest

import unittest
import sys
import logging
import gupsapp
import os
import platform
import xmlrunner


class DummyInterface(object):
    def __getattr__(self, *args, **kwargs):
        def dummy(*args, **kwargs):
            return self

        return dummy

    def __iter__(self):
        return self

    def next(self):
        raise StopIteration

    def layers(self):
        return QgsMapLayerRegistry.instance().mapLayers().values()


class PyQgisUnitTest(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        """Called before each test is run.  This method should be used
        to create any test data or other resources needed for the tests.

        :return:
        """
        log.info("setUpClass")

        log.debug("platform.system == %s", platform.system())
        if platform.system() == 'Windows':
            # configure python to play nicely with QGIS in Windows using default install paths
            # the preferred method of setting these value is as system environment
            osgeo4w_root = r'C:/OSGeo4W64'
            os.environ['PATH'] = '{}/bin{}{}'.format(osgeo4w_root, os.pathsep, os.environ['PATH'])
            sys.path.insert(0, '{}/apps/qgis/python'.format(osgeo4w_root))
            sys.path.insert(1, '{}/apps/python27/lib/site-packages'.format(osgeo4w_root))
            prefix_path = '{}/apps/qgis'.format(osgeo4w_root)
            log.debug("prefix_path == %s", prefix_path)
            QgsApplication.setPrefixPath(prefix_path, True)
        else:
            # MAC default: /Applications/QGIS.app/Contents
            QgsApplication.setPrefixPath("/Applications/QGIS.app/Contents/MacOS", True)

        QgsApplication.initQgis()

        if len(QgsProviderRegistry.instance().providerList()) == 0:
            raise RuntimeError('No data providers available.')

        QtCore.QCoreApplication.setOrganizationName('QGIS')
        QtCore.QCoreApplication.setApplicationName('QGIS GUPS')

    @classmethod
    def tearDownClass(cls):
        QgsApplication.exitQgis()


    def setUp(self):
        """Segmentation Fault: 11 occurs when these lines are performed as part of this
        setUp method.

        :return:
        """
        self.app = QgsApplication([], True)
        # create the plugin
        self.iface = DummyInterface()
        self.plugin = gupsapp.classFactory(self.iface)

    def tearDown(self):
        """Causes Segmentation fault: 11 BEFORE tests complete with QgisApplication.exitQgis().

        :return:
        """
        # QgsApplication.exitQgis()
        QgsApplication.exit()

    def test_one(self):
        """Sample test attempts to load a shape file into a layer and verify that the layer is valid.

        :return:
        """
        """Segmentation Fault: 11 does NOT occur when these lines are performed in as
        part of the test methods.

        # initialize QGIS application
        app = QgsApplication([], True)

        # create the plugin
        iface = DummyInterface()
        plugin = gupsapp.classFactory(iface)
        """

        # load a sample shape file
        if platform.system() == "Windows":
            # default GUPSGIS path for Windows
            home = os.environ['USERPROFILE']
        else:
            # default GUPSGIS path for Unix/Mac
            home = os.environ['HOME']

        layer = QgsVectorLayer("{}/GUPSGIS/gupsdata/BBSP/shape/55025/GUPS15_2014_aial_55025.shp".format(home),
                               "GUPS15_2014_aial_55025", "ogr")

        log.debug("layer == %s ", layer)
        self.assertTrue(layer.isValid(), 'Failed to load "{}".'.format(layer.source()))
        assert layer.isValid()

    def test_area(self):
        """Tests the area calculation of CreateChangePolygons.

        :return:
        """
        """Segmentation Fault: 11 does NOT occur when these lines are performed in as
        part of the test methods.

        app = QgsApplication([], True)
        # create the plugin
        iface = DummyInterface()
        plugin = gupsapp.classFactory(iface)
        """

        # create a polygon with an area of 1e6 sqKm
        gPolygon = QgsGeometry.fromPolygon([[QgsPoint(0, 0), QgsPoint(1e6, 0), QgsPoint(1e6, 1), QgsPoint(0, 1)]])
        log.debug("gPolygon.wkbType() == %s", gPolygon.wkbType())

        # create a CreateChangePolygons module
        self.plugin.delayCreateChangePolygons()
        # calculate the area in acres
        area = self.plugin.createchangepolygons.updateTable(gPolygon)

        # convert to acres
        expected_area_in_acres = 1.0 * 247.11
        self.assertTrue(area == expected_area_in_acres)

if __name__ == "__main__":
    logging.basicConfig(format='%(asctime)s %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p')
    log = logging.getLogger(__file__)
    log.setLevel(logging.DEBUG)
    log.info("main info")

    unittest.main(testRunner=xmlrunner.XMLTestRunner(output='test-reports'),
                  # these make sure that some options that are not applicable
                  # remain hidden from the help menu.
                  failfast=False, buffer=False, catchbreak=False)