# SPDX-FileCopyrightText: 2024-2025 Univention GmbH
# SPDX-License-Identifier: AGPL-3.0-only
"""Format UCS Test results as JUnit report."""
import errno
import os
import shutil
import sys
from datetime import datetime
from typing import IO, Any
from xml.sax.saxutils import XMLGenerator
from xml.sax.xmlreader import AttributesImpl
from univention.testing.data import TestCase, TestFormatInterface, TestResult
from univention.testing.format.text import Raw
__all__ = ['Junit']
[docs]
class Junit(TestFormatInterface):
"""
Create Junit report.
<http://windyroad.org/dl/Open%20Source/JUnit.xsd>
"""
def __init__(self, stream: IO[str] = sys.stdout) -> None:
super().__init__(stream)
self.outdir = "test-reports"
self.now = datetime.today()
self.raw = Raw(stream)
[docs]
def begin_test(self, case: TestCase, prefix: str = '') -> None:
"""Called before each test."""
super().begin_test(case, prefix)
self.now = datetime.today().replace(microsecond=0)
print('\r', end='', file=self.stream)
self.raw.begin_test(case, prefix)
self.stream.flush()
[docs]
def end_run(self):
print('') # clear \r
self.stream.flush()
[docs]
def end_test(self, result: TestResult) -> None:
"""Called after each test."""
self.raw.end_test(result, end='')
failures = errors = skipped = disabled = 0
if result.reason.eofs == 'O':
pass
elif result.reason.eofs == 'S':
skipped = 1
elif result.reason.eofs == 'F':
failures = 1
elif result.reason.eofs == 'E':
errors = 1
else:
errors = 1
classname = result.case.uid.replace("/", ".")
classname = classname.removesuffix('.py')
filename = os.path.join(self.outdir, f'{result.case.uid}.xml')
if result.case.is_pytest and os.path.exists(filename):
return # pytest itself already writes the junit file! create one if pytest did not
dirname = os.path.dirname(filename)
try:
os.makedirs(dirname)
except OSError as ex:
if ex.errno != errno.EEXIST:
raise
if result.case.external_junit and os.path.exists(result.case.external_junit):
shutil.copyfile(result.case.external_junit, filename)
return
with open(filename, 'w') as f_report:
xml = XMLGenerator(f_report, encoding='utf-8')
xml.startDocument()
xml.startElement('testsuite', AttributesImpl({
'name': classname,
'tests': '%d' % (1,),
'failures': '%d' % (failures,),
'errors': '%d' % (errors,),
'time': f'{result.duration / 1000.0:0.3f}',
'disabled': '%d' % (disabled,),
'skipped': '%d' % (skipped,),
'timestamp': self.now.isoformat(),
'hostname': os.uname()[1],
}))
xml.startElement('properties', AttributesImpl({}))
xml.startElement('property', AttributesImpl({
'name': 'hostname',
'value': result.environment.hostname,
}))
xml.endElement('property')
xml.startElement('property', AttributesImpl({
'name': 'architecture',
'value': result.environment.architecture,
}))
xml.endElement('property')
xml.startElement('property', AttributesImpl({
'name': 'role',
'value': result.environment.role,
}))
xml.endElement('property')
xml.startElement('property', AttributesImpl({
'name': 'version',
'value': f'{result.environment.ucs_version}',
}))
xml.endElement('property')
if result.case.description:
xml.startElement('property', AttributesImpl({
'name': 'description',
'value': result.case.description or result.case.uid,
}))
xml.endElement('property')
xml.endElement('properties')
xml.startElement('testcase', AttributesImpl({
'name': result.environment.hostname,
# 'assertions': '%d' % (0,),
'time': f'{result.duration / 1000.0:0.3f}',
'classname': classname,
# 'status': '???',
}))
if skipped:
try:
_mime, content = result.artifacts['check']
except KeyError:
msg = ''
else:
msg = '\n'.join([f'{c}' for c in content])
xml.startElement('skipped', AttributesImpl({
'message': msg,
}))
xml.endElement('skipped')
elif errors:
xml.startElement('error', AttributesImpl({
'type': 'TestError',
'message': f'{result.result}',
}))
xml.endElement('error')
elif failures:
xml.startElement('failure', AttributesImpl({
'type': 'TestFailure',
'message': f'{result.reason!s} ({result.case.description or result.case.uid})',
}))
xml.endElement('failure')
try:
_mime, content = result.artifacts['stdout']
except KeyError:
pass
else:
xml.startElement('system-out', AttributesImpl({}))
xml.characters(self.utf8(content))
xml.endElement('system-out')
try:
_mime, content = result.artifacts['stderr']
except KeyError:
pass
else:
xml.startElement('system-err', AttributesImpl({}))
xml.characters(self.utf8(content))
xml.endElement('system-err')
xml.endElement('testcase')
xml.endElement('testsuite')
xml.endDocument()
super().end_test(result)
[docs]
def utf8(self, data: Any) -> str:
if isinstance(data, str):
data = data.encode('utf-8', 'replace').decode('utf-8')
elif isinstance(data, bytes):
data = data.decode('utf-8', 'replace').encode('utf-8')
return data
if __name__ == '__main__':
import doctest
doctest.testmod()