Source code for safe_qgis.utilities_test

"""Helper module for gui test suite
"""

import os
import sys
import hashlib
import logging

from PyQt4 import QtGui, QtCore
from qgis.core import (QgsApplication,
                      QgsVectorLayer,
                      QgsRasterLayer,
                      QgsRectangle,
                      QgsCoordinateReferenceSystem,
                      )
from qgis.gui import QgsMapCanvas
from qgis_interface import QgisInterface

# For testing and demoing
from safe.common.testing import TESTDATA
from safe_qgis.safe_interface import (readKeywordsFromFile,
                                      temp_dir,
                                      unique_filename)
LOGGER = logging.getLogger('InaSAFE')

QGISAPP = None  # Static vainasafele used to hold hand to running QGis app
CANVAS = None
PARENT = None
IFACE = None
GEOCRS = 4326  # constant for EPSG:GEOCRS Geographic CRS id
GOOGLECRS = 900913  # constant for EPSG:GOOGLECRS Google Mercator id
DEVNULL = open(os.devnull, 'w')
CONTROL_IMAGE_DIR = os.path.join(os.path.dirname(__file__),
    'test_data/test_images')


[docs]def assertHashesForFile(theHashes, theFilename): """Assert that a files has matches one of a list of expected hashes""" myHash = hashForFile(theFilename) myMessage = ('Unexpected hash' '\nGot: %s' '\nExpected: %s' '\nPlease check graphics %s visually ' 'and add to list of expected hashes ' 'if it is OK on this platform.' % (myHash, theHashes, theFilename)) assert myHash in theHashes, myMessage
[docs]def assertHashForFile(theHash, theFilename): """Assert that a files has matches its expected hash""" myHash = hashForFile(theFilename) myMessage = ('Unexpected hash' '\nGot: %s' '\nExpected: %s' % (myHash, theHash)) assert myHash == theHash, myMessage
[docs]def hashForFile(theFilename): """Return an md5 checksum for a file""" myPath = theFilename myData = file(myPath, 'rb').read() myHash = hashlib.md5() myHash.update(myData) myHash = myHash.hexdigest() return myHash
[docs]def getQgisTestApp(): """ Start one QGis application to test agaist Input NIL Output handle to qgis app If QGis is already running the handle to that app will be returned """ global QGISAPP # pylint: disable=W0603 if QGISAPP is None: myGuiFlag = True # All test will run qgis in safe_qgis mode QGISAPP = QgsApplication(sys.argv, myGuiFlag) # Note: This block is not needed for QGIS > 1.8 which will # automatically check the QGIS_PREFIX_PATH var so it is here # for backwards compatibility only if 'QGIS_PREFIX_PATH' in os.environ: myPath = os.environ['QGIS_PREFIX_PATH'] myUseDefaultPathFlag = True QGISAPP.setPrefixPath(myPath, myUseDefaultPathFlag) QGISAPP.initQgis() s = QGISAPP.showSettings() LOGGER.debug(s) global PARENT # pylint: disable=W0603 if PARENT is None: PARENT = QtGui.QWidget() global CANVAS # pylint: disable=W0603 if CANVAS is None: CANVAS = QgsMapCanvas(PARENT) CANVAS.resize(QtCore.QSize(400, 400)) global IFACE # pylint: disable=W0603 if IFACE is None: # QgisInterface is a stub implementation of the QGIS plugin interface IFACE = QgisInterface(CANVAS) return QGISAPP, CANVAS, IFACE, PARENT
[docs]def unitTestDataPath(theSubdir=None): """Return the absolute path to the InaSAFE unit test data dir. .. note:: This is not the same thing as the SVN inasafe_data dir. Rather this is a new dataset where the test datasets are all tiny for fast testing and the datasets live in the same repo as the code. Args: * theSubdir: (Optional) Additional subdir to add to the path - typically 'hazard' or 'exposure'. """ from safe.common.testing import UNITDATA myPath = UNITDATA if theSubdir is not None: myPath = os.path.abspath(os.path.join(myPath, theSubdir)) return myPath
[docs]def loadLayer(theLayerFile, theDirectory=TESTDATA): """Helper to load and return a single QGIS layer Args: theLayerFile: Pathname to raster or vector file DIR: Optional parameter stating the parent dir. If None, pathname is assumed to be absolute Returns: QgsMapLayer, str (for layer type) """ # Extract basename and absolute path myFilename = os.path.split(theLayerFile)[-1] # In case path was absolute myBaseName, myExt = os.path.splitext(myFilename) if theDirectory is None: myPath = theLayerFile else: myPath = os.path.join(theDirectory, theLayerFile) myKeywordPath = myPath[:-4] + '.keywords' # Determine if layer is hazard or exposure myKeywords = readKeywordsFromFile(myKeywordPath) myType = 'undefined' if 'category' in myKeywords: myType = myKeywords['category'] myMessage = 'Could not read %s' % myKeywordPath assert myKeywords is not None, myMessage # Create QGis Layer Instance if myExt in ['.asc', '.tif']: myLayer = QgsRasterLayer(myPath, myBaseName) elif myExt in ['.shp']: myLayer = QgsVectorLayer(myPath, myBaseName, 'ogr') else: myMessage = 'File %s had illegal extension' % myPath raise Exception(myMessage) myMessage = 'Layer "%s" is not valid' % str(myLayer.source()) assert myLayer.isValid(), myMessage return myLayer, myType
[docs]def setCanvasCrs(theEpsgId, theOtfpFlag=False): """Helper to set the crs for the CANVAS before a test is run. Args: * theEpsgId - Valid EPSG identifier (int) * theOtfpFlag - whether on the fly projections should be enabled on the CANVAS. Default to False. """ # Enable on-the-fly reprojection CANVAS.mapRenderer().setProjectionsEnabled(theOtfpFlag) # Create CRS Instance myCrs = QgsCoordinateReferenceSystem() myCrs.createFromId(theEpsgId, QgsCoordinateReferenceSystem.EpsgCrsId) # Reproject all layers to WGS84 geographic CRS CANVAS.mapRenderer().setDestinationCrs(myCrs)
[docs]def setPadangGeoExtent(): """Zoom to an area occupied by both both Padang layers""" myRect = QgsRectangle(100.21, -1.05, 100.63, -0.84) CANVAS.setExtent(myRect)
[docs]def setJakartaGeoExtent(): """Zoom to an area occupied by both Jakarta layers in Geo""" myRect = QgsRectangle(106.52, -6.38, 107.14, -6.07) CANVAS.setExtent(myRect)
[docs]def setJakartaGoogleExtent(): """Zoom to an area occupied by both Jakarta layers in 900913 crs """ myRect = QgsRectangle(11873524, -695798, 11913804, -675295) CANVAS.setExtent(myRect)
[docs]def setBatemansBayGeoExtent(): """Zoom to an area occupied by both Batemans Bay layers in geo crs""" myRect = QgsRectangle(150.152, -35.710, 150.187, -35.7013) CANVAS.setExtent(myRect)
[docs]def setYogyaGeoExtent(): """Zoom to an area occupied by both Jakarta layers in Geo""" myRect = QgsRectangle(110.348, -7.732, 110.368, -7.716) CANVAS.setExtent(myRect)
[docs]def setGeoExtent(theBoundingBox): """Zoom to an area specified given bounding box (list)""" myRect = QgsRectangle(*theBoundingBox) CANVAS.setExtent(myRect)
[docs]def checkImages(theControlImages, theTestImagePath, theTolerance=1000): """Compare a test image against a collection of known good images. Args: * theControlImagePath: list of file names. Give only the basename +ext as the test image path (CONTROL_IMAGE_DIR) will be prepended. * theTestImagePath: The Image being checked (must have same dimensions as the control image). Must be full path to image. * theTolerance: How many pixels may be different between the two images. Returns: (bool, str, str) where: * bool is success or failure indicator * str is the file path of the resulting difference image * str is a message providing analysis comparison notes Raises: None """ myMessages = '' for myControlImage in theControlImages: myFullPath = os.path.join(CONTROL_IMAGE_DIR, myControlImage) myFlag, myMessage = checkImage(myFullPath, theTestImagePath, theTolerance) myMessages += myMessage # As soon as one passes we are done! if myFlag: break return myFlag, myMessages
[docs]def checkImage(theControlImagePath, theTestImagePath, theTolerance=1000): """Compare a test image against a known good image. Args: * theControlImagePath: The image representing expected output * theTestImagePath: The Image being checked (must have same dimensions as the control image). * theTolerance: How many pixels may be different between the two images. Returns: (bool, str, str) where: * bool is success or failure indicator * str is a message providing analysis comparison notes Raises: None """ try: if not os.path.exists(theTestImagePath): raise OSError myTestImage = QtGui.QImage(theTestImagePath) except OSError: myMessage = 'Test image:\n%s\ncould not be loaded' % theTestImagePath return False, myMessage try: if not os.path.exists(theControlImagePath): raise OSError myControlImage = QtGui.QImage(theControlImagePath) except OSError: myMessage = ('Control image:\n%s\ncould not be loaded.\n' 'Test image is:\n%s\n' % ( theControlImagePath, theTestImagePath)) return False, myMessage if (myControlImage.width() != myTestImage.width() or myControlImage.height() != myTestImage.height()): myMessage = ('Control and test images are different sizes.\n' 'Control image : %s (%i x %i)\n' 'Test image : %s (%i x %i)\n' 'If this test has failed look at the above images ' 'to try to determine what may have change or ' 'adjust the tolerance if needed.' % (theControlImagePath, myControlImage.width(), myControlImage.height(), theTestImagePath, myTestImage.width(), myTestImage.height())) return False, myMessage myImageWidth = myControlImage.width() myImageHeight = myControlImage.height() myMismatchCount = 0 myDifferenceImage = QtGui.QImage(myImageWidth, myImageHeight, QtGui.QImage.Format_ARGB32_Premultiplied) myDifferenceImage.fill(152 + 219 * 256 + 249 * 256 * 256) myControlPixel = QtGui.QColor().rgb() myTestPixel = QtGui.QColor().rgb() for myY in range(myImageHeight): for myX in range(myImageWidth): myControlPixel = myControlImage.pixel(myX, myY) myTestPixel = myTestImage.pixel(myX, myY) if (myControlPixel != myTestPixel): myMismatchCount = myMismatchCount + 1 myDifferenceImage.setPixel(myX, myY, QtGui.qRgb(255, 0, 0)) myDifferenceFilePath = unique_filename(prefix='difference', suffix='.png', dir=temp_dir('test')) myDifferenceImage.save(myDifferenceFilePath, "PNG") #allow pixel deviation of 1 percent myPixelCount = myImageWidth * myImageHeight # FIXME (Ole): Use relative error i.e. mismatchcount/total pixels if myMismatchCount > theTolerance: mySuccessFlag = False else: mySuccessFlag = True myMessage = ('%i of %i pixels are mismatched. Tolerance is %i.\n' 'Control image : %s\n' 'Test image : %s\n' 'Difference image: %s\n' 'If this test has failed look at the above images ' 'to try to determine what may have change or ' 'adjust the tolerance if needed.' % (myMismatchCount, myPixelCount, theTolerance, theControlImagePath, theTestImagePath, myDifferenceFilePath)) return mySuccessFlag, myMessage
[docs]class RedirectStdStreams(object): """Context manager for redirection of stdout and stderr This is from http://stackoverflow.com/questions/6796492/ python-temporarily-redirect-stdout-stderr In this context, the class is used to get rid of QGIS output in the test suite - BUT IT DOESN'T WORK (Maybe because QGIS starts its providers in a different process?) Usage: devnull = open(os.devnull, 'w') print('Fubar') with RedirectStdStreams(stdout=devnull, stderr=devnull): print("You'll never see me") print("I'm back!") """ def __init__(self, stdout=None, stderr=None): self._stdout = stdout or sys.stdout self._stderr = stderr or sys.stderr def __enter__(self): self.old_stdout, self.old_stderr = sys.stdout, sys.stderr self.old_stdout.flush() self.old_stderr.flush() sys.stdout, sys.stderr = self._stdout, self._stderr def __exit__(self, exc_type, exc_value, traceback): self._stdout.flush() self._stderr.flush() sys.stdout = self.old_stdout sys.stderr = self.old_stderr