summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--framework/backends/junit.py140
-rw-r--r--unittests/framework/backends/test_junit.py165
2 files changed, 273 insertions, 32 deletions
diff --git a/framework/backends/junit.py b/framework/backends/junit.py
index 94ead6f01..e71f2427b 100644
--- a/framework/backends/junit.py
+++ b/framework/backends/junit.py
@@ -32,7 +32,7 @@ except ImportError:
import six
-from framework import grouptools, results, status, exceptions
+from framework import grouptools, results, exceptions
from framework.core import PIGLIT_CONFIG
from .abstract import FileBackend
from .register import Registry
@@ -78,7 +78,7 @@ class JUnitWriter(object):
# set different root names.
classname = 'piglit.' + classname
- return (classname, testname)
+ return (classname, junit_escape(testname))
@staticmethod
def _set_xml_err(element, data, expected_result):
@@ -108,8 +108,12 @@ class JUnitWriter(object):
def _make_result(element, result, expected_result):
"""Adds the skipped, failure, or error element."""
res = None
+ # If the result is skip, then just add the skipped message and go on
if result == 'skip':
res = etree.SubElement(element, 'skipped')
+ elif result == 'incomplete':
+ res = etree.SubElement(element, 'failure',
+ message='Incomplete run.')
elif result in ['fail', 'dmesg-warn', 'dmesg-fail']:
if expected_result == "failure":
res = etree.SubElement(element, 'skipped',
@@ -139,56 +143,124 @@ class JUnitWriter(object):
if res is not None:
res.attrib['type'] = six.text_type(result)
- def __call__(self, f, name, data):
- classname, testname = self._make_names(name)
+ def _make_root(self, testname, classname, data):
+ """Creates and returns the root element."""
+ element = etree.Element('testcase',
+ name=self._make_full_test_name(testname),
+ classname=classname,
+ # Incomplete will not have a time.
+ time=str(data.time.total),
+ status=str(data.result))
+
+ return element
+ def _make_full_test_name(self, testname):
# Jenkins will display special pages when the test has certain names.
# https://jenkins-ci.org/issue/18062
# https://jenkins-ci.org/issue/19810
# The testname variable is used in the calculate_result closure, and
# must not have the suffix appended.
- full_test_name = testname + self._test_suffix
- if full_test_name in _JUNIT_SPECIAL_NAMES:
- testname += '_'
- full_test_name = testname + self._test_suffix
+ return testname + self._test_suffix
- # Create the root element
- element = etree.Element('testcase', name=full_test_name,
- classname=classname,
- # Incomplete will not have a time.
- time=str(data.time.total),
- status=str(data.result))
+ def _expected_result(self, name):
+ """Get the expected result of the test."""
+ name = name.replace("=", ".").replace(":", ".")
+ expected_result = "pass"
+
+ if name in self._expected_failures:
+ expected_result = "failure"
+ # a test can either fail or crash, but not both
+ assert name not in self._expected_crashes
+
+ if name in self._expected_crashes:
+ expected_result = "error"
+
+ return expected_result
+
+ def __call__(self, f, name, data):
+ classname, testname = self._make_names(name)
+ element = self._make_root(testname, classname, data)
+ expected_result = self._expected_result(
+ '{}.{}'.format(classname, testname).lower())
# If this is an incomplete status then none of these values will be
# available, nor
if data.result != 'incomplete':
- expected_result = "pass"
+ self._set_xml_err(element, data, expected_result)
- # replace special characters and make case insensitive
- lname = (classname + "." + testname).lower()
- lname = lname.replace("=", ".")
- lname = lname.replace(":", ".")
+ # Add stdout
+ out = etree.SubElement(element, 'system-out')
+ out.text = data.out
- if lname in self._expected_failures:
- expected_result = "failure"
- # a test can either fail or crash, but not both
- assert lname not in self._expected_crashes
+ # Prepend command line to stdout
+ out.text = data.command + '\n' + out.text
- if lname in self._expected_crashes:
- expected_result = "error"
+ self._make_result(element, data.result, expected_result)
- self._set_xml_err(element, data, expected_result)
+ f.write(six.text_type(etree.tostring(element).decode('utf-8')))
+
+
+class JUnitSubtestWriter(JUnitWriter):
+ """A JUnitWriter derived class that treats subtest at testsuites.
+
+ This class will turn a piglit test with subtests into a testsuite element
+ with each subtest as a testcase element. This subclass is needed because
+ not all JUnit readers (like the JUnit plugin for Jenkins) handle nested
+ testsuites correctly.
+ """
+
+ def _make_root(self, testname, classname, data):
+ if data.subtests:
+ testname = '{}.{}'.format(classname, testname)
+ element = etree.Element('testsuite',
+ name=testname,
+ time=str(data.time.total),
+ tests=six.text_type(len(data.subtests)))
+ for test, result in six.iteritems(data.subtests):
+ etree.SubElement(element,
+ 'testcase',
+ name=self._make_full_test_name(test),
+ classname=testname,
+ status=six.text_type(result))
+
+ else:
+ element = super(JUnitSubtestWriter, self)._make_root(
+ testname, classname, data)
+ return element
+
+ def __call__(self, f, name, data):
+ classname, testname = self._make_names(name)
+ element = self._make_root(testname, classname, data)
+
+ # If this is an incomplete status then none of these values will be
+ # available, nor
+ if data.result != 'incomplete':
+ self._set_xml_err(element, data, 'pass')
# Add stdout
out = etree.SubElement(element, 'system-out')
out.text = data.out
-
# Prepend command line to stdout
out.text = data.command + '\n' + out.text
- self._make_result(element, data.result, expected_result)
+ if data.subtests:
+ for subname, result in six.iteritems(data.subtests):
+ # replace special characters and make case insensitive
+ elem = element.find('.//testcase[@name="{}"]'.format(
+ self._make_full_test_name(subname)))
+ assert elem is not None
+ self._make_result(
+ elem, result,
+ self._expected_result('{}.{}.{}'.format(
+ classname, testname, subname).lower()))
+ else:
+ self._make_result(element, data.result,
+ self._expected_result('{}.{}'.format(
+ classname, testname).lower()))
else:
- etree.SubElement(element, 'failure', message='Incomplete run.')
+ self._make_result(element, data.result,
+ self._expected_result('{}.{}'.format(
+ classname, testname).lower()))
f.write(six.text_type(etree.tostring(element).decode('utf-8')))
@@ -203,7 +275,7 @@ class JUnitBackend(FileBackend):
_file_extension = 'xml'
_write = None # this silences the abstract-not-subclassed warning
- def __init__(self, dest, junit_suffix='', **options):
+ def __init__(self, dest, junit_suffix='', junit_subtests=False, **options):
super(JUnitBackend, self).__init__(dest, **options)
# make dictionaries of all test names expected to crash/fail
@@ -218,8 +290,12 @@ class JUnitBackend(FileBackend):
for fail, _ in PIGLIT_CONFIG.items("expected-crashes"):
expected_crashes[fail.lower()] = True
- self._write = JUnitWriter(junit_suffix, expected_failures,
- expected_crashes)
+ if not junit_subtests:
+ self._write = JUnitWriter(
+ junit_suffix, expected_failures, expected_crashes)
+ else:
+ self._write = JUnitSubtestWriter( # pylint: disable=redefined-variable-type
+ junit_suffix, expected_failures, expected_crashes)
def initialize(self, metadata):
""" Do nothing
diff --git a/unittests/framework/backends/test_junit.py b/unittests/framework/backends/test_junit.py
index 8f09dac16..6bc9e6aa6 100644
--- a/unittests/framework/backends/test_junit.py
+++ b/unittests/framework/backends/test_junit.py
@@ -254,3 +254,168 @@ class TestJUnitWriter(object):
schema = etree.XMLSchema(file=JUNIT_SCHEMA) # pylint: disable=no-member
with open(test_file, 'r') as f:
assert schema.validate(etree.parse(f))
+
+
+class TestJUnitSubtestWriter(object):
+ """Tests for the JUnitWriter class."""
+
+ def test_junit_replace(self, tmpdir):
+ """backends.junit.JUnitBackend.write_test: grouptools.SEPARATOR is
+ replaced with '.'.
+ """
+ result = results.TestResult()
+ result.time.end = 1.2345
+ result.out = 'this is stdout'
+ result.err = 'this is stderr'
+ result.command = 'foo'
+ result.subtests['foo'] = 'pass'
+ result.subtests['bar'] = 'fail'
+
+ test = backends.junit.JUnitBackend(six.text_type(tmpdir),
+ junit_subtests=True)
+ test.initialize(shared.INITIAL_METADATA)
+ with test.write_test(grouptools.join('a', 'group', 'test1')) as t:
+ t(result)
+ test.finalize()
+
+ test_value = etree.parse(six.text_type(tmpdir.join('results.xml')))
+ test_value = test_value.getroot()
+
+ assert test_value.find('.//testsuite/testsuite').attrib['name'] == \
+ 'piglit.a.group.test1'
+
+ def test_junit_replace_suffix(self, tmpdir):
+ """backends.junit.JUnitBackend.write_test: grouptools.SEPARATOR is
+ replaced with '.'.
+ """
+ result = results.TestResult()
+ result.time.end = 1.2345
+ result.out = 'this is stdout'
+ result.err = 'this is stderr'
+ result.command = 'foo'
+ result.subtests['foo'] = 'pass'
+ result.subtests['bar'] = 'fail'
+
+ test = backends.junit.JUnitBackend(six.text_type(tmpdir),
+ junit_subtests=True,
+ junit_suffix='.foo')
+ test.initialize(shared.INITIAL_METADATA)
+ with test.write_test(grouptools.join('a', 'group', 'test1')) as t:
+ t(result)
+ test.finalize()
+
+ test_value = etree.parse(six.text_type(tmpdir.join('results.xml')))
+ test_value = test_value.getroot()
+
+ suite = test_value.find('.//testsuite/testsuite')
+ assert suite.attrib['name'] == 'piglit.a.group.test1'
+ assert suite.find('.//testcase[@name="{}"]'.format('foo.foo')) is not None
+
+ def test_subtest_skip(self, tmpdir):
+ result = results.TestResult()
+ result.time.end = 1.2345
+ result.out = 'this is stdout'
+ result.err = 'this is stderr'
+ result.command = 'foo'
+ result.subtests['foo'] = 'pass'
+ result.subtests['bar'] = 'skip'
+
+ test = backends.junit.JUnitBackend(six.text_type(tmpdir),
+ junit_subtests=True)
+ test.initialize(shared.INITIAL_METADATA)
+ with test.write_test(grouptools.join('a', 'group', 'test1')) as t:
+ t(result)
+ test.finalize()
+
+ test_value = etree.parse(six.text_type(tmpdir.join('results.xml')))
+ test_value = test_value.getroot()
+
+ suite = test_value.find('.//testsuite/testsuite')
+ assert suite.attrib['name'] == 'piglit.a.group.test1'
+ assert suite.find('.//testcase[@name="{}"]/skipped'.format('bar')) \
+ is not None
+
+ def test_result_skip(self, tmpdir):
+ result = results.TestResult()
+ result.time.end = 1.2345
+ result.out = 'this is stdout'
+ result.err = 'this is stderr'
+ result.command = 'foo'
+ result.result = 'skip'
+
+ test = backends.junit.JUnitBackend(six.text_type(tmpdir),
+ junit_subtests=True)
+ test.initialize(shared.INITIAL_METADATA)
+ with test.write_test(grouptools.join('a', 'group', 'test1')) as t:
+ t(result)
+ test.finalize()
+
+ test_value = etree.parse(six.text_type(tmpdir.join('results.xml')))
+ test_value = test_value.getroot()
+
+ elem = test_value.find('.//testsuite/testcase[@name="test1"]/skipped')
+ assert elem is not None
+
+ def test_classname(self, tmpdir):
+ result = results.TestResult()
+ result.time.end = 1.2345
+ result.out = 'this is stdout'
+ result.err = 'this is stderr'
+ result.command = 'foo'
+ result.subtests['foo'] = 'pass'
+ result.subtests['bar'] = 'skip'
+
+ test = backends.junit.JUnitBackend(six.text_type(tmpdir),
+ junit_subtests=True)
+ test.initialize(shared.INITIAL_METADATA)
+ with test.write_test(grouptools.join('a', 'group', 'test1')) as t:
+ t(result)
+ test.finalize()
+
+ test_value = etree.parse(six.text_type(tmpdir.join('results.xml')))
+ test_value = test_value.getroot()
+
+ suite = test_value.find('.//testsuite/testsuite')
+ assert suite.find('.//testcase[@classname="piglit.a.group.test1"]') \
+ is not None
+
+ class TestValid(object):
+ @pytest.fixture
+ def test_file(self, tmpdir):
+ tmpdir.mkdir('foo')
+ p = tmpdir.join('foo')
+
+ result = results.TestResult()
+ result.time.end = 1.2345
+ result.out = 'this is stdout'
+ result.err = 'this is stderr'
+ result.command = 'foo'
+ result.pid = 1034
+ result.subtests['foo'] = 'pass'
+ result.subtests['bar'] = 'fail'
+
+ test = backends.junit.JUnitBackend(six.text_type(p),
+ junit_subtests=True)
+ test.initialize(shared.INITIAL_METADATA)
+ with test.write_test(grouptools.join('a', 'group', 'test1')) as t:
+ t(result)
+
+ result.result = 'fail'
+ with test.write_test(grouptools.join('a', 'test', 'test1')) as t:
+ t(result)
+ test.finalize()
+
+ return six.text_type(p.join('results.xml'))
+
+ def test_xml_well_formed(self, test_file):
+ """backends.junit.JUnitBackend.write_test: produces well formed xml."""
+ etree.parse(test_file)
+
+ @pytest.mark.skipif(etree.__name__ != 'lxml.etree',
+ reason="This test requires lxml")
+ def test_xml_valid(self, test_file):
+ """backends.junit.JUnitBackend.write_test: produces valid JUnit xml."""
+ # This XMLSchema class is unique to lxml
+ schema = etree.XMLSchema(file=JUNIT_SCHEMA) # pylint: disable=no-member
+ with open(test_file, 'r') as f:
+ assert schema.validate(etree.parse(f))