Index: /branches/sc_branches/psps_testing/build.py
===================================================================
--- /branches/sc_branches/psps_testing/build.py	(revision 29226)
+++ /branches/sc_branches/psps_testing/build.py	(revision 29227)
@@ -1,14 +1,16 @@
 #!/usr/bin/python
 
+from utilities.psps_logger import PsPsLogger
+import logging
+
 # Setup logging
-import logging
-datefmt = '%H:%M:%S'
-fmt = '[%(asctime)8s/@%(filename)s/%(funcName)s()/%(lineno)03d %(levelname)5s] %(message)s'
-logging.basicConfig(level=logging.INFO, format=fmt)
-logger = logging.Logger("main")
-handler = logging.StreamHandler()
-handler.setFormatter(logging.Formatter(fmt, datefmt))
-logger.addHandler(handler)
-logger.setLevel(logging.INFO)
+# datefmt = '%H:%M:%S'
+# fmt = '[%(asctime)8s/@%(filename)s/%(funcName)s()/%(lineno)03d %(levelname)5s] %(message)s'
+# logging.basicConfig(level=logging.INFO, format=fmt)
+# logger = logging.Logger("main")
+# handler = logging.StreamHandler()
+# handler.setFormatter(logging.Formatter(fmt, datefmt))
+# logger.addHandler(handler)
+# logger.setLevel(logging.INFO)
 
 #
@@ -16,17 +18,42 @@
 
 FILES_TO_CHECK = [ 'data/samples/B00029152.tar.gz',
-                   'data/samples/B00030978.tar.gz',
-                   'data/samples/B00031481.tar.gz',
-                   'data/samples/B00032090.tar.gz',
-                   'data/samples/B00032701.tar.gz',
-                   'data/samples/B00033107.tar.gz',
-                   'data/samples/B00033820.tar.gz',
-                   'data/samples/B00034120.tar.gz',
-                   'data/samples/B00034574.tar.gz',
-                   'data/samples/B00035357.tar.gz',
+                   # 'data/samples/B00030978.tar.gz',
+                   # 'data/samples/B00031481.tar.gz',
+                   # 'data/samples/B00032090.tar.gz',
+                   # 'data/samples/B00032701.tar.gz',
+                   # 'data/samples/B00033107.tar.gz',
+                   # 'data/samples/B00033820.tar.gz',
+                   # 'data/samples/B00034120.tar.gz',
+                   # 'data/samples/B00034574.tar.gz',
+                   # 'data/samples/B00035357.tar.gz',
                    ]
 
 if __name__ == '__main__':
-    for file_to_check in FILES_TO_CHECK:
-        logger.info('Testing for entry %s' % (file_to_check))
-        print BatchFileTester(file_to_check).test()
+    import sys
+    current_argument_position = 1
+    testMode = False
+    PsPsLogger.setLevel(logging.INFO)
+    PsPsLogger.set_stdout()
+    while current_argument_position < len(sys.argv):
+        if sys.argv[current_argument_position] == '-v':
+            PsPsLogger.setLevel(logging.DEBUG)
+            current_argument_position += 1
+        elif sys.argv[current_argument_position] == '-o':
+            PsPsLogger.set_fileout(sys.argv[current_argument_position+1])
+            current_argument_position += 2
+        elif sys.argv[current_argument_position] == 'test':
+            testMode = True
+            current_argument_position += 1
+        else:
+            filename = sys.argv[current_argument_position]
+            current_argument_position += 1
+    PsPsLogger.info('Command line was: [%s]' % ' '.join(sys.argv))
+
+    if testMode:
+        for file_to_check in FILES_TO_CHECK:
+            PsPsLogger.info('Testing for entry %s' % (file_to_check))
+            print BatchFileTester(file_to_check).test().success
+    else:
+            PsPsLogger.info('Testing for file: [%s]' % (filename))
+            print BatchFileTester(filename).test().success
+        
Index: /branches/sc_branches/psps_testing/psi/psi_inquisitor.py
===================================================================
--- /branches/sc_branches/psps_testing/psi/psi_inquisitor.py	(revision 29226)
+++ /branches/sc_branches/psps_testing/psi/psi_inquisitor.py	(revision 29227)
@@ -2,6 +2,8 @@
 from suds.xsd.doctor import Import, ImportDoctor
 from suds import sudsobject
-from utilities.util import convert
+from utilities.util import psi_convert
 import unicodedata
+from utilities.psps_logger import PsPsLogger
+from utilities.test_report import TestReport
 
 class PsiInquisitor:
@@ -24,6 +26,9 @@
         # exec 'from %s import Configuration' % configuration_file
         # self.configuration = Configuration
+        PsPsLogger.debug('Creating PsiInquisitor from %s file' % configuration_file)
         self.configuration = __import__(configuration_file, globals(), locals(), 
-                                        ['Configuration'], -1).Configuration
+                                        ['Configuration'], -1).PsiConfiguration
+        TestReport.info('PsiInquisitor created from %s' % configuration_file)
+        TestReport.info(self.configuration.str())
         self.authClient = Client(self.configuration.authUrl)
         self.sessionID = self.authClient.service.login(self.configuration.userID, 
@@ -39,6 +44,6 @@
         Make a query in the fast queue.
         
+        >>> psiInquisitor = PsiInquisitor('psi.web01_configuration')
         >>> query = 'select frameID from FrameMeta where frameID=105439;'
-        >>> psiInquisitor = PsiInquisitor('psi.web01_configuration')
         >>> result = psiInquisitor.query(query)
         >>> # we cannot guarantee the order in result
@@ -49,7 +54,54 @@
         >>> print result['types']
         {'[frameID]': 'Integer'}
+
+        >>> query = 'select surveyID from FrameMeta where frameID=105439;'
+        >>> result = psiInquisitor.query(query)
+        >>> # we cannot guarantee the order in result
+        >>> print result['items']
+        [{'surveyID': 0}]
+        >>> print result['fields']
+        ['[surveyID]']
+        >>> print result['types']
+        {'[surveyID]': 'Byte'}
+
+        >>> query = 'select cameraID from FrameMeta where frameID=105439;'
+        >>> result = psiInquisitor.query(query)
+        >>> # we cannot guarantee the order in result
+        >>> print result['items']
+        [{'cameraID': 1}]
+        >>> print result['fields']
+        ['[cameraID]']
+        >>> print result['types']
+        {'[cameraID]': 'Integer'}
+        >>> # It should be something expressing that it is a 16-bits integer
+
+        >>> # REAL 4 bytes
+        >>> query = 'select photoScat from FrameMeta where frameID=105439;'
+        >>> result = psiInquisitor.query(query)
+        >>> # we cannot guarantee the order in result
+        >>> print result['items']
+        [{'photoScat': 0.051822260000000002}]
+        >>> print result['fields']
+        ['[photoScat]']
+        >>> print result['types']
+        {'[photoScat]': 'Float'}
+        >>> # There should be sthg to make the difference between f32 and f64
+
+        >>> # FLOAT 8 bytes
+        >>> query = 'select expStart from FrameMeta where frameID=105439;'
+        >>> result = psiInquisitor.query(query)
+        >>> # we cannot guarantee the order in result
+        >>> print result['items']
+        [{'expStart': 55163.4545890027}]
+        >>> print result['fields']
+        ['[expStart]']
+        >>> print result['types']
+        {'[expStart]': 'Float'}
+        >>> # There should be sthg to make the difference between f32 and f64
+
         >>> # This test has to be completed with exhaustive ones...
         >>> # e.g. unit tests against the csr data base.
         """
+        PsPsLogger.debug('Querying PSI: [%s]' % sql_query)
         results = self.jobsClient.service.executeQuickJob(self.sessionID, 
                                                           self.configuration.schemaGroup,
@@ -94,10 +146,14 @@
                                                                '').replace(']', 
                                                                            '')
-                    element[field_name] = convert(types_dictionary[contents['fields'][i]],
+                    element[field_name] = psi_convert(types_dictionary[contents['fields'][i]],
                                                   values[i])
                 contents['items'].append(element)
+        PsPsLogger.debug('Number of answers: %d' % len(contents['items']))
         return contents
 
 if __name__ == '__main__':
+    import logging
+    PsPsLogger.set_stderr()
+    PsPsLogger.setLevel(logging.DEBUG)
     import doctest
     doctest.testmod()
Index: /branches/sc_branches/psps_testing/psi/web01_configuration.py
===================================================================
--- /branches/sc_branches/psps_testing/psi/web01_configuration.py	(revision 29226)
+++ /branches/sc_branches/psps_testing/psi/web01_configuration.py	(revision 29227)
@@ -1,11 +1,41 @@
-class Configuration:
+from utilities.psps_logger import PsPsLogger
+
+class PsiConfiguration:
     """
     The configuration described as a class...
     """
     server = 'http://web01.psps.ifa.hawaii.edu/'
-    authUrl = server + 'DFetch/WSDL/AuthService.php.wsdl'
     userID = 'schastel'
     password = 'd1str1ct13'
     schemaGroup = 'PS1_SCHEMA';
     context     = 'PS1 3PI';
+    authUrl = server + 'DFetch/WSDL/AuthService.php.wsdl'
     jobsUrl = server + 'DFetch/WSDL/JobsService.php.wsdl'
+
+    @staticmethod
+    def str():
+        """
+        >>> print PsiConfiguration.str()
+        PsiConfiguration
+          Server: http://web01.psps.ifa.hawaii.edu/
+          User: schastel
+          Password: d1str1ct13
+          SchemaGroup: PS1_SCHEMA
+          Context: PS1 3PI
+          AuthUrl: http://web01.psps.ifa.hawaii.edu/DFetch/WSDL/AuthService.php.wsdl
+          JobsUrl: http://web01.psps.ifa.hawaii.edu/DFetch/WSDL/JobsService.php.wsdl
+        """
+        return """Configuration
+  Server: %s
+  User: %s
+  Password: %s
+  SchemaGroup: %s
+  Context: %s
+  AuthUrl: %s
+  JobsUrl: %s""" % (PsiConfiguration.server, 
+                    PsiConfiguration.userID,
+                    PsiConfiguration.password,
+                    PsiConfiguration.schemaGroup,
+                    PsiConfiguration.context,
+                    PsiConfiguration.authUrl,
+                    PsiConfiguration.jobsUrl)
Index: /branches/sc_branches/psps_testing/testers/batch_file.py
===================================================================
--- /branches/sc_branches/psps_testing/testers/batch_file.py	(revision 29226)
+++ /branches/sc_branches/psps_testing/testers/batch_file.py	(revision 29227)
@@ -1,4 +1,2 @@
-# Strings managed as files
-from StringIO import StringIO
 # Regular expressions
 import re
@@ -7,4 +5,7 @@
 import os
 from utilities.file_manipulation import FileManipulation
+from utilities.test_report import TestReport
+from utilities.test_product import TestProduct
+# Sub-tests
 from testers.batch_manifest_file import BatchManifestFileTester
 from testers.fits_file import FitsFileTester
@@ -13,4 +14,6 @@
 # Global definitions
 from utilities.configuration import Configuration
+# Logging
+from utilities.psps_logger import PsPsLogger
 
 class BatchFileTester:
@@ -22,5 +25,5 @@
       of the files contained in the archive
 
-    >>> print BatchFileTester("data/psut/ok/B00029152.tar.gz").test()
+    >>> print BatchFileTester("data/psut/ok/B00000010.tar.gz").test()
     PSDC-940-006-01, 3.4.1: OK
     PSDC-940-006-01, 3.4: OK
@@ -42,8 +45,12 @@
         dummy/
         """
+        PsPsLogger.debug('Creating BatchFileTester object with filename = [%s]' % filename)
+        TestReport.info('Tests performed on file [%s]' % filename)
         self._filename = filename
         self.expected_basename = os.path.basename(self._filename
                                                   ).replace(Configuration.EXTENSION, 
                                                             '') + '/'
+        PsPsLogger.debug(' BatchFileTester object expected basename = [%s]' 
+                           % self.expected_basename)
 
     def test(self, start_dependant_tests = True):
@@ -52,17 +59,21 @@
         tests are succesful.
         """
-        report = StringIO()
-        testsOk = True
-        (message, status) = self._test_file_name("PSDC-940-006-01, 3.4.1")
-        report.write(message)
-        testsOk = testsOk and status
-        (message, status) = self._test_file_format("PSDC-940-006-01, 3.4")
-        report.write('\n' + message)
-        testsOk = testsOk and status
-        # Now runs the dependant tests
-        if testsOk and start_dependant_tests:
-            # Uncompress the archive (since it's valid) to some temp dir
+        PsPsLogger.debug('Starting BatchFileTester object test')
+        TestReport.title('Batch file tests')
+        # Set up global test product
+        product = TestProduct()
+        product.success = True
+        # Subtest
+        subproduct = self._test_file_name("PSDC-940-006-01, 3.4.1")
+        product.success =  product.success and subproduct.success
+        # Subtest
+        subproduct = self._test_file_format("PSDC-940-006-01, 3.4")
+        product.success =  product.success and subproduct.success
+        # Now runs the dependent tests
+        if product.success and start_dependant_tests:
+            # Uncompress the archive (since it is valid) to some temp dir
             tempDir = tempfile.mkdtemp()
             command = '/bin/tar xvfz %s -C %s' % (self._filename, tempDir)
+            PsPsLogger.debug('Unpacking data (%s)' % command)
             (status,
              child_stdout,
@@ -71,15 +82,23 @@
             batchManifestFileTester = BatchManifestFileTester(self.expected_basename,
                                                               tempDir)
-            (_report, fits_info) = batchManifestFileTester.test()
-            report.write('\n' + _report)
-            # Run the tests on the fits file
-            fitsFileTester = FitsFileTester(fits_info['filename'], 
-                                            self.expected_basename, tempDir,
-                                            fits_info['filetype'])
-            (_report, info) = fitsFileTester.test()
-            report.write('\n' + _report)
+            subproduct = batchManifestFileTester.test()
+            product.success =  product.success and subproduct.success
+            if not product.success:
+                FileManipulation.delete_directory(tempDir)
+                return product
+            # Run the tests on the fits file (subproduct.filename and
+            # subproduct.filetype are supposed to convey such
+            # information)
+            fitsFileTester = FitsFileTester(subproduct.filename, 
+                                            self.expected_basename, 
+                                            tempDir,
+                                            subproduct.filetype)
+            subproduct = fitsFileTester.test()
+            product.success =  product.success and subproduct.success
             # End of test... Delete the temporary directory
             FileManipulation.delete_directory(tempDir)
-        return report.getvalue()
+        else:
+            PsPsLogger.debug('Dependant tests not run (condition on upstream tests is %s and start_dependant_tests variable is %s)' % (str(testsOk), str(start_dependant_tests)))
+        return product
 
     def _test_file_name(self, requirement):
@@ -94,38 +113,41 @@
 
         >>> batchFileTester = BatchFileTester('B12345678.tar.gz')
-        >>> print batchFileTester._test_file_name("REQ_ID")
-        ('REQ_ID: OK', True)
+        >>> print batchFileTester._test_file_name("(UNIT_TEST_SAMPLE_REQ)").success
+        True
 
         >>> batchFileTester = BatchFileTester('B1234567.tar.gz')
-        >>> print batchFileTester._test_file_name("REQ_ID")
-        ("REQ_ID: KO (invalid archive file basename 'B1234567.tar.gz')", False)
+        >>> print batchFileTester._test_file_name("(UNIT_TEST_SAMPLE_REQ)").success
+        False
 
         >>> batchFileTester = BatchFileTester('B123456789.tar.gz')
-        >>> print batchFileTester._test_file_name("REQ_ID")
-        ("REQ_ID: KO (invalid archive file basename 'B123456789.tar.gz')", False)
+        >>> print batchFileTester._test_file_name("(UNIT_TEST_SAMPLE_REQ)").success
+        False
 
         >>> batchFileTester = BatchFileTester('B123a5678.tar.gz')
-        >>> print batchFileTester._test_file_name("REQ_ID")
-        ("REQ_ID: KO (invalid archive file basename 'B123a5678.tar.gz')", False)
+        >>> print batchFileTester._test_file_name("(UNIT_TEST_SAMPLE_REQ)").success
+        False
 
         >>> batchFileTester = BatchFileTester('b12345678.tar.gz')
-        >>> print batchFileTester._test_file_name("REQ_ID")
-        ("REQ_ID: KO (invalid archive file basename 'b12345678.tar.gz')", False)
+        >>> print batchFileTester._test_file_name("(UNIT_TEST_SAMPLE_REQ)").success
+        False
 
         >>> batchFileTester = BatchFileTester('B12345678.tgz')
-        >>> print batchFileTester._test_file_name("REQ_ID")
-        ("REQ_ID: KO (invalid archive file basename 'B12345678.tgz')", False)
+        >>> print batchFileTester._test_file_name("(UNIT_TEST_SAMPLE_REQ)").success
+        False
         
         >>> batchFileTester = BatchFileTester('B12345678.TAR.GZ')
-        >>> print batchFileTester._test_file_name("REQ_ID")
-        ("REQ_ID: KO (invalid archive file basename 'B12345678.TAR.GZ')", False)
-        """
-        report = StringIO()
+        >>> print batchFileTester._test_file_name("(UNIT_TEST_SAMPLE_REQ)").success
+        False
+        """
+        PsPsLogger.debug('Running tests on file format')
         basename = os.path.basename(self._filename)
+        product = TestProduct()
         if re.match('B\d{8}\.tar.gz', basename) == None:
-            return ('%s: KO (invalid archive file basename \'%s\')' % (requirement, basename), False)
+            TestReport.ko(requirement, 
+                          'invalid archive file basename \'%s\'' % basename )
         else:
-            report.write('%s: OK' % requirement)
-        return (report.getvalue(), True)
+            TestReport.ok(requirement)
+            product.success = True
+        return product
 
     def _test_file_format(self, requirement):
@@ -152,34 +174,38 @@
               (named BatchManifest.xml) and exactly one FITS file.
 
-        >>> batchFileTester = BatchFileTester('data/psut/ok/B00029152.tar.gz')
-        >>> print batchFileTester._test_file_format("REQ_ID")
-        ('REQ_ID: OK', True)
+        >>> batchFileTester = BatchFileTester('data/psut/ok/B00000010.tar.gz')
+        >>> print batchFileTester._test_file_format("(UNIT_TEST_SAMPLE_REQ)").success
+        True
 
         >>> batchFileTester = BatchFileTester('This_file_does_not_exist.tar.gz')
-        >>> print batchFileTester._test_file_format("REQ_ID")
-        ("REQ_ID: KO (invalid tar gzipped archive file 'This_file_does_not_exist.tar.gz')", False)
+        >>> print batchFileTester._test_file_format("(UNIT_TEST_SAMPLE_REQ)").success
+        False
 
         >>> batchFileTester = BatchFileTester('data/psut/ko/B00000001.tar.gz')
-        >>> print batchFileTester._test_file_format("REQ_ID")
-        ("REQ_ID: KO (invalid directory name in archive 'data/psut/ko/B00000001.tar.gz': Got tmp/ instead of B00000001/)", False)
+        >>> print batchFileTester._test_file_format("(UNIT_TEST_SAMPLE_REQ)").success
+        False
 
         >>> batchFileTester = BatchFileTester('data/psut/ko/B00000002.tar.gz')
-        >>> print batchFileTester._test_file_format("REQ_ID")
-        ("REQ_ID: KO (missing XML Manifest file in archive 'data/psut/ko/B00000002.tar.gz')", False)
+        >>> print batchFileTester._test_file_format("(UNIT_TEST_SAMPLE_REQ)").success
+        False
 
         >>> batchFileTester = BatchFileTester('data/psut/ko/B00000003.tar.gz')
-        >>> print batchFileTester._test_file_format("REQ_ID")
-        ("REQ_ID: KO (missing FITS file in archive 'data/psut/ko/B00000003.tar.gz')", False)
+        >>> print batchFileTester._test_file_format("(UNIT_TEST_SAMPLE_REQ)").success
+        False
 
         >>> batchFileTester = BatchFileTester('data/psut/ko/B00000004.tar.gz')
-        >>> print batchFileTester._test_file_format("REQ_ID")
-        ("REQ_ID: KO (unexpected file 'B00000004/extra' in archive 'data/psut/ko/B00000004.tar.gz')", False)
-        """
+        >>> print batchFileTester._test_file_format("(UNIT_TEST_SAMPLE_REQ)").success
+        False
+        """
+        PsPsLogger.debug('Running tests on file format')
         (status,
          child_stdout,
          child_stderr) = FileManipulation.system('/bin/tar tfz ' + self._filename)
+        product = TestProduct()
         # Test 1: if the archive is not valid
         if status != 0:
-            return (requirement + ': KO (invalid tar gzipped archive file \'%s\')' % self._filename, False)
+            TestReport.ko(requirement, 
+                          'invalid tar gzipped archive file \'%s\'' % self._filename)
+            return product
         # Now check the results of the standard output which must be:
         #   <basename>/
@@ -199,5 +225,8 @@
             if status == 0:
                 if line != self.expected_basename:
-                    return (requirement + ': KO (invalid directory name in archive \'%s\': Got %s instead of %s)' % (self._filename, line, self.expected_basename), False)
+                    TestReport.ko(requirement,
+                                  'invalid directory name in archive \'%s\': Got %s instead of %s' 
+                                  % (self._filename, line, self.expected_basename))
+                    return product
                 status = 1
             elif status == 1:
@@ -207,12 +236,35 @@
                     xml_file_in_archive = True
                 else:
-                    return (requirement + ': KO (unexpected file \'%s\' in archive \'%s\')' % (line, self._filename), False)
+                    TestReport.ko(requirement, 
+                                  'unexpected file \'%s\' in archive \'%s\'' % (line, self._filename))
+                    return product
         if not xml_file_in_archive:
-            return (requirement + ': KO (missing XML Manifest file in archive \'%s\')' % (self._filename), False)
+            TestReport.ko(requirement, 
+                          'missing XML Manifest file in archive \'%s\'' % (self._filename))
+            return product
         if not fits_file_in_archive:
-            return (requirement + ': KO (missing FITS file in archive \'%s\')' % (self._filename), False)
-        return (requirement + ': OK', True)
-
+            TestReport.ko(requirement, 
+                          'missing FITS file in archive \'%s\'' % (self._filename))
+            return product
+        product.success = True
+        return product
+
+################################################################
+#
+# __main__
+#
+################################################################
 if __name__ == '__main__':
-    import doctest
-    doctest.testmod()
+    import logging
+    import sys
+    PsPsLogger.setLevel(logging.DEBUG)
+    current_argument_position = 1
+    while current_argument_position < len(sys.argv):
+        if sys.argv[current_argument_position] == '-v':
+            PsPsLogger.set_stderr()
+            current_argument_position += 1
+        elif sys.argv[current_argument_position] == 'test':
+            print 'Running unittest for BatchFileTester class'
+            import doctest
+            doctest.testmod()
+            sys.exit(0)
Index: /branches/sc_branches/psps_testing/testers/batch_manifest_file.py
===================================================================
--- /branches/sc_branches/psps_testing/testers/batch_manifest_file.py	(revision 29226)
+++ /branches/sc_branches/psps_testing/testers/batch_manifest_file.py	(revision 29227)
@@ -1,4 +1,2 @@
-# Strings managed as files
-from StringIO import StringIO
 # System call and so on
 from utilities.file_manipulation import FileManipulation
@@ -9,4 +7,8 @@
 # Load some utilities to compute md5 checksum
 import hashlib
+# Logging
+from utilities.psps_logger import PsPsLogger
+from utilities.test_report import TestReport
+from utilities.test_product import TestProduct
 
 class BatchManifestFileTester:
@@ -36,5 +38,5 @@
         Creates an instance of a BatchManifestFileTester
 
-        >>> basename = 'B00029152'
+        >>> basename = 'B00000010'
         >>> tgz_filename = 'data/psut/ok/' + basename + '.tar.gz'
         >>> # Create the temporary directory for tests
@@ -43,5 +45,5 @@
         >>> bmft = BatchManifestFileTester(basename, '.tmp')
         >>> print bmft.manifest
-        .tmp/B00029152/BatchManifest.xml
+        .tmp/B00000010/BatchManifest.xml
         >>> # Delete temporary directory
         >>> BatchManifestFileTester._remove_test_environment()
@@ -57,19 +59,20 @@
 
         >>> # Unit test: valid file
-        >>> basename = 'B00029152'
+        >>> basename = 'B00000010'
         >>> tgz_filename = 'data/psut/ok/' + basename + '.tar.gz'
         >>> # Create the temporary directory for tests
         >>> BatchManifestFileTester._setup_test_environment(basename, tgz_filename)
         >>> bmft = BatchManifestFileTester(basename, '.tmp')
-        >>> (report, fits_info) = bmft.test()
-        >>> print report
-        PSDC-940-006-01, 3.5.1.2: OK
-        PSDC-940-006-01, 3.5.1.1: OK
-        PSDC-940-006-01, 3.5.1 (file attributes/name): OK
-        PSDC-940-006-01, 3.5.1 (file attributes/size): OK
-        PSDC-940-006-01, 3.5.1 (file attributes/MD5 checksum of file): OK
-        >>> print fits_info
-        {'filetype': u'P2', 'filename': u'00105439.FITS'}
-        >>> BatchManifestFileTester._remove_test_environment()
+        >>> product = bmft.test()
+        >>> print product.success
+        True
+        >>> print product.filetype
+        P2
+        >>> print product.filename
+        00000010.FITS
+
+        # >>> print fits_info
+        # {'filetype': u'P2', 'filename': u'00000010.FITS'}
+        # >>> BatchManifestFileTester._remove_test_environment()
 
         >>> # Unit test: invalid manifest file name in archive
@@ -79,9 +82,7 @@
         >>> BatchManifestFileTester._setup_test_environment(basename, tgz_filename)
         >>> bmft = BatchManifestFileTester(basename, '.tmp')
-        >>> (report, fits_info) = bmft.test()
-        >>> print report
-        PSDC-940-006-01, 3.5.1.2: KO (No .tmp/B00000005/BatchManifest.xml file)
-        >>> print fits_info
-        None
+        >>> product = bmft.test()
+        >>> print product.success
+        False
         >>> BatchManifestFileTester._remove_test_environment()
 
@@ -92,10 +93,7 @@
         >>> BatchManifestFileTester._setup_test_environment(basename, tgz_filename)
         >>> bmft = BatchManifestFileTester(basename, '.tmp')
-        >>> (report, fits_info) = bmft.test()
-        >>> print report
-        PSDC-940-006-01, 3.5.1.2: OK
-        PSDC-940-006-01, 3.5.1.1: KO (XML validation through XSD failed: command was [/usr/bin/xmllint --schema data/xsd/BatchManifest.xsd --noout .tmp/B00000006/BatchManifest.xml])
-        >>> print fits_info
-        None
+        >>> product = bmft.test()
+        >>> print product.success
+        False
         >>> BatchManifestFileTester._remove_test_environment()
 
@@ -106,11 +104,6 @@
         >>> BatchManifestFileTester._setup_test_environment(basename, tgz_filename)
         >>> bmft = BatchManifestFileTester(basename, '.tmp')
-        >>> (report, fits_filename) = bmft.test()
-        >>> print report
-        PSDC-940-006-01, 3.5.1.2: OK
-        PSDC-940-006-01, 3.5.1.1: OK
-        PSDC-940-006-01, 3.5.1 (file attributes/name): KO (no FITS file is named '.tmp/B00000007/00105439.FITS')
-        >>> print fits_info
-        None
+        >>> print product.success
+        False
         >>> BatchManifestFileTester._remove_test_environment()
 
@@ -121,12 +114,7 @@
         >>> BatchManifestFileTester._setup_test_environment(basename, tgz_filename)
         >>> bmft = BatchManifestFileTester(basename, '.tmp')
-        >>> (report, fits_info) = bmft.test()
-        >>> print report
-        PSDC-940-006-01, 3.5.1.2: OK
-        PSDC-940-006-01, 3.5.1.1: OK
-        PSDC-940-006-01, 3.5.1 (file attributes/name): OK
-        PSDC-940-006-01, 3.5.1 (file attributes/size): KO (actual size of FITS file is '0' instead of '10336320')
-        >>> print fits_info
-        None
+        >>> product = bmft.test()
+        >>> print product.success
+        False
         >>> BatchManifestFileTester._remove_test_environment()
 
@@ -137,36 +125,32 @@
         >>> BatchManifestFileTester._setup_test_environment(basename, tgz_filename)
         >>> bmft = BatchManifestFileTester(basename, '.tmp')
-        >>> (report, fits_info) = bmft.test()
-        >>> print report
-        PSDC-940-006-01, 3.5.1.2: OK
-        PSDC-940-006-01, 3.5.1.1: OK
-        PSDC-940-006-01, 3.5.1 (file attributes/name): OK
-        PSDC-940-006-01, 3.5.1 (file attributes/size): OK
-        PSDC-940-006-01, 3.5.1 (file attributes/MD5 checksum of file): KO (actual md5sum of FITS file is 'd41d8cd98f00b204e9800998ecf8427e' instead of '852461f5e14ff6ce5f0b0edd7166b515')
-        >>> print fits_info
-        None
-        >>> BatchManifestFileTester._remove_test_environment()
-        """
-
-        report = StringIO()
+        >>> product = bmft.test()
+        >>> print product.success
+        False
+        >>> BatchManifestFileTester._remove_test_environment()
+        """
+        TestReport.title('Batch manifest file tests')
         # Check file existence and proper naming in archive
-        requirement = 'PSDC-940-006-01, 3.5.1.2: '
+        requirement = 'PSDC-940-006-01, 3.5.1.2'
+        product = TestProduct()
         if not FileManipulation.exists(self.manifest):
-            report.write(requirement + 'KO (No %s file)' % (self.manifest))
-            return (report.getvalue(), None)
+            TestReport.ko(requirement, 
+                          'No %s file' % (self.manifest))
+            return product
         else:
-            report.write(requirement + 'OK')
+            TestReport.ok(requirement)
         # Check validation of XML by XSD through xmllint
-        requirement = 'PSDC-940-006-01, 3.5.1.1: '
-        command = '/usr/bin/xmllint --schema %s --noout %s'
+        requirement = 'PSDC-940-006-01, 3.5.1.1'
+        command = '%s --schema %s --noout %s'  % (Configuration.XMLLINT,
+                                                  Configuration.XSD_FILE,
+                                                  self.manifest)
         (status,
          stdout,
-         stderr) = FileManipulation.system(command % (Configuration.XSD_FILE,
-                                                      self.manifest) )
+         stderr) = FileManipulation.system(command)
         if status != 0:
-            report.write('\n' + requirement + 'KO (XML validation through XSD failed: command was [%s])' % (command % (Configuration.XSD_FILE, self.manifest) ))
-            return (report.getvalue(), None)
-        else:
-            report.write('\n' + requirement + 'OK')
+            TestReport.ko(requirement,
+                          'XML validation through XSD failed: command was [%s]' % (command))
+            return product
+        TestReport.ok(requirement)
         # Get XML information concerning FITS file
         # Note: We are only interested in the first <file> element
@@ -174,36 +158,54 @@
         fileList = xmldoc.getElementsByTagName('file')
         # File existence
-        requirement = 'PSDC-940-006-01, 3.5.1 (file attributes/name): '
+        requirement = 'PSDC-940-006-01, 3.5.1 (file attributes/name)'
         fitsFilename = fileList[0].attributes['name'].value
         fileToLookFor = self._manifest_relative_directory + '/' + fitsFilename
         if not FileManipulation.exists(fileToLookFor):
-            report.write('\n' + requirement + 'KO (no FITS file is named \'%s\')' % (fileToLookFor))
-            return (report.getvalue(), None)
-        report.write('\n' + requirement + 'OK')
+            TestReport.ko(requirement, 
+                          'no FITS file is named \'%s\'' % (fileToLookFor))
+            return product
+        TestReport.ok(requirement)
         # File size match
-        requirement = 'PSDC-940-006-01, 3.5.1 (file attributes/size): '
+        requirement = 'PSDC-940-006-01, 3.5.1 (file attributes/size)'
         expected_size = fileList[0].attributes['bytes'].value
         actual_size = str(FileManipulation.size(fileToLookFor))
         if actual_size != expected_size:
-            report.write('\n' + requirement + 'KO (actual size of FITS file is \'%s\' instead of \'%s\')' % (actual_size, expected_size))
-            return (report.getvalue(), None)
-        report.write('\n' + requirement + 'OK')
+            TestReport.ko(requirement,
+                          'actual size of FITS file is \'%s\' instead of \'%s\'' 
+                          % (actual_size, expected_size))
+            return product
+        TestReport.ok(requirement)
         # File md5sum
-        requirement = 'PSDC-940-006-01, 3.5.1 (file attributes/MD5 checksum of file): '
+        requirement = 'PSDC-940-006-01, 3.5.1 (file attributes/MD5 checksum of file)'
         expected_md5sum = fileList[0].attributes['md5'].value
         actual_md5sum = FileManipulation.md5(fileToLookFor)
         if actual_md5sum != expected_md5sum:
-            report.write('\n' + requirement + 'KO (actual md5sum of FITS file is \'%s\' instead of \'%s\')' % (actual_md5sum, expected_md5sum))
-            return (report.getvalue(), None)
-        report.write('\n' + requirement + 'OK')
+            TestReport.ko(requirement,
+                          'actual md5sum of FITS file is \'%s\' instead of \'%s\'' 
+                          % (actual_md5sum, expected_md5sum))
+            return product
+        TestReport.ok(requirement)
         # TODO: Perform some validation of the XML content that XSD can't do
         # e.g.: minObjId < maxObjId.
         # Note: if other objects need to be provided in the output, just add them to the dictionary...
         # e.g.: return (..., {'filename': fitsFilename, 'fitsFilesize': fitsFilesize})
-        return (report.getvalue(), 
-                { 'filename': fitsFilename,
-                  'filetype': xmldoc.getElementsByTagName('manifest')[0].attributes['type'].value})
+        # Extend test product with values useful in next tests
+        product.success = True
+        product.filename = fitsFilename
+        product.filetype = xmldoc.getElementsByTagName('manifest')[0].attributes['type'].value
+        return product
 
 if __name__ == '__main__':
-    import doctest
-    doctest.testmod()
+    import logging
+    import sys
+    PsPsLogger.setLevel(logging.DEBUG)
+    current_argument_position = 1
+    while current_argument_position < len(sys.argv):
+        if sys.argv[current_argument_position] == '-v':
+            PsPsLogger.set_stderr()
+            current_argument_position += 1
+        elif sys.argv[current_argument_position] == 'test':
+            print 'Running unittest for BatchManifestFileTester class'
+            import doctest
+            doctest.testmod()
+            sys.exit(0)
Index: /branches/sc_branches/psps_testing/testers/fits/p2.py
===================================================================
--- /branches/sc_branches/psps_testing/testers/fits/p2.py	(revision 29226)
+++ /branches/sc_branches/psps_testing/testers/fits/p2.py	(revision 29227)
@@ -1,6 +1,10 @@
 from psi.psi_inquisitor import PsiInquisitor
 import pyfits
-from StringIO import StringIO
 from utilities.util import match
+# Logging
+from utilities.psps_logger import PsPsLogger
+from utilities.configuration import Configuration
+from utilities.test_report import TestReport
+from utilities.test_product import TestProduct
 
 class P2FitsTester:
@@ -14,5 +18,5 @@
 
     Note that the purpose of this class is NOT to test the FITS
-    validity (which can be checked using some pyfits options).
+    validity (which can be checked using pyfits options).
     """
     def __init__(self, fits_file, psi_configuration):
@@ -25,8 +29,9 @@
         >>> p2ft = P2FitsTester(fits_file, psi_configuration)
         """
+        PsPsLogger.debug('Creating P2FitsTester object')
         self.fits = fits_file
         self.psi_inquisitor = PsiInquisitor(psi_configuration)
 
-    def test(self):
+    def test(self, be_tolerant_for_types = False):
         """
         Tests a FITS file supposed to contain a P2 frame data set.
@@ -36,28 +41,28 @@
         >>> 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()
+        >>> product = p2ft.test(False)
+        >>> print product.success
+        False
+
+        >>> product = p2ft.test(True)
+        >>> print product.success
+        True
+        """
+        PsPsLogger.info('Testing P2FitsTester object')
         # 1. There is no image for the PrimaryHDU (at 0)
-        (messages, other) = self.test_primary()
-        report.write(messages)
+        PsPsLogger.debug('  Testing P2FitsTester object: primary')
+        product = self.test_primary()
+        if not product.success:
+            return product
         # 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,
+        PsPsLogger.debug('  Testing P2FitsTester object: frameMeta')
+        product = self.test_frame_meta(be_tolerant_for_types)
+        # Fail if there is any failure and we are type-strict
+        if not product.success and not be_tolerant_for_types:
+            return product
+        # 3. There should be 'product.nOTA' sequences of [ImageMeta,
         # Detection, SkinnyObject, ObjectCalColor] frames
-        frameID = values['frameID']
-        nOTA = values['nOTA']
+        frameID = product.frameID
+        nOTA = product.nOTA
         if len(self.fits) != 4*nOTA + 2:
             report.write('\nInvalid number of OTA: from FITS file: %d / from FrameMeta %d'
@@ -65,36 +70,54 @@
             return (report.getvalue(), None)
         for ota_index in range(nOTA):
-            (messages, values) = self.test_detections_frames(frameID, ota_index) 
+            PsPsLogger.debug('  Testing P2FitsTester object: OTA (%d/%d)'
+                               % ((ota_index+1), nOTA))
+            (messages, values) = self.test_detections_frames(frameID, ota_index, 
+                                                             be_tolerant_for_types) 
+            PsPsLogger.debug(messages)
             report.write('\n' + messages)
         return (report.getvalue(), None)
 
-    def test_detections_frames(self, frameID, ota_index):
+    #####################################################
+    def test_detections_frames(self, frameID, ota_index, be_tolerant_for_types):
         """
         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
+        """
+        # Check that the 4-frame sequence is correct
         requirement = 'PSDC-940-006-01, TBD_test_detections_frames: '
         fits_index = 4*ota_index+2
+        product = TestProduct()
+        TestReport.title('Data frames tests')
         if self.fits[fits_index].name != 'IMAGEMETA':
-            return (requirement + 'KO (Not an ImageMeta frame)', None)
+            TestReport.ko(requirement,
+                          'Not an ImageMeta frame at index %d' % fits_index)
+            return product
         if self.fits[fits_index+1].name != 'DETECTION':
-            return (requirement + 'KO (Not a Detection frame)', None)
+            TestReport.ko(requirement,
+                          'Not a Detection frame at index %d' % fits_index)
+            return product
         if self.fits[fits_index+2].name != 'SKINNYOBJECT':
-            return (requirement + 'KO (Not a SkinnyObject frame)', None)
+            TestReport.ko(requirement,
+                          'Not a SkinnyObject frame at index %d' % fits_index)
+            return product
         if self.fits[fits_index+3].name != 'OBJECTCALCOLOR':
-            return (requirement + 'KO (Not a ObjectCalColor frame)', None)
+            TestReport.ko(requirement,
+                          'Not an ObjectCalColor frame at index %d' % fits_index)
+            return product
+        TestReport.ok(requirement)
         # Check ImageMeta values (!We get imageID from the FITS file
         # since they are not sorted)
+        PsPsLogger.debug('  Checking ImageMeta values')
         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)
+        TestReport.title('ImageMeta values tests (frame %d - %s)'
+                         % (ota_index, query))
+        subproduct = match(self.fits[fits_index].data, answer, requirement,
+                           tolerance = Configuration.TOLERANCE,
+                           max_failures = Configuration.LIMIT)
+        if not subproduct.success and not be_tolerant_for_types:
+            return product
         # Extract the number of detections
         nDetect = int(self.fits[fits_index].data.field('nDetect')[0])
@@ -103,16 +126,32 @@
         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)) )
+            TestReport.ko(requirement,
+                          'Got %d Detection items from PSI instead of %d for image %s'
+                          % (howMany, nDetect, str(imageID)) )
+            return product
         # Get the nDetect detections (they are assumed to be sorted in the FITS file)
+        PsPsLogger.debug('  Checking Detection values')
         query = 'SELECT * FROM Detection WHERE imageID = %s ORDER BY detectID' % str(imageID)
         answer = self.psi_inquisitor.query(query)
+        count = 0
         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):
+            PsPsLogger.debug('  Detection index: %d out of %d'
+                               % (detection_index+1, nDetect))
+            subproduct = match(self.fits[fits_index+1].data, 
+                               answer, 
+                               requirement, 
+                               tolerance = Configuration.TOLERANCE, 
+                               fits_index = detection_index,
+                               max_failures = Configuration.LIMIT)
+            if not subproduct.success:
+                TestReport.ko(requirement,
+                              'Comparison Matching failed')
+                if not be_tolerant_for_types:
+                    return product
+        product.success = True
+        return product
+
+    #################################################
+    def test_frame_meta(self, be_tolerant_for_types):
         """
         Tests the contents of the FrameMeta table. 
@@ -124,32 +163,59 @@
         """
         requirement = 'PSDC-940-006-01, 3.6.6.8: '
+        product = TestProduct()
+        TestReport.title('MetaFrame tests')
         if self.fits[1].header['NAXIS'] != 2:
-            return (requirement + 'KO (NAXIS != 2)', None)
+            TestReport.ko(requirement, 'NAXIS != 2')
+            return product
         if self.fits[1].header['NAXIS2'] != 1:
-            return (requirement + 'KO (NAXIS2 != 1)', None)
+            TestReport.ko(requirement, 'NAXIS2 != 1')
+            return product
         if self.fits[1].header['TFIELDS'] != 48:
-            return (requirement + 'KO (TFIELDS != 48)', None)
+            TestReport.ko(requirement, 'TFIELDS != 48')
+            return product
         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]) })
+        product = match(self.fits[1].data, 
+                        answer, 
+                        requirement,
+                        max_failures = Configuration.LIMIT,
+                        tolerance = Configuration.TOLERANCE)
+        if not product.success and not be_tolerant_for_types:
+            return product
+        # Extend the product with some useful values
+        product.frameID = int(self.fits[1].data.field('frameID')[0])
+        product.nOTA = int(self.fits[1].data.field('nOTA')[0])
+        return product
     
+    #######################
     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): '
+        TestReport.title('Primary frame tests')
+        requirement = 'PSDC-940-006-01, 3.6.3, 1'
+        product = TestProduct()
         if self.fits[0].header['NAXIS'] != 0:
-            return (requirement + 'KO', None)
+            TestReport.ko(requirement, 'Dimension should be 0')
+            return product
         # Add more tests?
-        return (requirement + 'OK', None)
+        product.success = True
+        TestReport.ok(requirement)
+        return product
 
 if __name__ == '__main__':
-    import doctest
-    doctest.testmod()
+    import logging
+    import sys
+    PsPsLogger.setLevel(logging.DEBUG)
+    current_argument_position = 1
+    while current_argument_position < len(sys.argv):
+        if sys.argv[current_argument_position] == '-v':
+            PsPsLogger.set_stderr()
+            PsPsLogger.set_fileout('dummy.log')
+            current_argument_position += 1
+        elif sys.argv[current_argument_position] == 'test':
+            print 'Running unittest for P2FitsTester functions'
+            import doctest
+            doctest.testmod()
+            sys.exit(0)
Index: /branches/sc_branches/psps_testing/testers/fits_file.py
===================================================================
--- /branches/sc_branches/psps_testing/testers/fits_file.py	(revision 29226)
+++ /branches/sc_branches/psps_testing/testers/fits_file.py	(revision 29227)
@@ -2,4 +2,8 @@
 import pyfits
 from testers.fits.p2 import P2FitsTester
+# Logging
+from utilities.psps_logger import PsPsLogger
+from utilities.configuration import Configuration
+from utilities.test_report import TestReport
 
 """
@@ -8,15 +12,4 @@
 """
 class FitsFileTester:
-    class Type:
-        """
-        Definition taken from 5.2
-        """
-        UNKNOWN = {'id': 00, 'code': ''}
-        INITIALIZATION = 10
-        DETECTION = 20
-        STACK = 30
-        DIFFERENCE = 40
-        OBJECTS = 50
-
     _TMPDIR = '.tmp'
     @staticmethod
@@ -60,8 +53,9 @@
         self.fitsFile = pyfits.open(self.filename)
         self.psi_configuration = psi_configuration
+        TestReport.info(str(self.psi_configuration))
 
     def __del__(self):
         """
-        Closes the FITS file
+        Explicitly closes the FITS file
         """
         self.fitsFile.close()
@@ -71,6 +65,6 @@
         Tests the contents of the FITS file
 
-        >>> fitsname = '00105439.FITS'
-        >>> basename = 'B00029152'
+        >>> fitsname = '00000010.FITS'
+        >>> basename = 'B00000010'
         >>> tgz_filename = 'data/psut/ok/' + basename + '.tar.gz'
 
@@ -78,6 +72,7 @@
         >>> FitsFileTester._setup_test_environment(basename, tgz_filename)
         >>> fft = FitsFileTester(fitsname, basename, '.tmp', 'P2')
-        >>> print fft.test()
-        ('BARF', None)
+        >>> product = fft.test()
+        >>> print product.success
+        False
         >>> # Delete temporary directory
         >>> FitsFileTester._remove_test_environment()
@@ -92,9 +87,21 @@
         """
         if self.fitsType == "P2":
-            return P2FitsTester(self.fitsFile, self.psi_configuration).test()
+            return P2FitsTester(self.fitsFile, 
+                                self.psi_configuration).test(Configuration.BE_TYPE_TOLERANT)
         else:
             raise Exception("Unsupported FITS type [%s]" % self.fitsType)
 
 if __name__ == '__main__':
-    import doctest
-    doctest.testmod()
+    import logging
+    import sys
+    PsPsLogger.setLevel(logging.DEBUG)
+    current_argument_position = 1
+    while current_argument_position < len(sys.argv):
+        if sys.argv[current_argument_position] == '-v':
+            PsPsLogger.set_stderr()
+            current_argument_position += 1
+        elif sys.argv[current_argument_position] == 'test':
+            print 'Running unittest for FitsFileTester class'
+            import doctest
+            doctest.testmod()
+            sys.exit(0)
Index: /branches/sc_branches/psps_testing/utilities/configuration.py
===================================================================
--- /branches/sc_branches/psps_testing/utilities/configuration.py	(revision 29226)
+++ /branches/sc_branches/psps_testing/utilities/configuration.py	(revision 29227)
@@ -8,2 +8,35 @@
     # The location of the validating XSD file
     XSD_FILE = 'data/xsd/BatchManifest.xsd'
+    # xmllint path
+    XMLLINT = '/usr/bin/xmllint'
+    # Limit to numeric tests failures in match function
+    LIMIT = 15 # Can be None, 100, -1
+    # Tolerance value for floating-point numbers
+    TOLERANCE = 1.0e-5
+    # Be type tolerant (at least until all those promotions concerns
+    # are not fixed). The value is used in FitsFileTester.test()
+    BE_TYPE_TOLERANT = True
+
+    def __repr__(self):
+        from StringIO import StringIO
+        representation = StringIO()
+        representation.write('Tests Configuration Values:\n')
+        representation.write('  File extension: [%s]\n' 
+                             % Configuration.EXTENSION)
+        representation.write('  Batch manifest name: [%s]\n' 
+                             % Configuration.BATCH_MANIFEST)
+        representation.write('  XSD file: [%s]\n' 
+                             % Configuration.XSD_FILE)
+        representation.write('  XML validation tool: [%s]\n' 
+                             % Configuration.XMLLINT)
+        representation.write('  Limit for test failure in a single test item: [%d]\n' 
+                             % Configuration.LIMIT)
+        representation.write('  Tolerance for floating-point comparison: [%s]\n' 
+                             % Configuration.TOLERANCE)
+        if Configuration.BE_TYPE_TOLERANT:
+            tolerant = 'never'
+        else:
+            tolerant = 'always'
+        representation.write('  Different types comparison [%s] fails'
+                             % tolerant)
+        return representation.getvalue()
Index: /branches/sc_branches/psps_testing/utilities/psps_logger.py
===================================================================
--- /branches/sc_branches/psps_testing/utilities/psps_logger.py	(revision 29226)
+++ /branches/sc_branches/psps_testing/utilities/psps_logger.py	(revision 29227)
@@ -2,5 +2,5 @@
 import sys
 
-class PsPsLogger:
+class PsPsLoggerClass:
     """
     Logging facility for PSPS Testing framework.
@@ -8,5 +8,5 @@
     http://code.activestate.com/recipes/52558-the-singleton-pattern-implemented-with-python/
 
-    Check PsPsLogger.__init__() to see how it is used.
+    Check PsPsLoggerClass.__init__() to see how it is used.
     """
     datefmt = '%H:%M:%S'
@@ -18,11 +18,11 @@
             """Sets logging up"""
             logging.Logger.__init__(self, __name__)
-            logging.basicConfig(level=logging.INFO, format=PsPsLogger.fmt)
+            logging.basicConfig(level=logging.INFO, format=PsPsLoggerClass.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))
+            handler.setFormatter(logging.Formatter(PsPsLoggerClass.fmt,
+                                                   PsPsLoggerClass.datefmt))
             self.addHandler(handler)
         def id(self):
@@ -37,7 +37,7 @@
         Creates singleton instance.
 
-        >>> logger1 = PsPsLogger()
+        >>> logger1 = PsPsLoggerClass()
         >>> logger1.debug('This message is not displayed')
-        >>> logger2 = PsPsLogger()
+        >>> logger2 = PsPsLoggerClass()
         >>> logger1.id() == logger2.id()
         True
@@ -45,14 +45,14 @@
         >>> logger1.warning('This message is not displayed')
         >>> logger1.setLevel(logging.INFO)
-        >>> PsPsLogger().debug('This message is not displayed')
+        >>> PsPsLoggerClass().debug('This message is not displayed')
         >>> logger1.setLevel(logging.DEBUG)
-        >>> PsPsLogger().debug('This message is not displayed')
+        >>> PsPsLoggerClass().debug('This message is not displayed')
         """
         # Check whether we already have an instance
-        if PsPsLogger.__instance is None:
+        if PsPsLoggerClass.__instance is None:
             # Create and remember instance
-            PsPsLogger.__instance = PsPsLogger.__impl()
+            PsPsLoggerClass.__instance = PsPsLoggerClass.__impl()
         # Store instance reference as the only member in the handle
-        self.__dict__['_PsPsLogger__instance'] = PsPsLogger.__instance
+        self.__dict__['_PsPsLoggerClass__instance'] = PsPsLoggerClass.__instance
 
     def __getattr__(self, attr):
@@ -67,5 +67,5 @@
         """
         Set up the logging to sys.stdout
-        >>> logger = PsPsLogger()
+        >>> logger = PsPsLoggerClass()
         >>> logger.set_stdout()
         >>> logger.setLevel(logging.DEBUG)
@@ -85,5 +85,5 @@
         """
         Set up the logging to sys.stderr
-        >>> logger = PsPsLogger()
+        >>> logger = PsPsLoggerClass()
         >>> logger.set_stderr()
         >>> logger.setLevel(logging.DEBUG)
@@ -103,7 +103,7 @@
         """ Set up the logging to some file """
         if fmt == None:
-            fmt = PsPsLogger.fmt
+            fmt = PsPsLoggerClass.fmt
         if datefmt == None:
-            datefmt = PsPsLogger.datefmt
+            datefmt = PsPsLoggerClass.datefmt
         handler = logging.StreamHandler(open(filename, 'w'))
         handler.setFormatter(logging.Formatter(fmt, datefmt))
@@ -117,4 +117,6 @@
         self.critical('The last is critical')
 
+PsPsLogger = PsPsLoggerClass()
+
 if __name__ == '__main__':
     if len(sys.argv)<2:
@@ -124,8 +126,8 @@
     # if sys.argv[1] == 'testfile':
     #     print 'Testing for %s' % sys.argv[2]
-    #     PsPsLogger().set_fileout(sys.argv[2], 
+    #     PsPsLoggerClass().set_fileout(sys.argv[2], 
     #                              '[%(asctime)8s %(levelname)5s] %(message)s', '%H:%M:%S')
-    #     PsPsLogger().setLevel(logging.INFO)
-    #     logger = PsPsLogger()
+    #     PsPsLoggerClass().setLevel(logging.INFO)
+    #     logger = PsPsLoggerClass()
     #     logger.setLevel(logging.DEBUG)
     #     print '5 messages'
Index: /branches/sc_branches/psps_testing/utilities/test_product.py
===================================================================
--- /branches/sc_branches/psps_testing/utilities/test_product.py	(revision 29227)
+++ /branches/sc_branches/psps_testing/utilities/test_product.py	(revision 29227)
@@ -0,0 +1,25 @@
+
+class TestProduct:
+    """
+    The class representing the product of any test. 
+
+    Basically, has only one field, 'success' set to False. Can be
+    enriched with anything useful for following tests.
+    """
+    def __init__(self):
+        """
+        Creates a test product and set the success status to False.
+
+        >>> print TestProduct().success
+        False
+        """
+        self.success = False
+
+if __name__ == '__main__':
+    import sys
+    if len(sys.argv) == 2 and sys.argv[1]=='test':
+        print 'Running TestProduct unit tests'
+        import doctest
+        doctest.testmod()
+        sys.exit(0)
+
Index: /branches/sc_branches/psps_testing/utilities/test_report.py
===================================================================
--- /branches/sc_branches/psps_testing/utilities/test_report.py	(revision 29227)
+++ /branches/sc_branches/psps_testing/utilities/test_report.py	(revision 29227)
@@ -0,0 +1,78 @@
+import logging
+import datetime
+from utilities.configuration import Configuration
+
+class TestReportClass(logging.Logger):
+    datefmt = '%H:%M:%S'
+    fmt = '[%(asctime)8s] | %(requirement)65s | %(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=TestReportClass.fmt)
+            handler = logging.StreamHandler(open('log/test_report.txt', 'w'))
+            handler.setFormatter(logging.Formatter(TestReportClass.fmt,
+                                                   TestReportClass.datefmt))
+            self.addHandler(handler)
+        def id(self):
+            """ Test method, return singleton id """
+            return id(self)
+    # storage for the instance reference
+    __instance = None
+
+    def info(self, message):
+        for line in message.split('\n'):
+            logging.Logger.info(self,
+                                line, 
+                                extra = {'requirement': 'INFO'})
+
+    def __init__(self):
+        """ 
+        Creates singleton instance.
+        """
+        # Check whether we already have an instance
+        if TestReportClass.__instance is None:
+            # Create and remember instance
+            TestReportClass.__instance = TestReportClass.__impl()
+        # Store instance reference as the only member in the handle
+        self.__dict__['_TestReportClass__instance'] = TestReportClass.__instance
+        logging.Logger.info(self,
+                            'Test performed on ' + str(datetime.date.today()),
+                            extra = {'requirement': 'This column usually shows the "Requirement ID"'})
+        self.info(str(Configuration()))
+
+    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 test_result(self, requirement, message):
+    #     self.info(message, 
+    #               extra = {'requirement': requirement})
+    def title(self, title):
+        logging.Logger.info(self,
+                            title,
+                            extra = {'requirement': '--------------------'})
+    def ok(self, requirement):
+         logging.Logger.info(self,
+                             'OK', 
+                             extra = {'requirement': requirement})
+    def ko(self, requirement, cause):
+         logging.Logger.info(self,
+                             'KO (%s)' % cause, 
+                             extra = {'requirement': requirement})
+
+TestReport = TestReportClass()
+
+if __name__ == '__main__':
+    if len(sys.argv)<2:
+        import doctest
+        doctest.testmod()
+        sys.exit(0)
+    TestReport.ok('Req1')
+    TestReport.ko('Req2', 'A good reason for failure')
Index: /branches/sc_branches/psps_testing/utilities/util.py
===================================================================
--- /branches/sc_branches/psps_testing/utilities/util.py	(revision 29226)
+++ /branches/sc_branches/psps_testing/utilities/util.py	(revision 29227)
@@ -1,6 +1,8 @@
-from StringIO import StringIO
 import numpy
-
-def equal(a, b, tolerance = 1.e-5):
+from utilities.psps_logger import PsPsLogger
+from utilities.test_report import TestReport
+from utilities.test_product import TestProduct
+
+def equal(a, b, tolerance = 1.e-15):
     """
     Compares two floating-point numbers and consider them equal if
@@ -22,12 +24,14 @@
     """
     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):
+def match(fits_data, 
+          psi_answer, 
+          requirement, 
+          tolerance = 1e-15, 
+          fits_index=0,
+          max_failures = None):
     """
     Compares the values of data contained in a FITS file and those
@@ -44,16 +48,12 @@
     >>> answer['items'] = [{}]
     >>> answer['items'][0]['key'] = 'value'
-    >>> (message, status) = match(fits, answer, 'Req: ')
-    >>> print message
-    Req: OK
-    >>> print status
-    0
+    >>> product = match(fits, answer, 'Req: ')
+    >>> print product.success
+    True
 
     >>> 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
+    >>> product = match(fits, answer, 'Req: ')
+    >>> print product.success
+    False
 
     >>> fits2 = DummyFits()
@@ -62,9 +62,7 @@
     >>> answer2['items'] = [{}]
     >>> answer2['items'][0]['sky'] = 745.895
-    >>> (message, status) = match(fits2, answer2, 'Req: ')
-    >>> print message
-    Req: OK
-    >>> print status
-    0
+    >>> product = match(fits2, answer2, 'Req: ')
+    >>> print product.success
+    True
 
     >>> fits2 = DummyFits()
@@ -73,54 +71,116 @@
     >>> 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
+    >>> product = match(fits2, answer2, 'Req: ')
+    >>> print product.success
+    False
 
     >>> 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()
+    >>> product = match(fits2, answer2, 'Req: ')
+    >>> print product.success
+    False
+
+    >>> fits2._fields['sky2'] = [1792.05]
+    >>> answer2['items'][0]['sky2'] = 1792.054
+    >>> product = match(fits2, answer2, 'Req: ')
+    >>> print product.success
+    False
+
+    """
+    product = TestProduct()
+    first_time = True
+    complain = True
+    PsPsLogger.debug('match(): %d rows to compare' % len(psi_answer['items']))
+    items_count = 1
+    tenPercent = len(psi_answer['items'])/10
+    nextTenthPercents = int(tenPercent)
     for items in psi_answer['items']:
+        if first_time:
+            PsPsLogger.debug('match(): %d items in columns' 
+                             % (len(items.items())))
+            first_time = False
+        if items_count == nextTenthPercents:
+            PsPsLogger.debug('match(): %d out of %d' 
+                             % (items_count, 
+                                len(psi_answer['items'])))
+            nextTenthPercents += tenPercent
+        items_count += 1
+        failures_count = 0
         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)
+            fits_value = fits_data.field(str(key))[fits_index]
+            if fits_value.__class__.__name__ != value.__class__.__name__:
+                if complain:
+                    TestReport.ko(requirement,
+                                  'Classes of FITS value (%s/%s) and PSI value (%s/%s) are different for key \'%s\''
+                                  % (fits_value.__class__.__name__,
+                                     fits_value,
+                                     value.__class__.__name__,
+                                     value,
+                                     str(key)))
+                failures_count += 1
+                complain = complain and __still_complain(max_failures, failures_count)
+            psi_value = fits_value.__class__(value)
+            same = (fits_data.field(str(key))[fits_index] == psi_value)
+            if not same and not isinstance(psi_value, str):
+                same = equal(fits_data.field(str(key))[0],
+                             psi_value,
+                             tolerance)
             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
+                if complain:
+                    TestReport.ko(requirement,
+                                  'Different values for key %s: PSI = [%s] (%s) / FITS = [%s] (%s) / FITS index = %d' % 
+                                  (key, 
+                                   psi_value, 
+                                   psi_value.__class__.__name__,
+                                   fits_data.field(key)[fits_index],
+                                   fits_data.field(key)[fits_index].__class__.__name__,
+                                   fits_index))
+                failures_count += 1
+                complain = complain and __still_complain(max_failures, failures_count)
+    if failures_count == 0:
+        product.success = True
+        TestReport.ok(requirement)
+    else:
+        if not complain:
+            # Write the summary of all errors
+            TestReport.ko(requirement,
+                          'Total of %d comparison errors for match (comparison over %d rows of %d columns)'
+                          % (failures_count, 
+                             len(psi_answer['items']),
+                             len(items.items())))
+    return product
+
+def __still_complain(max_failures, failures_count):
+    if max_failures is not None and failures_count >= max_failures:
+        TestReport.ko('INFO', 'Limit of failures reached: %d (total number of errors should follow' % failures_count)
+        return False
+    return True
+
+def psi_convert(type, value):
+    """
+    >>> a = psi_convert('Float', '55163.4545890027')
+    >>> b = 55163.4545890027
+    >>> print (a, b)
+    (55163.4545890027, 55163.4545890027)
     >>> equal(a,b)
     True
-    >>> a = convert('Real', '55163.4545890027')
-    >>> b = 55163.454589
+    >>> a = psi_convert('Real', '55163.4545890027')
+    >>> b = 55163.4545890027
+    >>> print (a, b)
+    (55163.453, 55163.4545890027)
     >>> equal(a,b)
     False
-    >>> equal(convert('String', 'Dummy'), 'Dummy')
-    True
-    >>> equal(convert('String', 'Dummy'), 'Dumy')
-    False
-    >>> convert('UnsupportedType', '45')
+    >>> equal(psi_convert('String', 'Dummy'), 'Dummy')
+    True
+    >>> equal(psi_convert('String', 'Dummy'), 'Dumy')
+    False
+    >>> psi_convert('UnsupportedType', '45')
     Traceback (most recent call last):
     ...
     Exception: Type 'UnsupportedType' is not supported
     """
+    # PsPsLogger.debug('Value class: %s (%s/%s)' 
+    #                    % (value.__class__.__name__, 
+    #                       str(value), type))
     if type == 'String':
         return str(value).replace('"', '')
@@ -137,5 +197,28 @@
     raise Exception('Type \'%s\' is not supported' % type)
 
+# def weak_convert(a, b):
+#     """
+#     If 'a' has a weaker type than 'b', returns the tuple
+#     (a, a.__class__(b)), otherwise returns  the tuple
+#     (b.__class__(a), b)
+
+#     Types must be comparable:
+#     - numpy.float32 < numpy.float64
+#     - numpy.int8 < numpy.int16 < numpy.int32 < numpy.int64
+#     """
+#     if 
+
 if __name__ == '__main__':
-    import doctest
-    doctest.testmod()
+    import logging
+    import sys
+    PsPsLogger.setLevel(logging.DEBUG)
+    current_argument_position = 1
+    while current_argument_position < len(sys.argv):
+        if sys.argv[current_argument_position] == '-v':
+            PsPsLogger.set_stderr()
+            current_argument_position += 1
+        elif sys.argv[current_argument_position] == 'test':
+            print 'Running unittest for util functions'
+            import doctest
+            doctest.testmod()
+            sys.exit(0)
