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: