From 698bf587ea4dff6858d72e8620ea93926380c286 Mon Sep 17 00:00:00 2001 From: Jürgen Knödlseder Date: Sun, 17 Apr 2016 22:43:00 +0200 Subject: [PATCH] Integrate csworkflow script and add unit test. --- cscripts/Makefile.am | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------- cscripts/__init__.py.in | 3 ++- cscripts/cscaldb.py | 10 ---------- cscripts/csworkflow.par | 29 +++++++++++++++++++++++++++++ cscripts/csworkflow.py | 342 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- test/data/workflow.xml | 36 +++--------------------------------- test/test_csworkflow.py | 149 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ test/test_python_cscripts.py | 6 +++++- 8 files changed, 407 insertions(+), 246 deletions(-) create mode 100644 cscripts/csworkflow.par create mode 100755 test/test_csworkflow.py diff --git a/cscripts/Makefile.am b/cscripts/Makefile.am index 34e95b1..188bff0 100644 --- a/cscripts/Makefile.am +++ b/cscripts/Makefile.am @@ -2,26 +2,70 @@ # Scripts to be distributed in $(pyexecdir)/cscripts pythonscripts = __init__.py \ - cspull.py cssens.py cstsdist.py cscaldb.py csresmap.py \ - csobsdef.py csspec.py cslightcrv.py csobsinfo.py \ - csinfo.py csmodelinfo.py csmodelmerge.py cstsmapmerge.py \ - csroot2caldb.py csobs2caldb.py \ - csfindobs.py csiactcopy.py csiactdata.py csiactobs.py \ + cscaldb.py \ + csinfo.py \ + cslightcrv.py \ + csmodelinfo.py \ + csmodelmerge.py \ + csobs2caldb.py \ + csobsdef.py \ + csobsinfo.py \ + cspull.py \ + csresmap.py \ + csroot2caldb.py \ + cssens.py \ + csspec.py \ + cstsdist.py \ + cstsmapmerge.py \ + csworkflow.py \ + csfindobs.py \ + csiactcopy.py \ + csiactdata.py \ + csiactobs.py \ obsutils.py # Parfiles to be distributed in $(prefix)/syspfiles -parfiles = cspull.par cssens.par cstsdist.par cscaldb.par csresmap.par \ - csobsdef.par csspec.par cslightcrv.par csobsinfo.par \ - csmodelinfo.par csmodelmerge.par cstsmapmerge.par \ - csroot2caldb.par csobs2caldb.par \ - csfindobs.par csiactcopy.par csiactdata.par csiactobs.par +parfiles = cscaldb.par \ + cslightcrv.par \ + csmodelinfo.par \ + csmodelmerge.par \ + csobs2caldb.par \ + csobsdef.par \ + csobsinfo.par \ + cspull.par \ + csresmap.par \ + csroot2caldb.par \ + cssens.par \ + csspec.par \ + cstsdist.par \ + cstsmapmerge.par \ + csworkflow.par \ + csfindobs.par \ + csiactcopy.par \ + csiactdata.par \ + csiactobs.par # Scripts for which symbolic links will be created in bindir -linkscripts = cspull cssens cstsdist cscaldb csresmap \ - csobsdef csspec cslightcrv csobsinfo \ - csinfo csmodelinfo csmodelmerge cstsmapmerge \ - csroot2caldb csobs2caldb \ - csfindobs csiactcopy csiactdata csiactobs +linkscripts = cscaldb \ + csinfo \ + cslightcrv \ + csmodelinfo \ + csmodelmerge \ + csobs2caldb \ + csobsdef \ + csobsinfo \ + cspull \ + csresmap \ + csroot2caldb \ + cssens \ + csspec \ + cstsdist \ + cstsmapmerge \ + csworkflow \ + csfindobs \ + csiactcopy \ + csiactdata \ + csiactobs # Test scripts to be distributed in $(pyexecdir)/cscripts/tests testscripts = tests/__init__.py \ @@ -35,6 +79,7 @@ testscripts = tests/__init__.py \ ../test/test_csobsinfo.py \ ../test/test_cspull.py \ ../test/test_csroot2caldb.py \ + ../test/test_csworkflow.py \ ../test/test_csfindobs.py \ ../test/test_csiactcopy.py \ ../test/test_csiactdata.py \ @@ -55,7 +100,8 @@ testdata = ../test/data/crab.xml \ ../test/data/model_cube_background.xml \ ../test/data/model_cube_background2.xml \ ../test/data/crab_bkgcube.xml \ - ../test/data/irf.root + ../test/data/irf.root \ + ../test/data/workflow.xml # Test data to be distributed in $(pyexecdir)/cscripts/tests/iactdata testiactdata = ../test/iactdata/hdu-index.fits.gz \ diff --git a/cscripts/__init__.py.in b/cscripts/__init__.py.in index e4260c5..7d299d2 100644 --- a/cscripts/__init__.py.in +++ b/cscripts/__init__.py.in @@ -39,7 +39,7 @@ __all__ = [ "csspec", "cstsdist", "cstsmapmerge", - "csfindobs", + "csworkflow", "obsutils" ] if cur_version > req_version: @@ -64,6 +64,7 @@ from cscripts.cssens import cssens from cscripts.csspec import csspec from cscripts.cstsdist import cstsdist from cscripts.cstsmapmerge import cstsmapmerge +from cscripts.csworkflow import csworkflow from cscripts import obsutils if cur_version > req_version: from cscripts.csfindobs import csfindobs diff --git a/cscripts/cscaldb.py b/cscripts/cscaldb.py index 44f38a9..d58f773 100755 --- a/cscripts/cscaldb.py +++ b/cscripts/cscaldb.py @@ -206,16 +206,6 @@ class cscaldb(ctools.cscript): """ Execute the script. """ - # Run the script - self.run() - - # Return - return - - def execute(self): - """ - Execute the script. - """ # Open logfile self.logFileOpen() diff --git a/cscripts/csworkflow.par b/cscripts/csworkflow.par new file mode 100644 index 0000000..381ba26 --- /dev/null +++ b/cscripts/csworkflow.par @@ -0,0 +1,29 @@ +############################################################################# +# # +# CTA Science Analysis Tools # +# # +# ------------------------------------------------------------------------- # +# # +# File: csworkflow.par # +# Version: 1.0.0 # +# # +# Author: Juergen Knoedlseder # +# Jurgen.Knodlseder@irap.omp.eu # +# IRAP # +# # +# Purpose: Parameter file for the csworkflow script # +# # +############################################################################# +# +# Input parameters +#================= +inflow, f, h, workflow.xml,,, "Input workflow XML file" + +# +# Standard parameters +#==================== +chatter, i, h, 2,0,4, "Chattiness of output" +clobber, b, h, yes,,, "Overwrite existing output files with new output files?" +debug, b, h, no,,, "Debugging mode activated" +mode, s, h, ql,,, "Mode of automatic parameters" +logfile, f, h, csworkflow.log,,, "Log filename" diff --git a/cscripts/csworkflow.py b/cscripts/csworkflow.py index 9a5c205..c9d6589 100755 --- a/cscripts/csworkflow.py +++ b/cscripts/csworkflow.py @@ -1,6 +1,6 @@ #! /usr/bin/env python # ========================================================================== -# This script executes a workflow defined in an XML file. +# Executes analysis workflow # # Copyright (C) 2016 Juergen Knoedlseder # @@ -29,138 +29,77 @@ import sys # ================ # class csworkflow(ctools.cscript): """ + Executes an analysis workflow. + + The ``csworkflow`` script executes an analysis workflow defined in an + XML file. """ + + # Constructor def __init__(self, *argv): """ Constructor. + + Parameters + ---------- + argv : list of str + List of IRAF command line parameter strings of the form + ``parameter=3``. + + Raises + ------ + TypeError + An invalid number of command line arguments was provided. """ # Set name and version - self.name = "csworkflow" - self.version = "1.0.0" + self._name = 'csworkflow' + self._version = '1.0.0' # Set members - self.workflow = gammalib.GXml() - self.actors = [] - - # Make sure that parfile exists - self.parfile() - - # Initialise application - if len(argv) == 0: - ctools.cscript.__init__(self, self.name, self.version) - elif len(argv) ==1: - ctools.cscript.__init__(self, self.name, self.version, *argv) - else: - raise TypeError("Invalid number of arguments given.") + self._workflow = gammalib.GXml() + self._actors = [] - # Set logger properties - self.log_header() - self.log.date(True) + # Initialise application by calling the appropriate class constructor + self._init_cscript(argv) # Return return - def __del__(self): - """ - Destructor. - """ - # Return - return - def parfile(self): - """ - Check if parfile exists. If parfile does not exist then create a - default parfile. This kluge avoids shipping the cscript with a parfile. - """ - # Set parfile name - parfile = self.name+".par" - - # Load parfile or create one - try: - pars = gammalib.GApplicationPars(parfile) - except: - # Signal if parfile was not found - print("Parfile "+parfile+" not found. Create default parfile.") - - # Create default parfile - pars = gammalib.GApplicationPars() - pars.append(gammalib.GApplicationPar("inflow","f","h","test/data/workflow.xml","","","Input workflow XML file")) - pars.append_standard() - pars.append(gammalib.GApplicationPar("logfile","f","h","csworkflow.log","","","Log filename")) - pars.save(parfile) - - # Return - return + # Private methods + # + # Query all user parameters + def _get_parameters(self): - def get_parameters(self): - """ - Get parameters from parfile. - """ # Load workflow XML file - xmlfile = self["inflow"].filename() - self.workflow = gammalib.GXml(xmlfile.url()) - - # Return - return - - def execute(self): - """ - Execute the script. - """ - # Run the script - self.run() - - # Return - return - - def run(self): - """ - Run the script. - """ - # Switch screen logging on in debug mode - if self.logDebug(): - self.log.cout(True) - - # Get parameters - self.get_parameters() + xmlfile = self['inflow'].filename() + self._workflow = gammalib.GXml(xmlfile.url()) # Write input parameters into logger - if self.logTerse(): - self.log_parameters() - self.log("\n") - - # Parse XML file - if self.logTerse(): - self.log("\n") - self.log.header1("Parse workflow XML file") - self.parse_workflow() - - # Execute workflow - if self.logTerse(): - self.log("\n") - self.log.header1("Execute workflow") - self.execute_workflow() + if self._logTerse(): + self._log_parameters() + self._log('\n') # Return return - def parse_workflow(self): - """ - """ + # Parse the workflow XML file + def _parse_workflow(self): + # Get workflow - workflow = self.workflow.element("workflow") + workflow = self._workflow.element('workflow') # Get number of actors - num_actors = workflow.elements("actor") + num_actors = workflow.elements('actor') # Initialise actors - self.actors = [] + self._actors = [] # Loop over all actors for i in range(num_actors): # Get actor - actor = workflow.element("actor", i) + actor = workflow.element('actor', i) # Initialise parameters and input actors input_parameters = [] @@ -169,40 +108,40 @@ class csworkflow(ctools.cscript): output_actors = [] # Get actor attributes - name = actor.attribute("name") - tool = actor.attribute("tool") + name = actor.attribute('name') + tool = actor.attribute('tool') # Get actor input parameters - if actor.elements("input") > 0: - actor_inputs = actor.element("input") - num_inputs = actor_inputs.elements("parameter") + if actor.elements('input') > 0: + actor_inputs = actor.element('input') + num_inputs = actor_inputs.elements('parameter') for k in range(num_inputs): - input = actor_inputs.element("parameter", k) - input_name = input.attribute("name") - input_value = input.attribute("value") - input_actor = input.attribute("actor") + input = actor_inputs.element('parameter', k) + input_name = input.attribute('name') + input_value = input.attribute('value') + input_actor = input.attribute('actor') parameter = {'name': input_name, \ 'value': input_value, \ 'actor': input_actor} input_parameters.append(parameter) - if input_actor != "": + if input_actor != '': if input_actor not in input_actors: input_actors.append(input_actor) # Get actor output parameters - if actor.elements("output") > 0: - actor_output = actor.element("output") - num_outputs = actor_output.elements("parameter") + if actor.elements('output') > 0: + actor_output = actor.element('output') + num_outputs = actor_output.elements('parameter') for k in range(num_outputs): - output = actor_output.element("parameter", k) - output_name = output.attribute("name") - output_value = output.attribute("value") - output_actor = output.attribute("actor") + output = actor_output.element('parameter', k) + output_name = output.attribute('name') + output_value = output.attribute('value') + output_actor = output.attribute('actor') parameter = {'name': output_name, \ 'value': output_value, \ 'actor': output_actor} output_parameters.append(parameter) - if output_actor != "": + if output_actor != '': if output_actor not in output_actors: output_actors.append(output_actor) @@ -225,55 +164,55 @@ class csworkflow(ctools.cscript): 'status': status} # Append entry - self.actors.append(entry) + self._actors.append(entry) # Log information about actors - if self.logNormal(): - self.log.parformat("Actor \""+name+"\"") - self.log(tool) - self.log("\n") + if self._logNormal(): + self._log.parformat('Actor "'+name+'"') + self._log(tool) + self._log('\n') if num_inputs == 0: - self.log.parformat(" Predecessor") - self.log("none\n") + self._log.parformat(' Predecessor') + self._log('none\n') else: if num_inputs == 1: - self.log.parformat(" Predecessor") + self._log.parformat(' Predecessor') else: - self.log.parformat(" Predecessors") + self._log.parformat(' Predecessors') for k in range(num_inputs): if k > 0: - self.log(", ") - self.log("\""+input_actors[k]+"\"") - self.log("\n") + self._log(', ') + self._log('"'+input_actors[k]+'"') + self._log('\n') # Return return - def execute_workflow(self): - """ - """ + # Execute a workflow + def _execute_workflow(self): + # Continue while there are actors - while len(self.actors) > 0: + while len(self._actors) > 0: # Find actors which are ready num_executed = 0 - for actor in self.actors: + for actor in self._actors: # If actor is ready then execute it if actor['status'] == 'ready': # Log execution start - if self.logNormal(): - self.log.parformat("Execute actor") - self.log("\""+actor['name']+"\"\n") + if self._logNormal(): + self._log.parformat('Execute actor') + self._log('"'+actor['name']+'"\n') # Execute actor - self.execute_actor(actor) + self._execute_actor(actor) # Log execution finish - if self.logNormal(): - self.log.parformat("Finished actor execution") - self.log("\""+actor['name']+"\"\n") + if self._logNormal(): + self._log.parformat('Finished actor execution') + self._log('"'+actor['name']+'"\n') # Set actor status to finished actor['status'] = 'finished' @@ -286,12 +225,12 @@ class csworkflow(ctools.cscript): break # Update actor status - for actor in self.actors: + for actor in self._actors: if actor['status'] != 'finished': input_actors = actor['input_actors'] ready = True for input_actor in input_actors: - a = self.get_actor(input_actor) + a = self._get_actor(input_actor) if a['status'] != 'finished': ready = False break @@ -301,29 +240,28 @@ class csworkflow(ctools.cscript): # Return return - def execute_actor(self, actor): - """ - Excute an actor. - """ + # Execute an actor + def _execute_actor(self, actor): + # Log input parameters - if self.logNormal(): + if self._logNormal(): pars = actor['input_parameters'] for par in pars: - self.log.parformat(" Input parameter") - self.log(par['name']) - self.log("=") - self.log(self.get_parameter_value(par)) - self.log("\n") + self._log.parformat(' Input parameter') + self._log(par['name']) + self._log('=') + self._log(self._get_parameter_value(par)) + self._log('\n') # Log output parameters - if self.logNormal(): + if self._logNormal(): pars = actor['output_parameters'] for par in pars: - self.log.parformat(" Output parameter") - self.log(par['name']) - self.log("=") - self.log(self.get_parameter_value(par)) - self.log("\n") + self._log.parformat(' Output parameter') + self._log(par['name']) + self._log('=') + self._log(self._get_parameter_value(par)) + self._log('\n') # Set actor tool if 'tool' in actor: @@ -333,8 +271,8 @@ class csworkflow(ctools.cscript): elif tool[:2] == 'cs': tool_eval = 'cscripts.'+tool+'()' else: - tool_eval = "" - if tool_eval == "": + tool_eval = '' + if tool_eval == '': return # Create actor tool object @@ -344,14 +282,14 @@ class csworkflow(ctools.cscript): pars = actor['input_parameters'] for par in pars: app_par = object[par['name']] - app_par.value(self.get_parameter_value(par)) + app_par.value(self._get_parameter_value(par)) object[par['name']] = app_par # Set actor output parameters pars = actor['output_parameters'] for par in pars: app_par = object[par['name']] - app_par.value(self.get_parameter_value(par)) + app_par.value(self._get_parameter_value(par)) object[par['name']] = app_par # Execute actor @@ -361,60 +299,94 @@ class csworkflow(ctools.cscript): # Return return - def get_parameter_value(self, par): - """ - Get the parameter value from the respective actor. - """ + # Get the parameter value from the respective actor + def _get_parameter_value(self, par): + # If the actor is empty then simply return the value - if (par['actor'] == ""): + if (par['actor'] == ''): value = par['value'] # ... otherwise get the value from the actor else: found = False - actor = self.get_actor(par['actor']) + actor = self._get_actor(par['actor']) for output in actor['output_parameters']: if output['name'] == par['value']: value = output['value'] found = True break if not found: - msg = "Parameter \""+par['value']+"\" is not an output "\ - "parameter of actor \""+par['actor']+"\"." + msg = 'Parameter "'+par['value']+'" is not an output '+ \ + 'parameter of actor "'+par['actor']+'".' raise RuntimeError(msg) # Return value return value - def get_actor(self, name): - """ - Get the actor with the specified name. - """ + # Get the actor with the specified name + def _get_actor(self, name): + # Return actor with specified name - for actor in self.actors: + for actor in self._actors: if actor['name'] == name: return actor # Throw an exception - msg = "Actor \""+name+"\" not found." + msg = 'Actor "'+name+'" not found.' raise RuntimeError(msg) # Return return + # Public methods + def run(self): + """ + Run the script. + """ + # Switch screen logging on in debug mode + if self._logDebug(): + self._log.cout(True) + + # Get parameters + self._get_parameters() + + # Parse XML file + if self._logTerse(): + self._log('\n') + self._log.header1('Parse workflow XML file') + self._parse_workflow() + + # Execute workflow + if self._logTerse(): + self._log('\n') + self._log.header1('Execute workflow') + self._execute_workflow() + + # Return + return + + def execute(self): + """ + Execute the script. + """ + # Open logfile + self.logFileOpen() + + # Run the script + self.run() + + # Return + return + + # ======================== # # Main routine entry point # # ======================== # if __name__ == '__main__': - """ - Executes workflow. - """ + # Create instance of application app = csworkflow(sys.argv) - # Open logfile - app.logFileOpen() - # Execute application app.execute() diff --git a/test/data/workflow.xml b/test/data/workflow.xml index 1f494a8..7a97759 100644 --- a/test/data/workflow.xml +++ b/test/data/workflow.xml @@ -46,7 +46,7 @@ - + @@ -70,7 +70,7 @@ - + @@ -93,37 +93,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/test/test_csworkflow.py b/test/test_csworkflow.py new file mode 100755 index 0000000..ef8d0f5 --- /dev/null +++ b/test/test_csworkflow.py @@ -0,0 +1,149 @@ +#! /usr/bin/env python +# ========================================================================== +# This scripts performs unit tests for the csworkflow script. +# +# Copyright (C) 2016 Juergen Knoedlseder +# +# 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 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# ========================================================================== +import os +import gammalib +import cscripts + + +# ================================ # +# Test class for csworkflow script # +# ================================ # +class Test(gammalib.GPythonTestSuite): + """ + Test class for csworkflow script. + + This test class makes unit tests for the cslightcrv script by using it + from the command line and from Python. + """ + + # Constructor + def __init__(self): + """ + Constructor. + """ + # Call base class constructor + gammalib.GPythonTestSuite.__init__(self) + + # Return + return + + # Set test functions + def set(self): + """ + Set all test functions. + """ + # Set test name + self.name('csworkflow') + + # Append tests + self.append(self._test_cmd, 'Test csworkflow on command line') + self.append(self._test_python, 'Test csworkflow from Python') + + # Return + return + + # Test csworkflow on command line + def _test_cmd(self): + + # Kluge to set the command (installed version has no README file) + if os.path.isfile('README'): + csworkflow = '../cscripts/csworkflow.py' + else: + csworkflow = 'csworkflow' + + # Remove result file + gammalib.GFilename('wf_crab_results.xml').remove() + + # Setup csworkflow command + cmd = csworkflow+' inflow="data/workflow.xml"'+ \ + ' logfile="csworkflow_cmd1.log" chatter=1' + + # Execute csworkflow, make sure we catch any exception + try: + rc = os.system(cmd+" >/dev/null 2>&1") + except: + pass + + # Check if execution was successful + self.test_assert(rc == 0, + 'Successful csworkflow execution on command line') + + # Check fit result + self._check_fit_result('wf_crab_results.xml') + + # Return + return + + # Test csworkflow from Python + def _test_python(self): + + # Remove result file + gammalib.GFilename('wf_crab_results.xml').remove() + + # Set-up csworkflow + workflow = cscripts.csworkflow() + workflow['inflow'] = 'data/workflow.xml' + workflow['logfile'] = 'csworkflow_py1.log' + workflow['chatter'] = 2 + + # Run script + workflow.logFileOpen() # Make sure we get a log file + workflow.run() + + # Check fit result + self._check_fit_result('wf_crab_results.xml') + + # Remove result file + gammalib.GFilename('wf_crab_results.xml').remove() + + # Set-up csworkflow + workflow = cscripts.csworkflow() + workflow['inflow'] = 'data/workflow.xml' + workflow['logfile'] = 'csworkflow_py2.log' + workflow['chatter'] = 3 + + # Execute script + workflow.execute() + + # Check fit result + self._check_fit_result('wf_crab_results.xml') + + # Return + return + + # Check fit result XML values + def _check_fit_result(self, filename): + + # Load fit results + models = gammalib.GModels(filename) + + # Check fit result values + self.test_value(models['Crab'][2].value(), + 5.73787e-16, 1.0e-5, 'Crab prefactor') + self.test_value(models['Crab'][3].value(), + -2.47386, 1.0e-5, 'Crab index') + self.test_value(models['CTABackgroundModel'][0].value(), + 1.01414, 1.0e-5, 'Background model prefactor') + self.test_value(models['CTABackgroundModel'][1].value(), + 0.00242827, 1.0e-5, 'Background model index') + + # Return + return diff --git a/test/test_python_cscripts.py b/test/test_python_cscripts.py index 6ac74d8..871558e 100755 --- a/test/test_python_cscripts.py +++ b/test/test_python_cscripts.py @@ -24,6 +24,7 @@ import gammalib import cscripts sys.path.append(os.path.dirname(os.path.abspath(__file__))) import test_cscaldb +import test_csfindobs import test_cslightcrv import test_csmodelinfo import test_csmodelmerge @@ -32,10 +33,10 @@ import test_csobsdef import test_csobsinfo import test_cspull import test_csroot2caldb -import test_csfindobs import test_csiactcopy import test_csiactdata import test_csiactobs +import test_csworkflow # ================== # @@ -94,6 +95,7 @@ def test(installed=False, debug=False): suite_csobsinfo = test_csobsinfo.Test() suite_cspull = test_cspull.Test() suite_csroot2caldb = test_csroot2caldb.Test() + suite_csworkflow = test_csworkflow.Test() # Setup unit tests suite_cscaldb.set() @@ -105,6 +107,7 @@ def test(installed=False, debug=False): suite_csobsinfo.set() suite_cspull.set() suite_csroot2caldb.set() + suite_csworkflow.set() # Append tests to container suites.append(suite_cscaldb) @@ -116,6 +119,7 @@ def test(installed=False, debug=False): suites.append(suite_csobsinfo) suites.append(suite_cspull) suites.append(suite_csroot2caldb) + suites.append(suite_csworkflow) # Append tests for Python 2.6+ (the IACT cscripts depend on the json # module which is only available since Python 2.6+ -- libgit2 0.21.4