diff --git a/.github/workflows/Pipeline.yml b/.github/workflows/Pipeline.yml index b09eb5bb..cb29fc97 100644 --- a/.github/workflows/Pipeline.yml +++ b/.github/workflows/Pipeline.yml @@ -9,14 +9,14 @@ on: jobs: UnitTestingParams: - uses: pyTooling/Actions/.github/workflows/Parameters.yml@dev + uses: pyTooling/Actions/.github/workflows/Parameters.yml@r1 with: name: sphinx-reports python_version_list: "3.9 3.10 3.11 3.12 pypy-3.9 pypy-3.10" # disable_list: "windows:pypy-3.8 windows:pypy-3.9 windows:pypy-3.10" UnitTesting: - uses: pyTooling/Actions/.github/workflows/UnitTesting.yml@dev + uses: pyTooling/Actions/.github/workflows/UnitTesting.yml@r1 needs: - UnitTestingParams with: @@ -27,7 +27,7 @@ jobs: coverage_sqlite_artifact: ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).codecoverage_sqlite }} StaticTypeCheck: - uses: pyTooling/Actions/.github/workflows/StaticTypeCheck.yml@dev + uses: pyTooling/Actions/.github/workflows/StaticTypeCheck.yml@r1 needs: - UnitTestingParams with: @@ -38,7 +38,7 @@ jobs: html_artifact: ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).statictyping_html }} DocCoverage: - uses: pyTooling/Actions/.github/workflows/CheckDocumentation.yml@dev + uses: pyTooling/Actions/.github/workflows/CheckDocumentation.yml@r1 needs: - UnitTestingParams with: @@ -47,7 +47,7 @@ jobs: # fail_below: 70 Package: - uses: pyTooling/Actions/.github/workflows/Package.yml@dev + uses: pyTooling/Actions/.github/workflows/Package.yml@r1 needs: - UnitTestingParams - UnitTesting @@ -56,7 +56,7 @@ jobs: artifact: ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).package_all }} PublishCoverageResults: - uses: pyTooling/Actions/.github/workflows/PublishCoverageResults.yml@dev + uses: pyTooling/Actions/.github/workflows/PublishCoverageResults.yml@r1 needs: - UnitTestingParams - UnitTesting @@ -69,7 +69,7 @@ jobs: codacy_token: ${{ secrets.CODACY_PROJECT_TOKEN }} PublishTestResults: - uses: pyTooling/Actions/.github/workflows/PublishTestResults.yml@dev + uses: pyTooling/Actions/.github/workflows/PublishTestResults.yml@r1 needs: - UnitTestingParams - UnitTesting @@ -77,7 +77,7 @@ jobs: merged_junit_artifact: ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).unittesting_xml }} IntermediateCleanUp: - uses: pyTooling/Actions/.github/workflows/IntermediateCleanUp.yml@dev + uses: pyTooling/Actions/.github/workflows/IntermediateCleanUp.yml@r1 needs: - UnitTestingParams - PublishCoverageResults @@ -88,14 +88,14 @@ jobs: xml_unittest_artifacts_prefix: ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).unittesting_xml }}- # VerifyDocs: -# uses: pyTooling/Actions/.github/workflows/VerifyDocs.yml@dev +# uses: pyTooling/Actions/.github/workflows/VerifyDocs.yml@r1 # needs: # - UnitTestingParams # with: # python_version: ${{ needs.UnitTestingParams.outputs.python_version }} HTMLDocumentation: - uses: pyTooling/Actions/.github/workflows/SphinxDocumentation.yml@dev + uses: pyTooling/Actions/.github/workflows/SphinxDocumentation.yml@r1 needs: - UnitTestingParams - PublishTestResults @@ -109,7 +109,7 @@ jobs: latex_artifact: ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).documentation_latex }} PDFDocumentation: - uses: pyTooling/Actions/.github/workflows/LaTeXDocumentation.yml@dev + uses: pyTooling/Actions/.github/workflows/LaTeXDocumentation.yml@r1 needs: - UnitTestingParams - HTMLDocumentation @@ -119,7 +119,7 @@ jobs: pdf_artifact: ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).documentation_pdf }} PublishToGitHubPages: - uses: pyTooling/Actions/.github/workflows/PublishToGitHubPages.yml@dev + uses: pyTooling/Actions/.github/workflows/PublishToGitHubPages.yml@r1 needs: - UnitTestingParams - HTMLDocumentation @@ -132,14 +132,14 @@ jobs: typing: ${{ fromJson(needs.UnitTestingParams.outputs.artifact_names).statictyping_html }} ReleasePage: - uses: pyTooling/Actions/.github/workflows/Release.yml@dev + uses: pyTooling/Actions/.github/workflows/Release.yml@r1 if: startsWith(github.ref, 'refs/tags') needs: - Package - PublishToGitHubPages PublishOnPyPI: - uses: pyTooling/Actions/.github/workflows/PublishOnPyPI.yml@dev + uses: pyTooling/Actions/.github/workflows/PublishOnPyPI.yml@r1 if: startsWith(github.ref, 'refs/tags') needs: - UnitTestingParams @@ -152,7 +152,7 @@ jobs: PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} ArtifactCleanUp: - uses: pyTooling/Actions/.github/workflows/ArtifactCleanUp.yml@dev + uses: pyTooling/Actions/.github/workflows/ArtifactCleanUp.yml@r1 needs: - UnitTestingParams - UnitTesting diff --git a/doc/requirements.txt b/doc/requirements.txt index de73195e..de35eab7 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -4,7 +4,6 @@ setuptools >= 69.0.0 pyTooling ~= 6.0 -colorama >= 0.4.6 # Enforce latest version on ReadTheDocs sphinx >= 7.2, < 8.0 diff --git a/run.ps1 b/run.ps1 index a8d7a176..d0cd713b 100644 --- a/run.ps1 +++ b/run.ps1 @@ -97,7 +97,7 @@ if ($install) { Write-Host -ForegroundColor Cyan "[ADMIN][UNINSTALL] Uninstalling $PackageName ..." py -3.12 -m pip uninstall -y $PackageName Write-Host -ForegroundColor Cyan "[ADMIN][INSTALL] Installing $PackageName from wheel ..." - py -3.12 -m pip install .\dist\$PackageName-0.5.1-py3-none-any.whl + py -3.12 -m pip install .\dist\$PackageName-0.6.0-py3-none-any.whl Write-Host -ForegroundColor Cyan "[ADMIN][INSTALL] Closing window in 5 seconds ..." Start-Sleep -Seconds 5 diff --git a/sphinx_reports/Adapter/JUnit.py b/sphinx_reports/Adapter/JUnit.py index 76bfc02b..e247c319 100644 --- a/sphinx_reports/Adapter/JUnit.py +++ b/sphinx_reports/Adapter/JUnit.py @@ -31,14 +31,15 @@ """ **A Sphinx extension providing uni test results embedded in documentation pages.** """ +from datetime import datetime, timedelta from pathlib import Path from xml.dom import minidom, Node from xml.dom.minidom import Element -from pyTooling.Decorators import export, readonly +from pyTooling.Decorators import export, readonly from sphinx_reports.Common import ReportExtensionError -from sphinx_reports.DataModel.Unittest import Testsuite, Testcase, TestsuiteSummary, Test +from sphinx_reports.DataModel.Unittest import Testsuite, Testcase, TestsuiteSummary, Test, TestcaseState @export @@ -72,7 +73,17 @@ def Convert(self) -> TestsuiteSummary: return self._testsuiteSummary def _ParseRootElement(self, root: Element) -> None: - self._testsuiteSummary = TestsuiteSummary("root") + name = root.getAttribute("name") if root.hasAttribute("name") else "root" + testsuiteRuntime = float(root.getAttribute("time")) if root.hasAttribute("time") else -1.0 + timestamp = datetime.fromisoformat(root.getAttribute("timestamp")) if root.hasAttribute("timestamp") else None + + self._testsuiteSummary = TestsuiteSummary(name, timedelta(seconds=testsuiteRuntime)) + + tests = root.getAttribute("tests") + skipped = root.getAttribute("skipped") + errors = root.getAttribute("errors") + failures = root.getAttribute("failures") + assertions = root.getAttribute("assertions") for rootNode in root.childNodes: if rootNode.nodeName == "testsuite": @@ -89,6 +100,7 @@ def _ParseTestsuite(self, testsuitesNode: Element) -> None: def _ParseTestcase(self, testsuiteNode: Element) -> None: className = testsuiteNode.getAttribute("classname") name = testsuiteNode.getAttribute("name") + time = float(testsuiteNode.getAttribute("time")) concurrentSuite = self._testsuiteSummary @@ -97,8 +109,30 @@ def _ParseTestcase(self, testsuiteNode: Element) -> None: try: concurrentSuite = concurrentSuite[testsuiteName] except KeyError: - new = Testsuite(testsuiteName) + new = Testsuite(testsuiteName, timedelta(seconds=time)) concurrentSuite._testsuites[testsuiteName] = new concurrentSuite = new - concurrentSuite._testcases[name] = Testcase(name) + testcase = Testcase(name, timedelta(seconds=time)) + concurrentSuite._testcases[name] = testcase + + for node in testsuiteNode.childNodes: + if node.nodeType == Node.ELEMENT_NODE: + if node.tagName == "skipped": + testcase._state = TestcaseState.Skipped + elif node.tagName == "failure": + testcase._state = TestcaseState.Failed + elif node.tagName == "error": + testcase._state = TestcaseState.Error + elif node.tagName == "system-out": + pass + elif node.tagName == "system-err": + pass + elif node.tagName == "properties": + pass + else: + raise UnittestError(f"Unknown element '{node.tagName}' in junit file.") + + if testcase._state is TestcaseState.Unknown: + testcase._state = TestcaseState.Passed + diff --git a/sphinx_reports/CodeCoverage.py b/sphinx_reports/CodeCoverage.py index 83c5a99b..1e97448c 100644 --- a/sphinx_reports/CodeCoverage.py +++ b/sphinx_reports/CodeCoverage.py @@ -32,7 +32,7 @@ **Report code coverage as Sphinx documentation page(s).** """ from pathlib import Path -from typing import Dict, Tuple, Any, List, Iterable, Mapping, Generator, TypedDict, Union +from typing import Dict, Tuple, Any, List, Iterable, Mapping, Generator, TypedDict, Union, Optional as Nullable from docutils import nodes from pyTooling.Decorators import export @@ -167,22 +167,30 @@ def _ConvertToColor(self, currentLevel: float, configKey: str) -> str: return self._levels[100][configKey] def _GenerateCoverageTable(self) -> nodes.table: - # Create a table and table header with 5 columns + # Create a table and table header with 10 columns table, tableGroup = self._PrepareTable( identifier=self._packageID, - columns={ - "Module": 500, - "Total Statements": 100, - "Excluded Statements": 100, - "Covered Statements": 100, - "Missing Statements": 100, - "Total Branches": 100, - "Covered Branches": 100, - "Partial Branches": 100, - "Missing Branches": 100, - "Coverage in %": 100 - }, - classes=["report-doccov-table"] + columns=[ + ("Package", [ + (" Module", 500) + ], None), + ("Statments", [ + ("Total", 100), + ("Excluded", 100), + ("Covered", 100), + ("Missing", 100) + ], None), + ("Branches", [ + ("Total", 100), + ("Covered", 100), + ("Partial", 100), + ("Missing", 100) + ], None), + ("Coverage", [ + ("in %", 100) + ], None) + ], + classes=["report-codecov-table"] ) tableBody = nodes.tbody() tableGroup += tableBody diff --git a/sphinx_reports/DataModel/CodeCoverage.py b/sphinx_reports/DataModel/CodeCoverage.py index 76f1003a..775cba27 100644 --- a/sphinx_reports/DataModel/CodeCoverage.py +++ b/sphinx_reports/DataModel/CodeCoverage.py @@ -31,10 +31,8 @@ """ **Abstract documentation coverage data model for Python code.** """ - -from enum import Flag from pathlib import Path -from typing import Optional as Nullable, Iterable, Dict, Union, Tuple +from typing import Optional as Nullable, Dict, Union from pyTooling.Decorators import export, readonly diff --git a/sphinx_reports/DataModel/Unittest.py b/sphinx_reports/DataModel/Unittest.py index c067d781..f2475b05 100644 --- a/sphinx_reports/DataModel/Unittest.py +++ b/sphinx_reports/DataModel/Unittest.py @@ -31,32 +31,52 @@ """ **Abstract documentation coverage data model for Python code.** """ - -from enum import Flag -from typing import Optional as Nullable, Iterable, Dict, Union, Tuple, List +from datetime import timedelta +from enum import Flag +from typing import Optional as Nullable, Dict, Union, Tuple from pyTooling.Decorators import export, readonly +from sphinx_reports.Common import ReportExtensionError + + +@export +class UnittestError(ReportExtensionError): + pass + @export -class TestState(Flag): +class TestcaseState(Flag): Unknown = 0 - Passed = 1 - Failed = 2 + Failed = 1 + Error = 2 Skipped = 3 + Passed = 4 @export class Base: - _name: str + _name: str + _state: TestcaseState + _time: timedelta - def __init__(self, name: str) -> None: + def __init__(self, name: str, time: timedelta) -> None: self._name = name + self._state = TestcaseState.Unknown + self._time = time @readonly def Name(self) -> str: return self._name + @readonly + def State(self) -> TestcaseState: + return self._state + + @readonly + def Time(self) -> timedelta: + return self._time + @export class Test(Base): @@ -68,21 +88,40 @@ def __init__(self, name: str) -> None: class Testcase(Base): _tests: Dict[str, Test] - def __init__(self, name: str) -> None: - super().__init__(name) + _assertions: int + + def __init__(self, name: str, time: timedelta) -> None: + super().__init__(name, time) self._tests = {} + self._assertions = 0 + + @readonly + def Assertions(self) -> int: + return self._assertions @export class TestsuiteBase(Base): _testsuites: Dict[str, "Testsuite"] - def __init__(self, name: str) -> None: - super().__init__(name) + _tests: int + _skipped: int + _errored: int + _failed: int + _passed: int + + def __init__(self, name: str, time: timedelta) -> None: + super().__init__(name, time) self._testsuites = {} + self._tests = 0 + self._skipped = 0 + self._errored = 0 + self._failed = 0 + self._passed = 0 + def __getitem__(self, key: str) -> "Testsuite": return self._testsuites[key] @@ -93,15 +132,52 @@ def __contains__(self, key: str) -> bool: def Testsuites(self) -> Dict[str, "Testsuite"]: return self._testsuites + @readonly + def Tests(self) -> int: + return self._tests + + @readonly + def Skipped(self) -> int: + return self._skipped + + @readonly + def Errored(self) -> int: + return self._errored + + @readonly + def Failed(self) -> int: + return self._failed + + @readonly + def Passed(self) -> int: + return self._passed + + def Aggregate(self) -> Tuple[int, int, int, int, int]: + tests = 0 + skipped = 0 + errored = 0 + failed = 0 + passed = 0 + + for testsuite in self._testsuites.values(): + t, s, e, f, p = testsuite.Aggregate() + tests += t + skipped += s + errored += e + failed += f + passed += p + + return tests, skipped, errored, failed, passed + @export class Testsuite(TestsuiteBase): _testcases: Dict[str, Testcase] - def __init__(self, name: str) -> None: - super().__init__(name) + def __init__(self, name: str, time: timedelta) -> None: + super().__init__(name, time) - self._testcases = {} + self._testcases = {} def __getitem__(self, key: str) -> Union["Testsuite", Testcase]: try: @@ -119,7 +195,70 @@ def __contains__(self, key: str) -> bool: def Testcases(self) -> Dict[str, Testcase]: return self._testcases + def Aggregate(self) -> Tuple[int, int, int, int, int]: + tests, skipped, errored, failed, passed = super().Aggregate() + + for testcase in self._testcases.values(): + if testcase._state is TestcaseState.Passed: + tests += 1 + passed += 1 + elif testcase._state is TestcaseState.Failed: + tests += 1 + failed += 1 + elif testcase._state is TestcaseState.Skipped: + tests += 1 + skipped += 1 + elif testcase._state is TestcaseState.Error: + tests += 1 + errored += 1 + elif testcase._state is TestcaseState.Unknown: + raise UnittestError(f"Found testcase '{testcase._name}' with unknown state.") + + self._tests = tests + self._skipped = skipped + self._errored = errored + self._failed = failed + self._passed = passed + + if errored > 0: + self._state = TestcaseState.Error + elif failed > 0: + self._state = TestcaseState.Failed + elif tests - skipped == passed: + self._state = TestcaseState.Passed + elif tests == skipped: + self._state = TestcaseState.Skipped + else: + self._state = TestcaseState.Unknown + + return tests, skipped, errored, failed, passed + @export class TestsuiteSummary(TestsuiteBase): - pass + _time: float + + def __init__(self, name: str, time: timedelta): + super().__init__(name, time) + + def Aggregate(self) -> Tuple[int, int, int, int, int]: + tests, skipped, errored, failed, passed = super().Aggregate() + + self._tests = tests + self._skipped = skipped + self._errored = errored + self._failed = failed + self._passed = passed + + if errored > 0: + self._state = TestcaseState.Error + elif failed > 0: + self._state = TestcaseState.Failed + elif tests - skipped == passed: + self._state = TestcaseState.Passed + elif tests == skipped: + self._state = TestcaseState.Skipped + else: + self._state = TestcaseState.Unknown + + return tests, skipped, errored, failed, passed diff --git a/sphinx_reports/DocCoverage.py b/sphinx_reports/DocCoverage.py index 4aae79eb..e5e45a7b 100644 --- a/sphinx_reports/DocCoverage.py +++ b/sphinx_reports/DocCoverage.py @@ -170,13 +170,13 @@ def _GenerateCoverageTable(self) -> nodes.table: # Create a table and table header with 5 columns table, tableGroup = self._PrepareTable( identifier=self._packageID, - columns={ - "Filename": 500, - "Total": 100, - "Covered": 100, - "Missing": 100, - "Coverage in %": 100 - }, + columns=[ + ("Filename", None, 500), + ("Total", None, 100), + ("Covered", None, 100), + ("Missing", None, 100), + ("Coverage in %", None, 100) + ], classes=["report-doccov-table"] ) tableBody = nodes.tbody() diff --git a/sphinx_reports/Sphinx.py b/sphinx_reports/Sphinx.py index 47afa75d..84fc8339 100644 --- a/sphinx_reports/Sphinx.py +++ b/sphinx_reports/Sphinx.py @@ -122,17 +122,53 @@ def _ParseLegendOption(self, optionName: str, default: Nullable[LegendPosition] except KeyError as ex: raise ReportExtensionError(f"{self.directiveName}::{optionName}: Value '{option}' is not a valid member of 'LegendPosition'.") from ex - def _PrepareTable(self, columns: Dict[str, int], identifier: str, classes: List[str]) -> Tuple[nodes.table, nodes.tgroup]: + def _PrepareTable(self, columns: List[Tuple[str, Nullable[List[Tuple[str, int]]], Nullable[int]]], identifier: str, classes: List[str]) -> Tuple[nodes.table, nodes.tgroup]: table = nodes.table("", identifier=identifier, classes=classes) - tableGroup = nodes.tgroup(cols=(len(columns))) - table += tableGroup + hasSecondHeaderRow = False + columnCount = 0 + for groupColumn in columns: + if groupColumn[1] is not None: + columnCount += len(groupColumn[1]) + hasSecondHeaderRow = True + else: + columnCount += 1 - tableRow = nodes.row() - for columnTitle, width in columns.items(): - tableGroup += nodes.colspec(colwidth=width) - tableRow += nodes.entry("", nodes.paragraph(text=columnTitle)) + tableGroup = nodes.tgroup(cols=columnCount) + table += tableGroup - tableGroup += nodes.thead("", tableRow) + # Setup column specifications + for _, more, width in columns: + if more is None: + tableGroup += nodes.colspec(colwidth=width) + else: + for _, width in more: + tableGroup += nodes.colspec(colwidth=width) + + # Setup primary header row + headerRow = nodes.row() + for columnTitle, more, _ in columns: + if more is None: + headerRow += nodes.entry("", nodes.paragraph(text=columnTitle), morerows=1) + else: + morecols = len(more) - 1 + headerRow += nodes.entry("", nodes.paragraph(text=columnTitle), morecols=morecols) + for i in range(morecols): + headerRow += None + + tableHeader = nodes.thead("", headerRow) + tableGroup += tableHeader + + # If present, setup secondary header row + if hasSecondHeaderRow: + tableRow = nodes.row() + for columnTitle, more, _ in columns: + if more is None: + tableRow += None + else: + for columnTitle, _ in more: + tableRow += nodes.entry("", nodes.paragraph(text=columnTitle)) + + tableHeader += tableRow return table, tableGroup diff --git a/sphinx_reports/Unittest.py b/sphinx_reports/Unittest.py index 250d970d..0b1f99bb 100644 --- a/sphinx_reports/Unittest.py +++ b/sphinx_reports/Unittest.py @@ -31,15 +31,16 @@ """ **Report unit test results as Sphinx documentation page(s).** """ -from pathlib import Path -from typing import Dict, Tuple, Any, List, Iterable, Mapping, Generator, TypedDict +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, LegendPosition +from sphinx_reports.Common import ReportExtensionError from sphinx_reports.Sphinx import strip, BaseDirective -from sphinx_reports.DataModel.Unittest import Testsuite, TestsuiteSummary, Testcase +from sphinx_reports.DataModel.Unittest import Testsuite, TestsuiteSummary, Testcase, TestcaseState from sphinx_reports.Adapter.JUnit import Analyzer @@ -97,16 +98,19 @@ def _CheckConfiguration(self) -> None: 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 5 columns + # Create a table and table header with 8 columns table, tableGroup = self._PrepareTable( identifier=self._reportID, - columns={ - "Testsuite / Testcase": 500, - "???": 100, - "????": 100, - "?????": 100, - "Status": 100 - }, + 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() @@ -116,18 +120,39 @@ 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}❌{testsuite.Name}")), - nodes.entry("", nodes.paragraph(text=f"")), # {testsuite.Expected}")), - nodes.entry("", nodes.paragraph(text=f"")), # {testsuite.Covered}")), + 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"")), # {testsuite.Coverage:.1%}")), + nodes.entry("", nodes.paragraph(text=f"{timeformat(testsuite.Time)}")), classes=["report-unittest-table-row"], ) @@ -138,22 +163,30 @@ def renderTestsuite(tableBody: nodes.tbody, testsuite: Testsuite, level: int) -> 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}✅{testcase.Name}")), + 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.Coverage:.1%}")), + 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)}✅{test.Name}")), + 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"], @@ -183,7 +216,7 @@ def run(self) -> List[nodes.Node]: # Assemble a list of Python source files analyzer = Analyzer(self._xmlReport) self._testsuite = analyzer.Convert() - # self._testsuite.Aggregate() + self._testsuite.Aggregate() container = nodes.container() container += self._GenerateTestSummaryTable() diff --git a/sphinx_reports/__init__.py b/sphinx_reports/__init__.py index e37f3344..bb5e222b 100644 --- a/sphinx_reports/__init__.py +++ b/sphinx_reports/__init__.py @@ -43,11 +43,10 @@ __email__ = "Paebbels@gmail.com" __copyright__ = "2023-2024, Patrick Lehmann" __license__ = "Apache License, Version 2.0" -__version__ = "0.5.1" +__version__ = "0.6.0" __keywords__ = ["Python3", "Sphinx", "Extension", "Report", "doc-string", "interrogate"] from hashlib import md5 -from importlib.resources import files from pathlib import Path from typing import Any, Tuple, Dict, Optional as Nullable, TypedDict, List, Callable