From e740136dd0a1f5db15d003514cbd64f28774875f Mon Sep 17 00:00:00 2001 From: Patrick Lehmann Date: Thu, 3 Oct 2024 22:54:50 +0200 Subject: [PATCH] Added more doc-strings and parameter checks. --- doc/Unittesting/index.rst | 32 +- doc/index.rst | 12 +- pyEDAA/Reports/OSVVM/AlertLog.py | 6 +- pyEDAA/Reports/Unittesting/JUnit/AntJUnit.py | 65 +-- .../Reports/Unittesting/JUnit/CTestJUnit.py | 65 +-- .../Unittesting/JUnit/GoogleTestJUnit.py | 65 +-- .../Reports/Unittesting/JUnit/PyTestJUnit.py | 65 +-- pyEDAA/Reports/Unittesting/JUnit/__init__.py | 449 +++++++++++++++-- pyEDAA/Reports/Unittesting/OSVVM.py | 10 +- pyEDAA/Reports/Unittesting/__init__.py | 460 +++++++++++++++--- tests/unit/Unittesting/JUnit.py | 4 +- 11 files changed, 979 insertions(+), 254 deletions(-) diff --git a/doc/Unittesting/index.rst b/doc/Unittesting/index.rst index decd7f69..72d5dfe3 100644 --- a/doc/Unittesting/index.rst +++ b/doc/Unittesting/index.rst @@ -46,8 +46,8 @@ the unified data model. :ref:`UNITTEST/DataModel/Testcase` ^^^ - A :dfn:`test case` is the leaf-element in the test entity hierarchy and describes a single test run. Test - cases are grouped by test suites. + A :dfn:`test case` is the leaf-element in the test entity hierarchy and describes an individual test run. + Test cases are grouped by test suites. .. grid-item-card:: :columns: 6 @@ -136,16 +136,16 @@ Common test suite's :data:`~pyEDAA.Reports.Unittesting.TestsuiteBase.Kind` field. :data:`~pyEDAA.Reports.Unittesting.Base.StartTime` - Every test entity has a time when it was started. In case of a test case, it's the time when a single test was - run. In case of a test suite, it's the time when the first test within this test suite was started. In case of a - test suite summary, it's the time when the whole regression test was started. + Every test entity has a time when it was started. In case of a test case, it's the time when an individual test + was run. In case of a test suite, it's the time when the first test within this test suite was started. In case + of a test suite summary, it's the time when the whole regression test was started. If the start time is unknown, set this value to ``None``. :data:`~pyEDAA.Reports.Unittesting.Base.SetupDuration` - Every test entity has a field to capture the setup duration of a test run. In case of a test case, it's the time - spend on setting up a single test run. In case of a test suite, it's the duration spend on preparing the group - of tests for the first test run. + Every test entity has a field to capture the setup duration of a test run. In case of a test case, it's the + time spend on setting up an individual test run. In case of a test suite, it's the duration spend on preparing + the group of tests for the first test run. If the setup duration can't be distinguished from the test's runtime, set this value to ``None``. @@ -156,8 +156,8 @@ Common :data:`~pyEDAA.Reports.Unittesting.Base.TeardownDuration` Every test entity has a field to capture the teardown duration of a test run. In case of a test case, it's the - time spend on tearing down a single test run. In case of a test suite, it's the duration spend on finalizing the - group of tests after the last test run. + time spend on tearing down an individual test run. In case of a test suite, it's the duration spend on + finalizing the group of tests after the last test run. If the teardown duration can't be distinguished from the test's runtime, set this value to ``None``. @@ -338,8 +338,8 @@ Testcase .. grid-item:: :columns: 6 - A :class:`~pyEDAA.Reports.Unittesting.Testcase` is the leaf-element in the test entity hierarchy and describes a - single test run. Test cases are grouped by test suites. + A :class:`~pyEDAA.Reports.Unittesting.Testcase` is the leaf-element in the test entity hierarchy and describes an + individual test run. Test cases are grouped by test suites. :data:`~pyEDAA.Reports.Unittesting.Testcase.Status` The overall status of a test case. @@ -577,10 +577,10 @@ Document :data:`~pyEDAA.Reports.Unittesting.Document.ModelConversionDuration` tbd - :meth:`~pyEDAA.Reports.Unittesting.Document.Read` + :meth:`~pyEDAA.Reports.Unittesting.Document.Analyze` tbd - :meth:`~pyEDAA.Reports.Unittesting.Document.Parse` + :meth:`~pyEDAA.Reports.Unittesting.Document.Convert` tbd .. grid-item:: @@ -606,11 +606,11 @@ Document ... @abstractmethod - def Read(self) -> None: + def Analyze(self) -> None: ... @abstractmethod - def Parse(self): + def Convert(self): ... diff --git a/doc/index.rst b/doc/index.rst index 078706be..d9e01d34 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -131,6 +131,12 @@ Report Formats * :ref:`Transform the hierarchy of reports ` * :ref:`Write Ant JUnit reports (also to other dialects) ` + .. rubric:: Supported file formats + + * :ref:`Ant JUnit4 XML format and various dialects ` + * :ref:`JUnit5 XML format (Open Test Reporting) ` + * :ref:`OSVVM YAML format ` + .. rubric:: Supported tools * :ref:`CTest ` @@ -140,12 +146,6 @@ Report Formats * :ref:`OSVVM ` * :ref:`pyTest ` - .. rubric:: Supported file formats - - * :ref:`Ant JUnit4 XML format and various dialects ` - * :ref:`JUnit5 XML format (Open Test Reporting) ` - * :ref:`OSVVM YAML format ` - .. #grid-item-card:: :columns: 4 diff --git a/pyEDAA/Reports/OSVVM/AlertLog.py b/pyEDAA/Reports/OSVVM/AlertLog.py index d538ea7c..382e96f7 100644 --- a/pyEDAA/Reports/OSVVM/AlertLog.py +++ b/pyEDAA/Reports/OSVVM/AlertLog.py @@ -231,7 +231,7 @@ def __init__(self, filename: Path, parse: bool = False) -> None: self._modelConversion = -1.0 if parse: - self.Read() + self.Analyze() self.Parse() @property @@ -246,7 +246,7 @@ def AnalysisDuration(self) -> timedelta: def ModelConversionDuration(self) -> timedelta: return timedelta(seconds=self._modelConversion) - def Read(self) -> None: + def Analyze(self) -> None: if not self._path.exists(): raise OSVVMException(f"OSVVM AlertLog YAML file '{self._path}' does not exist.") \ from FileNotFoundError(f"File '{self._path}' not found.") @@ -327,7 +327,7 @@ def _ParseIntFieldFromYAML(node: CommentedMap, fieldName: str) -> Nullable[int]: def Parse(self) -> None: if self._yamlDocument is None: ex = OSVVMException(f"OSVVM AlertLog YAML file '{self._path}' needs to be read and analyzed by a YAML parser.") - ex.add_note(f"Call 'Document.Read()' or create document using 'Document(path, parse=True)'.") + ex.add_note(f"Call 'Document.Analyze()' or create the document using 'Document(path, parse=True)'.") raise ex startConversion = perf_counter_ns() diff --git a/pyEDAA/Reports/Unittesting/JUnit/AntJUnit.py b/pyEDAA/Reports/Unittesting/JUnit/AntJUnit.py index b7475a46..0de1cad1 100644 --- a/pyEDAA/Reports/Unittesting/JUnit/AntJUnit.py +++ b/pyEDAA/Reports/Unittesting/JUnit/AntJUnit.py @@ -108,28 +108,6 @@ def Aggregate(self, strict: bool = True) -> TestsuiteAggregateReturnType: return tests, skipped, errored, failed, passed - def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generator[Union[TestsuiteType, Testcase], None, None]: - assert IterationScheme.PreOrder | IterationScheme.PostOrder not in scheme - - if IterationScheme.PreOrder in scheme: - if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites in scheme: - yield self - - if IterationScheme.IncludeTestcases in scheme: - for testcase in self._testclasses.values(): - yield testcase - - for testsuite in self._testsuites.values(): - yield from testsuite.Iterate(scheme | IterationScheme.IncludeSelf) - - if IterationScheme.PostOrder in scheme: - if IterationScheme.IncludeTestcases in scheme: - for testcase in self._testclasses.values(): - yield testcase - - if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites in scheme: - yield self - @classmethod def FromTestsuite(cls, testsuite: ut_Testsuite) -> "Testsuite": juTestsuite = cls( @@ -250,9 +228,19 @@ def ToTestsuiteSummary(self) -> ut_TestsuiteSummary: @export class Document(ju_Document): - _TESTCASE: ClassVar[Type[Testcase]] = Testcase - _TESTCLASS: ClassVar[Type[Testclass]] = Testclass - _TESTSUITE: ClassVar[Type[Testsuite]] = Testsuite + """ + A document reader and writer for the Ant + JUnit4 XML file format. + + This class reads, validates and transforms an XML file in the Ant + JUnit4 format into a JUnit data model. It can then + be converted into a unified test entity data model. + + In reverse, a JUnit data model instance with the specific Ant + JUnit4 file format can be created from a unified test + entity data model. This data model can be written as XML into a file. + """ + + _TESTCASE: ClassVar[Type[Testcase]] = Testcase + _TESTCLASS: ClassVar[Type[Testclass]] = Testclass + _TESTSUITE: ClassVar[Type[Testsuite]] = Testsuite @classmethod def FromTestsuiteSummary(cls, xmlReportFile: Path, testsuiteSummary: ut_TestsuiteSummary): @@ -271,9 +259,19 @@ def FromTestsuiteSummary(cls, xmlReportFile: Path, testsuiteSummary: ut_Testsuit return doc - def Read(self) -> None: + def Analyze(self) -> None: + """ + Analyze the XML file, parse the content into an XML data structure and validate the data structure using an XML + schema. + + .. hint:: + + The time spend for analysis will be made available via property :data:`AnalysisDuration`.. + + The used XML schema definition is specific to the Ant JUnit4 dialect. + """ xmlSchemaFile = "Ant-JUnit.xsd" - self._Read(xmlSchemaFile) + self._Analyze(xmlSchemaFile) def Write(self, path: Nullable[Path] = None, overwrite: bool = False, regenerate: bool = False) -> None: if path is None: @@ -294,10 +292,19 @@ def Write(self, path: Nullable[Path] = None, overwrite: bool = False, regenerate with path.open("wb") as file: file.write(tostring(self._xmlDocument, encoding="utf-8", xml_declaration=True, pretty_print=True)) - def Parse(self) -> None: + def Convert(self) -> None: + """ + Convert the parsed and validated XML data structure into a JUnit test entity hierarchy. + + .. hint:: + + The time spend for model conversion will be made available via property :data:`ModelConversionDuration`. + + :raises UnittestException: If XML was not read and parsed before. + """ if self._xmlDocument is None: ex = UnittestException(f"JUnit XML file '{self._path}' needs to be read and analyzed by an XML parser.") - ex.add_note(f"Call 'JUnitDocument.Read()' or create document using 'JUnitDocument(path, parse=True)'.") + ex.add_note(f"Call 'JUnitDocument.Analyze()' or create the document using 'JUnitDocument(path, parse=True)'.") raise ex startConversion = perf_counter_ns() diff --git a/pyEDAA/Reports/Unittesting/JUnit/CTestJUnit.py b/pyEDAA/Reports/Unittesting/JUnit/CTestJUnit.py index 5485c02c..8f920f32 100644 --- a/pyEDAA/Reports/Unittesting/JUnit/CTestJUnit.py +++ b/pyEDAA/Reports/Unittesting/JUnit/CTestJUnit.py @@ -108,28 +108,6 @@ def Aggregate(self, strict: bool = True) -> TestsuiteAggregateReturnType: return tests, skipped, errored, failed, passed - def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generator[Union[TestsuiteType, Testcase], None, None]: - assert IterationScheme.PreOrder | IterationScheme.PostOrder not in scheme - - if IterationScheme.PreOrder in scheme: - if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites in scheme: - yield self - - if IterationScheme.IncludeTestcases in scheme: - for testcase in self._testclasses.values(): - yield testcase - - for testsuite in self._testsuites.values(): - yield from testsuite.Iterate(scheme | IterationScheme.IncludeSelf) - - if IterationScheme.PostOrder in scheme: - if IterationScheme.IncludeTestcases in scheme: - for testcase in self._testclasses.values(): - yield testcase - - if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites in scheme: - yield self - @classmethod def FromTestsuite(cls, testsuite: ut_Testsuite) -> "Testsuite": juTestsuite = cls( @@ -250,9 +228,19 @@ def ToTestsuiteSummary(self) -> ut_TestsuiteSummary: @export class Document(ju_Document): - _TESTCASE: ClassVar[Type[Testcase]] = Testcase - _TESTCLASS: ClassVar[Type[Testclass]] = Testclass - _TESTSUITE: ClassVar[Type[Testsuite]] = Testsuite + """ + A document reader and writer for the CTest JUnit XML file format. + + This class reads, validates and transforms an XML file in the CTest JUnit format into a JUnit data model. It can then + be converted into a unified test entity data model. + + In reverse, a JUnit data model instance with the specific CTest JUnit file format can be created from a unified test + entity data model. This data model can be written as XML into a file. + """ + + _TESTCASE: ClassVar[Type[Testcase]] = Testcase + _TESTCLASS: ClassVar[Type[Testclass]] = Testclass + _TESTSUITE: ClassVar[Type[Testsuite]] = Testsuite @classmethod def FromTestsuiteSummary(cls, xmlReportFile: Path, testsuiteSummary: ut_TestsuiteSummary): @@ -271,9 +259,19 @@ def FromTestsuiteSummary(cls, xmlReportFile: Path, testsuiteSummary: ut_Testsuit return doc - def Read(self) -> None: + def Analyze(self) -> None: + """ + Analyze the XML file, parse the content into an XML data structure and validate the data structure using an XML + schema. + + .. hint:: + + The time spend for analysis will be made available via property :data:`AnalysisDuration`. + + The used XML schema definition is specific to the CTest JUnit dialect. + """ xmlSchemaFile = "CTest-JUnit.xsd" - self._Read(xmlSchemaFile) + self._Analyze(xmlSchemaFile) def Write(self, path: Nullable[Path] = None, overwrite: bool = False, regenerate: bool = False) -> None: if path is None: @@ -294,10 +292,19 @@ def Write(self, path: Nullable[Path] = None, overwrite: bool = False, regenerate with path.open("wb") as file: file.write(tostring(self._xmlDocument, encoding="utf-8", xml_declaration=True, pretty_print=True)) - def Parse(self) -> None: + def Convert(self) -> None: + """ + Convert the parsed and validated XML data structure into a JUnit test entity hierarchy. + + .. hint:: + + The time spend for model conversion will be made available via property :data:`ModelConversionDuration`. + + :raises UnittestException: If XML was not read and parsed before. + """ if self._xmlDocument is None: ex = UnittestException(f"JUnit XML file '{self._path}' needs to be read and analyzed by an XML parser.") - ex.add_note(f"Call 'JUnitDocument.Read()' or create document using 'JUnitDocument(path, parse=True)'.") + ex.add_note(f"Call 'JUnitDocument.Analyze()' or create the document using 'JUnitDocument(path, parse=True)'.") raise ex startConversion = perf_counter_ns() diff --git a/pyEDAA/Reports/Unittesting/JUnit/GoogleTestJUnit.py b/pyEDAA/Reports/Unittesting/JUnit/GoogleTestJUnit.py index 1332ea39..35718367 100644 --- a/pyEDAA/Reports/Unittesting/JUnit/GoogleTestJUnit.py +++ b/pyEDAA/Reports/Unittesting/JUnit/GoogleTestJUnit.py @@ -108,28 +108,6 @@ def Aggregate(self, strict: bool = True) -> TestsuiteAggregateReturnType: return tests, skipped, errored, failed, passed - def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generator[Union[TestsuiteType, Testcase], None, None]: - assert IterationScheme.PreOrder | IterationScheme.PostOrder not in scheme - - if IterationScheme.PreOrder in scheme: - if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites in scheme: - yield self - - if IterationScheme.IncludeTestcases in scheme: - for testcase in self._testclasses.values(): - yield testcase - - for testsuite in self._testsuites.values(): - yield from testsuite.Iterate(scheme | IterationScheme.IncludeSelf) - - if IterationScheme.PostOrder in scheme: - if IterationScheme.IncludeTestcases in scheme: - for testcase in self._testclasses.values(): - yield testcase - - if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites in scheme: - yield self - @classmethod def FromTestsuite(cls, testsuite: ut_Testsuite) -> "Testsuite": juTestsuite = cls( @@ -250,9 +228,19 @@ def ToTestsuiteSummary(self) -> ut_TestsuiteSummary: @export class Document(ju_Document): - _TESTCASE: ClassVar[Type[Testcase]] = Testcase - _TESTCLASS: ClassVar[Type[Testclass]] = Testclass - _TESTSUITE: ClassVar[Type[Testsuite]] = Testsuite + """ + A document reader and writer for the GoogelTest JUnit XML file format. + + This class reads, validates and transforms an XML file in the GoogelTest JUnit format into a JUnit data model. It can + then be converted into a unified test entity data model. + + In reverse, a JUnit data model instance with the specific GoogelTest JUnit file format can be created from a unified + test entity data model. This data model can be written as XML into a file. + """ + + _TESTCASE: ClassVar[Type[Testcase]] = Testcase + _TESTCLASS: ClassVar[Type[Testclass]] = Testclass + _TESTSUITE: ClassVar[Type[Testsuite]] = Testsuite @classmethod def FromTestsuiteSummary(cls, xmlReportFile: Path, testsuiteSummary: ut_TestsuiteSummary): @@ -271,9 +259,19 @@ def FromTestsuiteSummary(cls, xmlReportFile: Path, testsuiteSummary: ut_Testsuit return doc - def Read(self) -> None: + def Analyze(self) -> None: + """ + Analyze the XML file, parse the content into an XML data structure and validate the data structure using an XML + schema. + + .. hint:: + + The time spend for analysis will be made available via property :data:`AnalysisDuration`. + + The used XML schema definition is specific to the GoogleTest JUnit dialect. + """ xmlSchemaFile = "GoogleTest-JUnit.xsd" - self._Read(xmlSchemaFile) + self._Analyze(xmlSchemaFile) def Write(self, path: Nullable[Path] = None, overwrite: bool = False, regenerate: bool = False) -> None: if path is None: @@ -294,10 +292,19 @@ def Write(self, path: Nullable[Path] = None, overwrite: bool = False, regenerate with path.open("wb") as file: file.write(tostring(self._xmlDocument, encoding="utf-8", xml_declaration=True, pretty_print=True)) - def Parse(self) -> None: + def Convert(self) -> None: + """ + Convert the parsed and validated XML data structure into a JUnit test entity hierarchy. + + .. hint:: + + The time spend for model conversion will be made available via property :data:`ModelConversionDuration`. + + :raises UnittestException: If XML was not read and parsed before. + """ if self._xmlDocument is None: ex = UnittestException(f"JUnit XML file '{self._path}' needs to be read and analyzed by an XML parser.") - ex.add_note(f"Call 'JUnitDocument.Read()' or create document using 'JUnitDocument(path, parse=True)'.") + ex.add_note(f"Call 'JUnitDocument.Analyze()' or create the document using 'JUnitDocument(path, parse=True)'.") raise ex startConversion = perf_counter_ns() diff --git a/pyEDAA/Reports/Unittesting/JUnit/PyTestJUnit.py b/pyEDAA/Reports/Unittesting/JUnit/PyTestJUnit.py index 3fbb511a..0b71ebf1 100644 --- a/pyEDAA/Reports/Unittesting/JUnit/PyTestJUnit.py +++ b/pyEDAA/Reports/Unittesting/JUnit/PyTestJUnit.py @@ -108,28 +108,6 @@ def Aggregate(self, strict: bool = True) -> TestsuiteAggregateReturnType: return tests, skipped, errored, failed, passed - def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generator[Union[TestsuiteType, Testcase], None, None]: - assert IterationScheme.PreOrder | IterationScheme.PostOrder not in scheme - - if IterationScheme.PreOrder in scheme: - if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites in scheme: - yield self - - if IterationScheme.IncludeTestcases in scheme: - for testcase in self._testclasses.values(): - yield testcase - - for testsuite in self._testsuites.values(): - yield from testsuite.Iterate(scheme | IterationScheme.IncludeSelf) - - if IterationScheme.PostOrder in scheme: - if IterationScheme.IncludeTestcases in scheme: - for testcase in self._testclasses.values(): - yield testcase - - if IterationScheme.IncludeSelf | IterationScheme.IncludeTestsuites in scheme: - yield self - @classmethod def FromTestsuite(cls, testsuite: ut_Testsuite) -> "Testsuite": juTestsuite = cls( @@ -250,9 +228,19 @@ def ToTestsuiteSummary(self) -> ut_TestsuiteSummary: @export class Document(ju_Document): - _TESTCASE: ClassVar[Type[Testcase]] = Testcase - _TESTCLASS: ClassVar[Type[Testclass]] = Testclass - _TESTSUITE: ClassVar[Type[Testsuite]] = Testsuite + """ + A document reader and writer for the pyTest JUnit XML file format. + + This class reads, validates and transforms an XML file in the pyTest JUnit format into a JUnit data model. It can then + be converted into a unified test entity data model. + + In reverse, a JUnit data model instance with the specific pyTest JUnit file format can be created from a unified test + entity data model. This data model can be written as XML into a file. + """ + + _TESTCASE: ClassVar[Type[Testcase]] = Testcase + _TESTCLASS: ClassVar[Type[Testclass]] = Testclass + _TESTSUITE: ClassVar[Type[Testsuite]] = Testsuite @classmethod def FromTestsuiteSummary(cls, xmlReportFile: Path, testsuiteSummary: ut_TestsuiteSummary): @@ -271,9 +259,19 @@ def FromTestsuiteSummary(cls, xmlReportFile: Path, testsuiteSummary: ut_Testsuit return doc - def Read(self) -> None: + def Analyze(self) -> None: + """ + Analyze the XML file, parse the content into an XML data structure and validate the data structure using an XML + schema. + + .. hint:: + + The time spend for analysis will be made available via property :data:`AnalysisDuration`. + + The used XML schema definition is specific to the pyTest JUnit dialect. + """ xmlSchemaFile = "PyTest-JUnit.xsd" - self._Read(xmlSchemaFile) + self._Analyze(xmlSchemaFile) def Write(self, path: Nullable[Path] = None, overwrite: bool = False, regenerate: bool = False) -> None: if path is None: @@ -294,10 +292,19 @@ def Write(self, path: Nullable[Path] = None, overwrite: bool = False, regenerate with path.open("wb") as file: file.write(tostring(self._xmlDocument, encoding="utf-8", xml_declaration=True, pretty_print=True)) - def Parse(self) -> None: + def Convert(self) -> None: + """ + Convert the parsed and validated XML data structure into a JUnit test entity hierarchy. + + .. hint:: + + The time spend for model conversion will be made available via property :data:`ModelConversionDuration`. + + :raises UnittestException: If XML was not read and parsed before. + """ if self._xmlDocument is None: ex = UnittestException(f"JUnit XML file '{self._path}' needs to be read and analyzed by an XML parser.") - ex.add_note(f"Call 'JUnitDocument.Read()' or create document using 'JUnitDocument(path, parse=True)'.") + ex.add_note(f"Call 'JUnitDocument.Analyze()' or create the document using 'JUnitDocument(path, parse=True)'.") raise ex startConversion = perf_counter_ns() diff --git a/pyEDAA/Reports/Unittesting/JUnit/__init__.py b/pyEDAA/Reports/Unittesting/JUnit/__init__.py index 17ca2411..105c7abb 100644 --- a/pyEDAA/Reports/Unittesting/JUnit/__init__.py +++ b/pyEDAA/Reports/Unittesting/JUnit/__init__.py @@ -30,7 +30,65 @@ # ==================================================================================================================== # # """ -Reader for JUnit unit testing summary files in XML format. +The pyEDAA.Reports.Unittesting.JUnit package implements a hierarchy of test entities for the JUnit unit testing summary +file format (XML format). This test entity hierarchy is not derived from :class:`pyEDAA.Reports.Unittesting`, because it +doesn't match the unified data model. Nonetheless, both data models can be converted to each other. In addition, derived +data models are provided for the many dialects of that XML file format. See the list modules in this package for the +implemented dialects. + +The test entity hierarchy consists of test cases, test classes, test suites and a test summary. Test cases are the leaf +elements in the hierarchy and represent an individual test run. Next, test classes group test cases, because the +original Ant + JUnit format groups test cases (Java methods) in a Java class. Next, test suites are used to group +multiple test classes. Finally, the root element is a test summary. When such a summary is stored in a file format like +Ant + JUnit4 XML, a file format specific document is derived from a summary class. + +**Data Model** + +.. mermaid:: + + graph TD; + doc[Document] + sum[Summary] + ts1[Testsuite] + ts11[Testsuite] + ts2[Testsuite] + + tc111[Testclass] + tc112[Testclass] + tc23[Testclass] + + tc1111[Testcase] + tc1112[Testcase] + tc1113[Testcase] + tc1121[Testcase] + tc1122[Testcase] + tc231[Testcase] + tc232[Testcase] + tc233[Testcase] + + doc:::root -.-> sum:::summary + sum --> ts1:::suite + sum ---> ts2:::suite + ts1 --> ts11:::suite + + ts11 --> tc111:::cls + ts11 --> tc112:::cls + ts2 --> tc23:::cls + + tc111 --> tc1111:::case + tc111 --> tc1112:::case + tc111 --> tc1113:::case + tc112 --> tc1121:::case + tc112 --> tc1122:::case + tc23 --> tc231:::case + tc23 --> tc232:::case + tc23 --> tc233:::case + + classDef root fill:#4dc3ff + classDef summary fill:#80d4ff + classDef suite fill:#b3e6ff + classDef cls fill:#ff9966 + classDef case fill:#eeccff """ from datetime import datetime, timedelta from enum import Flag @@ -39,8 +97,8 @@ from time import perf_counter_ns from typing import Optional as Nullable, Iterable, Dict, Any, Generator, Tuple, Union, TypeVar, Type, ClassVar -from lxml.etree import XMLParser, parse, XMLSchema, XMLSyntaxError, _ElementTree, _Element, _Comment, XMLSchemaParseError -from lxml.etree import ElementTree, Element, SubElement, tostring +from lxml.etree import XMLParser, parse, XMLSchema, ElementTree, Element, SubElement, tostring +from lxml.etree import XMLSyntaxError, _ElementTree, _Element, _Comment, XMLSchemaParseError from pyTooling.Common import getFullyQualifiedName, getResourceFile from pyTooling.Decorators import export, readonly from pyTooling.Exceptions import ToolingException @@ -48,16 +106,15 @@ from pyTooling.Tree import Node from pyEDAA.Reports import Resources -from pyEDAA.Reports.Unittesting import UnittestException, DuplicateTestsuiteException, DuplicateTestcaseException, \ - TestsuiteKind -from pyEDAA.Reports.Unittesting import TestcaseStatus, TestsuiteStatus, IterationScheme +from pyEDAA.Reports.Unittesting import UnittestException, AlreadyInHierarchyException, DuplicateTestsuiteException, DuplicateTestcaseException +from pyEDAA.Reports.Unittesting import TestcaseStatus, TestsuiteStatus, TestsuiteKind, IterationScheme from pyEDAA.Reports.Unittesting import Document as ut_Document, TestsuiteSummary as ut_TestsuiteSummary from pyEDAA.Reports.Unittesting import Testsuite as ut_Testsuite, Testcase as ut_Testcase @export class JUnitException: - """An exception mixin for JUnit format specific exceptions.""" + """An exception-mixin for JUnit format specific exceptions.""" @export @@ -65,20 +122,50 @@ class UnittestException(UnittestException, JUnitException): pass +@export +class AlreadyInHierarchyException(AlreadyInHierarchyException, JUnitException): + """ + A unit test exception raised if the element is already part of a hierarchy. + + This exception is caused by an inconsistent data model. Elements added to the hierarchy should be part of the same + hierarchy should occur only once in the hierarchy. + + .. hint:: + + This is usually caused by a non-None parent reference. + """ + + @export class DuplicateTestsuiteException(DuplicateTestsuiteException, JUnitException): - pass + """ + A unit test exception raised on duplicate test suites (by name). + + This exception is raised, if a child test suite with same name already exist in the test suite. + + .. hint:: + + Test suite names need to be unique per parent element (test suite or test summary). + """ @export class DuplicateTestcaseException(DuplicateTestcaseException, JUnitException): - pass + """ + A unit test exception raised on duplicate test cases (by name). + + This exception is raised, if a child test case with same name already exist in the test suite. + + .. hint:: + + Test case names need to be unique per parent element (test suite). + """ @export class JUnitReaderMode(Flag): - Default = 0 - DecoupleTestsuiteHierarchyAndTestcaseClassName = 1 + Default = 0 #: Default behavior + DecoupleTestsuiteHierarchyAndTestcaseClassName = 1 #: Undocumented TestsuiteType = TypeVar("TestsuiteType", bound="Testsuite") @@ -88,10 +175,30 @@ class JUnitReaderMode(Flag): @export class Base(metaclass=ExtendedType, slots=True): + """ + Base-class for all test entities (test cases, test classes, test suites, ...). + + It provides a reference to the parent test entity, so bidirectional referencing can be used in the test entity + hierarchy. + + Every test entity has a name to identity it. It's also used in the parent's child element dictionaries to identify the + child. |br| + E.g. it's used as a test case name in the dictionary of test cases in a test class. + """ + _parent: Nullable["Testsuite"] _name: str def __init__(self, name: str, parent: Nullable["Testsuite"] = None): + """ + Initializes the fields of the base-class. + + :param name: Name of the test entity. + :param parent: Reference to the parent test entity. + :raises ValueError: If parameter 'name' is None. + :raises TypeError: If parameter 'name' is not a string. + :raises ValueError: If parameter 'name' is empty. + """ if name is None: raise ValueError(f"Parameter 'name' is None.") elif not isinstance(name, str): @@ -99,33 +206,78 @@ def __init__(self, name: str, parent: Nullable["Testsuite"] = None): if version_info >= (3, 11): # pragma: no cover ex.add_note(f"Got type '{getFullyQualifiedName(name)}'.") raise ex - - # TODO: check parameter parent + elif name.strip() == "": + raise ValueError(f"Parameter 'name' is empty.") self._parent = parent self._name = name @readonly def Parent(self) -> Nullable["Testsuite"]: + """ + Read-only property returning the reference to the parent test entity. + + :return: Reference to the parent entity. + """ return self._parent # QUESTION: allow Parent as setter? @readonly def Name(self) -> str: + """ + Read-only property returning the test entity's name. + + :return: + """ return self._name @export class BaseWithProperties(Base): + """ + Base-class for all test entities supporting properties (test cases, test suites, ...). + + Every test entity has fields for the test duration and number of executed assertions. + + Every test entity offers an internal dictionary for properties. + """ + _duration: Nullable[timedelta] _assertionCount: Nullable[int] _properties: Dict[str, Any] - def __init__(self, name: str, duration: Nullable[timedelta] = None, assertionCount: Nullable[int] = None, parent: Nullable["Testsuite"] = None): + def __init__( + self, + name: str, + duration: Nullable[timedelta] = None, + assertionCount: Nullable[int] = None, + parent: Nullable["Testsuite"] = None + ): + """ + Initializes the fields of the base-class. + + :param name: Name of the test entity. + :param duration: Duration of the entity's execution. + :param assertionCount: Number of assertions within the test. + :param parent: Reference to the parent test entity. + :raises TypeError: If parameter 'duration' is not a timedelta. + :raises TypeError: If parameter 'assertionCount' is not an integer. + """ super().__init__(name, parent) - # TODO: check parameter duration + if duration is not None and not isinstance(duration, timedelta): + ex = TypeError(f"Parameter 'duration' is not of type 'timedelta'.") + if version_info >= (3, 11): # pragma: no cover + ex.add_note(f"Got type '{getFullyQualifiedName(duration)}'.") + raise ex + + if assertionCount is not None and not isinstance(assertionCount, int): + ex = TypeError(f"Parameter 'assertionCount' is not of type 'int'.") + if version_info >= (3, 11): # pragma: no cover + ex.add_note(f"Got type '{getFullyQualifiedName(assertionCount)}'.") + raise ex + self._duration = duration self._assertionCount = assertionCount @@ -133,35 +285,107 @@ def __init__(self, name: str, duration: Nullable[timedelta] = None, assertionCou @readonly def Duration(self) -> timedelta: + """ + Read-only property returning the duration of a test entity run. + + .. note:: + + The JUnit format doesn't distinguish setup, run and teardown durations. + + :return: Duration of the entity's execution. + """ return self._duration @readonly @abstractmethod def AssertionCount(self) -> int: - pass + """ + Read-only property returning the number of assertions (checks) in a test case. + + .. note:: + + The JUnit format doesn't distinguish passed and failed assertions. + + :return: Number of assertions. + """ def __len__(self) -> int: + """ + Returns the number of annotated properties. + + Syntax: :pycode:`length = len(obj)` + + :return: Number of annotated properties. + """ return len(self._properties) - def __getitem__(self, key: str) -> Any: - return self._properties[key] + def __getitem__(self, name: str) -> Any: + """ + Access a property by name. + + Syntax: :pycode:`value = obj[name]` + + :param name: Name if the property. + :return: Value of the accessed property. + """ + return self._properties[name] + + def __setitem__(self, name: str, value: Any) -> None: + """ + Set the value of a property by name. + + If the property doesn't exist yet, it's created. + + Syntax: :pycode:`obj[name] = value` - def __setitem__(self, key: str, value: Any) -> None: - self._properties[key] = value + :param name: Name of the property. + :param value: Value of the property. + """ + self._properties[name] = value - def __delitem__(self, key: str) -> None: - del self._properties[key] + def __delitem__(self, name: str) -> None: + """ + Delete a property by name. - def __contains__(self, key: str) -> bool: - return key in self._properties + Syntax: :pycode:`del obj[name]` + + :param name: Name if the property. + """ + del self._properties[name] + + def __contains__(self, name: str) -> bool: + """ + Returns True, if a property was annotated by this name. + + Syntax: :pycode:`name in obj` + + :param name: Name of the property. + :return: True, if the property was annotated. + """ + return name in self._properties def __iter__(self) -> Generator[Tuple[str, Any], None, None]: + """ + Iterate all annotated properties. + + Syntax: :pycode:`for name, value in obj:` + + :return: A generator of property tuples (name, value). + """ yield from self._properties.items() @export class Testcase(BaseWithProperties): - _classname: str + """ + A testcase is the leaf-entity in the test entity hierarchy representing an individual test run. + + Test cases are grouped by test classes in the test entity hierarchy. These are again grouped by test suites. The root + of the hierarchy is a test summary. + + Every test case has an overall status like unknown, skipped, failed or passed. + """ + _status: TestcaseStatus def __init__( @@ -172,6 +396,17 @@ def __init__( assertionCount: Nullable[int] = None, parent: Nullable["Testclass"] = None ): + """ + Initializes the fields of a test case. + + :param name: Name of the test entity. + :param duration: Duration of the entity's execution. + :param status: Status of the test case. + :param assertionCount: Number of assertions within the test. + :param parent: Reference to the parent test class. + :raises TypeError: If parameter 'parent' is not a Testsuite. + :raises ValueError: If parameter 'assertionCount' is not consistent. + """ if parent is not None: if not isinstance(parent, Testclass): ex = TypeError(f"Parameter 'parent' is not of type 'Testclass'.") @@ -183,20 +418,51 @@ def __init__( super().__init__(name, duration, assertionCount, parent) + if not isinstance(status, TestcaseStatus): + ex = TypeError(f"Parameter 'status' is not of type 'TestcaseStatus'.") + if version_info >= (3, 11): # pragma: no cover + ex.add_note(f"Got type '{getFullyQualifiedName(status)}'.") + raise ex + self._status = status @readonly def Classname(self) -> str: + """ + Read-only property returning the class name of the test case. + + :return: The test case's class name. + + .. note:: + + In the JUnit format, a test case is uniquely identified by a tuple of class name and test case name. This + structure has been decomposed by this data model into 2 leaf-levels in the test entity hierarchy. Thus, the class + name is represented by its own level and instances of test classes. + """ if self._parent is None: raise UnittestException("Standalone Testcase instance is not linked to a Testclass.") return self._parent._name @readonly def Status(self) -> TestcaseStatus: + """ + Read-only property returning the status of the test case. + + :return: The test case's status. + """ return self._status @readonly def AssertionCount(self) -> int: + """ + Read-only property returning the number of assertions (checks) in a test case. + + .. note:: + + The JUnit format doesn't distinguish passed and failed assertions. + + :return: Number of assertions. + """ if self._assertionCount is None: return 0 return self._assertionCount @@ -258,6 +524,14 @@ def __str__(self) -> str: @export class TestsuiteBase(BaseWithProperties): + """ + Base-class for all test suites and for test summaries. + + A test suite is a mid-level grouping element in the test entity hierarchy, whereas the test summary is the root + element in that hierarchy. While a test suite groups test classes, a test summary can only group test suites. Thus, a + test summary contains no test classes and test cases. + """ + _startTime: Nullable[datetime] _status: TestsuiteStatus @@ -275,6 +549,25 @@ def __init__( status: TestsuiteStatus = TestsuiteStatus.Unknown, parent: Nullable["Testsuite"] = None ): + """ + Initializes the based-class fields of a test suite or test summary. + + :param name: Name of the test entity. + :param startTime: Time when the test entity was started. + :param duration: Duration of the entity's execution. + :param status: Overall status of the test entity. + :param parent: Reference to the parent test entity. + :raises TypeError: If parameter 'parent' is not a TestsuiteBase. + """ + if parent is not None: + if not isinstance(parent, TestsuiteBase): + ex = TypeError(f"Parameter 'parent' is not of type 'TestsuiteBase'.") + if version_info >= (3, 11): # pragma: no cover + ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.") + raise ex + + parent._testsuites[name] = self + super().__init__(name, duration, None, parent) self._startTime = startTime @@ -343,6 +636,12 @@ def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generato @export class Testclass(Base): + """ + A test class is a low-level element in the test entity hierarchy representing a group of tests. + + Test classes contain test cases and are grouped by a test suites. + """ + _testcases: Dict[str, "Testcase"] def __init__( @@ -351,12 +650,21 @@ def __init__( testcases: Nullable[Iterable["Testcase"]] = None, parent: Nullable["Testsuite"] = None ): + """ + Initializes the fields of the test class. + + :param classname: Classname of the test entity. + :param parent: Reference to the parent test suite. + :raises ValueError: If parameter 'classname' is None. + :raises TypeError: If parameter 'classname' is not a string. + :raises ValueError: If parameter 'classname' is empty. + """ if parent is not None: - # if not isinstance(parent, Testsuite): - # raise TypeError(f"Parameter 'parent' is not of type 'Testsuite'.") - # if version_info >= (3, 11): # pragma: no cover - # ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.") - # raise ex + if not isinstance(parent, Testsuite): + ex = TypeError(f"Parameter 'parent' is not of type 'Testsuite'.") + if version_info >= (3, 11): # pragma: no cover + ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.") + raise ex parent._testclasses[classname] = self @@ -366,7 +674,7 @@ def __init__( if testcases is not None: for testcase in testcases: if testcase._parent is not None: - raise ValueError(f"Testcase '{testcase._name}' is already part of a testsuite hierarchy.") + raise AlreadyInHierarchyException(f"Testcase '{testcase._name}' is already part of a testsuite hierarchy.") if testcase._name in self._testcases: raise DuplicateTestcaseException(f"Class already contains a testcase with same name '{testcase._name}'.") @@ -376,14 +684,29 @@ def __init__( @readonly def Classname(self) -> str: + """ + Read-only property returning the name of the test class. + + :return: The test class' name. + """ return self._name @readonly def Testcases(self) -> Dict[str, "Testcase"]: + """ + Read-only property returning a reference to the internal dictionary of test cases. + + :return: Reference to the dictionary of test cases. + """ return self._testcases @readonly def TestcaseCount(self) -> int: + """ + Read-only property returning the number of all test cases in the test entity hierarchy. + + :return: Number of test cases. + """ return len(self._testcases) @readonly @@ -432,6 +755,12 @@ def __str__(self) -> str: @export class Testsuite(TestsuiteBase): + """ + A testsuite is a mid-level element in the test entity hierarchy representing a logical group of tests. + + Test suites contain test classes and are grouped by a test summary, which is the root of the hierarchy. + """ + _hostname: str _testclasses: Dict[str, "Testclass"] @@ -445,6 +774,19 @@ def __init__( testclasses: Nullable[Iterable["Testclass"]] = None, parent: Nullable["TestsuiteSummary"] = None ): + """ + Initializes the fields of a test suite. + + :param name: Name of the test suite. + :param startTime: Time when the test suite was started. + :param duration: duration of the entity's execution. + :param status: Overall status of the test suite. + :param parent: Reference to the parent test summary. + :raises TypeError: If parameter 'testcases' is not iterable. + :raises TypeError: If element in parameter 'testcases' is not a Testcase. + :raises AlreadyInHierarchyException: If a test case in parameter 'testcases' is already part of a test entity hierarchy. + :raises DuplicateTestcaseException: If a test case in parameter 'testcases' is already listed (by name) in the list of test cases. + """ if parent is not None: if not isinstance(parent, TestsuiteSummary): ex = TypeError(f"Parameter 'parent' is not of type 'TestsuiteSummary'.") @@ -569,6 +911,14 @@ def Aggregate(self, strict: bool = True) -> TestsuiteAggregateReturnType: return tests, skipped, errored, failed, passed def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generator[Union[TestsuiteType, Testcase], None, None]: + """ + Iterate the test suite and its child elements according to the iteration scheme. + + If no scheme is given, use the default scheme. + + :param scheme: Scheme how to iterate the test suite and its child elements. + :returns: A generator for iterating the results filtered and in the order defined by the iteration scheme. + """ assert IterationScheme.PreOrder | IterationScheme.PostOrder not in scheme if IterationScheme.PreOrder in scheme: @@ -579,8 +929,8 @@ def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generato for testcase in self._testclasses.values(): yield testcase - for testsuite in self._testsuites.values(): - yield from testsuite.Iterate(scheme | IterationScheme.IncludeSelf) + for testclass in self._testclasses.values(): + yield from testclass.Iterate(scheme | IterationScheme.IncludeSelf) if IterationScheme.PostOrder in scheme: if IterationScheme.IncludeTestcases in scheme: @@ -813,8 +1163,8 @@ def __init__(self, xmlReportFile: Path, parse: bool = False, readerMode: JUnitRe self._xmlDocument = None if parse: - self.Read() - self.Parse() + self.Analyze() + self.Convert() @classmethod def FromTestsuiteSummary(cls, xmlReportFile: Path, testsuiteSummary: ut_TestsuiteSummary): @@ -833,11 +1183,21 @@ def FromTestsuiteSummary(cls, xmlReportFile: Path, testsuiteSummary: ut_Testsuit return doc - def Read(self) -> None: + def Analyze(self) -> None: + """ + Analyze the XML file, parse the content into an XML data structure and validate the data structure using an XML + schema. + + .. hint:: + + The time spend for analysis will be made available via property :data:`AnalysisDuration`. + + The used XML schema definition is generic to support "any" dialect. + """ xmlSchemaFile = "Generic-JUnit.xsd" - self._Read(xmlSchemaFile) + self._Analyze(xmlSchemaFile) - def _Read(self, xmlSchemaFile: str) -> None: + def _Analyze(self, xmlSchemaFile: str) -> None: if not self._path.exists(): raise UnittestException(f"JUnit XML file '{self._path}' does not exist.") \ from FileNotFoundError(f"File '{self._path}' not found.") @@ -894,10 +1254,19 @@ def Write(self, path: Nullable[Path] = None, overwrite: bool = False, regenerate with path.open("wb") as file: file.write(tostring(self._xmlDocument, encoding="utf-8", xml_declaration=True, pretty_print=True)) - def Parse(self) -> None: + def Convert(self) -> None: + """ + Convert the parsed and validated XML data structure into a JUnit test entity hierarchy. + + .. hint:: + + The time spend for model conversion will be made available via property :data:`ModelConversionDuration`. + + :raises UnittestException: If XML was not read and parsed before. + """ if self._xmlDocument is None: ex = UnittestException(f"JUnit XML file '{self._path}' needs to be read and analyzed by an XML parser.") - ex.add_note(f"Call 'JUnitDocument.Read()' or create document using 'JUnitDocument(path, parse=True)'.") + ex.add_note(f"Call 'JUnitDocument.Analyze()' or create the document using 'JUnitDocument(path, parse=True)'.") raise ex startConversion = perf_counter_ns() diff --git a/pyEDAA/Reports/Unittesting/OSVVM.py b/pyEDAA/Reports/Unittesting/OSVVM.py index ebbb2ca4..f6ec0fd4 100644 --- a/pyEDAA/Reports/Unittesting/OSVVM.py +++ b/pyEDAA/Reports/Unittesting/OSVVM.py @@ -78,10 +78,10 @@ def __init__(self, yamlReportFile: Path, parse: bool = False) -> None: self._yamlDocument = None if parse: - self.Read() - self.Parse() + self.Analyze() + self.Convert() - def Read(self) -> None: + def Analyze(self) -> None: if not self._path.exists(): raise UnittestException(f"OSVVM YAML file '{self._path}' does not exist.") \ from FileNotFoundError(f"File '{self._path}' not found.") @@ -209,10 +209,10 @@ def _ParseDurationFieldFromYAML(node: CommentedMap, fieldName: str) -> Nullable[ return timedelta(seconds=value) - def Parse(self) -> None: + def Convert(self) -> None: if self._yamlDocument is None: ex = UnittestException(f"OSVVM YAML file '{self._path}' needs to be read and analyzed by a YAML parser.") - ex.add_note(f"Call 'Document.Read()' or create document using 'Document(path, parse=True)'.") + ex.add_note(f"Call 'Document.Analyze()' or create document using 'Document(path, parse=True)'.") raise ex startConversion = perf_counter_ns() diff --git a/pyEDAA/Reports/Unittesting/__init__.py b/pyEDAA/Reports/Unittesting/__init__.py index e5ac644c..60618212 100644 --- a/pyEDAA/Reports/Unittesting/__init__.py +++ b/pyEDAA/Reports/Unittesting/__init__.py @@ -28,7 +28,50 @@ # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # -"""Abstraction of testsuites and testcases.""" +""" +The pyEDAA.Reports.Unittesting package implements a hierarchy of test entities. These are test cases, test suites and a +test summary provided as a class hierarchy. Test cases are the leaf elements in the hierarchy and abstract an +individual test run. Test suites are used to group multiple test cases or other test suites. The root element is a test +summary. When such a summary is stored in a file format like Ant + JUnit4 XML, a file format specific document is +derived from a summary class. + +**Data Model** + +.. mermaid:: + + graph TD; + doc[Document] + sum[Summary] + ts1[Testsuite] + ts2[Testsuite] + ts21[Testsuite] + tc11[Testcase] + tc12[Testcase] + tc13[Testcase] + tc21[Testcase] + tc22[Testcase] + tc211[Testcase] + tc212[Testcase] + tc213[Testcase] + + doc:::root -.-> sum:::summary + sum --> ts1:::suite + sum --> ts2:::suite + ts2 --> ts21:::suite + ts1 --> tc11:::case + ts1 --> tc12:::case + ts1 --> tc13:::case + ts2 --> tc21:::case + ts2 --> tc22:::case + ts21 --> tc211:::case + ts21 --> tc212:::case + ts21 --> tc213:::case + + classDef root fill:#4dc3ff + classDef summary fill:#80d4ff + classDef suite fill:#b3e6ff + classDef case fill:#eeccff +""" from datetime import timedelta, datetime from enum import Flag, IntEnum from pathlib import Path @@ -48,14 +91,44 @@ class UnittestException(ReportException): """Base-exception for all unit test related exceptions.""" +@export +class AlreadyInHierarchyException(UnittestException): + """ + A unit test exception raised if the element is already part of a hierarchy. + + This exception is caused by an inconsistent data model. Elements added to the hierarchy should be part of the same + hierarchy should occur only once in the hierarchy. + + .. hint:: + + This is usually caused by a non-None parent reference. + """ + + @export class DuplicateTestsuiteException(UnittestException): - """A unit test exception raised on duplicate test suites.""" + """ + A unit test exception raised on duplicate test suites (by name). + + This exception is raised, if a child test suite with same name already exist in the test suite. + + .. hint:: + + Test suite names need to be unique per parent element (test suite or test summary). + """ @export class DuplicateTestcaseException(UnittestException): - """A unit test exception raised on duplicate test cases.""" + """ + A unit test exception raised on duplicate test cases (by name). + + This exception is raised, if a child test case with same name already exist in the test suite. + + .. hint:: + + Test case names need to be unique per parent element (test suite). + """ @export @@ -106,17 +179,6 @@ def __matmul__(self, other: "TestcaseStatus") -> "TestcaseStatus": return resolved -@export -class TestsuiteKind(IntEnum): - """Enumeration describing the kind of test suite.""" - Root = 0 - Logical = 1 - Namespace = 2 - Package = 3 - Module = 4 - Class = 5 - - @export class TestsuiteStatus(Flag): """A flag enumeration describing the status of a test suite.""" @@ -143,20 +205,38 @@ class TestsuiteStatus(Flag): Flags = Warned | Errored | Aborted | SetupError | TearDownError +@export +class TestsuiteKind(IntEnum): + """Enumeration describing the kind of test suite.""" + Root = 0 #: Root element of the hierarchy. + Logical = 1 #: Represents a logical unit. + Namespace = 2 #: Represents a namespace. + Package = 3 #: Represents a package. + Module = 4 #: Represents a module. + Class = 5 #: Represents a class. + + @export class IterationScheme(Flag): - """A flag enumeration for selecting the test suite iteration scheme.""" - Unknown = 0 - IncludeSelf = 1 - IncludeTestsuites = 2 - IncludeTestcases = 4 + """ + A flag enumeration for selecting the test suite iteration scheme. - PreOrder = 16 - PostOrder = 32 + When a test entity hierarchy is (recursively) iterated, this iteration scheme describes how to iterate the hierarchy + and what elements to return as a result. + """ + Unknown = 0 #: Neutral element. + IncludeSelf = 1 #: Also include the element itself. + IncludeTestsuites = 2 #: Include test suites into the result. + IncludeTestcases = 4 #: Include test cases into the result. + + Recursive = 8 #: Iterate recursively. + + PreOrder = 16 #: Iterate in pre-order (top-down: current node, then child element left-to-right). + PostOrder = 32 #: Iterate in pre-order (bottom-up: child element left-to-right, then current node). - Default = IncludeTestsuites | IncludeTestcases | PreOrder - TestsuiteDefault = IncludeTestsuites | PreOrder - TestcaseDefault = IncludeTestcases | PreOrder + Default = IncludeTestsuites | Recursive | IncludeTestcases | PreOrder #: Recursively iterate all test entities in pre-order. + TestsuiteDefault = IncludeTestsuites | Recursive | PreOrder #: Recursively iterate only test suites in pre-order. + TestcaseDefault = IncludeTestcases | Recursive | PreOrder #: Recursively iterate only test cases in pre-order. TestsuiteType = TypeVar("TestsuiteType", bound="Testsuite") @@ -173,19 +253,20 @@ class Base(metaclass=ExtendedType, slots=True): hierarchy. Every test entity has a name to identity it. It's also used in the parent's child element dictionaries to identify the - child. E.g. it's used as a test case name in the dictionary of test cases in a test suite. + child. |br| + E.g. it's used as a test case name in the dictionary of test cases in a test suite. Every test entity has fields for time tracking. If known, a start time and a test duration can be set. For more - details, a setup duration and teardown duration can be added. All durations are summed in a total duration field. + details, a setup duration and teardown duration can be added. All durations are summed up in a total duration field. As tests can have warnings and errors or even fail, these messages are counted and aggregated in the test entity hierarchy. - Every test entity offers an internal dictionary for annotations. This feature is for example used by Ant + JUnit4's - XML property fields. + Every test entity offers an internal dictionary for annotations. |br| + This feature is for example used by Ant + JUnit4's XML property fields. """ - _parent: Nullable["Testsuite"] + _parent: Nullable["TestsuiteBase"] _name: str _startTime: Nullable[datetime] @@ -212,7 +293,7 @@ def __init__( errorCount: int = 0, fatalCount: int = 0, keyValuePairs: Nullable[Mapping[str, Any]] = None, - parent: Nullable["Testsuite"] = None + parent: Nullable["TestsuiteBase"] = None ): """ Initializes the fields of the base-class. @@ -222,7 +303,7 @@ def __init__( :param setupDuration: Duration it took to set up the entity. :param testDuration: Duration of the entity's test run. :param teardownDuration: Duration it took to tear down the entity. - :param totalDuration: Total duration of the entity's execution (setup + test + teardown) + :param totalDuration: Total duration of the entity's execution (setup + test + teardown). :param warningCount: Count of encountered warnings. :param errorCount: Count of encountered errors. :param fatalCount: Count of encountered fatal errors. @@ -232,7 +313,15 @@ def __init__( :raises ValueError: If parameter 'name' is None. :raises TypeError: If parameter 'name' is not a string. :raises ValueError: If parameter 'name' is empty. - :raises UnittestException: If parameter 'totalDuration' is not consistent. + :raises TypeError: If parameter 'testDuration' is not a timedelta. + :raises TypeError: If parameter 'setupDuration' is not a timedelta. + :raises TypeError: If parameter 'teardownDuration' is not a timedelta. + :raises TypeError: If parameter 'totalDuration' is not a timedelta. + :raises TypeError: If parameter 'warningCount' is not an integer. + :raises TypeError: If parameter 'errorCount' is not an integer. + :raises TypeError: If parameter 'fatalCount' is not an integer. + :raises TypeError: If parameter 'keyValuePairs' is not a Mapping. + :raises ValueError: If parameter 'totalDuration' is not consistent. """ if parent is not None and not isinstance(parent, TestsuiteBase): @@ -283,13 +372,13 @@ def __init__( if teardownDuration is not None: if totalDuration is not None: if totalDuration < (setupDuration + testDuration + teardownDuration): - raise UnittestException(f"Parameter 'totalDuration' can not be less than the sum of setup, test and teardown durations.") + raise ValueError(f"Parameter 'totalDuration' can not be less than the sum of setup, test and teardown durations.") else: # no total totalDuration = setupDuration + testDuration + teardownDuration # no teardown elif totalDuration is not None: if totalDuration < (setupDuration + testDuration): - raise UnittestException(f"Parameter 'totalDuration' can not be less than the sum of setup and test durations.") + raise ValueError(f"Parameter 'totalDuration' can not be less than the sum of setup and test durations.") # no teardown, no total else: totalDuration = setupDuration + testDuration @@ -297,13 +386,13 @@ def __init__( elif teardownDuration is not None: if totalDuration is not None: if totalDuration < (testDuration + teardownDuration): - raise UnittestException(f"Parameter 'totalDuration' can not be less than the sum of test and teardown durations.") + raise ValueError(f"Parameter 'totalDuration' can not be less than the sum of test and teardown durations.") else: # no setup, no total totalDuration = testDuration + teardownDuration # no setup, no teardown elif totalDuration is not None: if totalDuration < testDuration: - raise UnittestException(f"Parameter 'totalDuration' can not be less than test durations.") + raise ValueError(f"Parameter 'totalDuration' can not be less than test durations.") else: # no setup, no teardown, no total totalDuration = testDuration # no test @@ -352,7 +441,7 @@ def __init__( # QUESTION: allow Parent as setter? @readonly - def Parent(self) -> Nullable["Testsuite"]: + def Parent(self) -> Nullable["TestsuiteBase"]: """ Read-only property returning the reference to the parent test entity. @@ -486,7 +575,7 @@ def __contains__(self, key: str) -> bool: """ Returns True, if a key-value pairs was annotated by this key. - :param key: Name if the key-value pair. + :param key: Name of the key-value pair. :return: True, if the pair was annotated. """ return key in self._dict @@ -495,7 +584,7 @@ def __iter__(self) -> Generator[Tuple[str, Any], None, None]: """ Iterate all annotated key-value pairs. - :return: A generator of key-value pair tuples. + :return: A generator of key-value pair tuples (key, value). """ yield from self._dict.items() @@ -517,7 +606,7 @@ def __str__(self) -> str: @export class Testcase(Base): """ - A testcase is leaf-entity in the test entity hierarchy representing an individual test run. + A testcase is the leaf-entity in the test entity hierarchy representing an individual test run. Test cases are grouped by test suites in the test entity hierarchy. The root of the hierarchy is a test summary. @@ -568,6 +657,8 @@ def __init__( :param fatalCount: Count of encountered fatal errors. :param keyValuePairs: Mapping of key-value pairs to initialize the test case. :param parent: Reference to the parent test suite. + :raises TypeError: If parameter 'parent' is not a Testsuite. + :raises ValueError: If parameter 'assertionCount' is not consistent. """ if parent is not None: @@ -593,8 +684,32 @@ def __init__( parent ) + if not isinstance(status, TestcaseStatus): + ex = TypeError(f"Parameter 'status' is not of type 'TestcaseStatus'.") + if version_info >= (3, 11): # pragma: no cover + ex.add_note(f"Got type '{getFullyQualifiedName(status)}'.") + raise ex + self._status = status + if assertionCount is not None and not isinstance(assertionCount, int): + ex = TypeError(f"Parameter 'assertionCount' is not of type 'int'.") + if version_info >= (3, 11): # pragma: no cover + ex.add_note(f"Got type '{getFullyQualifiedName(assertionCount)}'.") + raise ex + + if failedAssertionCount is not None and not isinstance(failedAssertionCount, int): + ex = TypeError(f"Parameter 'failedAssertionCount' is not of type 'int'.") + if version_info >= (3, 11): # pragma: no cover + ex.add_note(f"Got type '{getFullyQualifiedName(failedAssertionCount)}'.") + raise ex + + if passedAssertionCount is not None and not isinstance(passedAssertionCount, int): + ex = TypeError(f"Parameter 'passedAssertionCount' is not of type 'int'.") + if version_info >= (3, 11): # pragma: no cover + ex.add_note(f"Got type '{getFullyQualifiedName(passedAssertionCount)}'.") + raise ex + self._assertionCount = assertionCount if assertionCount is not None: if failedAssertionCount is not None: @@ -781,6 +896,11 @@ def __init__( :param testsuites: List of test suites to initialize the test entity with. :param keyValuePairs: Mapping of key-value pairs to initialize the test entity with. :param parent: Reference to the parent test entity. + :raises TypeError: If parameter 'parent' is not a TestsuiteBase. + :raises TypeError: If parameter 'testsuites' is not iterable. + :raises TypeError: If element in parameter 'testsuites' is not a Testsuite. + :raises AlreadyInHierarchyException: If a test suite in parameter 'testsuites' is already part of a test entity hierarchy. + :raises DuplicateTestsuiteException: If a test suite in parameter 'testsuites' is already listed (by name) in the list of test suites. """ if parent is not None: if not isinstance(parent, TestsuiteBase): @@ -810,9 +930,21 @@ def __init__( self._testsuites = {} if testsuites is not None: + if not isinstance(testsuites, Iterable): + ex = TypeError(f"Parameter 'testsuites' is not iterable.") + if version_info >= (3, 11): # pragma: no cover + ex.add_note(f"Got type '{getFullyQualifiedName(testsuites)}'.") + raise ex + for testsuite in testsuites: + if not isinstance(testsuite, Testsuite): + ex = TypeError(f"Element of parameter 'testsuites' is not of type 'Testsuite'.") + if version_info >= (3, 11): # pragma: no cover + ex.add_note(f"Got type '{getFullyQualifiedName(testsuite)}'.") + raise ex + if testsuite._parent is not None: - raise ValueError(f"Testsuite '{testsuite._name}' is already part of a testsuite hierarchy.") + raise AlreadyInHierarchyException(f"Testsuite '{testsuite._name}' is already part of a testsuite hierarchy.") if testsuite._name in self._testsuites: raise DuplicateTestsuiteException(f"Testsuite already contains a testsuite with same name '{testsuite._name}'.") @@ -832,35 +964,81 @@ def __init__( @readonly def Kind(self) -> TestsuiteKind: + """ + Read-only property returning the kind of the test suite. + + Test suites are used to group test cases. This grouping can be due to language/framework specifics like tests + grouped by a module file or namespace. Others might be just logically grouped without any relation to a programming + language construct. + + Test summaries always return kind ``Root``. + + :return: Kind of the test suite. + """ return self._kind @readonly def Status(self) -> TestsuiteStatus: + """ + Read-only property returning the aggregated overall status of the test suite. + + :return: Overall status of the test suite. + """ return self._status @readonly def Testsuites(self) -> Dict[str, TestsuiteType]: + """ + Read-only property returning a reference to the internal dictionary of test suites. + + :return: Reference to the dictionary of test suite. + """ return self._testsuites @readonly def TestsuiteCount(self) -> int: + """ + Read-only property returning the number of all test suites in the test suite hierarchy. + + :return: Number of test suites. + """ return 1 + sum(testsuite.TestsuiteCount for testsuite in self._testsuites.values()) @readonly def TestcaseCount(self) -> int: + """ + Read-only property returning the number of all test cases in the test entity hierarchy. + + :return: Number of test cases. + """ return sum(testsuite.TestcaseCount for testsuite in self._testsuites.values()) @readonly def AssertionCount(self) -> int: + """ + Read-only property returning the number of all assertions in all test cases in the test entity hierarchy. + + :return: Number of assertions in all test cases. + """ return sum(ts.AssertionCount for ts in self._testsuites.values()) @readonly def FailedAssertionCount(self) -> int: + """ + Read-only property returning the number of all failed assertions in all test cases in the test entity hierarchy. + + :return: Number of failed assertions in all test cases. + """ raise NotImplementedError() # return self._assertionCount - (self._warningCount + self._errorCount + self._fatalCount) @readonly def PassedAssertionCount(self) -> int: + """ + Read-only property returning the number of all passed assertions in all test cases in the test entity hierarchy. + + :return: Number of passed assertions in all test cases. + """ raise NotImplementedError() # return self._assertionCount - (self._warningCount + self._errorCount + self._fatalCount) @@ -870,30 +1048,65 @@ def Tests(self) -> int: @readonly def Inconsistent(self) -> int: + """ + Read-only property returning the number of inconsistent tests in the test suite hierarchy. + + :return: Number of inconsistent tests. + """ return self._inconsistent @readonly def Excluded(self) -> int: + """ + Read-only property returning the number of excluded tests in the test suite hierarchy. + + :return: Number of excluded tests. + """ return self._excluded @readonly def Skipped(self) -> int: + """ + Read-only property returning the number of skipped tests in the test suite hierarchy. + + :return: Number of skipped tests. + """ return self._skipped @readonly def Errored(self) -> int: + """ + Read-only property returning the number of tests with errors in the test suite hierarchy. + + :return: Number of errored tests. + """ return self._errored @readonly def Weak(self) -> int: + """ + Read-only property returning the number of weak tests in the test suite hierarchy. + + :return: Number of weak tests. + """ return self._weak @readonly def Failed(self) -> int: + """ + Read-only property returning the number of failed tests in the test suite hierarchy. + + :return: Number of failed tests. + """ return self._failed @readonly def Passed(self) -> int: + """ + Read-only property returning the number of passed tests in the test suite hierarchy. + + :return: Number of passed tests. + """ return self._passed @readonly @@ -911,7 +1124,7 @@ def FatalCount(self) -> int: raise NotImplementedError() # return self._fatalCount - def Aggregate(self) -> TestsuiteAggregateReturnType: + def Aggregate(self, strict: bool = True) -> TestsuiteAggregateReturnType: tests = 0 inconsistent = 0 excluded = 0 @@ -928,7 +1141,7 @@ def Aggregate(self) -> TestsuiteAggregateReturnType: totalDuration = timedelta() for testsuite in self._testsuites.values(): - t, i, ex, s, e, w, f, p, wc, ec, fc, td = testsuite.Aggregate() + t, i, ex, s, e, w, f, p, wc, ec, fc, td = testsuite.Aggregate(strict) tests += t inconsistent += i excluded += ex @@ -947,8 +1160,25 @@ def Aggregate(self) -> TestsuiteAggregateReturnType: return tests, inconsistent, excluded, skipped, errored, weak, failed, passed, warningCount, errorCount, fatalCount, totalDuration def AddTestsuite(self, testsuite: TestsuiteType) -> None: + """ + Add a test suite to the list of test suites. + + :param testsuite: The test suite to add. + :raises ValueError: If parameter 'testsuite' is None. + :raises TypeError: If parameter 'testsuite' is not a Testsuite. + :raises AlreadyInHierarchyException: If parameter 'testsuite' is already part of a test entity hierarchy. + :raises DuplicateTestcaseException: If parameter 'testsuite' is already listed (by name) in the list of test suites. + """ + if testsuite is None: + raise ValueError("Parameter 'testsuite' is None.") + elif not isinstance(testsuite, Testsuite): + ex = TypeError(f"Parameter 'testsuite' is not of type 'Testsuite'.") + if version_info >= (3, 11): # pragma: no cover + ex.add_note(f"Got type '{getFullyQualifiedName(testsuite)}'.") + raise ex + if testsuite._parent is not None: - raise ValueError(f"Testsuite '{testsuite._name}' is already part of a testsuite hierarchy.") + raise AlreadyInHierarchyException(f"Testsuite '{testsuite._name}' is already part of a testsuite hierarchy.") if testsuite._name in self._testsuites: raise DuplicateTestsuiteException(f"Testsuite already contains a testsuite with same name '{testsuite._name}'.") @@ -957,22 +1187,23 @@ def AddTestsuite(self, testsuite: TestsuiteType) -> None: self._testsuites[testsuite._name] = testsuite def AddTestsuites(self, testsuites: Iterable[TestsuiteType]) -> None: - for testsuite in testsuites: - self.AddTestsuite(testsuite) - - def AddTestcase(self, testcase: "Testcase") -> None: - if testcase._parent is not None: - raise ValueError(f"Testcase '{testcase._name}' is already part of a testsuite hierarchy.") - - if testcase._name in self._testcases: - raise DuplicateTestcaseException(f"Testsuite already contains a testcase with same name '{testcase._name}'.") + """ + Add a list of test suites to the list of test suites. - testcase._parent = self - self._testcases[testcase._name] = testcase + :param testsuites: List of test suites to add. + :raises ValueError: If parameter 'testsuites' is None. + :raises TypeError: If parameter 'testsuites' is not iterable. + """ + if testsuites is None: + raise ValueError("Parameter 'testsuites' is None.") + elif not isinstance(testsuites, Iterable): + ex = TypeError(f"Parameter 'testsuites' is not iterable.") + if version_info >= (3, 11): # pragma: no cover + ex.add_note(f"Got type '{getFullyQualifiedName(testsuites)}'.") + raise ex - def AddTestcases(self, testcases: Iterable["Testcase"]) -> None: - for testcase in testcases: - self.AddTestcase(testcase) + for testsuite in testsuites: + self.AddTestsuite(testsuite) @abstractmethod def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generator[Union[TestsuiteType, Testcase], None, None]: @@ -1008,10 +1239,10 @@ def convertTestsuite(testsuite: Testsuite, parentNode: Node) -> None: @export class Testsuite(TestsuiteBase[TestsuiteType]): """ - A testsuite is an intermediate element in the test entity hierarchy representing a group of tests. + A testsuite is a mid-level element in the test entity hierarchy representing a group of tests. - Test suites contain test cases and optionally other test suites. Test suites can be grouped by test suites. The root - of hierarchy is a test summary. + Test suites contain test cases and optionally other test suites. Test suites can be grouped by test suites to form a + hierarchy of test entities. The root of the hierarchy is a test summary. """ _testcases: Dict[str, "Testcase"] @@ -1052,6 +1283,10 @@ def __init__( :param testcases: List of test cases to initialize the test suite with. :param keyValuePairs: Mapping of key-value pairs to initialize the test suite with. :param parent: Reference to the parent test entity. + :raises TypeError: If parameter 'testcases' is not iterable. + :raises TypeError: If element in parameter 'testcases' is not a Testcase. + :raises AlreadyInHierarchyException: If a test case in parameter 'testcases' is already part of a test entity hierarchy. + :raises DuplicateTestcaseException: If a test case in parameter 'testcases' is already listed (by name) in the list of test cases. """ super().__init__( name, @@ -1074,9 +1309,21 @@ def __init__( self._testcases = {} if testcases is not None: + if not isinstance(testcases, Iterable): + ex = TypeError(f"Parameter 'testcases' is not iterable.") + if version_info >= (3, 11): # pragma: no cover + ex.add_note(f"Got type '{getFullyQualifiedName(testcases)}'.") + raise ex + for testcase in testcases: + if not isinstance(testcase, Testcase): + ex = TypeError(f"Element of parameter 'testcases' is not of type 'Testcase'.") + if version_info >= (3, 11): # pragma: no cover + ex.add_note(f"Got type '{getFullyQualifiedName(testcase)}'.") + raise ex + if testcase._parent is not None: - raise ValueError(f"Testcase '{testcase._name}' is already part of a testsuite hierarchy.") + raise AlreadyInHierarchyException(f"Testcase '{testcase._name}' is already part of a testsuite hierarchy.") if testcase._name in self._testcases: raise DuplicateTestcaseException(f"Testsuite already contains a testcase with same name '{testcase._name}'.") @@ -1086,10 +1333,20 @@ def __init__( @readonly def Testcases(self) -> Dict[str, "Testcase"]: + """ + Read-only property returning a reference to the internal dictionary of test cases. + + :return: Reference to the dictionary of test cases. + """ return self._testcases @readonly def TestcaseCount(self) -> int: + """ + Read-only property returning the number of all test cases in the test entity hierarchy. + + :return: Number of test cases. + """ return super().TestcaseCount + len(self._testcases) @readonly @@ -1176,6 +1433,52 @@ def Aggregate(self, strict: bool = True) -> TestsuiteAggregateReturnType: return tests, inconsistent, excluded, skipped, errored, weak, failed, passed, warningCount, errorCount, fatalCount, totalDuration + def AddTestcase(self, testcase: "Testcase") -> None: + """ + Add a test case to the list of test cases. + + :param testcase: The test case to add. + :raises ValueError: If parameter 'testcase' is None. + :raises TypeError: If parameter 'testcase' is not a Testcase. + :raises AlreadyInHierarchyException: If parameter 'testcase' is already part of a test entity hierarchy. + :raises DuplicateTestcaseException: If parameter 'testcase' is already listed (by name) in the list of test cases. + """ + if testcase is None: + raise ValueError("Parameter 'testcase' is None.") + elif not isinstance(testcase, Testcase): + ex = TypeError(f"Parameter 'testcase' is not of type 'Testcase'.") + if version_info >= (3, 11): # pragma: no cover + ex.add_note(f"Got type '{getFullyQualifiedName(testcase)}'.") + raise ex + + if testcase._parent is not None: + raise ValueError(f"Testcase '{testcase._name}' is already part of a testsuite hierarchy.") + + if testcase._name in self._testcases: + raise DuplicateTestcaseException(f"Testsuite already contains a testcase with same name '{testcase._name}'.") + + testcase._parent = self + self._testcases[testcase._name] = testcase + + def AddTestcases(self, testcases: Iterable["Testcase"]) -> None: + """ + Add a list of test cases to the list of test cases. + + :param testcases: List of test cases to add. + :raises ValueError: If parameter 'testcases' is None. + :raises TypeError: If parameter 'testcases' is not iterable. + """ + if testcases is None: + raise ValueError("Parameter 'testcases' is None.") + elif not isinstance(testcases, Iterable): + ex = TypeError(f"Parameter 'testcases' is not iterable.") + if version_info >= (3, 11): # pragma: no cover + ex.add_note(f"Got type '{getFullyQualifiedName(testcases)}'.") + raise ex + + for testcase in testcases: + self.AddTestcase(testcase) + def Iterate(self, scheme: IterationScheme = IterationScheme.Default) -> Generator[Union[TestsuiteType, Testcase], None, None]: assert IterationScheme.PreOrder | IterationScheme.PostOrder not in scheme @@ -1335,27 +1638,52 @@ def __init__(self, path: Path): @readonly def Path(self) -> Path: + """ + Read-only property returning the path to the file of this document. + + :return: The document's path to the file. + """ return self._path @readonly def AnalysisDuration(self) -> timedelta: + """ + Read-only property returning analysis duration. + + .. note:: + + This includes usually the duration to validate and parse the file format, but it excludes the time to convert the + content to the test entity hierarchy. + + :return: Duration to analyze the document. + """ return timedelta(seconds=self._analysisDuration) @readonly def ModelConversionDuration(self) -> timedelta: + """ + Read-only property returning conversion duration. + + .. note:: + + This includes usually the duration to convert the document's content to the test entity hierarchy. It might also + include the duration to (re-)aggregate all states and statistics in the hierarchy. + + :return: Duration to convert the document. + """ return timedelta(seconds=self._modelConversion) @abstractmethod - def Read(self) -> None: - pass + def Analyze(self) -> None: + """Analyze and validate the document's content.""" # @abstractmethod # def Write(self, path: Nullable[Path] = None, overwrite: bool = False): # pass @abstractmethod - def Parse(self): - pass + def Convert(self): + """Convert the document's content to an instance of the test entity hierarchy.""" @export diff --git a/tests/unit/Unittesting/JUnit.py b/tests/unit/Unittesting/JUnit.py index fd7f6e74..f6d739e4 100644 --- a/tests/unit/Unittesting/JUnit.py +++ b/tests/unit/Unittesting/JUnit.py @@ -436,10 +436,10 @@ def test_Create_WithoutParse(self) -> None: self.assertLess(doc.AnalysisDuration, zeroTime) self.assertLess(doc.ModelConversionDuration, zeroTime) - doc.Read() + doc.Analyze() self.assertGreater(doc.AnalysisDuration, zeroTime) - doc.Parse() + doc.Convert() self.assertGreater(doc.ModelConversionDuration, zeroTime) def test_Create_WithParse(self) -> None: