提交 43349c11 编写于 作者: C Cleber Rosa

Result: port xUnit result

This moves all xUnit code to the plugin file.  With that, the core
avocado has no knowledge about this plugin, as it should be.  Still,
the `results.xml` is going to be generated by default, unless the
command line option `--xunit-job-result` is set to `off`.
Signed-off-by: NCleber Rosa <crosa@redhat.com>
上级 05200036
...@@ -42,7 +42,6 @@ from . import output ...@@ -42,7 +42,6 @@ from . import output
from . import multiplexer from . import multiplexer
from . import tree from . import tree
from . import test from . import test
from . import xunit
from . import jsonresult from . import jsonresult
from . import replay from . import replay
from .output import STD_OUTPUT from .output import STD_OUTPUT
...@@ -274,8 +273,6 @@ class Job(object): ...@@ -274,8 +273,6 @@ class Job(object):
The basic idea behind the output plugins is: The basic idea behind the output plugins is:
* If there are any active output plugins, use them * If there are any active output plugins, use them
* Always add Xunit and JSON plugins outputting to files inside the
results dir
* If at the end we only have 2 output plugins (Xunit and JSON), we can * If at the end we only have 2 output plugins (Xunit and JSON), we can
add the human output plugin. add the human output plugin.
""" """
...@@ -283,11 +280,6 @@ class Job(object): ...@@ -283,11 +280,6 @@ class Job(object):
# If there are any active output plugins, let's use them # If there are any active output plugins, let's use them
self._set_output_plugins() self._set_output_plugins()
# Setup the xunit plugin to output to the debug directory
xunit_file = os.path.join(self.logdir, 'results.xml')
xunit_plugin = xunit.xUnitResult(self, xunit_file)
self.result_proxy.add_output_plugin(xunit_plugin)
# Setup the json plugin to output to the debug directory # Setup the json plugin to output to the debug directory
json_file = os.path.join(self.logdir, 'results.json') json_file = os.path.join(self.logdir, 'results.json')
json_plugin = jsonresult.JSONResult(self, json_file) json_plugin = jsonresult.JSONResult(self, json_file)
......
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See LICENSE for more details.
#
# Copyright: Red Hat Inc. 2014
# Author: Ruda Moura <rmoura@redhat.com>
"""xUnit module."""
import datetime
import logging
import string
from xml.sax.saxutils import quoteattr
from .result import Result
# We use a subset of the XML format defined in this URL:
# https://svn.jenkins-ci.org/trunk/hudson/dtkit/dtkit-format/dtkit-junit-model/src/main/resources/com/thalesgroup/dtkit/junit/model/xsd/junit-4.xsd
PRINTABLE = string.ascii_letters + string.digits + string.punctuation + '\n\r '
class XmlResult(object):
"""
Handles the XML details for xUnit output.
"""
def __init__(self):
self.xml = ['<?xml version="1.0" encoding="UTF-8"?>']
def _escape_attr(self, attrib):
attrib = ''.join(_ if _ in PRINTABLE else "\\x%02x" % ord(_)
for _ in str(attrib))
return quoteattr(attrib)
def _escape_cdata(self, cdata):
cdata = ''.join(_ if _ in PRINTABLE else "\\x%02x" % ord(_)
for _ in str(cdata))
return cdata.replace(']]>', ']]>]]&gt;<![CDATA[')
def get_contents(self):
return '\n'.join(self.xml)
def start_testsuite(self, timestamp):
"""
Start a new testsuite node.
:param timestamp: Timestamp string in date/time format.
"""
self.testsuite = '<testsuite name="avocado" tests="{tests}" errors="{errors}" failures="{failures}" skipped="{skip}" time="{tests_total_time}" timestamp="%s">' % timestamp
self.testcases = []
def end_testsuite(self, tests, errors, failures, skip, tests_total_time):
"""
End of testsuite node.
:param tests: Number of tests.
:param errors: Number of test errors.
:param failures: Number of test failures.
:param skip: Number of test skipped.
:param total_time: The total time of test execution.
"""
values = {'tests': tests,
'errors': errors,
'failures': failures,
'skip': skip,
'tests_total_time': tests_total_time}
self.xml.append(self.testsuite.format(**values))
for tc in self.testcases:
self.xml.append(tc)
self.xml.append('</testsuite>')
def add_success(self, state):
"""
Add a testcase node of kind succeed.
:param state: result of :class:`avocado.core.test.Test.get_state`.
:type state: dict
"""
tc = '\t<testcase classname={class} name={name} time="{time}"/>'
values = {'class': self._escape_attr(state.get('class_name', "<unknown>")),
'name': self._escape_attr(state.get('name', "<unknown>")),
'time': state.get('time_elapsed', -1)}
self.testcases.append(tc.format(**values))
def add_skip(self, state):
"""
Add a testcase node of kind skipped.
:param state: result of :class:`avocado.core.test.Test.get_state`.
:type state: dict
"""
tc = '''\t<testcase classname={class} name={name} time="{time}">
\t\t<skipped />
\t</testcase>'''
values = {'class': self._escape_attr(state.get('class_name', "<unknown>")),
'name': self._escape_attr(state.get('name', "<unknown>")),
'time': state.get('time_elapsed', -1)}
self.testcases.append(tc.format(**values))
def add_failure(self, state):
"""
Add a testcase node of kind failed.
:param state: result of :class:`avocado.core.test.Test.get_state`.
:type state: dict
"""
tc = '''\t<testcase classname={class} name={name} time="{time}">
\t\t<failure type={type} message={reason}><![CDATA[{traceback}]]></failure>
\t\t<system-out><![CDATA[{systemout}]]></system-out>
\t</testcase>'''
values = {'class': self._escape_attr(state.get('class_name', "<unknown>")),
'name': self._escape_attr(state.get('name', "<unknown>")),
'time': state.get('time_elapsed', -1),
'type': self._escape_attr(state.get('fail_class', "<unknown>")),
'traceback': self._escape_cdata(state.get('traceback', "<unknown>")),
'systemout': self._escape_cdata(state.get('text_output', "<unknown>")),
'reason': self._escape_attr(str(state.get('fail_reason', "<unknown>")))}
self.testcases.append(tc.format(**values))
def add_error(self, state):
"""
Add a testcase node of kind error.
:param state: result of :class:`avocado.core.test.Test.get_state`.
:type state: dict
"""
tc = '''\t<testcase classname={class} name={name} time="{time}">
\t\t<error type={type} message={reason}><![CDATA[{traceback}]]></error>
\t\t<system-out><![CDATA[{systemout}]]></system-out>
\t</testcase>'''
values = {'class': self._escape_attr(state.get('class_name', "<unknown>")),
'name': self._escape_attr(state.get('name', "<unknown>")),
'time': state.get('time_elapsed', -1),
'type': self._escape_attr(state.get('fail_class', "<unknown>")),
'traceback': self._escape_cdata(state.get('traceback', "<unknown>")),
'systemout': self._escape_cdata(state.get('text_output', "<unknown>")),
'reason': self._escape_attr(str(state.get('fail_reason', "<unknown>")))}
self.testcases.append(tc.format(**values))
class xUnitResult(Result):
"""
xUnit Test Result class.
"""
command_line_arg_name = '--xunit'
def __init__(self, job, force_xunit_file=None):
"""
Creates an instance of xUnitResult.
:param job: an instance of :class:`avocado.core.job.Job`.
:param force_xunit_file: Override the output file defined in job.args
"""
Result.__init__(self, job)
if force_xunit_file:
self.output = force_xunit_file
else:
self.output = getattr(self.args, 'xunit_output', '-')
self.log = logging.getLogger("avocado.app")
self.xml = XmlResult()
def start_tests(self):
"""
Record a start tests event.
"""
Result.start_tests(self)
self.xml.start_testsuite(datetime.datetime.now())
def start_test(self, test):
"""
Record a start test event.
"""
Result.start_test(self, test)
def end_test(self, state):
"""
Record an end test event, accord to the given test status.
:param state: result of :class:`avocado.core.test.Test.get_state`.
:type state: dict
"""
Result.end_test(self, state)
status = state.get('status', "ERROR")
if status in ('PASS', 'WARN'):
self.xml.add_success(state)
elif status == 'SKIP':
self.xml.add_skip(state)
elif status == 'FAIL':
self.xml.add_failure(state)
else: # ERROR, INTERRUPTED, ...
self.xml.add_error(state)
def end_tests(self):
"""
Record an end tests event.
"""
Result.end_tests(self)
values = {'tests': self.tests_total,
'errors': self.errors + self.interrupted,
'failures': self.failed,
'skip': self.skipped,
'tests_total_time': self.tests_total_time}
self.xml.end_testsuite(**values)
contents = self.xml.get_contents()
if self.output == '-':
self.log.debug(contents)
else:
with open(self.output, 'w') as xunit_output:
xunit_output.write(contents)
...@@ -10,17 +10,109 @@ ...@@ -10,17 +10,109 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
# Copyright: Red Hat Inc. 2014 # Copyright: Red Hat Inc. 2014
# Author: Ruda Moura <rmoura@redhat.com> # Authors: Ruda Moura <rmoura@redhat.com>
# Cleber Rosa <crosa@redhat.com>
"""xUnit module.""" """xUnit module."""
import datetime
import logging
import os
import string
from xml.dom.minidom import Document, Element
from avocado.core.parser import FileOrStdoutAction from avocado.core.parser import FileOrStdoutAction
from avocado.core.plugin_interfaces import CLI from avocado.core.plugin_interfaces import CLI, Result
from avocado.core.result import register_test_result_class
from avocado.core.xunit import xUnitResult
class XUnitResult(Result):
UNKNOWN = '<unknown>'
PRINTABLE = string.ascii_letters + string.digits + string.punctuation + '\n\r '
def _escape_attr(self, attrib):
attrib = ''.join(_ if _ in self.PRINTABLE else "\\x%02x" % ord(_)
for _ in str(attrib))
return attrib
def _escape_cdata(self, cdata):
cdata = ''.join(_ if _ in self.PRINTABLE else "\\x%02x" % ord(_)
for _ in str(cdata))
return cdata.replace(']]>', ']]>]]&gt;<![CDATA[')
def _get_attr(self, container, attrib):
return self._escape_attr(container.get(attrib, self.UNKNOWN))
def _create_testcase_element(self, document, state):
testcase = document.createElement('testcase')
testcase.setAttribute('classname', self._get_attr(state, 'class_name'))
testcase.setAttribute('name', self._get_attr(state, 'name'))
testcase.setAttribute('time', self._get_attr(state, 'time_elapsed'))
return testcase
def _create_failure_or_error(self, document, test, element_type):
element = Element(element_type)
element.setAttribute('type', self._get_attr(test, 'fail_class'))
element.setAttribute('message', self._get_attr(test, 'fail_reason'))
traceback_content = self._escape_cdata(test.get('traceback', self.UNKNOWN))
traceback = document.createCDATASection(traceback_content)
element.appendChild(traceback)
system_out = Element('system-out')
system_out_cdata_content = self._escape_cdata(test.get('text_output', self.UNKNOWN))
system_out_cdata = document.createCDATASection(system_out_cdata_content)
system_out.appendChild(system_out_cdata)
element.appendChild(system_out)
return element
class XUnit(CLI): def _render(self, result):
document = Document()
testsuite = document.createElement('testsuite')
testsuite.setAttribute('name', 'avocado')
testsuite.setAttribute('tests', self._escape_attr(result.tests_total))
testsuite.setAttribute('errors', self._escape_attr(result.errors + result.interrupted))
testsuite.setAttribute('failures', self._escape_attr(result.failed))
testsuite.setAttribute('skipped', self._escape_attr(result.skipped))
testsuite.setAttribute('time', self._escape_attr(result.tests_total_time))
testsuite.setAttribute('timestamp', self._escape_attr(datetime.datetime.now()))
document.appendChild(testsuite)
for test in result.tests:
testcase = self._create_testcase_element(document, test)
status = test.get('status', 'ERROR')
if status in ('PASS', 'WARN'):
pass
elif status == 'SKIP':
testcase.appendChild(Element('skipped'))
elif status == 'FAIL':
element = self._create_failure_or_error(document, test, 'failure')
testcase.appendChild(element)
else:
element = self._create_failure_or_error(document, test, 'error')
testcase.appendChild(element)
testsuite.appendChild(testcase)
return document.toxml(encoding='UTF-8')
def render(self, result, job):
if not (hasattr(job.args, 'xunit_job_result') or
hasattr(job.args, 'xunit_output')):
return
content = self._render(result)
if getattr(job.args, 'xunit_job_result', 'off') == 'on':
xunit_path = os.path.join(job.logdir, 'results.xml')
with open(xunit_path, 'w') as xunit_file:
xunit_file.write(content)
xunit_path = getattr(job.args, 'xunit_output', 'None')
if xunit_path is not None:
if xunit_path == '-':
log = logging.getLogger("avocado.app")
log.debug(content)
else:
with open(xunit_path, 'w') as xunit_file:
xunit_file.write(content)
class XUnitCLI(CLI):
""" """
xUnit output xUnit output
...@@ -41,6 +133,11 @@ class XUnit(CLI): ...@@ -41,6 +133,11 @@ class XUnit(CLI):
help=('Enable xUnit result format and write it to FILE. ' help=('Enable xUnit result format and write it to FILE. '
"Use '-' to redirect to the standard output.")) "Use '-' to redirect to the standard output."))
run_subcommand_parser.output.add_argument(
'--xunit-job-result', dest='xunit_job_result',
choices=('on', 'off'), default='on',
help=('Enables default xUnit result in the job results directory. '
'File will be named "results.xml".'))
def run(self, args): def run(self, args):
if 'xunit_output' in args and args.xunit_output is not None: pass
register_test_result_class(args, xUnitResult)
...@@ -8,8 +8,9 @@ from StringIO import StringIO ...@@ -8,8 +8,9 @@ from StringIO import StringIO
from xml.dom import minidom from xml.dom import minidom
from avocado import Test from avocado import Test
from avocado.core import xunit
from avocado.core import job from avocado.core import job
from avocado.core.result import Result
from avocado.plugins import xunit
class ParseXMLError(Exception): class ParseXMLError(Exception):
...@@ -35,9 +36,10 @@ class xUnitSucceedTest(unittest.TestCase): ...@@ -35,9 +36,10 @@ class xUnitSucceedTest(unittest.TestCase):
self.tmpdir = tempfile.mkdtemp(prefix='avocado_' + __name__) self.tmpdir = tempfile.mkdtemp(prefix='avocado_' + __name__)
args = argparse.Namespace() args = argparse.Namespace()
args.xunit_output = self.tmpfile[1] args.xunit_output = self.tmpfile[1]
self.test_result = xunit.xUnitResult(FakeJob(args)) self.job = job.Job(args)
self.test_result = Result(FakeJob(args))
self.test_result.start_tests() self.test_result.start_tests()
self.test1 = SimpleTest(job=job.Job(), base_logdir=self.tmpdir) self.test1 = SimpleTest(job=self.job, base_logdir=self.tmpdir)
self.test1.status = 'PASS' self.test1.status = 'PASS'
self.test1.time_elapsed = 1.23 self.test1.time_elapsed = 1.23
unittests_path = os.path.dirname(os.path.abspath(__file__)) unittests_path = os.path.dirname(os.path.abspath(__file__))
...@@ -52,8 +54,9 @@ class xUnitSucceedTest(unittest.TestCase): ...@@ -52,8 +54,9 @@ class xUnitSucceedTest(unittest.TestCase):
self.test_result.start_test(self.test1) self.test_result.start_test(self.test1)
self.test_result.end_test(self.test1.get_state()) self.test_result.end_test(self.test1.get_state())
self.test_result.end_tests() self.test_result.end_tests()
self.assertTrue(self.test_result.xml) xunit_result = xunit.XUnitResult()
with open(self.test_result.output) as fp: xunit_result.render(self.test_result, self.job)
with open(self.job.args.xunit_output) as fp:
xml = fp.read() xml = fp.read()
try: try:
dom = minidom.parseString(xml) dom = minidom.parseString(xml)
......
...@@ -131,7 +131,7 @@ if __name__ == '__main__': ...@@ -131,7 +131,7 @@ if __name__ == '__main__':
'envkeep = avocado.plugins.envkeep:EnvKeep', 'envkeep = avocado.plugins.envkeep:EnvKeep',
'gdb = avocado.plugins.gdb:GDB', 'gdb = avocado.plugins.gdb:GDB',
'wrapper = avocado.plugins.wrapper:Wrapper', 'wrapper = avocado.plugins.wrapper:Wrapper',
'xunit = avocado.plugins.xunit:XUnit', 'xunit = avocado.plugins.xunit:XUnitCLI',
'json = avocado.plugins.json:JSON', 'json = avocado.plugins.json:JSON',
'journal = avocado.plugins.journal:Journal', 'journal = avocado.plugins.journal:Journal',
'html = avocado.plugins.html:HTML', 'html = avocado.plugins.html:HTML',
...@@ -154,6 +154,9 @@ if __name__ == '__main__': ...@@ -154,6 +154,9 @@ if __name__ == '__main__':
'avocado.plugins.job.prepost': [ 'avocado.plugins.job.prepost': [
'jobscripts = avocado.plugins.jobscripts:JobScripts', 'jobscripts = avocado.plugins.jobscripts:JobScripts',
], ],
'avocado.plugins.result': [
'xunit = avocado.plugins.xunit:XUnitResult',
],
}, },
zip_safe=False, zip_safe=False,
test_suite='selftests') test_suite='selftests')
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册