Revision: 4428
Author: jussi.ao.malinen
Date: Tue Dec  7 04:00:09 2010
Log: xunit compatible output based on original patch by Régis Desgroppes. issue 442
http://code.google.com/p/robotframework/source/detail?r=4428

Added:
 /trunk/atest/resources/xunit_checker.py
 /trunk/atest/resources/xunit_resource.txt
 /trunk/atest/robot/output/xunit.txt
 /trunk/atest/robot/rebot/xunit.txt
 /trunk/src/robot/serializing/xunitserializers.py
Modified:
 /trunk/src/robot/conf/settings.py
 /trunk/src/robot/rebot.py
 /trunk/src/robot/runner.py
 /trunk/src/robot/serializing/testoutput.py

=======================================
--- /dev/null
+++ /trunk/atest/resources/xunit_checker.py     Tue Dec  7 04:00:09 2010
@@ -0,0 +1,13 @@
+from xml.dom.minidom import parse
+
+def parse_xunit(path):
+    return parse(path)
+
+def get_root_element_name(dom):
+    return dom.documentElement.tagName
+
+def get_element_count_by_name(dom, name):
+    return len(get_elements_by_name(dom, name))
+
+def get_elements_by_name(dom, name):
+    return dom.getElementsByTagName(name)
=======================================
--- /dev/null
+++ /trunk/atest/resources/xunit_resource.txt   Tue Dec  7 04:00:09 2010
@@ -0,0 +1,19 @@
+*** Settings ***
+Library  xunit_checker.py
+
+
+*** Keywords ***
+Get Dom
+  ${result} =  Parse Xunit  ${OUTDIR}${/}xunit.xml
+  [return]  ${result}
+
+Check Root Element Is Test Suite
+  [Arguments]  ${dom}
+  ${root} =  Get Root Element Name  ${dom}
+  Should Be Equal  ${root}  testsuite
+
+Check Element Count
+  [arguments]  ${dom}  ${element}  ${count}
+  ${items} =  Get Element Count By Name  ${dom}  ${element}
+  ${count} =  Convert To Integer  ${count}
+  Should Be Equal  ${items}  ${count}
=======================================
--- /dev/null
+++ /trunk/atest/robot/output/xunit.txt Tue Dec  7 04:00:09 2010
@@ -0,0 +1,22 @@
+*** Settings ***
+Documentation   Tests for xunit-compatible xml-output.
+Default Tags    regression  pybot  jybot
+Resource        atest_resource.txt
+Resource        xunit_resource.txt
+
+*** Variables ***
+${TESTDATA}  misc/pass_and_fail.html
+
+*** Test Cases ***
+No Xunit Option Given
+    Run Tests  ${EMPTY}  ${TESTDATA}
+    Check Stdout Does Not Contain  XUnit
+
+Xunit Option Given
+    Run Tests  -x xunit.xml  ${TESTDATA}
+    Check Stdout Contains  XUnit
+    Should Exist  ${OUTDIR}${/}xunit.xml
+    ${dom} =  Get Dom
+    Check Root Element Is Test Suite  ${dom}
+    Check Element Count  ${dom}  testcase  2
+    Check Element Count  ${dom}  failure  1
=======================================
--- /dev/null
+++ /trunk/atest/robot/rebot/xunit.txt  Tue Dec  7 04:00:09 2010
@@ -0,0 +1,37 @@
+*** Settings ***
+Default Tags      regression    jybot    pybot
+Suite Setup       Create Input File
+Test Setup        Empty Directory     ${MYOUTDIR}
+Suite Teardown    Remove Temps
+Resource          ../../resources/rebot_resource.html
+Resource          xunit_resource.txt
+
+*** Variables ***
+${TESTDATA_TEST}    misc${/}many_tests.html
+${TESTDATA_SUITES}  misc${/}suites
+${MYOUTDIR}         ${TEMPDIR}${/}robot-test-xunit
+${INPUT FILE}       ${TEMPDIR}${/}robot-test-xunit-file.xml
+
+*** Test Cases ***
+No Xunit Option Given
+    Run Rebot  ${EMPTY}  ${INPUT FILE}
+    Stderr Should Be Empty
+    Check Stdout Does Not Contain  XUnit
+
+Xunit Option Given
+    Run Rebot  --xunitfile xunit.xml  ${INPUT FILE}
+    Stderr Should Be Empty
+    Check Stdout Contains  XUnit
+    Should Exist  ${OUTDIR}${/}xunit.xml
+    ${dom} =  Get Dom
+    Check Root Element Is Test Suite  ${dom}
+    Check Element Count  ${dom}  testcase  15
+    Check Element Count  ${dom}  failure   1
+
+*** Keywords ***
+Create Input File
+ Create Output With Robot ${INPUT FILE} ${EMPTY} ${TESTDATA_TEST} ${TESTDATA_SUITES}
+    Create Directory          ${MYOUTDIR}
+Remove Temps
+    Remove Directory    ${MYOUTDIR}  recursive
+    Remove File         ${INPUT FILE}
=======================================
--- /dev/null
+++ /trunk/src/robot/serializing/xunitserializers.py Tue Dec 7 04:00:09 2010
@@ -0,0 +1,105 @@
+#  Copyright 2008-2010 Nokia Siemens Networks Oyj
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+
+from robot import utils
+
+
+class XUnitSerializer:
+    """Provides an xUnit-compatible result file.
+
+    Attempts to adhere to the de facto schema guessed by Peter Reilly, see:
+    http://marc.info/?l=ant-dev&m=123551933508682
+    """
+
+    def __init__(self, output):
+        self._writer = utils.XmlWriter(output)
+        self._root_suite = None
+        self._detail_serializer = _NopSerializer()
+
+    def close(self):
+        self._writer.close()
+
+    def start_suite(self, suite):
+        if self._root_suite:
+            return
+        self._root_suite = suite
+        attrs = {'name': suite.name,
+                 'tests': str(suite.get_test_count()),
+                 'errors': '0',
+                 'failures': str(suite.all_stats.failed),
+                 'skip': '0'}
+        self._writer.start('testsuite', attrs)
+
+    def end_suite(self, suite):
+        if suite is self._root_suite:
+            self._writer.end('testsuite')
+
+    def start_test(self, test):
+        attrs = {'classname': test.parent.get_long_name(),
+                 'name': test.name,
+                 'time': self._time_as_seconds(test.elapsedtime)}
+        self._writer.start('testcase', attrs)
+        if test.status == 'FAIL':
+ self._detail_serializer = _FailedTestSerializer(self._writer, test)
+
+    def _time_as_seconds(self, millis):
+        return str(int(round(millis, -3) / 1000))
+
+    def end_test(self, test):
+        self._detail_serializer.end_test()
+        self._detail_serializer = _NopSerializer()
+        self._writer.end('testcase')
+
+    def start_keyword(self, kw):
+        pass
+
+    def end_keyword(self, kw):
+        pass
+
+    def message(self, msg):
+        self._detail_serializer.message(msg)
+
+
+class _FailedTestSerializer:
+    """Specific policy to serialize a failed test case details"""
+
+    def __init__(self, writer, test):
+        self._writer = writer
+        self._writer.start('failure',
+ {'message': test.message, 'type': 'AssertionError'})
+
+    def end_test(self):
+        self._writer.end('failure')
+
+    def message(self, msg):
+ """Populates the <failure> section, normally only with a 'Stacktrace'.
+
+ There is a weakness here because filtering is based on message level: + - DEBUG level is used by RF for 'Tracebacks' (what is expected here)
+        - INFO and TRACE are used for keywords and arguments (not errors)
+        - first FAIL message is already reported as <failure> attribute
+        """
+        if msg.level == 'DEBUG':
+            self._writer.content(msg.message)
+
+
+class _NopSerializer:
+    """Default policy when there's no detail to serialize"""
+
+    def end_test(self):
+        pass
+
+    def message(self, msg):
+        pass
=======================================
--- /trunk/src/robot/conf/settings.py   Thu Dec  2 01:29:37 2010
+++ /trunk/src/robot/conf/settings.py   Tue Dec  7 04:00:09 2010
@@ -34,6 +34,7 @@
                   'Log'              : ('log', 'log.html'),
                   'Report'           : ('report', 'report.html'),
                   'Summary'          : ('summary', 'NONE'),
+                  'XUnitFile'        : ('xunitfile', 'NONE'),
                   'SplitOutputs'     : ('splitoutputs', -1),
                   'TimestampOutputs' : ('timestampoutputs', False),
                   'LogTitle'         : ('logtitle', None),
@@ -112,14 +113,14 @@
     def __getitem__(self, name):
         if not self._cli_opts.has_key(name):
             raise KeyError("Non-existing setting '%s'" % name)
-        elif name in ['Output', 'Log', 'Report', 'Summary', 'DebugFile']:
+ elif name in ['Output', 'Log', 'Report', 'Summary', 'DebugFile', 'XUnitFile']:
             return self._get_output_file(name)
         return self._opts[name]

     def _get_output_file(self, type_):
"""Returns path of the requested ouput file and creates needed dirs.

-        Type can be 'Output', 'Log', 'Report' or 'Summary'.
+ Type can be 'Output', 'Log', 'Report', 'Summary', 'DebugFile' or 'XUnitFile'.
         """
         name = self._opts[type_]
         if name == 'NONE' and type_ in self._optional_outputs:
@@ -139,7 +140,7 @@
     def _get_output_extension(self, ext, type_):
         if ext != '':
             return ext
-        if type_ == 'Output':
+        if type_ in ('Output', 'XUnitFile'):
             return '.xml'
         if type_ in ['Log', 'Report', 'Summary']:
             return '.html'
@@ -195,13 +196,13 @@
                         'RunMode'       : ('runmode', []),
                         'WarnOnSkipped' : ('warnonskippedfiles', False),
                         'Variables'     : ('variable', []),
-                        'VariableFiles' : ('variablefile', []),
-                        'Listeners'     : ('listener', []),
-                        'DebugFile'     : ('debugfile', 'NONE') }
-    _optional_outputs = ['Log', 'Report', 'Summary', 'DebugFile']
-
+                        'VariableFiles' : ('variablefile', []),
+                        'Listeners'     : ('listener', []),
+                        'DebugFile'     : ('debugfile', 'NONE'),}
+ _optional_outputs = ['Log', 'Report', 'Summary', 'DebugFile', 'XUnitFile']
+
     def is_rebot_needed(self):
- return not ('NONE' == self['Log'] == self['Report'] == self['Summary']) + return not ('NONE' == self['Log'] == self['Report'] == self['Summary'] == self['XUnitFile'])

     def get_rebot_datasources_and_settings(self):
         datasources = [ self['Output'] ]
@@ -224,4 +225,4 @@
                         'RemoveKeywords' : ('removekeywords', 'NONE'),
                         'StartTime'      : ('starttime', 'N/A'),
                         'EndTime'        : ('endtime', 'N/A')}
-    _optional_outputs = ['Output', 'Log', 'Report', 'Summary']
+    _optional_outputs = ['Output', 'Log', 'Report', 'Summary', 'XUnitFile']
=======================================
--- /trunk/src/robot/rebot.py   Tue Aug 24 03:42:18 2010
+++ /trunk/src/robot/rebot.py   Tue Dec  7 04:00:09 2010
@@ -94,6 +94,8 @@
                           similarly as --log. Default is 'report.html'.
-S --summary file HTML summary report. Not created unless this option
                           is specified. Example: '--summary summary.html'
+ -x --xunitfile file xUnit compatible result file. Not created unless this
+                          option is specified.
  -T --timestampoutputs    When this option is used, timestamp in a format
'YYYYMMDD-hhmmss' is added to all generated output
                           files between their basename and extension. For
=======================================
--- /trunk/src/robot/runner.py  Thu Dec  2 01:29:37 2010
+++ /trunk/src/robot/runner.py  Tue Dec  7 04:00:09 2010
@@ -130,12 +130,13 @@
directory where tests are run from and the given path is considered relative to that unless it is absolute. -o --output file XML output file. Given path, similarly as paths given
-                          to --log, --report, --summary and --debugfile, is
- relative to --outputdir unless given as an absolute - path. Other output files are created from XML output - file after the test execution and XML output can also - be further processed with Rebot tool (e.g. combined
-                          with other XML output files). Default: output.xml
+                          to --log, --report, --summary, --debugfile and
+ --xunitfile, is relative to --outputdir unless given + as an absolute path. Other output files are created + from XML output file after the test execution and XML + output can also be further processed with Rebot tool
+                          (e.g. combined with other XML output files).
+                          Default: output.xml
-l --log file HTML log file. Can be disabled by giving a special
                           name 'NONE'. Default: log.html
                           Examples: '--log mylog.html', '-l NONE'
@@ -145,6 +146,8 @@
                           is specified. Example: '--summary summary.html'
  -b --debugfile file      Debug file written during execution. Not created
                           unless this option is specified.
+ -x --xunitfile file xUnit compatible result file. Not created unless this
+                          option is specified.
  -T --timestampoutputs    When this option is used, timestamp in a format
'YYYYMMDD-hhmmss' is added to all generated output
                           files between their basename and extension. For
=======================================
--- /trunk/src/robot/serializing/testoutput.py  Sat Jul  3 00:55:02 2010
+++ /trunk/src/robot/serializing/testoutput.py  Tue Dec  7 04:00:09 2010
@@ -27,6 +27,7 @@
from logserializers import LogSerializer, SplitLogSerializer, ErrorSerializer
 from reportserializers import (ReportSerializer, SplitReportSerializer,
                                TagDetailsSerializer)
+from xunitserializers import XUnitSerializer


 class RobotTestOutput:
@@ -53,6 +54,7 @@
                               settings['SplitOutputs'])
         self.serialize_log(settings['Log'], settings['LogTitle'],
                            settings['SplitOutputs'])
+        self.serialize_xunit(settings['XUnitFile'])

     def serialize_output(self, path, split=-1):
         if path == 'NONE':
@@ -64,6 +66,16 @@
         serializer.close()
         LOGGER.output_file('Output', path)

+    def serialize_xunit(self, path):
+        if path == 'NONE':
+            return
+        serializer = XUnitSerializer(path)
+        try:
+            self.suite.serialize(serializer)
+        finally:
+            serializer.close()
+        LOGGER.output_file('XUnit', path)
+
     def serialize_summary(self, path, title=None, background=None):
         outfile = self._get_outfile(path, 'summary')
         if not outfile:

Reply via email to