291 changes: 291 additions & 0 deletions tests/src/python/test_qgspallabeling_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
# -*- coding: utf-8 -*-
"""QGIS Unit tests for QgsPalLabeling: base suite setup
.. note:: from build dir: ctest -R PyQgsPalLabelingBase -V
Set env variable PAL_SUITE to run specific tests (define in __main__)
Set env variable PAL_VERBOSE to output individual test summary
Set env variable PAL_CONTROL_IMAGE to trigger building of new control images
.. note:: This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
"""
__author__ = 'Larry Shaffer'
__date__ = '07/09/2013'
__copyright__ = 'Copyright 2013, The QGIS Project'
# This will get replaced with a git SHA1 when you do a git archive
__revision__ = '$Format:%H$'

import os
import sys
import glob
import StringIO
from PyQt4.QtCore import *
from PyQt4.QtGui import *

from qgis.core import (
QGis,
QgsCoordinateReferenceSystem,
QgsDataSourceURI,
QgsLogger,
QgsMapLayerRegistry,
QgsMapRenderer,
QgsPalLabeling,
QgsPalLayerSettings,
QgsProviderRegistry,
QgsVectorLayer,
QgsRenderChecker
)

from utilities import (
getQgisTestApp,
TestCase,
unittest,
expectedFailure,
unitTestDataPath
)

QGISAPP, CANVAS, IFACE, PARENT = getQgisTestApp()


class TestQgsPalLabeling(TestCase):

_TestDataDir = unitTestDataPath()
_PalDataDir = os.path.join(_TestDataDir, 'labeling')
_PalFeaturesDb = os.path.join(_PalDataDir, 'pal_features_v3.sqlite')
_TestFontID = -1
_MapRegistry = None
_MapRenderer = None
_Canvas = None

@classmethod
def setUpClass(cls):
"""Run before all tests"""

# qgis instances
cls._QgisApp, cls._Canvas, cls._Iface, cls._Parent = \
QGISAPP, CANVAS, IFACE, PARENT

# verify that spatialite provider is available
msg = ('\nSpatialite provider not found, '
'SKIPPING TEST SUITE')
res = 'spatialite' in QgsProviderRegistry.instance().providerList()
assert res, msg

# load the FreeSansQGIS labeling test font
fontdb = QFontDatabase()
cls._TestFontID = fontdb.addApplicationFont(
os.path.join(cls._TestDataDir, 'font', 'FreeSansQGIS.ttf'))
msg = ('\nCould not store test font in font database, '
'SKIPPING TEST SUITE')
assert cls._TestFontID != -1, msg

cls._TestFont = fontdb.font('FreeSansQGIS', 'Medium', 48)
appfont = QApplication.font()
msg = ('\nCould not load test font from font database, '
'SKIPPING TEST SUITE')
assert cls._TestFont.toString() != appfont.toString(), msg

cls._TestFunction = ''
cls._TestGroup = ''
cls._TestGroupPrefix = ''
cls._TestGroupAbbr = ''

# initialize class MapRegistry, Canvas, MapRenderer, Map and PAL
cls._MapRegistry = QgsMapLayerRegistry.instance()
# set color to match render test comparisons background
cls._Canvas.setCanvasColor(QColor(152, 219, 249))
cls._Map = cls._Canvas.map()
cls._Map.resize(QSize(600, 400))
cls._MapRenderer = cls._Canvas.mapRenderer()
crs = QgsCoordinateReferenceSystem()
# default for labeling test data sources: WGS 84 / UTM zone 13N
crs.createFromSrid(32613)
cls._MapRenderer.setDestinationCrs(crs)
# TODO: match and store platform's native logical output dpi
cls._MapRenderer.setOutputSize(QSize(600, 400), 72)

cls._Pal = QgsPalLabeling()
cls._MapRenderer.setLabelingEngine(cls._Pal)
cls._PalEngine = cls._MapRenderer.labelingEngine()
msg = ('\nCould not initialize PAL labeling engine, '
'SKIPPING TEST SUITE')
assert cls._PalEngine, msg

@classmethod
def tearDownClass(cls):
"""Run after all tests"""
cls.removeAllLayers()

@classmethod
def removeAllLayers(cls):
cls._MapRegistry.removeAllMapLayers()

@classmethod
def loadFeatureLayer(cls, table):
uri = QgsDataSourceURI()
uri.setDatabase(cls._PalFeaturesDb)
uri.setDataSource('', table, 'geometry')
vlayer = QgsVectorLayer(uri.uri(), table, 'spatialite')
# .qml should contain only style for symbology
vlayer.loadNamedStyle(os.path.join(cls._PalDataDir,
'{0}.qml'.format(table)))
cls._MapRegistry.addMapLayer(vlayer)
cls._MapRenderer.setLayerSet([vlayer.id()])

# zoom to area of interest, which matches output aspect ratio
uri.setDataSource('', 'aoi', 'geometry')
aoilayer = QgsVectorLayer(uri.uri(), table, 'spatialite')
cls._MapRenderer.setExtent(aoilayer.extent())
cls._Canvas.zoomToFullExtent()
return vlayer

def configTest(self, prefix, abbr):
"""Call in setUp() function of test subclass"""
self._TestGroupPrefix = prefix
self._TestGroupAbbr = abbr

# insert test's Class.function marker into debug output stream
# this helps visually track down the start of a test's debug output
testid = self.id().split('.')
self._TestGroup = testid[1]
self._TestFunction = testid[2]
testheader = '\n#####_____ {0}.{1} _____#####\n'.\
format(self._TestGroup, self._TestFunction)
QgsLogger.debug(testheader)

# define the shorthand name of the test (to minimize file name length)
self._Test = '{0}_{1}'.format(self._TestGroupAbbr,
self._TestFunction.replace('test_', ''))

def defaultSettings(self):
lyr = QgsPalLayerSettings()
lyr.enabled = True
lyr.fieldName = 'text' # default in data sources
lyr.textFont = self._TestFont
lyr.textNamedStyle = 'Medium'
return lyr

@staticmethod
def settingsDict(lyr):
"""Return a dict of layer-level labeling settings
.. note:: QgsPalLayerSettings is not a QObject, so we can not collect
current object properties, and the public properties of the C++ obj
can't be listed with __dict__ or vars(). So, we sniff them out relative
to their naming convention (camelCase), as reported by dir().
"""
res = {}
for attr in dir(lyr):
if attr[0].islower() and not attr.startswith("__"):
value = getattr(lyr, attr)
if not callable(value):
res[attr] = value
return res

def saveContolImage(self):
if 'PAL_CONTROL_IMAGE' not in os.environ:
return
testgrpdir = 'expected_' + self._TestGroupPrefix
testdir = os.path.join(self._TestDataDir, 'control_images',
testgrpdir, self._Test)
if not os.path.exists(testdir):
os.makedirs(testdir)
imgbasepath = os.path.join(testdir, self._Test)
imgpath = imgbasepath + '.png'
for f in glob.glob(imgbasepath + '.*'):
if os.path.exists(f):
os.remove(f)
self._Map.render()
self._Canvas.saveAsImage(imgpath)

def renderCheck(self):
chk = QgsRenderChecker()
chk.setControlPathPrefix('expected_' + self._TestGroupPrefix)
chk.setControlName(self._Test)
chk.setMapRenderer(self._MapRenderer)
res = chk.runTest(self._Test)
msg = '\nRender check failed for "{0}"'.format(self._Test)
return res, msg


class TestQgsPalLabelingBase(TestQgsPalLabeling):

@classmethod
def setUpClass(cls):
TestQgsPalLabeling.setUpClass()
cls.layer = TestQgsPalLabeling.loadFeatureLayer('point')

def setUp(self):
"""Run before each test."""
self.configTest('pal_base', 'base')

def tearDown(self):
"""Run after each test."""
pass

def test_default_pal_disabled(self):
# Verify PAL labeling is disabled for layer by default
palset = self.layer.customProperty('labeling', '').toString()
msg = '\nExpected: Empty string\nGot: {0}'.format(palset)
self.assertEqual(palset, '', msg)

def test_settings_enable_pal(self):
# Verify default PAL settings enable PAL labeling for layer
lyr = QgsPalLayerSettings()
lyr.writeToLayer(self.layer)
palset = self.layer.customProperty('labeling', '').toString()
msg = '\nExpected: Empty string\nGot: {0}'.format(palset)
self.assertEqual(palset, 'pal', msg)

def test_layer_pal_activated(self):
# Verify, via engine, that PAL labeling can be activated for layer
lyr = self.defaultSettings()
lyr.writeToLayer(self.layer)
msg = '\nLayer labeling not activated, as reported by labelingEngine'
self.assertTrue(self._PalEngine.willUseLayer(self.layer), msg)

def test_write_read_settings(self):
# Verify written PAL settings are same when read from layer
# load and write default test settings
lyr1 = self.defaultSettings()
lyr1dict = self.settingsDict(lyr1)
# print lyr1dict
lyr1.writeToLayer(self.layer)

# read settings
lyr2 = QgsPalLayerSettings()
lyr2.readFromLayer(self.layer)
lyr2dict = self.settingsDict(lyr1)
# print lyr2dict

msg = '\nLayer settings read not same as settings written'
self.assertDictEqual(lyr1dict, lyr2dict, msg)


def runSuite(module, tests):
"""This allows for a list of test names to be selectively run.
Also, ensures unittest verbose output comes at end, after debug output"""
loader = unittest.defaultTestLoader
if 'PAL_SUITE' in os.environ and tests:
suite = loader.loadTestsFromNames(tests, module)
else:
suite = loader.loadTestsFromModule(module)
verb = 2 if 'PAL_VERBOSE' in os.environ else 0

out = StringIO.StringIO()
res = unittest.TextTestRunner(stream=out, verbosity=verb).run(suite)
if verb:
print '\nIndividual test summary:'
print '\n' + out.getvalue()
out.close()
return res

if __name__ == '__main__':
# NOTE: unless PAL_SUITE env var is set
# all test class methods will be run
b = 'TestQgsPalLabelingBase.'
tests = [b + 'test_write_read_settings']
res = runSuite(sys.modules[__name__], tests)
sys.exit(not res.wasSuccessful())
107 changes: 107 additions & 0 deletions tests/src/python/test_qgspallabeling_canvas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# -*- coding: utf-8 -*-
"""QGIS unit tests for QgsPalLabeling: label rendering to screen canvas
.. note:: from build dir: ctest -R PyQgsPalLabelingCanvas -V
Set env variable PAL_SUITE to run specific tests (define in __main__)
Set env variable PAL_VERBOSE to output individual test summary
Set env variable PAL_CONTROL_IMAGE to trigger building of new control images
.. note:: This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
"""

__author__ = 'Larry Shaffer'
__date__ = '07/09/2013'
__copyright__ = 'Copyright 2013, The QGIS Project'
# This will get replaced with a git SHA1 when you do a git archive
__revision__ = '$Format:%H$'

import sys
import os
from PyQt4.QtCore import *
from PyQt4.QtGui import *

from qgis.core import *

from utilities import (
unittest,
expectedFailure,
)

from test_qgspallabeling_base import TestQgsPalLabeling, runSuite


class TestQgsPalLabelingPoint(TestQgsPalLabeling):
"""Most layer-level labeling properties shared across feature types are
tested in this class"""

@classmethod
def setUpClass(cls):
TestQgsPalLabeling.setUpClass()
cls.layer = TestQgsPalLabeling.loadFeatureLayer('point')

def setUp(self):
"""Run before each test."""
self.configTest('pal_canvas', 'sp')
self.lyr = self.defaultSettings()

def tearDown(self):
"""Run after each test."""
pass

def test_default_label(self):
# Verify basic default label placement and text size in points
self.lyr.writeToLayer(self.layer)
self.saveContolImage()
self.assertTrue(self.renderCheck())

def test_text_size_map_unit(self):
# Verify label text size in map units
self.lyr.fontSizeInMapUnits = True
tmpFont = QFont(self._TestFont)
tmpFont.setPointSizeF(0.25)
self.lyr.textFont = tmpFont
self.lyr.writeToLayer(self.layer)
self.saveContolImage()
self.assertTrue(self.renderCheck())

def test_text_color(self):
# Verify label color change
self.lyr.textColor = Qt.blue
self.lyr.writeToLayer(self.layer)
self.saveContolImage()
self.assertTrue(self.renderCheck())


# class TestQgsPalLabelingLine(TestQgsPalLabeling):
# """Layer-level property tests for line features"""
#
# @classmethod
# def setUpClass(cls):
# TestQgsPalLabeling.setUpClass()
# cls.layer = TestQgsPalLabeling.loadFeatureLayer('line')
#
# def setUp(self):
# """Run before each test."""
# self.configTest('pal_canvas', 'sl')
# self.lyr = self.defaultSettings()
#
# def tearDown(self):
# """Run after each test."""
# pass


if __name__ == '__main__':
# NOTE: unless PAL_SUITE env var is set
# all test class methods will be run
sp = 'TestQgsPalLabelingPoint.'
sl = 'TestQgsPalLabelingLine.'
sc = 'TestQgsPalLabelingCurved.'
sg = 'TestQgsPalLabelingPolygon.'
mf = 'TestQgsPalLabelingMultiFeature.'

tests = [sp + 'test_text_size_map_unit']
res = runSuite(sys.modules[__name__], tests)
sys.exit(not res.wasSuccessful())
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
0.005
0
0
-0.005
609508.50249999994412065
4825130.99749999959021807
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
0.005
0
0
-0.005
609508.50249999994412065
4825130.99749999959021807
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
0.005
0
0
-0.005
609508.50249999994412065
4825130.99749999959021807
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/testdata/labeling/pal_features_v3.sqlite
Binary file not shown.
24 changes: 24 additions & 0 deletions tests/testdata/labeling/point.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
<qgis version="1.9.0-Master" minimumScale="-4.65661e-10" maximumScale="1e+08" minLabelScale="0" maxLabelScale="1e+08" hasScaleBasedVisibilityFlag="0" scaleBasedLabelVisibilityFlag="0">
<renderer-v2 symbollevels="0" type="singleSymbol">
<symbols>
<symbol alpha="1" type="marker" name="0">
<layer pass="0" class="SimpleMarker" locked="0">
<prop k="angle" v="0"/>
<prop k="color" v="61,128,45,255"/>
<prop k="color_border" v="0,0,0,255"/>
<prop k="name" v="circle"/>
<prop k="offset" v="0,0"/>
<prop k="offset_unit" v="MM"/>
<prop k="outline_width" v="0"/>
<prop k="outline_width_unit" v="MM"/>
<prop k="scale_method" v="area"/>
<prop k="size" v="2"/>
<prop k="size_unit" v="MM"/>
</layer>
</symbol>
</symbols>
<rotation field=""/>
<sizescale field="" scalemethod="area"/>
</renderer-v2>
</qgis>