diff --git a/src/dltlyse/core/analyser.py b/src/dltlyse/core/analyser.py index a91b3ce..6166638 100644 --- a/src/dltlyse/core/analyser.py +++ b/src/dltlyse/core/analyser.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022. BMW Car IT GmbH. All rights reserved. +# Copyright (C) 2022-23. BMW Car IT GmbH. All rights reserved. """DLT file analyser""" from contextlib import contextmanager @@ -428,7 +428,7 @@ def run_analyse(self, traces, xunit, no_sort, is_live, testsuite_name="dltlyse") # for the method (e.g. avoid to access attribute with dots, function # inlining, loop unrolling, ...). The inner most loop is called over # 10 million times when the input file is large. Any small/tiny change - # could causes performance pentlty. + # could causes performance penalty. filters = self.get_filters() # add filter for lifecycle start message in case it is missing diff --git a/src/dltlyse/core/report.py b/src/dltlyse/core/report.py index 4c60e8b..31a93a7 100644 --- a/src/dltlyse/core/report.py +++ b/src/dltlyse/core/report.py @@ -1,8 +1,11 @@ -# Copyright (C) 2022. BMW Car IT GmbH. All rights reserved. +# Copyright (C) 2022-23. BMW Car IT GmbH. All rights reserved. """Reporting for dltlyse""" -from collections import Counter +import datetime as dt import logging +import socket import xml.etree.ElementTree as etree +from collections import Counter + ATTACHMENT_TEMPLATE = "[[ATTACHMENT|{filename}]]" @@ -67,6 +70,7 @@ def __init__( message="", metadata=None, attach=None, + timestamp=None, ): self.classname = classname self.testname = testname @@ -79,6 +83,7 @@ def __init__( self.attach = attach self.metadata = Metadata(metadata) + self.timestamp = str(dt.datetime.now(dt.timezone.utc)) if timestamp is None else str(timestamp) def __repr__(self): return repr(self.__dict__) @@ -99,7 +104,9 @@ def render_xml(self): self.state = "error" # Prepare test case root - root = etree.Element("testcase", classname="dltlyse." + self.classname, name=self.testname, time="0") + root = etree.Element( + "testcase", classname="dltlyse." + self.classname, name=self.testname, time="0", timestamp=self.timestamp + ) # Set attachment root.text = "".join(ATTACHMENT_TEMPLATE.format(filename=filename) for filename in self.attach) @@ -123,10 +130,17 @@ def render_xml(self): class XUnitReport(object): """Template class producing report in xUnit format""" - def __init__(self, outfile="", testsuite_name="dltlyse"): + def __init__( + self, outfile="", testsuite_name="dltlyse", hardware=None, software=None, hostname=None, id_=None, package=None + ): self.results = [] self.outfile = outfile self.testsuite_name = testsuite_name + self.hardware = hardware + self.software = software + self.hostname = socket.gethostname() if hostname is None else hostname + self.id = id_ + self.package = package def add_results(self, results): """Adds a result to the report""" @@ -145,15 +159,25 @@ def _generate_summary(self): def render_xml(self): """Return a xml element to present report""" summary = self._generate_summary() + root_attributes = { + "name": self.testsuite_name, + "tests": summary["number_of_tests"], + "errors": summary["number_of_errors"], + "failures": summary["number_of_failures"], + "skip": summary["number_of_skipped"], + "hostname": self.hostname, + } + if self.id: + root_attributes["id"] = str(self.id) + if self.package: + root_attributes["package"] = str(self.package) - root = etree.Element( - "testsuite", - name=self.testsuite_name, - tests=summary["number_of_tests"], - errors=summary["number_of_errors"], - failures=summary["number_of_failures"], - skip=summary["number_of_skipped"], - ) + root = etree.Element("testsuite", **root_attributes) + + if self.hardware and isinstance(self.hardware, dict): + etree.SubElement(root, "hardware", self.hardware) + if self.software and isinstance(self.software, dict): + etree.SubElement(root, "software", self.software) result_elements = [] for result in self.results: diff --git a/tests/unittests/test_plugin_report.py b/tests/unittests/test_plugin_report.py index 2b02091..f8adfd7 100644 --- a/tests/unittests/test_plugin_report.py +++ b/tests/unittests/test_plugin_report.py @@ -1,8 +1,10 @@ -# Copyright (C) 2022. BMW Car IT GmbH. All rights reserved. +# Copyright (C) 2022-23. BMW Car IT GmbH. All rights reserved. """Test plugin_metadata decorator and xunit report functions""" -import xml.etree.ElementTree as etree +import datetime as dt import inspect +import socket from unittest.mock import patch, mock_open +import xml.etree.ElementTree as etree from dltlyse.core.plugin_base import Plugin, plugin_metadata from dltlyse.core.report import logger, Metadata, Result, XUnitReport @@ -79,6 +81,8 @@ def generate_test_result(attach=None, extra=""): """Prepare test result data and xml string""" attach = attach or [] + timestamp = dt.datetime.now() + result = Result( classname="TestPlugin", testname="TestPlugin-shot-description", @@ -86,15 +90,16 @@ def generate_test_result(attach=None, extra=""): stdout="TestPlugin-stdoutput", message="TestPlugin-success-message", attach=attach, + timestamp=timestamp, ) xml_str = ( - '' + '' "{}" "TestPlugin-stdoutput" "{}" "" - ).format("".join("[[ATTACHMENT|{}]]".format(filename) for filename in attach), extra) + ).format(timestamp, "".join("[[ATTACHMENT|{}]]".format(filename) for filename in attach), extra) return result, xml_str @@ -199,14 +204,16 @@ def test_metadata_render_recursive(): def test_result_equal(): - result = Result() - other = Result(metadata={"key": "should-not-have-effect"}) + timestamp = str(dt.datetime.now()) + result = Result(timestamp=timestamp) + other = Result(metadata={"key": "should-not-have-effect"}, timestamp=timestamp) assert result == other def test_result_render_xml_error_state(): # pylint: disable=invalid-name """Test the warning message when the test state is undefined.""" + result = Result(classname="noclass", state="nostate") with patch.object(logger, "warning") as logger_mock: @@ -220,6 +227,7 @@ def test_result_render_xml_fail(): """Tests that result is rendered when the state is error.""" state = "error" state_type = "error" + timestamp = str(dt.datetime.now()) result = Result( classname="TestPlugin", @@ -227,16 +235,18 @@ def test_result_render_xml_fail(): state=state, stdout="TestPlugin-stdoutput", message="TestPlugin-{}-message".format(state), + timestamp=timestamp, ) assert equal_xml_tree( result.render_xml(), ( - '' + '' '<{state} message="TestPlugin-{state}-message" type="{state_type}"/>' "TestPlugin-stdoutput" "" - ).format(state=state, state_type=state_type), + ).format(timestamp=timestamp, state=state, state_type=state_type), ) @@ -291,7 +301,8 @@ def test_xunit_report_render_xml(): with patch("dltlyse.core.report.Result.render_xml", return_value=etree.Element("testcase")): assert equal_xml_tree( xunit_report.render_xml(), - '', + f'', ) @@ -318,3 +329,38 @@ def test_xunit_report_render(): write_xml = "".join(args[0].decode() for args, _ in mocked_file().write.call_args_list) assert write_xml == "\n" + + +def test_xunit_report_check_software_hardware(): + """Tests that xunit report contains software and hardware sections.""" + xunit_report = XUnitReport( + software={"OS": "Windows NT 3.5 Daytona"}, hardware={"CPU": "Intel 486DX2-66", "RAM": "8Mb"} + ) + xunit_report.outfile = "mocked-file" + + with patch("dltlyse.core.report.open", mock_open()) as mocked_file: + xunit_report.render() + + write_xml = "".join(args[0].decode() for args, _ in mocked_file().write.call_args_list) + print(write_xml) + check_params = [ + '', + '', + ] + for param in check_params: + assert param in write_xml + + +def test_xunit_report_check_testsuite_params(): + """Tests that xunit report contains additional testsuite params.""" + xunit_report = XUnitReport(package="package.gz", id_="some_strange_id", hostname="test1") + xunit_report.outfile = "mocked-file" + + with patch("dltlyse.core.report.open", mock_open()) as mocked_file: + xunit_report.render() + + write_xml = "".join(args[0].decode() for args, _ in mocked_file().write.call_args_list) + print(write_xml) + check_params = ['package="package.gz"', 'id="some_strange_id"', 'hostname="test1"'] + for param in check_params: + assert param in write_xml