Index: /branches/sc_branches/psps_testing/README
===================================================================
--- /branches/sc_branches/psps_testing/README	(revision 29437)
+++ /branches/sc_branches/psps_testing/README	(revision 29438)
@@ -1,17 +1,28 @@
-1. Testing PSPS ingestion
-=========================
-1.0 Currently:
-   Roy's 10 inputs are half(well... less than half)-tested through build.py
+This is the PSPS test framework. Currently, it allows:
+- Testing itself;
+- Testing if data ingestion is ok. See part 2;
+- That's it.
 
-1.1 Testing a valid input
-   (not yet implemented but will be)
-   psps_test.py <input.tar.gz>
-
-2. Testing the framework
-========================
-2.1. Running the unit tests:
+1. Testing the test framework
+=============================
+1.1. Running the unit tests:
 ./build.sh unittest
 
-2.2. Coverage
+1.2. Coverage
 ./build.sh coverage
 
+1.3. Documentation
+Generate it with
+./build.sh documentation
+
+2. Testing PSPS data ingestion
+==============================
+2.1 Testing a valid input
+      (not yet implemented but will be)
+    psps_test.py <input.tar.gz> <report.xml>
+    - <input.tar.gz>: IPP batch (output of ipp2psps)
+    - <report.xml>: Test report 
+      (can be converted to html and/or pdf using TODO)
+
+(currently, edit psps_test.py and replace input file name)
+
Index: /branches/sc_branches/psps_testing/build.sh
===================================================================
--- /branches/sc_branches/psps_testing/build.sh	(revision 29437)
+++ /branches/sc_branches/psps_testing/build.sh	(revision 29438)
@@ -7,5 +7,10 @@
 
 PYTHON=/usr/bin/python
+UNITTEST_LOG=unittest.log
 COVERAGE=/usr/bin/python-coverage
+DOCUMENTATION=/usr/bin/epydoc
+DOC_FOLDER=documentation
+# Check epydoc available formats
+DOC_FORMAT=html
 
 FILES="testers/__init__.py \
@@ -19,4 +24,5 @@
   utilities/file_manipulation.py \
   utilities/psps_logger.py \
+  utilities/abstract_test_report.py \
   utilities/test_report.py \
   utilities/test_product.py \
@@ -28,8 +34,23 @@
 case "$1" in
     unittest)
+	something_wrong=0
+	/bin/rm -f $UNITTEST_LOG
 	for file in $FILES; do
-	    echo "Unit test for $file ($PYTHON -m doctest $file)"
-	    $PYTHON -m doctest $file
+	    echo -n "Unit test for $file ($PYTHON -m doctest $file): "
+	    $PYTHON -m doctest $file >> $UNITTEST_LOG 2>> $UNITTEST_LOG
+	    status=$?
+	    if [ "$status" -eq "0" ]; then 
+		echo "OK"
+	    else
+		echo "KO"
+		(( something_wrong= $something_wrong + 1 ))
+	    fi
 	done
+	if [ "$something_wrong" -eq "0" ]; then 
+	    echo "All tests pass"
+	else
+	    echo "!!! $something_wrong tests failed"
+	    echo "!!! Look at $UNITTEST_LOG for details"
+	fi
 	;;
     coverage)
@@ -40,4 +61,9 @@
 	echo "### Coverage results ###"
 	$COVERAGE -r -o /usr,$HOME/local -m
+	;;
+    doc|documentation)
+	$DOCUMENTATION -o $DOC_FOLDER --$DOC_FORMAT `find . -name \*.py | grep -v weirdos | grep -v html | grep -v create_simple_fits` -v
+	echo "Documentation is at:"
+	echo "  file://`pwd`/documentation/index.html"
 	;;
     all)
@@ -51,5 +77,5 @@
 	;;
     *)
-	echo "Usage: $0 [ all | unittest | coverage]"
+	echo "Usage: $0 [ all | unittest | coverage | [doc|documentation]]"
 	exit 1
 	;;
Index: /branches/sc_branches/psps_testing/psi/psi_inquisitor.py
===================================================================
--- /branches/sc_branches/psps_testing/psi/psi_inquisitor.py	(revision 29437)
+++ /branches/sc_branches/psps_testing/psi/psi_inquisitor.py	(revision 29438)
@@ -2,8 +2,21 @@
 from suds.xsd.doctor import Import, ImportDoctor
 from suds import sudsobject
+from suds import WebFault
 from utilities.util import psi_convert
 import unicodedata
 from utilities.psps_logger import PsPsLogger
 from utilities.test_report import TestReport
+
+from suds import WebFault
+class PsiException(WebFault):
+    """
+    >>> fault = WebFault('fault', 'document')
+    >>> raise PsiException(fault)
+    Traceback (most recent call last):
+    ...
+    PsiException
+    """
+    def __init__(self, webFault):
+        WebFault.__init__(self, webFault.fault, webFault.document)
 
 class PsiInquisitor:
@@ -31,4 +44,5 @@
         TestReport.info('PsiInquisitor created from %s' % configuration_file)
         TestReport.info(self.configuration.str())
+        Client.log = PsPsLogger
         self.authClient = Client(self.configuration.authUrl)
         self.sessionID = self.authClient.service.login(self.configuration.userID, 
@@ -104,8 +118,11 @@
         """
         PsPsLogger.debug('Querying PSI: [%s]' % sql_query)
-        results = self.jobsClient.service.executeQuickJob(self.sessionID, 
-                                                          self.configuration.schemaGroup,
-                                                          sql_query, 
-                                                          self.configuration.context)
+        try:
+            results = self.jobsClient.service.executeQuickJob(self.sessionID, 
+                                                              self.configuration.schemaGroup,
+                                                              sql_query, 
+                                                              self.configuration.context)
+        except WebFault, e:
+            raise PsiException(e)
         lines = results.split('\n')
         first_line_not_seen = True
Index: /branches/sc_branches/psps_testing/psi/web01_configuration.py
===================================================================
--- /branches/sc_branches/psps_testing/psi/web01_configuration.py	(revision 29437)
+++ /branches/sc_branches/psps_testing/psi/web01_configuration.py	(revision 29438)
@@ -26,5 +26,5 @@
           JobsUrl: http://web01.psps.ifa.hawaii.edu/DFetch/WSDL/JobsService.php.wsdl
         """
-        return """Configuration
+        return """PsiConfiguration
   Server: %s
   User: %s
Index: /branches/sc_branches/psps_testing/testers/batch_file.py
===================================================================
--- /branches/sc_branches/psps_testing/testers/batch_file.py	(revision 29437)
+++ /branches/sc_branches/psps_testing/testers/batch_file.py	(revision 29438)
@@ -21,11 +21,10 @@
     The purpose of this class is to test the common features of batch
     files. Namely, they are:
-    - the form of the file name
-    - the contents of the file (a gzipped tar archive file) and the form
-      of the files contained in the archive
+      - the form of the file name
+      - the contents of the file (a gzipped tar archive file) and the form
+        of the files contained in the archive
 
     >>> print BatchFileTester("data/psut/ok/B00000010.tar.gz").test().success
-    False
-    >>> # Will be True one day...
+    True
     """
     def __init__(self, filename):
@@ -59,5 +58,5 @@
         """
         PsPsLogger.debug('Starting BatchFileTester object test')
-        TestReport.title('Batch file tests')
+        TestReport.addSection('Batch file tests')
         # Set up global test product
         product = TestProduct()
@@ -166,10 +165,12 @@
         Derived Requirement 3.3.1: The maximum size of a batch file
         shall not exceed 100 gigabytes (TBR).
-
-        -> Test 1: The file is a valid tar gzipped file
-        -> Test 2: The file uncompresses in the <basename> directory
-                where <basename> is the file base name
-        -> Test 3: The tar gzipped file contains exactly one XML file
-              (named BatchManifest.xml) and exactly one FITS file.
+        
+        Test 1: The file is a valid tar gzipped file
+
+        Test 2: The file uncompresses in the <basename> directory
+        where <basename> is the file base name
+
+        Test 3: The tar gzipped file contains exactly one XML file
+        (named BatchManifest.xml) and exactly one FITS file.
 
         >>> batchFileTester = BatchFileTester('data/psut/ok/B00000010.tar.gz')
Index: /branches/sc_branches/psps_testing/testers/batch_manifest_file.py
===================================================================
--- /branches/sc_branches/psps_testing/testers/batch_manifest_file.py	(revision 29437)
+++ /branches/sc_branches/psps_testing/testers/batch_manifest_file.py	(revision 29438)
@@ -71,5 +71,5 @@
         00000010.FITS
         """
-        TestReport.title('Batch manifest file tests')
+        TestReport.addSection('Batch manifest file tests')
         # Check batch file existence and correct naming in archive
         if not self._test_batch_file_exists().success:
Index: /branches/sc_branches/psps_testing/testers/fits/p2.py
===================================================================
--- /branches/sc_branches/psps_testing/testers/fits/p2.py	(revision 29437)
+++ /branches/sc_branches/psps_testing/testers/fits/p2.py	(revision 29438)
@@ -49,34 +49,42 @@
         True
         """
-        PsPsLogger.info('Testing P2FitsTester object')
-        # 1. There is no image for the PrimaryHDU (at 0)
-        PsPsLogger.debug('  Testing P2FitsTester object: primary')
-        product = self.test_primary()
-        if not product.success:
-            return product
-        # 2. Look at the FrameMeta information
-        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 = product.frameID
-        nOTA = product.nOTA
-        if len(self.fits) != 4*nOTA + 2:
-            TestReport.ko('Invalid number of OTA: from FITS file: %d / from FrameMeta %d'
-                          % (len(self.fits)-2/4, nOTA), 
+        try:
+            PsPsLogger.info('Testing P2FitsTester object')
+            # 1. There is no image for the PrimaryHDU (at 0)
+            PsPsLogger.debug('  Testing P2FitsTester object: primary')
+            product = self.test_primary()
+            if not product.success:
+                return product
+            # 2. Look at the FrameMeta information
+            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 = product.frameID
+            nOTA = product.nOTA
+            if len(self.fits) != 4*nOTA + 2:
+                TestReport.ko('Invalid number of OTA: from FITS file: %d / from FrameMeta %d'
+                              % (len(self.fits)-2/4, nOTA), 
+                              {'requirement': 'TBD'})
+                return TestProduct()
+            for ota_index in range(nOTA):
+                PsPsLogger.debug('  Testing P2FitsTester object: OTA (%d/%d)'
+                                 % ((ota_index+1), nOTA))
+                subproduct = self.test_detections_frames(frameID, ota_index, 
+                                                         be_tolerant_for_types)
+                if subproduct.success:
+                    TestReport.ok(requirement)
+                product.success = product.success and subproduct.success
+            return product
+        except KeyError, e:
+            PsPsLogger.error('  Exception caught!')
+            TestReport.ko('Unexpected exception',
                           {'requirement': 'TBD'})
+            TestReport.info(e)
             return TestProduct()
-        for ota_index in range(nOTA):
-            PsPsLogger.debug('  Testing P2FitsTester object: OTA (%d/%d)'
-                               % ((ota_index+1), nOTA))
-            subproduct = self.test_detections_frames(frameID, ota_index, 
-                                                     be_tolerant_for_types)
-            if subproduct.success:
-                TestReport.ok(requirement)
-            product.success = product.success and subproduct.success
-        return product
+            
 
     #####################################################
@@ -91,5 +99,5 @@
         fits_index = 4*ota_index+2
         product = TestProduct()
-        TestReport.title('Data frames tests')
+        TestReport.addSection('Data frames tests')
         if self.fits[fits_index].name != 'IMAGEMETA':
             TestReport.ko(requirement,
@@ -115,6 +123,6 @@
         query = 'SELECT * FROM ImageMeta WHERE imageID = %s' % str(imageID)
         answer = self.psi_inquisitor.query(query)
-        TestReport.title('ImageMeta values tests (frame %d - %s)'
-                         % (ota_index, query))
+        TestReport.addSection('ImageMeta values tests (frame %d - %s)'
+                              % (ota_index, query))
         subproduct = match(self.fits[fits_index].data, answer, requirement,
                            tolerance = Configuration.TOLERANCE,
@@ -158,13 +166,13 @@
         """
         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:
+          - 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: '
         product = TestProduct()
-        TestReport.title('MetaFrame tests')
+        TestReport.addSection('MetaFrame tests')
         if self.fits[1].header['NAXIS'] != 2:
             TestReport.ko(requirement, 'NAXIS != 2')
@@ -196,5 +204,5 @@
         Check that the primary header i.e. fits[0] has no dimension.
         """
-        TestReport.title('Primary frame tests')
+        TestReport.addSection('Primary frame tests')
         requirement = 'PSDC-940-006-01, 3.6.3, 1'
         product = TestProduct()
Index: /branches/sc_branches/psps_testing/utilities/abstract_test_report.py
===================================================================
--- /branches/sc_branches/psps_testing/utilities/abstract_test_report.py	(revision 29438)
+++ /branches/sc_branches/psps_testing/utilities/abstract_test_report.py	(revision 29438)
@@ -0,0 +1,285 @@
+import datetime
+from xml.dom.minidom import Document, getDOMImplementation
+
+class TestReportException(Exception):
+    def __init__(self, message):
+        Exception.__init__(self, message)
+
+class AbstractTestReport:
+    """
+    A class for tests reports.
+
+    A tests report is a XML Document.
+    """
+    DEFAULT = 'Default'
+    # storage for the instance reference
+    __instances = None
+    __domimpl = None
+    STATUS_PASS = 'OK'
+    STATUS_FAIL = 'KO'
+
+    def __init__(self, instance_name = DEFAULT):
+        """
+        Creates an instance and stores it in the __instances dictionnary.
+        >>> AbstractTestReport.clear()
+        >>> tr1 = AbstractTestReport()
+        >>> tr2 = AbstractTestReport()
+        >>> tr1.id() == tr2.id()
+        True
+        >>> tr3 = AbstractTestReport('Dummy')
+        >>> tr1.id() == tr3.id()
+        False
+        """
+        self._name = instance_name
+        # Check if we have a valid implementation for DOM and instances dictionnary
+        if AbstractTestReport.__domimpl is None:
+            AbstractTestReport.__domimpl = getDOMImplementation()
+        if AbstractTestReport.__instances is None:
+            AbstractTestReport.__instances = dict()
+        # Check if we already have an instance named instance_name
+        if instance_name not in AbstractTestReport.__instances:
+            AbstractTestReport.__instances[instance_name] = AbstractTestReport.__domimpl.createDocument(None, 
+                                                                                        'test_report',
+                                                                                        None)
+        self.current = dict()
+
+    def id(self):
+        """Unique identification of object"""
+        return id(AbstractTestReport.__instances[self._name])
+    @staticmethod
+    def clear():
+        """Clears the instances set"""
+        AbstractTestReport.__instances = None
+        AbstractTestReport.__domimpl = None
+    @staticmethod
+    def getInstancesNames():
+        return AbstractTestReport.__instances.keys()
+    @staticmethod
+    def remove(instance_name):
+        """
+        Removes an instance identified by its name
+        
+        >>> AbstractTestReport.clear()
+        >>> tr1 = AbstractTestReport('TR1')
+        >>> tr2 = AbstractTestReport('TR2')
+        >>> tr3 = AbstractTestReport('TR3')
+        >>> keys = AbstractTestReport.getInstancesNames()
+        >>> keys.sort()
+        >>> print keys
+        ['TR1', 'TR2', 'TR3']
+        >>> AbstractTestReport.remove('TR2')
+        >>> keys = AbstractTestReport.getInstancesNames()
+        >>> keys.sort()
+        >>> print keys
+        ['TR1', 'TR3']
+        >>> AbstractTestReport.remove('TR4')
+        Traceback (most recent call last):
+        ...
+        KeyError: 'TR4'
+        """
+        del AbstractTestReport.__instances[instance_name]
+
+    @staticmethod
+    def _create_text_node(node, name, value, instance_name = DEFAULT):
+        document = AbstractTestReport.__instances[instance_name]
+        new_element = document.createElement(name)
+        text = document.createTextNode(value)
+        new_element.appendChild(text)
+        try:
+            node.childNodes[-1].appendChild(new_element)
+        except IndexError:
+            node.appendChild(new_element)
+        return new_element
+
+    def setTitle(self, title, instance_name = DEFAULT):
+        """
+        >>> AbstractTestReport.clear()
+        >>> tr = AbstractTestReport()
+        >>> tr.setTitle('My title')
+        >>> print tr.toxml()
+        <?xml version="1.0" ?><test_report><title>My title</title></test_report>
+        """
+        document = AbstractTestReport.__instances[instance_name]
+        AbstractTestReport._create_text_node(document, 'title', title)
+
+    def addSection(self, title, instance_name = DEFAULT):
+        """
+        >>> AbstractTestReport.clear()
+        >>> tr = AbstractTestReport()
+        >>> tr.setTitle('My title')
+        >>> tr.addSection('First Section')
+        >>> print tr.toxml()
+        <?xml version="1.0" ?><test_report><title>My title</title><section name="First Section"/></test_report>
+        """
+        document = AbstractTestReport.__instances[instance_name]
+        section_element = document.createElement('section')
+        section_element.setAttribute('name', title)
+        document.childNodes[-1].appendChild(section_element)
+        self.current['section'] = section_element
+        self.current['last'] = section_element
+
+    def addSubsection(self, title, instance_name = DEFAULT):
+        """
+        >>> AbstractTestReport.clear()
+        >>> tr = AbstractTestReport()
+        >>> tr.setTitle('My title')
+        >>> tr.addSubsection('First Subsection')
+        Traceback (most recent call last):
+        ...
+        TestReportException: A section has to be added before creating a subsection
+        >>> tr.addSection('First Section')
+        >>> tr.addSubsection('First Subsection')
+        >>> tr.addSection('Second Section')
+        >>> tr.addSubsection('Second Subsection')
+        >>> tr.addSubsection('Third Subsection')
+        >>> print tr.toxml()
+        <?xml version="1.0" ?><test_report><title>My title</title><section name="First Section"><subsection name="First Subsection"/></section><section name="Second Section"><subsection name="Second Subsection"/><subsection name="Third Subsection"/></section></test_report>
+        """
+        try:
+            self.current['section']
+        except KeyError:
+            raise TestReportException('A section has to be added before creating a subsection')
+        document = AbstractTestReport.__instances[instance_name]
+        subsection_element = document.createElement('subsection')
+        subsection_element.setAttribute('name', title)
+        self.current['section'].appendChild(subsection_element)
+        self.current['subsection'] = subsection_element
+        self.current['last'] = subsection_element
+
+    def addSubsubsection(self, title, instance_name = DEFAULT):
+        """
+        >>> AbstractTestReport.clear()
+        >>> tr = AbstractTestReport()
+        >>> tr.setTitle('My title')
+        >>> tr.addSubsubsection('First Subsubsection')
+        Traceback (most recent call last):
+        ...
+        TestReportException: A subsection has to be added before creating a subsubsection
+        >>> tr.addSection('First Section')
+        >>> tr.addSubsection('First Subsection')
+        >>> tr.addSubsubsection('First Subsubsection')
+        >>> tr.addSection('Second Section')
+        >>> tr.addSubsection('Second Subsection')
+        >>> tr.addSubsubsection('Second Subsubsection')
+        >>> tr.addSubsection('Third Subsection')
+        >>> print tr.toxml()
+        <?xml version="1.0" ?><test_report><title>My title</title><section name="First Section"><subsection name="First Subsection"><subsubsection name="First Subsubsection"/></subsection></section><section name="Second Section"><subsection name="Second Subsection"><subsubsection name="Second Subsubsection"/></subsection><subsection name="Third Subsection"/></section></test_report>
+        """
+        try:
+            self.current['subsection']
+        except KeyError:
+            raise TestReportException('A subsection has to be added before creating a subsubsection')
+        document = AbstractTestReport.__instances[instance_name]
+        subsubsection_element = document.createElement('subsubsection')
+        subsubsection_element.setAttribute('name', title)
+        self.current['subsection'].appendChild(subsubsection_element)
+        self.current['subsubsection'] = subsubsection_element
+        self.current['last'] = subsubsection_element
+
+    def addTestResult(self, instance_name, requirement, status, cause = None):
+        """
+        See ok and ko methods
+        """
+        document = AbstractTestReport.__instances[instance_name]
+        test_element = document.createElement('test')
+        current_time = datetime.datetime.utcnow().isoformat()
+        test_element.setAttribute('when', current_time)
+        test_element.setAttribute('requirement', requirement)
+        text = AbstractTestReport._create_text_node(test_element, 'status', status)
+        if cause is not None:
+            text.setAttribute('documentation', cause)
+        try:
+            self.current['last'].appendChild(test_element)
+        except KeyError:
+            document.childNodes[-1].appendChild(test_element)
+
+    def ok(self, requirement, instance_name = DEFAULT):
+        """
+        Appends a test_result element with OK/pass status to the
+        'last' inserted level.
+
+        >>> AbstractTestReport.clear()
+        >>> tr = AbstractTestReport()
+        >>> tr.setTitle('My title')
+        >>> tr.addSection('First Section')
+        >>> tr.addSubsection('First Subsection')
+        >>> tr.addSubsubsection('First Subsubsection')
+        >>> tr.ok('Dummy req')
+        """
+        self.addTestResult(instance_name, requirement, AbstractTestReport.STATUS_PASS)
+
+    def ko(self, requirement, cause, instance_name = DEFAULT):
+        """
+        Appends a test_result element with KO/fail status to the
+        'last' inserted level.
+
+        >>> AbstractTestReport.clear()
+        >>> tr = AbstractTestReport()
+        >>> tr.setTitle('My title')
+        >>> tr.addSection('First Section')
+        >>> tr.addSubsection('First Subsection')
+        >>> tr.addSubsubsection('First Subsubsection')
+        >>> tr.ko('Failed req', 'A cause')
+        >>> tr.ko('Failed req', None)
+        """
+        self.addTestResult(instance_name, requirement, AbstractTestReport.STATUS_FAIL, cause)
+
+    def toxml(self, instance_name = DEFAULT):
+        """
+        >>> AbstractTestReport.clear()
+        >>> tr = AbstractTestReport()
+        >>> print tr.toxml()
+        <?xml version="1.0" ?><test_report/>
+        """
+        return AbstractTestReport.__instances[instance_name].toxml()
+
+    def toprettyxml(self, indent="\t", newl="\n", instance_name = DEFAULT):
+        """
+        >>> AbstractTestReport.clear()
+        >>> tr = AbstractTestReport()
+        >>> print tr.toprettyxml()
+        <?xml version="1.0" ?>
+        <test_report/>
+        <BLANKLINE>
+        """
+        return AbstractTestReport.__instances[self._name].toprettyxml(indent, newl)
+
+################################################################################
+#
+# Unit tests can be run by executing:
+#      python test_report.py unittest
+# Code coverage can therefore be computed using:
+#      python-coverage -x test_report.py unittest
+#      python-coverage -r -m test_report.py
+#
+if __name__ == '__main__': # pragma: no cover
+    import sys
+    if len(sys.argv) > 1:
+        if sys.argv[1] == 'unittest':
+            import doctest
+            doctest.testmod(exclude_empty = True)
+            sys.exit(0)
+        elif sys.argv[1] == '--help':
+            print AbstractTestReport.__doc__
+        elif sys.argv[1] == 'demo':
+            AbstractTestReport.clear()
+            tr1 = AbstractTestReport()
+            tr1.setTitle('Report 1')
+            tr1.addSection('Introduction')
+            tr1.addSubsection('Objectives')
+            tr1.ok('REQ-010')
+            tr1.addSubsection('Bibliography')
+            tr1.addSection('Anything Else')
+            tr1.ko('REQ-020-010', 'Failed for some reason')
+            tr1.addSubsection('Yes')
+            tr1.addSubsubsection('Less')
+            tr1.addSubsubsection('More')
+            tr1.ko('REQ-020-020-100', 'Failed for some other reason')
+            tr1.addSubsection('No')
+            tr1.addSubsection('Blah')
+            tr1.addSection('Conclusion')
+            print tr1.toprettyxml(indent = '  ')
+        elif sys.argv[1] == 'read':
+            from xml.dom.minidom import parse
+            dom = parse('dummy.xml') # parse an XML file by name
+            print dom.toxml()
Index: /branches/sc_branches/psps_testing/utilities/psps_logger.py
===================================================================
--- /branches/sc_branches/psps_testing/utilities/psps_logger.py	(revision 29437)
+++ /branches/sc_branches/psps_testing/utilities/psps_logger.py	(revision 29438)
@@ -123,4 +123,5 @@
         import doctest
         doctest.testmod()
+        PsPsLogger.set_fileout('/dev/null')
         PsPsLogger.test()
         sys.exit(0)
Index: /branches/sc_branches/psps_testing/utilities/test_report.py
===================================================================
--- /branches/sc_branches/psps_testing/utilities/test_report.py	(revision 29437)
+++ /branches/sc_branches/psps_testing/utilities/test_report.py	(revision 29438)
@@ -2,19 +2,16 @@
 import datetime
 from utilities.configuration import Configuration
+from utilities.abstract_test_report import AbstractTestReport
 
-class TestReportClass(logging.Logger):
-    datefmt = '%H:%M:%S'
-    fmt = '[%(asctime)8s] | %(requirement)65s | %(message)s'
-    class __impl(logging.Logger):
+class TestReportClass(AbstractTestReport):
+    """
+    An implementation of AbstractTestReport for PSPS testing test
+    reports.
+    """
+    class __impl(AbstractTestReport):
         """ 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)
+            """Sets up AbstractTestReport"""
+            AbstractTestReport.__init__(self)
         def id(self):
             """ Test method, return singleton id """
@@ -23,9 +20,9 @@
     __instance = None
 
-    def info(self, message):
-        for line in message.split('\n'):
-            logging.Logger.info(self,
-                                line, 
-                                extra = {'requirement': 'INFO'})
+    def info(self, message, extra=None):
+        self.addTestResult(AbstractTestReport.DEFAULT,
+                           'INFO',
+                           '', 
+                           message)
 
     def __init__(self):
@@ -44,7 +41,7 @@
         # 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('Test performed on ' + str(datetime.date.today()),
+                  extra = {'requirement': 
+                           'This column usually shows the "Requirement ID"'})
         self.info(str(Configuration()))
 
@@ -57,34 +54,26 @@
         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})
+    def save(self, filename, prettyxml=True):
+        f = open(filename, "w")
+        if prettyxml:
+            f.write(self.toprettyxml())
+        else:
+            f.write(self.toxml())
+        f.close()
 
 TestReport = TestReportClass()
 
-if __name__ == '__main__':
+if __name__ == '__main__': # pragma: no cover
     import logging
     import sys
     current_argument_position = 1
     while current_argument_position < len(sys.argv):
-        if sys.argv[current_argument_position] == '-debug': # pragma: no cover
+        if sys.argv[current_argument_position] == '-debug':
             PsPsLogger.setLevel(logging.DEBUG)
             PsPsLogger.set_stderr()
-            current_argument_position += 1
-        elif sys.argv[current_argument_position] == 'test':
-            print 'Running unittest for TestReport class'
+        elif sys.argv[current_argument_position] == 'unittest':
             import doctest
             doctest.testmod()
             sys.exit(0)
+        current_argument_position += 1
+    print('  Usage: %s [--debug] unittest' % sys.argv[0])
Index: /branches/sc_branches/psps_testing/utilities/util.py
===================================================================
--- /branches/sc_branches/psps_testing/utilities/util.py	(revision 29437)
+++ /branches/sc_branches/psps_testing/utilities/util.py	(revision 29438)
@@ -125,10 +125,10 @@
                 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)))
+                                    '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)
@@ -142,11 +142,11 @@
                 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))
+                                    '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)
@@ -158,8 +158,8 @@
             # 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())))
+                            '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
 
