Skip to content

Commit

Permalink
Add 'xcresulttool get test-reports' output parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
priitlatt committed Oct 11, 2024
1 parent f4389f7 commit c8c2a13
Show file tree
Hide file tree
Showing 16 changed files with 1,637 additions and 25 deletions.
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v5.0.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
Expand All @@ -9,16 +9,16 @@ repos:
hooks:
- id: add-trailing-comma
- repo: https://github.com/psf/black
rev: 24.4.2
rev: 24.10.0
hooks:
- id: black
language_version: python3.11
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.5.0
rev: v0.6.9
hooks:
- id: ruff
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.10.1
rev: v1.11.2
hooks:
- id: mypy
additional_dependencies: [types-requests]
Expand Down
163 changes: 158 additions & 5 deletions src/codemagic/models/xctests/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@

import pathlib
import re
from abc import ABC
from abc import abstractmethod
from datetime import datetime
from typing import Iterable
from typing import Iterator
from typing import List
from typing import Optional
from typing import Union

from codemagic.models.junit import Error
from codemagic.models.junit import Failure
Expand All @@ -20,13 +24,42 @@
from .xcresult import ActionsInvocationRecord
from .xcresult import ActionTestableSummary
from .xcresult import ActionTestMetadata
from .xcresult import XcTestResultsSummary
from .xcresult import XcTestResultsTests
from .xcresult import XcTestSuite as XcTestSuite
from .xcresult.xcode_16_xcresult import XcDevice
from .xcresult.xcode_16_xcresult import XcTestCase
from .xcresulttool import XcResultTool


class XcResultConverter:
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)

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

@classmethod
def _timestamp(cls, date: datetime) -> str:
def _timestamp(cls, date: Union[datetime, float, int]) -> str:
if isinstance(date, (float, int)):
date = datetime.fromtimestamp(date)
return date.strftime("%Y-%m-%dT%H:%M:%S")

@classmethod
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():
Expand Down Expand Up @@ -140,7 +173,127 @@ def actions_invocation_record_to_junit(cls, actions_invocation_record: ActionsIn
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 xcresult_to_junit(cls, xcresult: pathlib.Path) -> TestSuites:
actions_invocation_record = ActionsInvocationRecord.from_xcresult(xcresult)
return cls.actions_invocation_record_to_junit(actions_invocation_record)
def _iter_test_suite_nodes(cls, tests_results_tests: XcTestResultsTests) -> Iterable[XcTestSuite]:
for test_plan in tests_results_tests.test_plans:
for test_bundle in test_plan.test_bundles:
yield from test_bundle.test_suites

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

test_bundle = xc_test_suite.parent
test_plan = test_bundle.parent
tests = test_plan.parent
if not tests.devices:
return None
return tests.devices[0]

@classmethod
def _get_test_suite_name(cls, xc_test_suite: XcTestSuite) -> str:
name = xc_test_suite.name or ""
device_info = ""

device = cls._get_test_suite_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}"

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

@classmethod
def _get_test_case_failure(cls, xc_test_case: XcTestCase) -> Optional[Failure]:
if not xc_test_case.is_failed():
return None

messages = xc_test_case.get_failure_messages()
return Failure(
message=messages[0] if messages else "",
type="Error" if any("caught error" in m for m in messages) else "Failure",
failure_description="\n".join(messages) if len(messages) > 1 else None,
)

@classmethod
def _get_test_case_skipped(cls, xc_test_case: XcTestCase) -> Optional[Skipped]:
if not xc_test_case.is_skipped():
return None
messages = xc_test_case.get_skipped_messages()
return Skipped(message="\n".join(messages))

@classmethod
def _get_test_case(cls, xc_test_case: XcTestCase) -> TestCase:
return TestCase(
name=xc_test_case.get_method_name(),
classname=xc_test_case.get_classname(),
failure=cls._get_test_case_failure(xc_test_case),
time=xc_test_case.get_duration(),
status=xc_test_case.result,
skipped=cls._get_test_case_skipped(xc_test_case),
)

@classmethod
def _get_test_suite_properties(
cls,
xc_test_suite: XcTestSuite,
xc_test_result_summary: XcTestResultsSummary,
) -> List[Property]:
properties: List[Property] = [
Property(name="started_time", value=cls._timestamp(xc_test_result_summary.start_time)),
Property(name="ended_time", value=cls._timestamp(xc_test_result_summary.finish_time)),
Property(name="title", value=xc_test_suite.name),
]
device = cls._get_test_suite_run_destination(xc_test_suite)
if device:
properties.extend(
[
Property(name="device_name", value=device.model_name),
Property(name="device_architecture", value=device.architecture),
Property(name="device_identifier", value=device.device_id),
Property(name="device_operating_system", value=device.os_version),
Property(name="device_platform", value=device.platform),
],
)

return sorted(properties, key=lambda p: p.name)

@classmethod
def _get_test_suite(cls, xc_test_suite: XcTestSuite, xc_test_result_summary: XcTestResultsSummary) -> TestSuite:
return TestSuite(
name=cls._get_test_suite_name(xc_test_suite),
tests=len(xc_test_suite.test_cases),
disabled=sum(xc_test_case.is_disabled() for xc_test_case in xc_test_suite.test_cases),
errors=sum(xc_test_case.is_error() for xc_test_case in xc_test_suite.test_cases),
failures=sum(xc_test_case.is_failed() for xc_test_case in xc_test_suite.test_cases),
package=xc_test_suite.name,
skipped=sum(xc_test_case.is_skipped() for xc_test_case in xc_test_suite.test_cases),
time=sum(xc_test_case.get_duration() for xc_test_case in xc_test_suite.test_cases),
timestamp=cls._timestamp(xc_test_result_summary.finish_time),
testcases=[cls._get_test_case(xc_test_case) for xc_test_case in xc_test_suite.test_cases],
properties=cls._get_test_suite_properties(xc_test_suite, xc_test_result_summary),
)

def convert(self) -> TestSuites:
tests_output = XcResultTool.get_test_report_tests(self.xcresult)
summary_output = XcResultTool.get_test_report_summary(self.xcresult)

test_results_tests = XcTestResultsTests.from_dict(tests_output)
test_results_summary = XcTestResultsSummary.from_dict(summary_output)

test_suites: List[TestSuite] = []
for test_suite in self._iter_test_suite_nodes(test_results_tests):
test_suites.append(self._get_test_suite(test_suite, test_results_summary))

return TestSuites(
name=test_results_summary.title,
test_suites=test_suites,
)
41 changes: 41 additions & 0 deletions src/codemagic/models/xctests/xcresult/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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 .xcode_16_xcresult import XcTestDetails
from .xcode_16_xcresult import XcTestResultsSummary
from .xcode_16_xcresult import XcTestResultsTests
from .xcode_16_xcresult import XcTestSuite
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
is created according to the description from
`xcrun xcresulttool formatDescription get`.
"""

from __future__ import annotations

import pathlib
Expand All @@ -19,6 +20,8 @@
from typing import TypeVar
from typing import Union

from codemagic.models.xctests.xcresulttool import XcResultTool


class _BaseAbstractRecord(metaclass=ABCMeta):
def __init__(self, data: Dict, xcresult: pathlib.Path):
Expand All @@ -34,8 +37,6 @@ def __init__(self, data: Dict, xcresult: pathlib.Path):

@lru_cache()
def _get_cached_object_from_bundle(xcresult: pathlib.Path, object_id: Optional[str] = None) -> Dict[str, Any]:
from .xcresulttool import XcResultTool

if object_id is None:
return XcResultTool.get_bundle(xcresult)
else:
Expand Down
Loading

0 comments on commit c8c2a13

Please sign in to comment.