Index: /branches/sc_branches/psps_testing/psi/psi_inquisitor.py
===================================================================
--- /branches/sc_branches/psps_testing/psi/psi_inquisitor.py	(revision 29139)
+++ /branches/sc_branches/psps_testing/psi/psi_inquisitor.py	(revision 29140)
@@ -2,4 +2,6 @@
 from suds.xsd.doctor import Import, ImportDoctor
 from suds import sudsobject
+from utilities.util import convert
+import unicodedata
 
 class PsiInquisitor:
@@ -42,9 +44,9 @@
         >>> # we cannot guarantee the order in result
         >>> print result['items']
-        [{u'[frameID]': u'105439'}]
+        [{'frameID': 105439}]
         >>> print result['fields']
-        [u'[frameID]']
+        ['[frameID]']
         >>> print result['types']
-        {u'[frameID]': u'Integer'}
+        {'[frameID]': 'Integer'}
         >>> # This test has to be completed with exhaustive ones...
         >>> # e.g. unit tests against the csr data base.
@@ -62,4 +64,8 @@
         line_count = 0
         for line in lines:
+            # Unicode normalization comes from
+            # http://www.peterbe.com/plog/unicode-to-ascii. I don't
+            # know where the NFKD means...
+            line = unicodedata.normalize('NFKD', line).encode('ascii','ignore')
             if first_line_not_seen:
                 # Build up the field list and the types dictionary
@@ -85,5 +91,9 @@
                 element = {}
                 for i in range(len(values)):
-                    element[contents['fields'][i]] = values[i]
+                    field_name = contents['fields'][i].replace('[', 
+                                                               '').replace(']', 
+                                                                           '')
+                    element[field_name] = convert(types_dictionary[contents['fields'][i]],
+                                                  values[i])
                 contents['items'].append(element)
         return contents
Index: /branches/sc_branches/psps_testing/testers/fits/p2.py
===================================================================
--- /branches/sc_branches/psps_testing/testers/fits/p2.py	(revision 29139)
+++ /branches/sc_branches/psps_testing/testers/fits/p2.py	(revision 29140)
@@ -1,13 +1,155 @@
-
+from psi.psi_inquisitor import PsiInquisitor
+import pyfits
+from StringIO import StringIO
+from utilities.util import match
 
 class P2FitsTester:
     """
+    The content of a P2 frame data is detailed in PSDC-940-006-01,
+    paragraph 3.6.3, item 1).
+
     This is the class that tests the contents of a P2 FITS file
-    provided by IPP. The contents can be checked against the database
+    provided by IPP. The contents are checked against the database
     through SOAP queries.
+
+    Note that the purpose of this class is NOT to test the FITS
+    validity (which can be checked using some pyfits options).
     """
-    def __init__(self, fitsFile):
-        self.fits = fitsFile
+    def __init__(self, fits_file, psi_configuration):
+        """
+        Creates a P2FitsTester object.
+
+        >>> import pyfits
+        >>> fits_file = pyfits.open('data/psut/ok/fits/p2_was_00105439.FITS')
+        >>> psi_configuration = 'psi.web01_configuration'
+        >>> p2ft = P2FitsTester(fits_file, psi_configuration)
+        """
+        self.fits = fits_file
+        self.psi_inquisitor = PsiInquisitor(psi_configuration)
 
     def test(self):
-        return ("BARF", None)
+        """
+        Tests a FITS file supposed to contain a P2 frame data set.
+
+        >>> import pyfits
+        >>> fits_file = pyfits.open('data/psut/ok/fits/p2_was_00105439.FITS')
+        >>> psi_configuration = 'psi.web01_configuration'
+        >>> p2ft = P2FitsTester(fits_file, psi_configuration)
+        >>> (message, values) = p2ft.test()
+        >>> print message # doctest:+ELLIPSIS
+        PSDC-940-006-01, 3.6.3, 1): OK
+        PSDC-940-006-01, 3.6.6.8: OK
+        PSDC-940-006-01, TBD_test_detections_frames: OK (frame 00 - SELECT * FROM ImageMeta WHERE imageID = 10543901)
+        PSDC-940-006-01, TBD_test_detections_frames: OK (frame 01 - SELECT * FROM ImageMeta WHERE imageID = 10543902)
+        ...
+        PSDC-940-006-01, TBD_test_detections_frames: OK (frame 57 - SELECT * FROM ImageMeta WHERE imageID = 10543976)
+        >>> print values
+        None
+        """
+        report = StringIO()
+        # 1. There is no image for the PrimaryHDU (at 0)
+        (messages, other) = self.test_primary()
+        report.write(messages)
+        # 2. Look at the FrameMeta information
+        (messages, values) = self.test_frame_meta()
+        report.write('\n' + messages)
+        if values is None:
+            return (report.getvalue(), None)
+        # 3. There should be values['ota'] sequences of [ImageMeta,
+        # Detection, SkinnyObject, ObjectCalColor] frames
+        frameID = values['frameID']
+        nOTA = values['nOTA']
+        if len(self.fits) != 4*nOTA + 2:
+            report.write('\nInvalid number of OTA: from FITS file: %d / from FrameMeta %d'
+                         % (len(self.fits)-2/4, nOTA))
+            return (report.getvalue(), None)
+        for ota_index in range(nOTA):
+            (messages, values) = self.test_detections_frames(frameID, ota_index) 
+            report.write('\n' + messages)
+        return (report.getvalue(), None)
+
+    def test_detections_frames(self, frameID, ota_index):
+        """
+        Tests the contents of the 4-frame sequence [ImageMeta,
+        Detection, SkinnyObject, ObjectCalColor].
+        Those contents can be queried by: TODO
+        
+        """
+        report = StringIO()
+        # Check that the 4 sequence is correct
+        requirement = 'PSDC-940-006-01, TBD_test_detections_frames: '
+        fits_index = 4*ota_index+2
+        if self.fits[fits_index].name != 'IMAGEMETA':
+            return (requirement + 'KO (Not an ImageMeta frame)', None)
+        if self.fits[fits_index+1].name != 'DETECTION':
+            return (requirement + 'KO (Not a Detection frame)', None)
+        if self.fits[fits_index+2].name != 'SKINNYOBJECT':
+            return (requirement + 'KO (Not a SkinnyObject frame)', None)
+        if self.fits[fits_index+3].name != 'OBJECTCALCOLOR':
+            return (requirement + 'KO (Not a ObjectCalColor frame)', None)
+        # Check ImageMeta values (!We get imageID from the FITS file
+        # since they are not sorted)
+        imageID = self.fits[fits_index].data.field('imageID')[0]
+        query = 'SELECT * FROM ImageMeta WHERE imageID = %s' % str(imageID)
+        answer = self.psi_inquisitor.query(query)
+        (message, status) = match(self.fits[fits_index].data, answer, requirement)
+        if status != 0:
+            return (message + ' (frame %02d - %s)' % (ota_index, query), None)
+        report.write(message)
+        # Extract the number of detections
+        nDetect = int(self.fits[fits_index].data.field('nDetect')[0])
+        # Check if we have the same number from PSI
+        query = 'SELECT count(*) AS HowMany FROM Detection WHERE imageID = %s' % str(imageID)
+        howMany = self.psi_inquisitor.query(query)['items'][0]['HowMany']
+        if nDetect != howMany:
+            report.write('\nGot %d Detection items from PSI instead of %d for image %s'
+                         % (howMany, nDetect, str(imageID)) )
+        # Get the nDetect detections (they are assumed to be sorted in the FITS file)
+        query = 'SELECT * FROM Detection WHERE imageID = %s ORDER BY detectID' % str(imageID)
+        answer = self.psi_inquisitor.query(query)
+        for detection_index in range(nDetect):
+            (message, status) = match(self.fits[fits_index+1].data, answer, 
+                                      requirement, detection_index)
+            report.write('\n' + message)
+        return (report.getvalue() + ' (frame %02d - %s)' % (ota_index, query), None)
+
+    def test_frame_meta(self):
+        """
+        Tests the contents of the FrameMeta table. 
+        - 48 columns x 1 row
+        - This table contains values that are useful, namely: frameID,
+          nOTA (number of OTA).
+        - The data can be found in PSI using the query:
+                   SELECT * FROM FrameMeta WHERE frameID = <frameID>;
+        """
+        requirement = 'PSDC-940-006-01, 3.6.6.8: '
+        if self.fits[1].header['NAXIS'] != 2:
+            return (requirement + 'KO (NAXIS != 2)', None)
+        if self.fits[1].header['NAXIS2'] != 1:
+            return (requirement + 'KO (NAXIS2 != 1)', None)
+        if self.fits[1].header['TFIELDS'] != 48:
+            return (requirement + 'KO (TFIELDS != 48)', None)
+        frameID = self.fits[1].data.field('frameID')[0]
+        query = "SELECT * FROM FrameMeta WHERE frameID = %d" % frameID
+        answer = self.psi_inquisitor.query(query)
+        (message, status) = match(self.fits[1].data, answer, requirement)
+        if status != 0:
+            return (message, None)
+        else:
+            return (message, 
+                    {'frameID': int(self.fits[1].data.field('frameID')[0]),
+                     'nOTA': int(self.fits[1].data.field('nOTA')[0]) })
+    
+    def test_primary(self):
+        """
+        Check that the primary header i.e. fits[0] has no dimension.
+        """
+        requirement = 'PSDC-940-006-01, 3.6.3, 1): '
+        if self.fits[0].header['NAXIS'] != 0:
+            return (requirement + 'KO', None)
+        # Add more tests?
+        return (requirement + 'OK', None)
+
+if __name__ == '__main__':
+    import doctest
+    doctest.testmod()
Index: /branches/sc_branches/psps_testing/testers/fits_file.py
===================================================================
--- /branches/sc_branches/psps_testing/testers/fits_file.py	(revision 29139)
+++ /branches/sc_branches/psps_testing/testers/fits_file.py	(revision 29140)
@@ -40,5 +40,6 @@
         FileManipulation.system('/bin/rm -rf %s' % FitsFileTester._TMPDIR)
 
-    def __init__(self, fitsname, basename, tempDir, fitsType):
+    def __init__(self, fitsname, basename, tempDir, fitsType, 
+                 psi_configuration = 'psi.web01_configuration'):
         """
         Creates an instance of a FitsFileTester
@@ -58,4 +59,5 @@
         self.fitsType = fitsType
         self.fitsFile = pyfits.open(self.filename)
+        self.psi_configuration = psi_configuration
 
     def __del__(self):
@@ -90,5 +92,5 @@
         """
         if self.fitsType == "P2":
-            return P2FitsTester(self.fitsFile).test()
+            return P2FitsTester(self.fitsFile, self.psi_configuration).test()
         else:
             raise Exception("Unsupported FITS type [%s]" % self.fitsType)
Index: /branches/sc_branches/psps_testing/utilities/psps_logger.py
===================================================================
--- /branches/sc_branches/psps_testing/utilities/psps_logger.py	(revision 29140)
+++ /branches/sc_branches/psps_testing/utilities/psps_logger.py	(revision 29140)
@@ -0,0 +1,144 @@
+import logging
+import sys
+
+class PsPsLogger:
+    """
+    Logging facility for PSPS Testing framework.
+    The Singleton pattern is taken from 
+    http://code.activestate.com/recipes/52558-the-singleton-pattern-implemented-with-python/
+
+    Check PsPsLogger.__init__() to see how it is used.
+    """
+    datefmt = '%H:%M:%S'
+    fmt = '[%(asctime)8s/@%(filename)s/%(funcName)s()/%(lineno)03d %(levelname)5s] %(message)s'
+
+    class __impl(logging.Logger):
+        """ Implementation of the singleton interface """
+        def __init__(self):
+            """Sets logging up"""
+            logging.Logger.__init__(self, __name__)
+            logging.basicConfig(level=logging.INFO, format=PsPsLogger.fmt)
+            try:
+                handler = logging.NullHandler()
+            except AttributeError: # logging.NullHandler only python >= 2.7
+                handler = logging.StreamHandler(open('/dev/null', 'w'))
+            handler.setFormatter(logging.Formatter(PsPsLogger.fmt,
+                                                   PsPsLogger.datefmt))
+            self.addHandler(handler)
+        def id(self):
+            """ Test method, return singleton id """
+            return id(self)
+
+    # storage for the instance reference
+    __instance = None
+
+    def __init__(self):
+        """ 
+        Creates singleton instance.
+
+        >>> logger1 = PsPsLogger()
+        >>> logger1.debug('This message is not displayed')
+        >>> logger2 = PsPsLogger()
+        >>> logger1.id() == logger2.id()
+        True
+        >>> logger1.setLevel(logging.DEBUG)
+        >>> logger1.warning('This message is not displayed')
+        >>> logger1.setLevel(logging.INFO)
+        >>> PsPsLogger().debug('This message is not displayed')
+        >>> logger1.setLevel(logging.DEBUG)
+        >>> PsPsLogger().debug('This message is not displayed')
+        """
+        # Check whether we already have an instance
+        if PsPsLogger.__instance is None:
+            # Create and remember instance
+            PsPsLogger.__instance = PsPsLogger.__impl()
+        # Store instance reference as the only member in the handle
+        self.__dict__['_PsPsLogger__instance'] = PsPsLogger.__instance
+
+    def __getattr__(self, attr):
+        """ Delegate access to implementation """
+        return getattr(self.__instance, attr)
+
+    def __setattr__(self, attr, value):
+        """ Delegate access to implementation """
+        return setattr(self.__instance, attr, value)
+
+    def set_stdout(self, fmt = None, datefmt = None):
+        """
+        Set up the logging to sys.stdout
+        >>> logger = PsPsLogger()
+        >>> logger.set_stdout()
+        >>> logger.setLevel(logging.DEBUG)
+        >>> logger.test()
+        >>> logger.setLevel(logging.INFO)
+        >>> logger.test()
+        >>> logger.setLevel(logging.WARNING)
+        >>> logger.test()
+        >>> logger.setLevel(logging.ERROR)
+        >>> logger.test()
+        >>> logger.setLevel(logging.CRITICAL)
+        >>> logger.test()
+        """
+        self.set_fileout('/dev/stdout', fmt, datefmt)
+
+    def set_stderr(self, fmt = None, datefmt = None):
+        """
+        Set up the logging to sys.stderr
+        >>> logger = PsPsLogger()
+        >>> logger.set_stderr()
+        >>> logger.setLevel(logging.DEBUG)
+        >>> logger.test()
+        >>> logger.setLevel(logging.INFO)
+        >>> logger.test()
+        >>> logger.setLevel(logging.WARNING)
+        >>> logger.test()
+        >>> logger.setLevel(logging.ERROR)
+        >>> logger.test()
+        >>> logger.setLevel(logging.CRITICAL)
+        >>> logger.test()
+        """
+        self.set_fileout('/dev/stderr', fmt, datefmt)
+
+    def set_fileout(self, filename, fmt = None, datefmt = None):
+        """ Set up the logging to some file """
+        if fmt == None:
+            fmt = PsPsLogger.fmt
+        if datefmt == None:
+            datefmt = PsPsLogger.datefmt
+        handler = logging.StreamHandler(open(filename, 'w'))
+        handler.setFormatter(logging.Formatter(fmt, datefmt))
+        self.addHandler(handler)
+
+    def test(self):
+        self.debug('A debug message')
+        self.info('Some information')
+        self.warning('A warning')
+        self.error('An error')
+        self.critical('The last is critical')
+
+if __name__ == '__main__':
+    if len(sys.argv)<2:
+        import doctest
+        doctest.testmod()
+        sys.exit(0)
+    # if sys.argv[1] == 'testfile':
+    #     print 'Testing for %s' % sys.argv[2]
+    #     PsPsLogger().set_fileout(sys.argv[2], 
+    #                              '[%(asctime)8s %(levelname)5s] %(message)s', '%H:%M:%S')
+    #     PsPsLogger().setLevel(logging.INFO)
+    #     logger = PsPsLogger()
+    #     logger.setLevel(logging.DEBUG)
+    #     print '5 messages'
+    #     logger.test()
+    #     logger.setLevel(logging.INFO)
+    #     print '4 messages'
+    #     logger.test()
+    #     logger.setLevel(logging.WARNING)
+    #     print '3 messages'
+    #     logger.test()
+    #     logger.setLevel(logging.ERROR)
+    #     print '2 messages'
+    #     logger.test()
+    #     print '1 messages'
+    #     logger.setLevel(logging.CRITICAL)
+    #     logger.test()
Index: /branches/sc_branches/psps_testing/utilities/util.py
===================================================================
--- /branches/sc_branches/psps_testing/utilities/util.py	(revision 29140)
+++ /branches/sc_branches/psps_testing/utilities/util.py	(revision 29140)
@@ -0,0 +1,141 @@
+from StringIO import StringIO
+import numpy
+
+def equal(a, b, tolerance = 1.e-5):
+    """
+    Compares two floating-point numbers and consider them equal if
+    their absolute difference is less than the tolerance.
+
+    >>> equal(1., 2., 10.)
+    True
+    >>> equal(1., 2.)
+    False
+    >>> a = 1.
+    >>> # b is 10 times 0.1
+    >>> b = 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1
+    >>> equal(a, b)
+    True
+    >>> equal(a, b, 1.e-15)
+    True
+    >>> equal(a, b, 1.e-16)
+    False
+    """
+    try:
+        if a.__class__ != b.__class__:
+            # print 'Warning! Comparing different types: %s and %s' % (a.__class__.__name__, b.__class__.__name__)
+            b = a.__class__(b)
+        return abs(a-b)<tolerance
+    except TypeError:
+        return a == b
+
+def match(fits_data, psi_answer, requirement, tolerance = 1e-5, fits_index=0):
+    """
+    Compares the values of data contained in a FITS file and those
+    from PSI up to a defined tolerance for the floating-point values.
+
+    >>> class DummyFits:
+    ...   def __init__(self):
+    ...      self._fields = {}
+    ...   def field(self, key):
+    ...      return self._fields[key]
+    >>> fits = DummyFits()
+    >>> fits._fields['key'] = ['value']
+    >>> answer = {}
+    >>> answer['items'] = [{}]
+    >>> answer['items'][0]['key'] = 'value'
+    >>> (message, status) = match(fits, answer, 'Req: ')
+    >>> print message
+    Req: OK
+    >>> print status
+    0
+
+    >>> answer['items'][0]['key'] = 'VALUE'
+    >>> (message, status) = match(fits, answer, 'Req: ')
+    >>> print message
+    Req: KO Different values for element key (from psi = "VALUE", from FITS = "value")
+    >>> print status
+    1
+
+    >>> fits2 = DummyFits()
+    >>> fits2._fields['sky'] = [745.895]
+    >>> answer2 = {}
+    >>> answer2['items'] = [{}]
+    >>> answer2['items'][0]['sky'] = 745.895
+    >>> (message, status) = match(fits2, answer2, 'Req: ')
+    >>> print message
+    Req: OK
+    >>> print status
+    0
+
+    >>> fits2 = DummyFits()
+    >>> fits2._fields['sky'] = [745.895]
+    >>> answer2 = {}
+    >>> answer2['items'] = [{}]
+    >>> answer2['items'][0]['sky'] = 745.896
+    >>> (message, status) = match(fits2, answer2, 'Req: ')
+    >>> print message
+    Req: KO Different values for element sky (from psi = "745.896", from FITS = "745.895")
+    >>> print status
+    1
+
+    >>> fits2._fields['sky2'] = [745.895]
+    >>> answer2['items'][0]['sky2'] = 745.896
+    >>> (message, status) = match(fits2, answer2, 'Req: ')
+    >>> print message
+    Req: KO Different values for element sky2 (from psi = "745.896", from FITS = "745.895")
+    Req: KO Different values for element sky (from psi = "745.896", from FITS = "745.895")
+    >>> print status
+    1
+    """
+    status = 0
+    report = StringIO()
+    for items in psi_answer['items']:
+        for (key, value) in items.items():
+            same = (fits_data.field(str(key))[fits_index] == value)
+            if not same and not isinstance(value, str):
+                same = equal(fits_data.field(str(key))[0], value)
+            if not same:
+                if status == 1:
+                    report.write('\n')
+                report.write('%sKO Different values for element %s (from psi = "%s", from FITS = "%s")' % (requirement, key, str(value), str(fits_data.field(key)[fits_index])))
+                status = 1
+    if status == 0:
+        report.write(requirement + 'OK')
+    return (report.getvalue(), status)
+
+def convert(type, value):
+    """
+    >>> a = convert('Float', '55163.4545890027')
+    >>> b = 55163.454589
+    >>> equal(a,b)
+    True
+    >>> a = convert('Real', '55163.4545890027')
+    >>> b = 55163.454589
+    >>> equal(a,b)
+    False
+    >>> equal(convert('String', 'Dummy'), 'Dummy')
+    True
+    >>> equal(convert('String', 'Dummy'), 'Dumy')
+    False
+    >>> convert('UnsupportedType', '45')
+    Traceback (most recent call last):
+    ...
+    Exception: Type 'UnsupportedType' is not supported
+    """
+    if type == 'String':
+        return str(value).replace('"', '')
+    elif type == 'Integer':
+        return numpy.int64(value)
+    elif type == 'Byte':
+        return numpy.int8(value)
+    elif type == 'Real':
+        return numpy.float32(value)
+    elif type == 'Float':
+        return numpy.float64(value)
+    elif type == 'DateTime':
+        return str(value)
+    raise Exception('Type \'%s\' is not supported' % type)
+
+if __name__ == '__main__':
+    import doctest
+    doctest.testmod()
