Source code for sphinx_reports.Unittest

# ==================================================================================================================== #
#            _     _                                           _                                                       #
#  ___ _ __ | |__ (_)_ __ __  __     _ __ ___ _ __   ___  _ __| |_ ___                                                 #
# / __| '_ \| '_ \| | '_ \\ \/ /____| '__/ _ \ '_ \ / _ \| '__| __/ __|                                                #
# \__ \ |_) | | | | | | | |>  <_____| | |  __/ |_) | (_) | |  | |_\__ \                                                #
# |___/ .__/|_| |_|_|_| |_/_/\_\    |_|  \___| .__/ \___/|_|   \__|___/                                                #
#     |_|                                    |_|                                                                       #
# ==================================================================================================================== #
# Authors:                                                                                                             #
#   Patrick Lehmann                                                                                                    #
#                                                                                                                      #
# License:                                                                                                             #
# ==================================================================================================================== #
# Copyright 2023-2024 Patrick Lehmann - Bötzingen, Germany                                                             #
#                                                                                                                      #
# Licensed under the Apache License, Version 2.0 (the "License");                                                      #
# you may not use this file except in compliance with the License.                                                     #
# You may obtain a copy of the License at                                                                              #
#                                                                                                                      #
#   http://www.apache.org/licenses/LICENSE-2.0                                                                         #
#                                                                                                                      #
# Unless required by applicable law or agreed to in writing, software                                                  #
# distributed under the License is distributed on an "AS IS" BASIS,                                                    #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.                                             #
# See the License for the specific language governing permissions and                                                  #
# limitations under the License.                                                                                       #
#                                                                                                                      #
# SPDX-License-Identifier: Apache-2.0                                                                                  #
# ==================================================================================================================== #
#
"""
**Report unit test results as Sphinx documentation page(s).**
"""
from datetime import timedelta
from pathlib  import Path
from typing   import Dict, Tuple, Any, List, Mapping, Generator, TypedDict

from docutils             import nodes
from pyTooling.Decorators import export

from sphinx_reports.Common             import ReportExtensionError
from sphinx_reports.Sphinx             import strip, BaseDirective
from sphinx_reports.DataModel.Unittest import Testsuite, TestsuiteSummary, Testcase, TestcaseState
from sphinx_reports.Adapter.JUnit      import Analyzer


class report_DictType(TypedDict):
	xml_report: str


[docs] @export class UnittestSummary(BaseDirective): """ This directive will be replaced by a table representing unit test results. """ has_content = False required_arguments = 0 optional_arguments = 1 option_spec = { "reportid": strip, } directiveName: str = "unittest-summary" configPrefix: str = "unittest" configValues: Dict[str, Tuple[Any, str, Any]] = { f"{configPrefix}_testsuites": ({}, "env", Dict) } #: A dictionary of all configuration values used by this domain. (name: (default, rebuilt, type)) _reportID: str _xmlReport: Path _testsuite: TestsuiteSummary def _CheckOptions(self) -> None: # Parse all directive options or use default values self._reportID = self._ParseStringOption("reportid") def _CheckConfiguration(self) -> None: from sphinx_reports import ReportDomain # Check configuration fields and load necessary values try: allTestsuites: Dict[str, report_DictType] = self.config[f"{ReportDomain.name}_{self.configPrefix}_testsuites"] except (KeyError, AttributeError) as ex: raise ReportExtensionError(f"Configuration option '{ReportDomain.name}_{self.configPrefix}_testsuites' is not configured.") from ex try: testsuiteConfiguration = allTestsuites[self._reportID] except KeyError as ex: raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_testsuites: No configuration found for '{self._reportID}'.") from ex try: self._xmlReport = Path(testsuiteConfiguration["xml_report"]) except KeyError as ex: raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_testsuites:{self._reportID}.xml_report: Configuration is missing.") from ex if not self._xmlReport.exists(): raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_testsuites:{self._reportID}.xml_report: Unittest report file '{self._xmlReport}' doesn't exist.") from FileNotFoundError(self._xmlReport) def _GenerateTestSummaryTable(self) -> nodes.table: # Create a table and table header with 8 columns table, tableGroup = self._PrepareTable( identifier=self._reportID, columns=[ ("Testsuite / Testcase", None, 500), ("Testcases", None, 100), ("Skipped", None, 100), ("Errored", None, 100), ("Failed", None, 100), ("Passed", None, 100), ("Assertions", None, 100), ("Runtime (HH:MM:SS.sss)", None, 100), ], classes=["report-unittest-table"] ) tableBody = nodes.tbody() tableGroup += tableBody def sortedValues(d: Mapping[str, Testsuite]) -> Generator[Testsuite, None, None]: for key in sorted(d.keys()): yield d[key] def stateToSymbol(state: TestcaseState) -> str: if state is TestcaseState.Passed: return "✅" elif state is TestcaseState.Unknown: return "❓" else: return "❌" def timeformat(delta: timedelta) -> str: # Compute by hand, because timedelta._to_microseconds is not officially documented microseconds = (delta.days * 86_400 + delta.seconds) * 1_000_000 + delta.microseconds milliseconds = (microseconds + 500) // 1000 seconds = milliseconds // 1000 minutes = seconds // 60 hours = minutes // 60 return f"{hours:02}:{minutes % 60:02}:{seconds % 60:02}.{milliseconds % 1000:03}" def renderRoot(tableBody: nodes.tbody, testsuite: TestsuiteSummary) -> None: for ts in sortedValues(testsuite._testsuites): renderTestsuite(tableBody, ts, 0) def renderTestsuite(tableBody: nodes.tbody, testsuite: Testsuite, level: int) -> None: state = stateToSymbol(testsuite._state) tableBody += nodes.row( "", nodes.entry("", nodes.paragraph(text=f"{'  '*level}{state}{testsuite.Name}")), nodes.entry("", nodes.paragraph(text=f"{testsuite.Tests}")), nodes.entry("", nodes.paragraph(text=f"{testsuite.Skipped}")), nodes.entry("", nodes.paragraph(text=f"{testsuite.Errored}")), nodes.entry("", nodes.paragraph(text=f"{testsuite.Failed}")), nodes.entry("", nodes.paragraph(text=f"{testsuite.Passed}")), nodes.entry("", nodes.paragraph(text=f"")), # {testsuite.Uncovered}")), nodes.entry("", nodes.paragraph(text=f"{timeformat(testsuite.Time)}")), classes=["report-unittest-table-row"], ) for ts in sortedValues(testsuite._testsuites): renderTestsuite(tableBody, ts, level + 1) for testcase in sortedValues(testsuite._testcases): renderTestcase(tableBody, testcase, level + 1) def renderTestcase(tableBody: nodes.tbody, testcase: Testcase, level: int) -> None: state = stateToSymbol(testcase._state) tableBody += nodes.row( "", nodes.entry("", nodes.paragraph(text=f"{'  '*level}{state}{testcase.Name}")), nodes.entry("", nodes.paragraph(text=f"")), # {testsuite.Expected}")), nodes.entry("", nodes.paragraph(text=f"")), # {testsuite.Covered}")), nodes.entry("", nodes.paragraph(text=f"")), # {testsuite.Uncovered}")), nodes.entry("", nodes.paragraph(text=f"")), # {testsuite.Uncovered}")), nodes.entry("", nodes.paragraph(text=f"")), # {testsuite.Uncovered}")), nodes.entry("", nodes.paragraph(text=f"{testcase.Assertions}")), nodes.entry("", nodes.paragraph(text=f"{timeformat(testcase.Time)}")), classes=["report-unittest-table-row"], ) for test in sortedValues(testcase._tests): state = stateToSymbol(test._state) tableBody += nodes.row( "", nodes.entry("", nodes.paragraph(text=f"{'  '*(level+1)}{state}{test.Name}")), nodes.entry("", nodes.paragraph(text=f"")), # {test.Expected}")), nodes.entry("", nodes.paragraph(text=f"")), # {test.Covered}")), nodes.entry("", nodes.paragraph(text=f"")), # {test.Covered}")), nodes.entry("", nodes.paragraph(text=f"")), # {test.Covered}")), nodes.entry("", nodes.paragraph(text=f"")), # {test.Covered}")), nodes.entry("", nodes.paragraph(text=f"")), # {test.Uncovered}")), nodes.entry("", nodes.paragraph(text=f"")), # {test.Coverage :.1%}")), classes=["report-unittest-table-row"], ) renderRoot(tableBody, self._testsuite) # # Add a summary row # tableBody += nodes.row( # "", # nodes.entry("", nodes.paragraph(text=f"Overall ({self._testsuite.FileCount} files):")), # nodes.entry("", nodes.paragraph(text=f"{self._testsuite.Expected}")), # nodes.entry("", nodes.paragraph(text=f"{self._testsuite.Covered}")), # nodes.entry("", nodes.paragraph(text=f"{self._testsuite.Uncovered}")), # nodes.entry("", nodes.paragraph(text=f"{self._testsuite.Coverage:.1%}"), # # classes=[self._ConvertToColor(self._coverage.coverage(), "class")] # ), # classes=["report-unittest-summary-row"] # ) return table
[docs] def run(self) -> List[nodes.Node]: self._CheckOptions() self._CheckConfiguration() # Assemble a list of Python source files analyzer = Analyzer(self._xmlReport) self._testsuite = analyzer.Convert() self._testsuite.Aggregate() container = nodes.container() container += self._GenerateTestSummaryTable() return [container]