Skip to content

Commit

Permalink
Inline conditionals
Browse files Browse the repository at this point in the history
  • Loading branch information
priitlatt committed Oct 16, 2024
1 parent b249a40 commit dd94aea
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 92 deletions.
83 changes: 42 additions & 41 deletions src/codemagic/models/xctests/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@
from .xcresult import ActionsInvocationRecord
from .xcresult import ActionTestableSummary
from .xcresult import ActionTestMetadata
from .xcresult import XcDevice
from .xcresult import XcSummary
from .xcresult import XcTestNode
from .xcresult import XcTestNodeType
from .xcresult import XcTestResult
from .xcresult import XcTests
from .xcresult.xcode_16_xcresult import XcDevice
from .xcresult.xcode_16_xcresult import XcTestNode
from .xcresulttool import XcResultTool


Expand Down Expand Up @@ -180,11 +181,19 @@ def convert(self) -> TestSuites:

class Xcode16XcResultConverter(XcResultConverter):
@classmethod
def _get_test_node_run_destination(cls, xc_test_node: XcTestNode) -> Optional[XcDevice]:
def _iter_nodes(cls, root_node: XcTestNode, node_type: XcTestNodeType) -> Iterator[XcTestNode]:
if root_node.node_type is node_type:
yield root_node
else:
for child in root_node.children:
yield from cls._iter_nodes(child, node_type)

@classmethod
def _get_run_destination(cls, root_node: XcTestNode) -> Optional[XcDevice]:
# TODO: support multiple run destinations
# As a first iteration only one test destination is supported as in legacy mode

parent: Union[XcTests, XcTestNode] = xc_test_node
parent: Union[XcTests, XcTestNode] = root_node
while isinstance(parent, XcTestNode):
parent = parent.parent

Expand All @@ -195,13 +204,10 @@ def _get_test_node_run_destination(cls, xc_test_node: XcTestNode) -> Optional[Xc

@classmethod
def _get_test_suite_name(cls, xc_test_suite: XcTestNode) -> str:
if xc_test_suite.node_type is not XcTestNodeType.TEST_SUITE:
raise ValueError("Not a test suite node", xc_test_suite.node_type)

name = xc_test_suite.name or ""
device_info = ""

device = cls._get_test_node_run_destination(xc_test_suite)
device = cls._get_run_destination(xc_test_suite)
if device:
platform = re.sub("simulator", "", device.platform, flags=re.IGNORECASE).strip()
device_info = f"{platform} {device.os_version} {device.model_name}"
Expand All @@ -212,13 +218,10 @@ def _get_test_suite_name(cls, xc_test_suite: XcTestNode) -> str:

@classmethod
def _get_test_case_error(cls, xc_test_case: XcTestNode) -> Optional[Error]:
if xc_test_case.node_type is not XcTestNodeType.TEST_CASE:
raise ValueError("Not a test case node", xc_test_case.node_type)

if not xc_test_case.is_failed():
if xc_test_case.result is not XcTestResult.FAILED:
return None

failure_messages_nodes = xc_test_case.iter_children(XcTestNodeType.FAILURE_MESSAGE)
failure_messages_nodes = cls._iter_nodes(xc_test_case, XcTestNodeType.FAILURE_MESSAGE)
failure_messages = [node.name for node in failure_messages_nodes if node.name]
return Error(
message=failure_messages[0] if failure_messages else "",
Expand All @@ -228,43 +231,47 @@ def _get_test_case_error(cls, xc_test_case: XcTestNode) -> Optional[Error]:

@classmethod
def _get_test_case_skipped(cls, xc_test_case: XcTestNode) -> Optional[Skipped]:
if xc_test_case.node_type is not XcTestNodeType.TEST_CASE:
raise ValueError("Not a test case node", xc_test_case.node_type)

if not xc_test_case.is_skipped():
if xc_test_case.result is not XcTestResult.SKIPPED:
return None

failure_messages_nodes = xc_test_case.iter_children(XcTestNodeType.FAILURE_MESSAGE)
skipped_message_nodes = [node for node in failure_messages_nodes if node.is_skipped()]
failure_messages_nodes = cls._iter_nodes(xc_test_case, XcTestNodeType.FAILURE_MESSAGE)
skipped_message_nodes = (node for node in failure_messages_nodes if node.result is XcTestResult.SKIPPED)
skipped_messages = [node.name for node in skipped_message_nodes if node.name]

return Skipped(message="\n".join(skipped_messages))

@classmethod
def _get_test_case(cls, xc_test_case: XcTestNode, xc_test_suite: XcTestNode) -> TestCase:
if xc_test_case.node_type is not XcTestNodeType.TEST_CASE:
raise ValueError("Not a test case node", xc_test_case.node_type)
if xc_test_suite.node_type is not XcTestNodeType.TEST_SUITE:
raise ValueError("Not a test suite node", xc_test_suite.node_type)
def _get_test_node_duration(cls, xc_test_case: XcTestNode) -> float:
if not xc_test_case.duration:
return 0.0

method_name = ""
duration = xc_test_case.duration.replace(",", ".")
if duration.endswith("s"):
duration = duration[:-1]
return float(duration)

@classmethod
def _get_test_case(cls, xc_test_case: XcTestNode, xc_test_suite: XcTestNode) -> TestCase:
if xc_test_case.name:
method_name = xc_test_case.name
elif xc_test_case.node_identifier:
method_name = xc_test_case.node_identifier.split("/")[-1]
else:
method_name = ""

classname = ""
if xc_test_case.node_identifier:
classname = xc_test_case.node_identifier.split("/", maxsplit=1)[0]
elif xc_test_suite.name:
classname = xc_test_suite.name
else:
classname = ""

return TestCase(
name=method_name,
classname=classname,
error=cls._get_test_case_error(xc_test_case),
time=xc_test_case.get_duration(),
status=xc_test_case.result,
time=cls._get_test_node_duration(xc_test_case),
status=xc_test_case.result.value if xc_test_case.result else None,
skipped=cls._get_test_case_skipped(xc_test_case),
)

Expand All @@ -274,10 +281,7 @@ def _get_test_suite_properties(
xc_test_suite: XcTestNode,
xc_test_result_summary: XcSummary,
) -> List[Property]:
if xc_test_suite.node_type is not XcTestNodeType.TEST_SUITE:
raise ValueError("Not a test suite node", xc_test_suite.node_type)

device = cls._get_test_node_run_destination(xc_test_suite)
device = cls._get_run_destination(xc_test_suite)

properties: List[Property] = [Property(name="title", value=xc_test_suite.name)]
if xc_test_result_summary.start_time:
Expand All @@ -299,10 +303,7 @@ def _get_test_suite_properties(

@classmethod
def _get_test_suite(cls, xc_test_suite: XcTestNode, xc_test_result_summary: XcSummary) -> TestSuite:
if xc_test_suite.node_type is not XcTestNodeType.TEST_SUITE:
raise ValueError("Not a test suite node", xc_test_suite.node_type)

xc_test_cases = list(xc_test_suite.iter_children(XcTestNodeType.TEST_CASE))
xc_test_cases = list(cls._iter_nodes(xc_test_suite, XcTestNodeType.TEST_CASE))

timestamp = None
if xc_test_result_summary.finish_time:
Expand All @@ -311,12 +312,12 @@ def _get_test_suite(cls, xc_test_suite: XcTestNode, xc_test_result_summary: XcSu
return TestSuite(
name=cls._get_test_suite_name(xc_test_suite),
tests=len(xc_test_cases),
disabled=sum(xc_test_case.is_disabled() for xc_test_case in xc_test_cases),
errors=sum(xc_test_case.is_failed() for xc_test_case in xc_test_cases),
disabled=0, # Disabled tests are completely excluded from reports
errors=sum(1 for xc_test_case in xc_test_cases if xc_test_case.result is XcTestResult.FAILED),
failures=None, # Xcode doesn't differentiate errors from failures, consider everything as error
package=xc_test_suite.name,
skipped=sum(xc_test_case.is_skipped() for xc_test_case in xc_test_cases),
time=sum(xc_test_case.get_duration() for xc_test_case in xc_test_cases),
skipped=sum(1 for xc_test_case in xc_test_cases if xc_test_case.result is XcTestResult.SKIPPED),
time=sum(cls._get_test_node_duration(xc_test_case) for xc_test_case in xc_test_cases),
timestamp=timestamp,
testcases=[cls._get_test_case(xc_test_case, xc_test_suite) for xc_test_case in xc_test_cases],
properties=cls._get_test_suite_properties(xc_test_suite, xc_test_result_summary),
Expand All @@ -332,7 +333,7 @@ def convert(self) -> TestSuites:
test_suites = [
self._get_test_suite(xc_test_suite_node, xc_summary)
for xc_test_node in xc_tests.test_nodes
for xc_test_suite_node in xc_test_node.iter_children(XcTestNodeType.TEST_SUITE)
for xc_test_suite_node in self._iter_nodes(xc_test_node, XcTestNodeType.TEST_SUITE)
]

return TestSuites(
Expand Down
7 changes: 7 additions & 0 deletions src/codemagic/models/xctests/xcresult/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,14 @@
from .legacy_xcresult import TestAssociatedError
from .legacy_xcresult import TestFailureIssueSummary
from .legacy_xcresult import TypeDefinition
from .xcode_16_xcresult import XcConfiguration
from .xcode_16_xcresult import XcDevice
from .xcode_16_xcresult import XcSummary
from .xcode_16_xcresult import XcTestFailure
from .xcode_16_xcresult import XcTestInsight
from .xcode_16_xcresult import XcTestNode
from .xcode_16_xcresult import XcTestNodeType
from .xcode_16_xcresult import XcTestPlanConfiguration
from .xcode_16_xcresult import XcTestResult
from .xcode_16_xcresult import XcTests
from .xcode_16_xcresult import XcTestStatistic
63 changes: 12 additions & 51 deletions src/codemagic/models/xctests/xcresult/xcode_16_xcresult.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@
from abc import abstractmethod
from typing import Any
from typing import Dict
from typing import Iterator
from typing import List
from typing import Optional
from typing import Type
from typing import TypeVar
from typing import Union

XcSchemaModelT = TypeVar("XcSchemaModelT", bound="XcSchemaModel")
XcModelT = TypeVar("XcModelT", bound="XcModel")


class XcTestResult(str, enum.Enum):
Expand Down Expand Up @@ -43,18 +42,18 @@ class XcTestNodeType(str, enum.Enum):


@dataclasses.dataclass
class XcSchemaModel(ABC):
class XcModel(ABC):
@classmethod
@abstractmethod
def from_dict(cls: Type[XcSchemaModelT], d: Dict[str, Any]) -> XcSchemaModelT:
def from_dict(cls: Type[XcModelT], d: Dict[str, Any]) -> XcModelT:
"""
Load model from `xcresulttool get test-results <subcommand>` output
"""
raise NotImplementedError()


@dataclasses.dataclass
class XcSummary(XcSchemaModel):
class XcSummary(XcModel):
"""
Model definitions for `xcresulttool get test-results summary` output.
Check schema with `xcrun xcresulttool help get test-results summary`.
Expand Down Expand Up @@ -96,7 +95,7 @@ def from_dict(cls, d: Dict[str, Any]) -> XcSummary:


@dataclasses.dataclass
class XcTestPlanConfiguration(XcSchemaModel):
class XcTestPlanConfiguration(XcModel):
device: XcDevice
test_plan_configuration: XcConfiguration
passed_tests: int
Expand All @@ -117,7 +116,7 @@ def from_dict(cls, d: Dict[str, Any]) -> XcTestPlanConfiguration:


@dataclasses.dataclass
class XcTestStatistic(XcSchemaModel):
class XcTestStatistic(XcModel):
subtitle: str
title: str

Expand All @@ -130,7 +129,7 @@ def from_dict(cls, d: Dict[str, Any]) -> XcTestStatistic:


@dataclasses.dataclass
class XcTestFailure(XcSchemaModel):
class XcTestFailure(XcModel):
test_name: str
target_name: str
failure_text: str
Expand All @@ -147,7 +146,7 @@ def from_dict(cls, d: Dict[str, Any]) -> XcTestFailure:


@dataclasses.dataclass
class XcTestInsight(XcSchemaModel):
class XcTestInsight(XcModel):
impact: str
category: str
text: str
Expand All @@ -162,7 +161,7 @@ def from_dict(cls, d: Dict[str, Any]) -> XcTestInsight:


@dataclasses.dataclass
class XcTests(XcSchemaModel):
class XcTests(XcModel):
"""
Model definitions for `xcresulttool get test-results tests` output.
Check schema with `xcrun xcresulttool help get test-results tests.
Expand All @@ -185,14 +184,11 @@ def from_dict(cls, d: Dict[str, Any]) -> XcTests:


@dataclasses.dataclass
class XcTestNode(XcSchemaModel):
# Required properties
class XcTestNode(XcModel):
node_type: XcTestNodeType
name: str
# Optional properties with defaults
tags: List[str] = dataclasses.field(default_factory=list)
children: List[XcTestNode] = dataclasses.field(default_factory=list)
# Optional properties
node_identifier: Optional[str] = None
details: Optional[str] = None
duration: Optional[str] = None
Expand All @@ -217,44 +213,9 @@ def from_dict(cls, d: Dict[str, Any]) -> XcTestNode:
child.parent = node
return node

def iter_children(self, child_type: XcTestNodeType) -> Iterator[XcTestNode]:
if self.node_type is child_type:
yield self
else:
for child in self.children:
yield from child.iter_children(child_type)

def get_parent(self, parent_type: XcTestNodeType) -> Optional[XcTestNode]:
if not isinstance(self.parent, XcTestNode):
return None
if self.parent.node_type is parent_type:
return self.parent
return self.parent.get_parent(parent_type)

@classmethod
def is_disabled(cls) -> bool:
return False # Disabled tests are completely excluded from reports

def is_failed(self) -> bool:
return self.result is XcTestResult.FAILED

def is_skipped(self) -> bool:
return self.result is XcTestResult.SKIPPED

def is_passed(self) -> bool:
return self.result is XcTestResult.PASSED

def get_duration(self) -> float:
if not self.duration:
return 0.0
duration = self.duration.replace(",", ".")
if duration.endswith("s"):
duration = duration[:-1]
return float(duration)


@dataclasses.dataclass
class XcDevice(XcSchemaModel):
class XcDevice(XcModel):
device_name: str
architecture: str
model_name: str
Expand All @@ -275,7 +236,7 @@ def from_dict(cls, d: Dict[str, Any]) -> XcDevice:


@dataclasses.dataclass
class XcConfiguration(XcSchemaModel):
class XcConfiguration(XcModel):
configuration_id: str
configuration_name: str

Expand Down

0 comments on commit dd94aea

Please sign in to comment.