Skip to content

Commit b702d3c

Browse files
committedJul 22, 2012
Initial test framework for running python tests as part of the CTest suite.
1 parent c517a90 commit b702d3c

File tree

6 files changed

+355
-0
lines changed

6 files changed

+355
-0
lines changed
 

‎cmake/UsePythonTest.cmake

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Add a python test from a python file
2+
# One cannot simply do:
3+
# SET(ENV{PYTHONPATH} ${LIBRARY_OUTPUT_PATH})
4+
# SET(my_test "from test_mymodule import *\;test_mymodule()")
5+
# ADD_TEST(PYTHON-TEST-MYMODULE python -c ${my_test})
6+
# Since cmake is only transmitting the ADD_TEST line to ctest thus you are loosing
7+
# the env var. The only way to store the env var is to physically write in the cmake script
8+
# whatever PYTHONPATH you want and then add the test as 'cmake -P python_test.cmake'
9+
#
10+
# Usage:
11+
# SET_SOURCE_FILES_PROPERTIES(test.py PROPERTIES PYTHONPATH
12+
# "${LIBRARY_OUTPUT_PATH}:${VTK_DIR}")
13+
# ADD_PYTHON_TEST(PYTHON-TEST test.py)
14+
#
15+
# Copyright (c) 2006-2010 Mathieu Malaterre <mathieu.malaterre@gmail.com>
16+
#
17+
# Redistribution and use is allowed according to the terms of the New
18+
# BSD license.
19+
# For details see the accompanying COPYING-CMAKE-SCRIPTS
20+
#
21+
22+
# Need python interpreter:
23+
FIND_PACKAGE(PythonInterp REQUIRED)
24+
MARK_AS_ADVANCED(PYTHON_EXECUTABLE)
25+
26+
MACRO(ADD_PYTHON_TEST TESTNAME FILENAME)
27+
GET_SOURCE_FILE_PROPERTY(loc ${FILENAME} LOCATION)
28+
GET_SOURCE_FILE_PROPERTY(pyenv ${FILENAME} PYTHONPATH)
29+
IF(CMAKE_CONFIGURATION_TYPES)
30+
# I cannot use CMAKE_CFG_INTDIR since it expand to "$(OutDir)"
31+
IF(pyenv)
32+
SET(pyenv "${pyenv};${LIBRARY_OUTPUT_PATH}/${CMAKE_BUILD_TYPE}")
33+
ELSE(pyenv)
34+
SET(pyenv ${LIBRARY_OUTPUT_PATH}/${CMAKE_BUILD_TYPE})
35+
#SET(pyenv ${LIBRARY_OUTPUT_PATH}/${CMAKE_CFG_INTDIR})
36+
#SET(pyenv ${LIBRARY_OUTPUT_PATH}/${CMAKE_CONFIG_TYPE})
37+
#SET(pyenv ${LIBRARY_OUTPUT_PATH}/\${CMAKE_CONFIG_TYPE})
38+
ENDIF(pyenv)
39+
ELSE(CMAKE_CONFIGURATION_TYPES)
40+
IF(pyenv)
41+
SET(pyenv ${pyenv}:${LIBRARY_OUTPUT_PATH})
42+
ELSE(pyenv)
43+
SET(pyenv ${LIBRARY_OUTPUT_PATH})
44+
ENDIF(pyenv)
45+
ENDIF(CMAKE_CONFIGURATION_TYPES)
46+
STRING(REGEX REPLACE ";" " " wo_semicolumn "${ARGN}")
47+
FILE(WRITE ${CMAKE_CURRENT_BINARY_DIR}/${TESTNAME}.cmake
48+
"
49+
SET(ENV{PYTHONPATH} ${pyenv}:\$ENV{PYTHONPATH})
50+
SET(ENV{LD_LIBRARY_PATH} ${pyenv}:\$ENV{LD_LIBRARY_PATH})
51+
MESSAGE(\"${pyenv}\")
52+
EXECUTE_PROCESS(
53+
COMMAND ${PYTHON_EXECUTABLE} ${loc} ${wo_semicolumn}
54+
#WORKING_DIRECTORY @LIBRARY_OUTPUT_PATH@
55+
RESULT_VARIABLE import_res
56+
OUTPUT_VARIABLE import_output
57+
ERROR_VARIABLE import_output
58+
)
59+
60+
# Pass the output back to ctest
61+
IF(import_output)
62+
MESSAGE("\${import_output}")
63+
ENDIF(import_output)
64+
IF(import_res)
65+
MESSAGE(SEND_ERROR "\${import_res}")
66+
ENDIF(import_res)
67+
"
68+
)
69+
ADD_TEST(${TESTNAME} ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/${TESTNAME}.cmake)
70+
ENDMACRO(ADD_PYTHON_TEST)
71+
72+
# Byte compile recursively a directory (DIRNAME)
73+
MACRO(ADD_PYTHON_COMPILEALL_TEST DIRNAME)
74+
# First get the path:
75+
GET_FILENAME_COMPONENT(temp_path "${PYTHON_LIBRARIES}" PATH)
76+
# Find the python script:
77+
GET_FILENAME_COMPONENT(PYTHON_COMPILE_ALL_PY "${temp_path}/../compileall.py" ABSOLUTE)
78+
# add test, use DIRNAME to create uniq name for the test:
79+
ADD_TEST(COMPILE_ALL-${DIRNAME} ${PYTHON_EXECUTABLE} "${PYTHON_COMPILE_ALL_PY}" -q ${DIRNAME})
80+
ENDMACRO(ADD_PYTHON_COMPILEALL_TEST)

‎tests/src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ IF (ENABLE_TESTS)
22
ADD_SUBDIRECTORY(core)
33
ADD_SUBDIRECTORY(gui)
44
ADD_SUBDIRECTORY(analysis)
5+
ADD_SUBDIRECTORY(python)
56
ENDIF (ENABLE_TESTS)

‎tests/src/python/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
INCLUDE(UsePythonTest)
2+
ADD_PYTHON_TEST(QGisApp test_qgisapp.py)

‎tests/src/python/qgis_interface.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""
2+
InaSAFE Disaster risk assessment tool developed by AusAid -
3+
**QGIS plugin implementation.**
4+
5+
Contact : ole.moller.nielsen@gmail.com
6+
7+
.. note:: This program is free software; you can redistribute it and/or modify
8+
it under the terms of the GNU General Public License as published by
9+
the Free Software Foundation; either version 2 of the License, or
10+
(at your option) any later version.
11+
12+
.. note:: This source code was copied from the 'postgis viewer' application
13+
with original authors:
14+
Copyright (c) 2010 by Ivan Mincik, ivan.mincik@gista.sk
15+
Copyright (c) 2011 German Carrillo, geotux_tuxman@linuxmail.org
16+
17+
"""
18+
19+
__author__ = 'tim@linfiniti.com'
20+
__version__ = '0.5.0'
21+
__revision__ = '$Format:%H$'
22+
__date__ = '10/01/2011'
23+
__copyright__ = ('Copyright (c) 2010 by Ivan Mincik, ivan.mincik@gista.sk and '
24+
'Copyright (c) 2011 German Carrillo, '
25+
'geotux_tuxman@linuxmail.org')
26+
27+
28+
from PyQt4.QtCore import QObject
29+
from qgis.core import QgsMapLayerRegistry
30+
31+
32+
class QgisInterface(QObject):
33+
"""Class to expose qgis objects and functionalities to plugins.
34+
35+
This class is here for enabling us to run unit tests only,
36+
so most methods are simply stubs.
37+
"""
38+
39+
def __init__(self, canvas):
40+
"""Constructor"""
41+
QObject.__init__(self)
42+
self.canvas = canvas
43+
44+
def zoomFull(self):
45+
"""Zoom to the map full extent"""
46+
pass
47+
48+
def zoomToPrevious(self):
49+
"""Zoom to previous view extent"""
50+
pass
51+
52+
def zoomToNext(self):
53+
"""Zoom to next view extent"""
54+
pass
55+
56+
def zoomToActiveLayer(self):
57+
"""Zoom to extent of active layer"""
58+
pass
59+
60+
def addVectorLayer(self, vectorLayerPath, baseName, providerKey):
61+
"""Add a vector layer"""
62+
pass
63+
64+
def addRasterLayer(self, rasterLayerPath, baseName):
65+
"""Add a raster layer given a raster layer file name"""
66+
pass
67+
68+
def activeLayer(self):
69+
"""Get pointer to the active layer (layer selected in the legend)"""
70+
myLayers = QgsMapLayerRegistry.instance().mapLayers()
71+
for myItem in myLayers:
72+
return myLayers[myItem]
73+
74+
def addToolBarIcon(self, qAction):
75+
"""Add an icon to the plugins toolbar"""
76+
pass
77+
78+
def removeToolBarIcon(self, qAction):
79+
"""Remove an action (icon) from the plugin toolbar"""
80+
pass
81+
82+
def addToolBar(self, name):
83+
"""Add toolbar with specified name"""
84+
pass
85+
86+
def mapCanvas(self):
87+
"""Return a pointer to the map canvas"""
88+
return self.canvas
89+
90+
def mainWindow(self):
91+
"""Return a pointer to the main window
92+
93+
In case of QGIS it returns an instance of QgisApp
94+
"""
95+
pass
96+
97+
def addDockWidget(self, area, dockwidget):
98+
""" Add a dock widget to the main window """
99+
pass

‎tests/src/python/test_qgisapp.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import unittest
2+
from utilities import (getQgisTestApp,
3+
setCanvasCrs,
4+
GEOCRS,
5+
GOOGLECRS
6+
)
7+
QGISAPP, CANVAS, IFACE, PARENT = getQgisTestApp()
8+
9+
class TestQGisApp(unittest.TestCase):
10+
11+
def testValidThemeName(self):
12+
"""That can set the app to use a valid theme"""
13+
QGISAPP.setThemeName('gis')
14+
myExpectedResult = 'gis'
15+
myResult = QGISAPP.themeName()
16+
myMessage = ('Expected:\n%s\nGot:\n%s\n' %
17+
(myExpectedResult, myResult))
18+
19+
mySettings = QGISAPP.showSettings()
20+
print mySettings
21+
22+
assert myExpectedResult == myResult, myMessage
23+
24+
def testInvalidThemeName(self):
25+
"""That setting the app to use an invalid theme will fallback to 'default'"""
26+
QGISAPP.setThemeName('fooobar')
27+
myExpectedResult = 'default'
28+
myResult = QGISAPP.themeName()
29+
myMessage = ('Expected:\n%s\nGot:\n%s\n' %
30+
(myExpectedResult, myResult))
31+
assert myExpectedResult == myResult, myMessage
32+
33+
34+
35+
36+
if __name__ == '__main__':
37+
unittest.main()
38+

‎tests/src/python/utilities.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import os
2+
import sys
3+
from PyQt4 import QtGui, QtCore
4+
from qgis.core import (QgsApplication,
5+
QgsVectorLayer,
6+
QgsRasterLayer,
7+
QgsRectangle,
8+
QgsCoordinateReferenceSystem)
9+
from qgis.gui import QgsMapCanvas
10+
from qgis_interface import QgisInterface
11+
import hashlib
12+
13+
QGISAPP = None # Static vainasafele used to hold hand to running QGis app
14+
CANVAS = None
15+
PARENT = None
16+
IFACE = None
17+
GEOCRS = 4326 # constant for EPSG:GEOCRS Geographic CRS id
18+
GOOGLECRS = 900913 # constant for EPSG:GOOGLECRS Google Mercator id
19+
20+
21+
def assertHashesForFile(theHashes, theFilename):
22+
"""Assert that a files has matches one of a list of expected hashes"""
23+
myHash = hashForFile(theFilename)
24+
myMessage = ('Unexpected hash'
25+
'\nGot: %s'
26+
'\nExpected: %s'
27+
'\nPlease check graphics %s visually '
28+
'and add to list of expected hashes '
29+
'if it is OK on this platform.'
30+
% (myHash, theHashes, theFilename))
31+
assert myHash in theHashes, myMessage
32+
33+
34+
def assertHashForFile(theHash, theFilename):
35+
"""Assert that a files has matches its expected hash"""
36+
myHash = hashForFile(theFilename)
37+
myMessage = ('Unexpected hash'
38+
'\nGot: %s'
39+
'\nExpected: %s' % (myHash, theHash))
40+
assert myHash == theHash, myMessage
41+
42+
43+
def hashForFile(theFilename):
44+
"""Return an md5 checksum for a file"""
45+
myPath = theFilename
46+
myData = file(myPath).read()
47+
myHash = hashlib.md5()
48+
myHash.update(myData)
49+
myHash = myHash.hexdigest()
50+
return myHash
51+
52+
53+
def getQgisTestApp():
54+
""" Start one QGis application to test agaist
55+
56+
Input
57+
NIL
58+
59+
Output
60+
handle to qgis app
61+
62+
63+
If QGis is already running the handle to that app will be returned
64+
"""
65+
66+
global QGISAPP # pylint: disable=W0603
67+
68+
if QGISAPP is None:
69+
myGuiFlag = True # All test will run qgis in gui mode
70+
QGISAPP = QgsApplication(sys.argv, myGuiFlag)
71+
if 'QGISPATH' in os.environ:
72+
myPath = os.environ['QGISPATH']
73+
myUseDefaultPathFlag = True
74+
QGISAPP.setPrefixPath(myPath, myUseDefaultPathFlag)
75+
76+
QGISAPP.initQgis()
77+
s = QGISAPP.showSettings()
78+
print s
79+
80+
global PARENT # pylint: disable=W0603
81+
if PARENT is None:
82+
PARENT = QtGui.QWidget()
83+
84+
global CANVAS # pylint: disable=W0603
85+
if CANVAS is None:
86+
CANVAS = QgsMapCanvas(PARENT)
87+
CANVAS.resize(QtCore.QSize(400, 400))
88+
89+
global IFACE # pylint: disable=W0603
90+
if IFACE is None:
91+
# QgisInterface is a stub implementation of the QGIS plugin interface
92+
IFACE = QgisInterface(CANVAS)
93+
94+
return QGISAPP, CANVAS, IFACE, PARENT
95+
96+
97+
def unitTestDataPath(theSubdir=None):
98+
"""Return the absolute path to the InaSAFE unit test data dir.
99+
100+
.. note:: This is not the same thing as the SVN inasafe_data dir. Rather
101+
this is a new dataset where the test datasets are all tiny for fast
102+
testing and the datasets live in the same repo as the code.
103+
104+
Args:
105+
* theSubdir: (Optional) Additional subdir to add to the path - typically
106+
'hazard' or 'exposure'.
107+
"""
108+
myPath = __file__
109+
if theSubdir is not None:
110+
myPath = os.path.abspath(os.path.join(myPath,
111+
'unit_test_data',
112+
theSubdir))
113+
else:
114+
myPath = os.path.abspath(os.path.join(myPath, 'unit_test_data'))
115+
return myPath
116+
117+
def setCanvasCrs(theEpsgId, theOtfpFlag=False):
118+
"""Helper to set the crs for the CANVAS before a test is run.
119+
120+
Args:
121+
122+
* theEpsgId - Valid EPSG identifier (int)
123+
* theOtfpFlag - whether on the fly projections should be enabled
124+
on the CANVAS. Default to False.
125+
"""
126+
# Enable on-the-fly reprojection
127+
CANVAS.mapRenderer().setProjectionsEnabled(theOtfpFlag)
128+
129+
# Create CRS Instance
130+
myCrs = QgsCoordinateReferenceSystem()
131+
myCrs.createFromId(theEpsgId, QgsCoordinateReferenceSystem.EpsgCrsId)
132+
133+
# Reproject all layers to WGS84 geographic CRS
134+
CANVAS.mapRenderer().setDestinationCrs(myCrs)
135+

0 commit comments

Comments
 (0)
Please sign in to comment.