Skip to content

Commit

Permalink
Test coverage is now over 50%
Browse files Browse the repository at this point in the history
  • Loading branch information
carolinux committed Jan 1, 2015
1 parent 8fef42a commit 3dd92c1
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 53 deletions.
17 changes: 17 additions & 0 deletions os_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import platform

LINUX="linux"
MACOS="macos"
WINDOWS="windows"


def get_os():
"""Determine OS"""
# details of platform implementation
# https://hg.python.org/cpython/file/2.7/Lib/platform.py#l1568
if "linux" in platform.platform().lower():
return LINUX
elif "macos" in platform.platform().lower():
return MACOS
elif "windows" in platform.platform().lower():
return WINDOWS
2 changes: 1 addition & 1 deletion resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# Created by: The Resource Compiler for PyQt (Qt v4.6.2)
#
# WARNING! All changes made in this file will be lost!

from collections import OrderedDict,defaultdict
from PyQt4 import QtCore

qt_resource_data = "\
Expand Down
153 changes: 120 additions & 33 deletions test_functionality.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,50 +3,137 @@
# if using ipython: do this on bash before
# export QT_API=pyqt
from qgis.core import *
#from qgis.gui import *
from qgis.gui import *
import os
from mock import Mock
from datetime import datetime, timedelta
from PyQt4 import QtCore, QtGui, QtTest
import timemanagercontrol
from timemanagercontrol import FRAME_FILENAME_PREFIX
import timevectorlayer
import time_util
import os_util

import tempfile
import shutil
import unittest
import glob
import math

__author__="Karolina Alexiou"
__email__="karolina.alexiou@teralytics.ch"


TEST_DATA_DIR="testdata"

# discover your prefix by loading the Python console from within QGIS and
# running QgsApplication.showSettings().split("\t")
# and looking for Prefix
QgsApplication.setPrefixPath("/usr", True)

QgsApplication.initQgis()
QtCore.QCoreApplication.setOrganizationName('QGIS')
QtCore.QCoreApplication.setApplicationName('QGIS2')
class RiggedTimeManagerControl(timemanagercontrol.TimeManagerControl):
"""A subclass of TimeManagerControl which makes testing easier (with the downside of not
testing some signal behavior)"""

def saveCurrentMap(self,fileName):
"""We can't export a real screenshot from the test harness, so we just create a blank
file"""
print fileName
open(fileName, 'w').close()

def playAnimation(self,painter=None):
"""We just continue the animation after playing until it stops via the
self.animationActivated flag"""
super(RiggedTimeManagerControl, self).playAnimation()
if not self.animationActivated:
return
else:
self.playAnimation()


class Foo(unittest.TestCase):

def setUp(self):
iface = Mock()
self.ctrl = RiggedTimeManagerControl(iface)
self.ctrl.initGui(test=True)
self.tlm = self.ctrl.getTimeLayerManager()


@classmethod
def setUpClass(cls):
# FIXME discover your prefix by loading the Python console from within QGIS and
# running QgsApplication.showSettings().split("\t")
# and looking for Prefix
QgsApplication.setPrefixPath("/usr", True)

QgsApplication.initQgis()
QtCore.QCoreApplication.setOrganizationName('QGIS')
QtCore.QCoreApplication.setApplicationName('QGIS2')

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

def registerTweetsTimeLayer(self, fromAttr="T", toAttr="T"):
self.layer = QgsVectorLayer(os.path.join(TEST_DATA_DIR, 'tweets.shp'), 'tweets', 'ogr')
self.timeLayer = timevectorlayer.TimeVectorLayer(self.layer,fromAttr,toAttr,True,
time_util.DEFAULT_FORMAT,0)
self.tlm.registerTimeLayer(self.timeLayer)

def test_go_back_and_forth_2011(self):
self.go_back_and_forth("T","T")

def test_go_back_and_forth_1965(self):
self.go_back_and_forth("T1965","T1965")

def test_go_back_and_forth_1765(self):
self.go_back_and_forth("T1765","T1765")

def test_go_back_and_forth_1165(self):
self.go_back_and_forth("T1165","T1165")

def go_back_and_forth(self,fromAttr, toAttr):

self.registerTweetsTimeLayer(fromAttr, toAttr)
# The currentTimePosition should now be the first date in the shapefile
start_time = time_util.strToDatetime(self.timeLayer.getMinMaxValues()[0])
assert( start_time ==self.tlm.getCurrentTimePosition())
self.tlm.setTimeFrameType("hours")
self.tlm.stepForward()
assert( start_time + timedelta(hours=1)==self.tlm.getCurrentTimePosition())
self.tlm.stepForward()
assert( start_time + timedelta(hours=2)==self.tlm.getCurrentTimePosition())
self.tlm.stepBackward()
assert( start_time + timedelta(hours=1)==self.tlm.getCurrentTimePosition())

def test_export(self):
self.registerTweetsTimeLayer()
tmpdir = tempfile.mkdtemp()
self.tlm.setTimeFrameType("hours")
start_time = time_util.strToDatetime(self.timeLayer.getMinMaxValues()[0])
end_time = time_util.strToDatetime(self.timeLayer.getMinMaxValues()[1])
layer_duration_in_hours = (end_time-start_time).total_seconds() / 3600.0
self.ctrl.exportVideoAtPath(tmpdir)
screenshots_generated = glob.glob(os.path.join(tmpdir, FRAME_FILENAME_PREFIX+"*"))
self.assertEqual(len(screenshots_generated), math.ceil(layer_duration_in_hours + 1))
for i in range(int(math.ceil(layer_duration_in_hours + 1))):
fn = self.ctrl.generate_frame_filename(tmpdir,i, start_time + timedelta(hours=i))
self.assertIn(fn, screenshots_generated)
shutil.rmtree(tmpdir)

def test_export_when_not_starting_from_beginning(self):
pass

def test_with_interval_bigger_than_range(self):
#TODO
pass


# def test_fail(self):
# assert(False)

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


if __name__=="__main__":
unittest.main()

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

iface = Mock()
import timemanagercontrol
import timevectorlayer
import time_util
ctrl = timemanagercontrol.TimeManagerControl(iface)
tlm = ctrl.getTimeLayerManager()
layer = QgsVectorLayer(os.path.join(TEST_DATA_DIR, 'tweets.shp'), 'tweets', 'ogr')
assert(layer.isValid())
timeLayer = timevectorlayer.TimeVectorLayer(layer,"T","T",True,time_util.DEFAULT_FORMAT,0)
tlm.registerTimeLayer(timeLayer)
# The currentTimePosition should now be the first date in the shapefile
START_TIME = datetime(2011, 10, 8, 17, 44, 21)
assert( START_TIME ==tlm.getCurrentTimePosition())
tlm.setTimeFrameType("hours")
tlm.stepForward()
assert( START_TIME + timedelta(hours=1)==tlm.getCurrentTimePosition())

#print dir(layer)
# QgsMapLayerRegistry().instance().addMapLayer(layer) doesnt werk :((


QgsApplication.exitQgis()
8 changes: 6 additions & 2 deletions time_util.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import time
import re # for hacking strftime

from datetime import datetime
from datetime import datetime, timedelta
from PyQt4.QtCore import QDateTime


Expand Down Expand Up @@ -47,7 +47,11 @@ def datetime_at_end_of_day(dt):

def epoch_to_datetime(seconds_from_epoch):
"""Convert seconds since 1970-1-1 (UNIX epoch) to a datetime"""
return datetime.utcfromtimestamp(seconds_from_epoch)
#FIXME: Maybe this doesnt work on windows for negative timestamps
# http://stackoverflow.com/questions/22082103/on-windows-how-to-convert-a-timestamps-before-1970-into-something-manageable
# return datetime.utcfromtimestamp(seconds_from_epoch)
# but this should:
return datetime.datetime(1970, 1, 1) + timedelta(seconds=seconds_from_epoch)

def datetime_to_epoch(dt):
""" convert a datetime to seconds after (or possibly before) 1970-1-1 """
Expand Down
65 changes: 48 additions & 17 deletions timemanagercontrol.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from time_util import *

DEFAULT_FRAME_LENGTH = 2000
FRAME_FILENAME_PREFIX = "frame"

class TimeManagerControl(QObject):
"""Controls the logic behind the GUI. Signals are processed here."""
Expand All @@ -34,6 +35,7 @@ def __init__(self,iface):
self.iface.newProjectCreated.connect(self.restoreDefaults)
self.iface.newProjectCreated.connect(self.disableAnimationExport)

# this signal is responsible for keeping the animation running
self.iface.mapCanvas().renderComplete.connect(self.waitAfterRenderComplete)

# establish connections to QgsMapLayerRegistry
Expand Down Expand Up @@ -69,12 +71,16 @@ def getTimeLayerManager(self):
def debug(self, msg):
QMessageBox.information(self.iface.mainWindow(),'Info', msg)

def initGui(self):
"""initialize the plugin dock"""
#QMessageBox.information(self.iface.mainWindow(),'Debug','TimeManagerControl.initGui()')
def initGui(self, test=False):
"""initialize the plugin dock. If in testing mode, skip the Gui"""

if test:
from mock import Mock
if test:
self.guiControl = Mock()
else:
self.guiControl = TimeManagerGuiControl(self.iface,self.timeLayerManager)

self.guiControl = TimeManagerGuiControl(self.iface,self.timeLayerManager)

self.guiControl.showOptions.connect(self.showOptionsDialog)
self.guiControl.exportVideo.connect(self.exportVideo)
Expand All @@ -90,13 +96,16 @@ def initGui(self):
self.guiControl.saveOptionsEnd.connect(self.timeLayerManager.refresh) # sets the time restrictions again
self.guiControl.signalAnimationOptions.connect(self.setAnimationOptions)
self.guiControl.registerTimeLayer.connect(self.timeLayerManager.registerTimeLayer)

print "guiControls initialized"

# create actions
# F8 button press - show time manager settings
self.actionShowSettings = QAction(u"Show Time Manager Settings", self.iface.mainWindow())
self.iface.registerMainWindowAction(self.actionShowSettings, "F8")
self.guiControl.addActionShowSettings(self.actionShowSettings)
self.actionShowSettings.triggered.connect(self.showOptionsDialog)
if not test:
self.actionShowSettings = QAction(u"Show Time Manager Settings", self.iface.mainWindow())
self.iface.registerMainWindowAction(self.actionShowSettings, "F8")
self.guiControl.addActionShowSettings(self.actionShowSettings)
self.actionShowSettings.triggered.connect(self.showOptionsDialog)

# establish connections to timeLayerManager
self.timeLayerManager.timeRestrictionsRefreshed.connect(self.guiControl.refreshGuiWithCurrentTime)
Expand All @@ -116,16 +125,24 @@ def showOptionsDialog(self):
self.stopAnimation()
self.guiControl.showOptionsDialog(self.timeLayerManager.getTimeLayerList(),self.animationFrameLength,self.playBackwards,self.loopAnimation)

def exportVideo(self):
"""export 'video' - currently only image sequence"""
self.saveAnimationPath = str(QFileDialog.getExistingDirectory (self.iface.mainWindow(),'Pick export destination',self.saveAnimationPath))

def exportVideoAtPath(self, path):
self.saveAnimationPath = path
if self.saveAnimationPath:
self.saveAnimation = True
self.loopAnimation = False # on export looping has to be deactivated
self.toggleAnimation()
QMessageBox.information(self.iface.mainWindow(),'Export Video','Image sequence from '
'current position onwards'
' is being saved to '+self.saveAnimationPath+'.\n\nPlease wait until the process is finished.')
#FIXME make QMessageBox testable
# QMessageBox.information(self.iface.mainWindow(),'Export Video','Image sequence from '
# 'current position
# onwards' ' is being saved to '+self.saveAnimationPath+'.\n\nPlease wait until the process is finished.')

def exportVideo(self):
"""export 'video' - currently only image sequence"""
path = str(QFileDialog.getExistingDirectory (self.iface.mainWindow(),
'Pick export '
'destination',self.saveAnimationPath))
self.exportVideoAtPath(path)

def unload(self):
"""unload the plugin"""
Expand Down Expand Up @@ -171,6 +188,7 @@ def toggleOnOff(self,turnOn):

def startAnimation(self):
"""kick-start the animation, afterwards the animation will run based on signal chains"""
print "aaaaa"
self.waitAfterRenderComplete()

def waitAfterRenderComplete(self, painter=None):
Expand All @@ -179,6 +197,11 @@ def waitAfterRenderComplete(self, painter=None):
self.playAnimation(painter)
else:
QTimer.singleShot(self.animationFrameLength,self.playAnimation)

def generate_frame_filename(self, path, frame_index, currentTime):
return os.path.join(path,"{}{}_{}.png".format(FRAME_FILENAME_PREFIX,
str(frame_index).zfill(
self.exportNameDigits), currentTime))

def playAnimation(self,painter=None):
"""play animation in map window"""
Expand All @@ -190,8 +213,14 @@ def playAnimation(self,painter=None):
currentTime = self.timeLayerManager.getCurrentTimePosition()

if self.saveAnimation:
fileName = os.path.join(self.saveAnimationPath,"frame{}_{}.png".format(str(
self.animationFrameCounter).zfill(self.exportNameDigits), currentTime))
fileName = self.generate_frame_filename(self.saveAnimationPath,
self.animationFrameCounter, currentTime)

# try accessing the file or fail with informative exception
try:
open(fileName, 'a').close()
except:
raise Exception("Cannot write to file {}".format(fileName))
self.saveCurrentMap(fileName)
#self.debug("saving animation for time: {}".format(currentTime))
self.animationFrameCounter += 1
Expand Down Expand Up @@ -221,7 +250,9 @@ def saveCurrentMap(self,fileName):
def stopAnimation(self):
"""stop the animation in case it's running"""
if self.saveAnimation:
QMessageBox.information(self.iface.mainWindow(),'Export finished','The export finished successfully!')
#FIXME make QMessageBox testable
#QMessageBox.information(self.iface.mainWindow(),'Export finished','The export
# finished successfully!')
self.saveAnimation = False
self.animationActivated = False
self.guiControl.turnPlayButtonOff()
Expand Down
1 change: 1 addition & 0 deletions timevectorlayer.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def debug(self, msg):
QMessageBox.information(self.iface.mainWindow(),'Info', msg)

def getMinMaxValues(self):
"""Returns str"""
provider = self.layer.dataProvider()
fromTimeAttributeIndex = provider.fieldNameIndex(self.fromTimeAttribute)
toTimeAttributeIndex = provider.fieldNameIndex(self.toTimeAttribute)
Expand Down

0 comments on commit 3dd92c1

Please sign in to comment.