提交 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
from . import multiplexer
from . import tree
from . import test
from . import xunit
from . import jsonresult
from . import replay
from .output import STD_OUTPUT
......@@ -274,8 +273,6 @@ class Job(object):
The basic idea behind the output plugins is:
* 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
add the human output plugin.
"""
......@@ -283,11 +280,6 @@ class Job(object):
# If there are any active output plugins, let's use them
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
json_file = os.path.join(self.logdir, 'results.json')
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 @@
# See LICENSE for more details.
#
# Copyright: Red Hat Inc. 2014
# Author: Ruda Moura <rmoura@redhat.com>
# Authors: Ruda Moura <rmoura@redhat.com>
# Cleber Rosa <crosa@redhat.com>
"""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.plugin_interfaces import CLI
from avocado.core.result import register_test_result_class
from avocado.core.xunit import xUnitResult
from avocado.core.plugin_interfaces import CLI, Result
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
......@@ -41,6 +133,11 @@ class XUnit(CLI):
help=('Enable xUnit result format and write it to FILE. '
"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):
if 'xunit_output' in args and args.xunit_output is not None:
register_test_result_class(args, xUnitResult)
pass
......@@ -8,8 +8,9 @@ from StringIO import StringIO
from xml.dom import minidom
from avocado import Test
from avocado.core import xunit
from avocado.core import job
from avocado.core.result import Result
from avocado.plugins import xunit
class ParseXMLError(Exception):
......@@ -35,9 +36,10 @@ class xUnitSucceedTest(unittest.TestCase):
self.tmpdir = tempfile.mkdtemp(prefix='avocado_' + __name__)
args = argparse.Namespace()
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.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.time_elapsed = 1.23
unittests_path = os.path.dirname(os.path.abspath(__file__))
......@@ -52,8 +54,9 @@ class xUnitSucceedTest(unittest.TestCase):
self.test_result.start_test(self.test1)
self.test_result.end_test(self.test1.get_state())
self.test_result.end_tests()
self.assertTrue(self.test_result.xml)
with open(self.test_result.output) as fp:
xunit_result = xunit.XUnitResult()
xunit_result.render(self.test_result, self.job)
with open(self.job.args.xunit_output) as fp:
xml = fp.read()
try:
dom = minidom.parseString(xml)
......
......@@ -131,7 +131,7 @@ if __name__ == '__main__':
'envkeep = avocado.plugins.envkeep:EnvKeep',
'gdb = avocado.plugins.gdb:GDB',
'wrapper = avocado.plugins.wrapper:Wrapper',
'xunit = avocado.plugins.xunit:XUnit',
'xunit = avocado.plugins.xunit:XUnitCLI',
'json = avocado.plugins.json:JSON',
'journal = avocado.plugins.journal:Journal',
'html = avocado.plugins.html:HTML',
......@@ -154,6 +154,9 @@ if __name__ == '__main__':
'avocado.plugins.job.prepost': [
'jobscripts = avocado.plugins.jobscripts:JobScripts',
],
'avocado.plugins.result': [
'xunit = avocado.plugins.xunit:XUnitResult',
],
},
zip_safe=False,
test_suite='selftests')
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册