Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
Version 0.70.0
-------------

This release drops support for parsing XcResult bundles with `xcresulttool` from Xcode versions older than 16.0. Some of the changes to **Python API are backwards incompatible**.

Xcode 16.0 or newer is required to convert XcResult bundles to JUnit test reports. This affects `xcode-project` actions `run-tests`, `test-summary` and `junit-test-results`.

**Development**
- **Breaking**: Remove `LegacyXcResultConverter` from `codemagic.models.xctests.converter`.
- **Breaking**: Remove module `codemagic.models.xctests.xcresult.legacy_xcresult` along with all of its definitions (`ActionsInvocationRecord` etc.) that were also exported from packages `codemagic.models.xctests.xcresult` and `codemagic.models.xctests`.
- **Breaking**: Remove methods `is_legacy`, `get_bundle` and `get_object` from `XcResultTool`.
- **Breaking**: Merge `Xcode16XcResultConverter` into `XcResultConverter`.

Version 0.69.1
-------------

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "codemagic-cli-tools"
version = "0.69.1"
version = "0.70.0"
description = "CLI tools used in Codemagic builds"
authors = [{ name = "Priit Lätt", email = "priit@nevercode.io" }]
requires-python = ">=3.8,<4"
Expand Down
2 changes: 1 addition & 1 deletion src/codemagic/__version__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
__title__ = "codemagic-cli-tools"
__description__ = "CLI tools used in Codemagic builds"
__version__ = "0.69.1.dev"
__version__ = "0.70.0.dev"
__url__ = "https://github.com/codemagic-ci-cd/cli-tools"
__licence__ = "GNU General Public License v3.0"
1 change: 0 additions & 1 deletion src/codemagic/models/xctests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from .collector import XcResultCollector
from .converter import XcResultConverter
from .xcresult import ActionsInvocationRecord
from .xcresulttool import XcResultTool
from .xcresulttool import XcResultToolError
145 changes: 2 additions & 143 deletions src/codemagic/models/xctests/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

import pathlib
import re
from abc import ABC
from abc import abstractmethod
from datetime import datetime
from datetime import timedelta
from typing import Iterator
Expand All @@ -13,18 +11,12 @@
from typing import cast

from codemagic.models.junit import Error
from codemagic.models.junit import Failure
from codemagic.models.junit import Property
from codemagic.models.junit import Skipped
from codemagic.models.junit import TestCase
from codemagic.models.junit import TestSuite
from codemagic.models.junit import TestSuites

from .xcresult import ActionDeviceRecord
from .xcresult import ActionRecord
from .xcresult import ActionsInvocationRecord
from .xcresult import ActionTestableSummary
from .xcresult import ActionTestMetadata
from .xcresult import XcDevice
from .xcresult import XcSummary
from .xcresult import XcTestNode
Expand All @@ -34,15 +26,7 @@
from .xcresulttool import XcResultTool


class XcResultConverter(ABC):
def __new__(cls, *args, **kwargs):
if cls is XcResultConverter:
if XcResultTool.is_legacy():
cls = LegacyXcResultConverter
else:
cls = Xcode16XcResultConverter
return object.__new__(cls)

class XcResultConverter:
def __init__(self, xcresult: pathlib.Path):
self.xcresult = xcresult

Expand All @@ -56,131 +40,6 @@ def _timestamp(cls, date: Union[datetime, float, int]) -> str:
def xcresult_to_junit(cls, xcresult: pathlib.Path) -> TestSuites:
return cls(xcresult).convert()

@abstractmethod
def convert(self) -> TestSuites:
raise NotImplementedError()


class LegacyXcResultConverter(XcResultConverter):
@classmethod
def _get_test_case_error(cls, test: ActionTestMetadata) -> Optional[Error]:
if not test.is_error():
return None
return Error(
message=test.get_error_message(),
type=test.get_error_type(),
error_description=test.get_failure_description(),
)

@classmethod
def _get_test_case_failure(cls, test: ActionTestMetadata) -> Optional[Failure]:
if not test.is_failure():
return None
return Failure(
message=test.get_failure_message(),
type=test.get_failure_type(),
failure_description=test.get_failure_description(),
)

@classmethod
def _get_test_case_skipped(cls, test: ActionTestMetadata) -> Optional[Skipped]:
if not test.is_skipped():
return None
return Skipped(message=test.get_skipped_message())

@classmethod
def _get_test_case(cls, test: ActionTestMetadata) -> TestCase:
return TestCase(
name=test.get_method_name(),
classname=test.get_classname(),
error=cls._get_test_case_error(test),
failure=cls._get_test_case_failure(test),
time=test.duration,
status=test.test_status,
skipped=cls._get_test_case_skipped(test),
)

@classmethod
def _get_test_suite_run_destination(cls, action: ActionRecord) -> Optional[ActionDeviceRecord]:
if action.run_destination and action.run_destination.target_device_record:
return action.run_destination.target_device_record
return None

@classmethod
def _get_test_suite_properties(cls, action: ActionRecord) -> List[Property]:
properties: List[Property] = [
Property(name="started_time", value=cls._timestamp(action.started_time)),
Property(name="ended_time", value=cls._timestamp(action.ended_time)),
]
if action.title:
properties.append(Property(name="title", value=action.title))

device = cls._get_test_suite_run_destination(action)
if device:
properties.extend(
[
Property(name="device_name", value=device.model_name),
Property(name="device_architecture", value=device.native_architecture),
Property(name="device_identifier", value=device.identifier),
Property(name="device_operating_system", value=device.operating_system_version_with_build_number),
Property(name="device_platform", value=device.platform_record.user_description),
],
)
return sorted(properties, key=lambda p: p.name)

@classmethod
def _get_test_suite_name(cls, action: ActionRecord, testable_summary: ActionTestableSummary):
name = testable_summary.name or ""
device_info = ""

device = cls._get_test_suite_run_destination(action)
if device:
platform_name = device.platform_record.user_description
platform = re.sub("simulator", "", platform_name, flags=re.IGNORECASE).strip()
device_info = f"{platform} {device.operating_system_version} {device.model_name}"

if name and device_info:
return f"{name} [{device_info}]"
return name or device_info

@classmethod
def _get_test_suite(cls, action: ActionRecord, testable_summary: ActionTestableSummary) -> TestSuite:
tests = testable_summary.get_tests()
return TestSuite(
name=cls._get_test_suite_name(action, testable_summary),
tests=len(tests),
disabled=sum(t.is_disabled() for t in tests),
errors=sum(t.is_error() for t in tests),
failures=sum(t.is_failure() for t in tests),
package=testable_summary.name,
skipped=sum(t.is_skipped() for t in tests),
time=sum((test.duration or 0) for test in tests),
timestamp=cls._timestamp(action.ended_time),
testcases=[cls._get_test_case(test) for test in tests],
properties=cls._get_test_suite_properties(action),
)

@classmethod
def _get_action_test_suites(cls, action: ActionRecord) -> Iterator[TestSuite]:
run_summaries = action.action_result.action_test_plan_run_summaries
test_summaries = run_summaries.summaries if run_summaries else []
for test_summary in test_summaries:
for testable_summary in test_summary.testable_summaries:
yield cls._get_test_suite(action, testable_summary)

@classmethod
def actions_invocation_record_to_junit(cls, actions_invocation_record: ActionsInvocationRecord) -> TestSuites:
test_suites: List[TestSuite] = []
for action in actions_invocation_record.actions:
test_suites.extend(cls._get_action_test_suites(action))
return TestSuites(name="", test_suites=test_suites)

def convert(self) -> TestSuites:
actions_invocation_record = ActionsInvocationRecord.from_xcresult(self.xcresult)
return self.actions_invocation_record_to_junit(actions_invocation_record)


class Xcode16XcResultConverter(XcResultConverter):
@classmethod
def _iter_nodes(
cls,
Expand All @@ -197,7 +56,7 @@ def _iter_nodes(
@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
# As a first iteration only the first test destination is used

parent: Union[XcTests, XcTestNode] = root_node
while isinstance(parent, XcTestNode):
Expand Down
37 changes: 0 additions & 37 deletions src/codemagic/models/xctests/xcresult/__init__.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,3 @@
from .legacy_xcresult import ActionDeviceRecord
from .legacy_xcresult import ActionPlatformRecord
from .legacy_xcresult import ActionRecord
from .legacy_xcresult import ActionResult
from .legacy_xcresult import ActionRunDestinationRecord
from .legacy_xcresult import ActionSDKRecord
from .legacy_xcresult import ActionsInvocationMetadata
from .legacy_xcresult import ActionsInvocationRecord
from .legacy_xcresult import ActionTestableSummary
from .legacy_xcresult import ActionTestActivitySummary
from .legacy_xcresult import ActionTestAttachment
from .legacy_xcresult import ActionTestFailureSummary
from .legacy_xcresult import ActionTestMetadata
from .legacy_xcresult import ActionTestNoticeSummary
from .legacy_xcresult import ActionTestPerformanceMetricSummary
from .legacy_xcresult import ActionTestPlanRunSummaries
from .legacy_xcresult import ActionTestPlanRunSummary
from .legacy_xcresult import ActionTestSummary
from .legacy_xcresult import ActionTestSummaryGroup
from .legacy_xcresult import ActionTestSummaryIdentifiableObject
from .legacy_xcresult import ArchiveInfo
from .legacy_xcresult import CodeCoverageInfo
from .legacy_xcresult import DocumentLocation
from .legacy_xcresult import EntityIdentifier
from .legacy_xcresult import IssueSummary
from .legacy_xcresult import Reference
from .legacy_xcresult import ResultIssueSummaries
from .legacy_xcresult import ResultMetrics
from .legacy_xcresult import SortedKeyValueArray
from .legacy_xcresult import SortedKeyValueArrayPair
from .legacy_xcresult import SourceCodeContext
from .legacy_xcresult import SourceCodeFrame
from .legacy_xcresult import SourceCodeLocation
from .legacy_xcresult import SourceCodeSymbolInfo
from .legacy_xcresult import TestAssociatedError
from .legacy_xcresult import TestFailureIssueSummary
from .legacy_xcresult import TypeDefinition
from .xcresult import XcConfiguration
from .xcresult import XcDevice
from .xcresult import XcSummary
Expand Down
Loading
Loading