# ==================================================================================================================== #
# _____ ____ _ _ ____ _ #
# _ __ _ _| ____| _ \ / \ / \ | _ \ ___ _ __ ___ _ __| |_ ___ #
# | '_ \| | | | _| | | | |/ _ \ / _ \ | |_) / _ \ '_ \ / _ \| '__| __/ __| #
# | |_) | |_| | |___| |_| / ___ \ / ___ \ _| _ < __/ |_) | (_) | | | |_\__ \ #
# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)_| \_\___| .__/ \___/|_| \__|___/ #
# |_| |___/ |_| #
# ==================================================================================================================== #
# Authors: #
# Patrick Lehmann #
# #
# License: #
# ==================================================================================================================== #
# Copyright 2026-2026 Electronic Design Automation Abstraction (EDA²) #
# #
# 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 #
# ==================================================================================================================== #
#
"""
The records of Open Test Reporting's XML formats: the infrastructure of an execution and the result of a test or
container.
The infrastructure belongs to an :class:`~pyEDAA.Reports.Unittesting.OpenTestReporting.Execution`, a result to a
:class:`~pyEDAA.Reports.Unittesting.OpenTestReporting.Node` of the tree in
:mod:`pyEDAA.Reports.Unittesting.OpenTestReporting`.
"""
from __future__ import annotations
from enum import Enum
from typing import TYPE_CHECKING, Dict, Optional as Nullable
from pyTooling.Common import getFullyQualifiedName
from pyTooling.Decorators import export, readonly
from pyTooling.MetaClasses import ExtendedType
if TYPE_CHECKING:
from pyEDAA.Reports.Unittesting.OpenTestReporting import Execution, Node
[docs]
@export
class ResultStatus(Enum):
"""An enumeration of the statuses of a test's or container's result (``<result status>``)."""
Successful = "SUCCESSFUL" #: Executed and passed.
Skipped = "SKIPPED" #: Not executed, e.g. a disabled test.
Aborted = "ABORTED" #: Started, but aborted, e.g. by a failed assumption.
Failed = "FAILED" #: Executed and failed, e.g. by a failed assertion or an exception.
Errored = "ERRORED" #: Executed, but an error outside the test occurred.
[docs]
@export
class Infrastructure(metaclass=ExtendedType, slots=True):
"""
The infrastructure the tests were run on (``<infrastructure>``).
The core namespace states the host, the user, the operating system and the CPU cores. Elements of extensions, like
the Java version, are collected in :data:`Extensions`.
"""
_parent: Nullable[Execution] #: The execution the infrastructure belongs to.
_hostName: Nullable[str] #: Name of the host.
_userName: Nullable[str] #: Name of the user.
_operatingSystem: Nullable[str] #: Name of the operating system.
_cpuCoreCount: Nullable[int] #: Count of CPU cores.
_extensions: Dict[str, str] #: Texts and attribute values of extension elements.
[docs]
def __init__(
self,
hostName: Nullable[str] = None,
userName: Nullable[str] = None,
operatingSystem: Nullable[str] = None,
cpuCoreCount: Nullable[int] = None,
*,
parent: Nullable[Execution] = None
) -> None:
"""
Initializes an infrastructure description, and sets it as the infrastructure of its execution.
:param hostName: Optional, name of the host.
:param userName: Optional, name of the user.
:param operatingSystem: Optional, name of the operating system.
:param cpuCoreCount: Optional, count of CPU cores.
:param parent: Optional, the execution the infrastructure belongs to; it becomes the execution's
:attr:`~pyEDAA.Reports.Unittesting.OpenTestReporting.Execution.Infrastructure`.
Default: ``None``.
:raises TypeError: If parameter 'hostName' is not of type :class:`str`.
:raises TypeError: If parameter 'userName' is not of type :class:`str`.
:raises TypeError: If parameter 'operatingSystem' is not of type :class:`str`.
:raises TypeError: If parameter 'cpuCoreCount' is not of type :class:`int`.
:raises TypeError: If parameter 'parent' is not of type
:class:`~pyEDAA.Reports.Unittesting.OpenTestReporting.Execution`.
"""
from pyEDAA.Reports.Unittesting.OpenTestReporting import Execution
for parameterName, value in (
("hostName", hostName),
("userName", userName),
("operatingSystem", operatingSystem)
):
if value is not None and not isinstance(value, str):
ex = TypeError(f"Parameter '{parameterName}' is not of type 'str'.")
ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
raise ex
if cpuCoreCount is not None and not isinstance(cpuCoreCount, int):
ex = TypeError("Parameter 'cpuCoreCount' is not of type 'int'.")
ex.add_note(f"Got type '{getFullyQualifiedName(cpuCoreCount)}'.")
raise ex
if parent is not None and not isinstance(parent, Execution):
ex = TypeError("Parameter 'parent' is not of type 'Execution'.")
ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
raise ex
self._parent = parent
self._hostName = hostName
self._userName = userName
self._operatingSystem = operatingSystem
self._cpuCoreCount = cpuCoreCount
self._extensions = {}
if parent is not None:
parent._infrastructure = self
@readonly
def Parent(self) -> Nullable[Execution]:
"""
Read-only property to access the execution the infrastructure belongs to (:attr:`_parent`).
:returns: The execution; ``None`` if the infrastructure belongs to no execution.
"""
return self._parent
@readonly
def HostName(self) -> Nullable[str]:
"""
Read-only property to access the name of the host (:attr:`_hostName`).
:returns: The host name, or ``None`` if it wasn't reported.
"""
return self._hostName
@readonly
def UserName(self) -> Nullable[str]:
"""
Read-only property to access the name of the user (:attr:`_userName`).
:returns: The user name, or ``None`` if it wasn't reported.
"""
return self._userName
@readonly
def OperatingSystem(self) -> Nullable[str]:
"""
Read-only property to access the name of the operating system (:attr:`_operatingSystem`).
:returns: The operating system, or ``None`` if it wasn't reported.
"""
return self._operatingSystem
@readonly
def CPUCoreCount(self) -> Nullable[int]:
"""
Read-only property to access the count of CPU cores (:attr:`_cpuCoreCount`).
:returns: The count of CPU cores, or ``None`` if it wasn't reported.
"""
return self._cpuCoreCount
@readonly
def Extensions(self) -> Dict[str, str]:
"""
Read-only property to access the texts and attribute values of extension elements (:attr:`_extensions`).
An element's text is stored by the element's local name, e.g. ``javaVersion``, an attribute's value by the local
name and the attribute's name, e.g. ``heapSize.max``.
:returns: The dictionary of extension values.
"""
return self._extensions
[docs]
@export
class Result(metaclass=ExtendedType, slots=True):
"""
The result of a test or container (``<result>``).
Besides a status, a result can state the reason for it, e.g. why a test was skipped, and the exception ending a test
(Java extension, ``<java:throwable>``).
"""
_parent: Nullable[Node] #: The test or container the result belongs to.
_status: Nullable[ResultStatus] #: Status of the result.
_reason: Nullable[str] #: Reason for the status.
_exceptionType: Nullable[str] #: Fully qualified type of the exception.
_isAssertionError: Nullable[bool] #: True, if the exception is an assertion error.
_stackTrace: Nullable[str] #: Stack trace of the exception, with its type and message.
[docs]
def __init__(
self,
status: Nullable[ResultStatus] = None,
reason: Nullable[str] = None,
exceptionType: Nullable[str] = None,
isAssertionError: Nullable[bool] = None,
stackTrace: Nullable[str] = None,
*,
parent: Nullable[Node] = None
) -> None:
"""
Initializes a result, and sets it as the result of its test or container.
:param status: Optional, status of the result.
:param reason: Optional, reason for the status.
:param exceptionType: Optional, fully qualified type of the exception.
:param isAssertionError: Optional, true, if the exception is an assertion error.
:param stackTrace: Optional, stack trace of the exception.
:param parent: Optional, the test or container the result belongs to; it becomes the node's
:attr:`~pyEDAA.Reports.Unittesting.OpenTestReporting.Node.Result`. Default: ``None``.
:raises TypeError: If parameter 'status' is not of type :class:`ResultStatus`.
:raises TypeError: If parameter 'reason' is not of type :class:`str`.
:raises TypeError: If parameter 'exceptionType' is not of type :class:`str`.
:raises TypeError: If parameter 'isAssertionError' is not of type :class:`bool`.
:raises TypeError: If parameter 'stackTrace' is not of type :class:`str`.
:raises TypeError: If parameter 'parent' is not of type
:class:`~pyEDAA.Reports.Unittesting.OpenTestReporting.Node`.
"""
from pyEDAA.Reports.Unittesting.OpenTestReporting import Node
if status is not None and not isinstance(status, ResultStatus):
ex = TypeError("Parameter 'status' is not of type 'ResultStatus'.")
ex.add_note(f"Got type '{getFullyQualifiedName(status)}'.")
raise ex
for parameterName, value in (
("reason", reason),
("exceptionType", exceptionType),
("stackTrace", stackTrace)
):
if value is not None and not isinstance(value, str):
ex = TypeError(f"Parameter '{parameterName}' is not of type 'str'.")
ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
raise ex
if isAssertionError is not None and not isinstance(isAssertionError, bool):
ex = TypeError("Parameter 'isAssertionError' is not of type 'bool'.")
ex.add_note(f"Got type '{getFullyQualifiedName(isAssertionError)}'.")
raise ex
if parent is not None and not isinstance(parent, Node):
ex = TypeError("Parameter 'parent' is not of type 'Node'.")
ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
raise ex
self._parent = parent
self._status = status
self._reason = reason
self._exceptionType = exceptionType
self._isAssertionError = isAssertionError
self._stackTrace = stackTrace
if parent is not None:
parent._result = self
@readonly
def Parent(self) -> Nullable[Node]:
"""
Read-only property to access the test or container the result belongs to (:attr:`_parent`).
:returns: The node; ``None`` if the result belongs to no node.
"""
return self._parent
@readonly
def Status(self) -> Nullable[ResultStatus]:
"""
Read-only property to access the status of the result (:attr:`_status`).
:returns: The status, or ``None`` if it wasn't reported.
"""
return self._status
@readonly
def Reason(self) -> Nullable[str]:
"""
Read-only property to access the reason for the status (:attr:`_reason`).
:returns: The reason, or ``None`` if it wasn't reported.
"""
return self._reason
@readonly
def ExceptionType(self) -> Nullable[str]:
"""
Read-only property to access the fully qualified type of the exception (:attr:`_exceptionType`).
A failed JUnit assertion raises ``org.opentest4j.AssertionFailedError``.
:returns: The exception's type, or ``None`` if no exception was reported.
"""
return self._exceptionType
@readonly
def IsAssertionError(self) -> Nullable[bool]:
"""
Read-only property to access whether the exception is an assertion error (:attr:`_isAssertionError`).
:returns: True, if the exception is an assertion error, or ``None`` if no exception was reported.
"""
return self._isAssertionError
@readonly
def StackTrace(self) -> Nullable[str]:
"""
Read-only property to access the stack trace of the exception (:attr:`_stackTrace`).
:returns: The stack trace, starting with the exception's type and message, or ``None`` if no exception was
reported.
"""
return self._stackTrace
@readonly
def Message(self) -> Nullable[str]:
"""
Read-only property to return the message explaining the status.
It's the reason, if one was reported, else the exception's message: the stack trace's text before its first frame,
without the exception's type.
:returns: The message, or ``None`` if neither a reason nor an exception with a message was reported.
"""
if self._reason is not None:
return self._reason
elif self._stackTrace is None:
return None
message = self._stackTrace.split("\n\tat ", 1)[0].rstrip("\r\n")
if message == self._exceptionType:
return None
return message.removeprefix(f"{self._exceptionType}: ")