From 1f58c1ffa43d1e2dbf41f7bf7141eb37822af609 Mon Sep 17 00:00:00 2001 From: Artemii Date: Wed, 30 Sep 2026 14:55:06 +0200 Subject: [PATCH 1/6] Fix: support xcresult node types --- CHANGELOG.md | 13 ++++++++++++ pyproject.toml | 2 +- src/codemagic/__version__.py | 2 +- src/codemagic/models/xctests/converter.py | 5 +++++ .../models/xctests/xcresult/xcresult.py | 2 ++ .../converter/test_xcode_16_converter.py | 20 +++++++++++++++++++ .../models/xctests/xcresult/test_xcresult.py | 12 +++++++++++ uv.lock | 2 +- 8 files changed, 55 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4197134..6007011c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +Version 0.69.1 +------------- + +**Bugfixes** +- Support `Expected Failure` and `Skip Message` test node types for XCTest results + which were added in `xcresulttool` test-results schema 0.2.0 (default on Xcode 27). + Fixes actions + - `xcode-project run-tests`, + - `xcode-project test-summary`, + - `xcode-project junit-test-results`. + Also restore skip messages in JUnit output when Apple emits `Skip Message` + children instead of `Failure Message` with result `Skipped`. + Version 0.69.0 ------------- diff --git a/pyproject.toml b/pyproject.toml index c6fa3143..5adfb9a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "codemagic-cli-tools" -version = "0.69.0" +version = "0.69.1" description = "CLI tools used in Codemagic builds" authors = [{ name = "Priit Lätt", email = "priit@nevercode.io" }] requires-python = ">=3.8,<4" diff --git a/src/codemagic/__version__.py b/src/codemagic/__version__.py index e49d2a2c..05e914a8 100644 --- a/src/codemagic/__version__.py +++ b/src/codemagic/__version__.py @@ -1,5 +1,5 @@ __title__ = "codemagic-cli-tools" __description__ = "CLI tools used in Codemagic builds" -__version__ = "0.69.0.dev" +__version__ = "0.69.1.dev" __url__ = "https://github.com/codemagic-ci-cd/cli-tools" __licence__ = "GNU General Public License v3.0" diff --git a/src/codemagic/models/xctests/converter.py b/src/codemagic/models/xctests/converter.py index df628469..750e6891 100644 --- a/src/codemagic/models/xctests/converter.py +++ b/src/codemagic/models/xctests/converter.py @@ -238,10 +238,15 @@ def _get_test_case_skipped(cls, xc_test_case: XcTestNode) -> Optional[Skipped]: if xc_test_case.result is not XcTestResult.SKIPPED: return None + # Schema 0.1.0 / Xcode 16: skip reason is a Failure Message child with result 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] + # Schema 0.2.0+ / Xcode 27: skip reason is a Skip Message child. + skip_message_nodes = cls._iter_nodes(xc_test_case, XcTestNodeType.SKIP_MESSAGE) + skipped_messages.extend(node.name for node in skip_message_nodes if node.name) + return Skipped(message="\n".join(skipped_messages)) @classmethod diff --git a/src/codemagic/models/xctests/xcresult/xcresult.py b/src/codemagic/models/xctests/xcresult/xcresult.py index e24f0749..84ef92fd 100644 --- a/src/codemagic/models/xctests/xcresult/xcresult.py +++ b/src/codemagic/models/xctests/xcresult/xcresult.py @@ -40,6 +40,8 @@ class XcTestNodeType(str, enum.Enum): EXPRESSION = "Expression" TEST_VALUE = "Test Value" RUNTIME_WARNING = "Runtime Warning" + SKIP_MESSAGE = "Skip Message" + EXPECTED_FAILURE = "Expected Failure" @dataclasses.dataclass diff --git a/tests/models/xctests/converter/test_xcode_16_converter.py b/tests/models/xctests/converter/test_xcode_16_converter.py index 82da7463..6540ee1c 100644 --- a/tests/models/xctests/converter/test_xcode_16_converter.py +++ b/tests/models/xctests/converter/test_xcode_16_converter.py @@ -12,6 +12,9 @@ from codemagic.models.junit import TestSuites from codemagic.models.xctests import XcResultTool from codemagic.models.xctests.converter import Xcode16XcResultConverter +from codemagic.models.xctests.xcresult import XcTestNode +from codemagic.models.xctests.xcresult import XcTestNodeType +from codemagic.models.xctests.xcresult import XcTestResult @pytest.fixture @@ -151,3 +154,20 @@ def test_converter(mock_datetime, expected_properties): def test_parse_xcresult_test_node_duration_value(duration, expected_value): value = Xcode16XcResultConverter.parse_xcresult_test_node_duration_value(duration) assert value == pytest.approx(expected_value) + + +def test_get_test_case_skipped_from_skip_message_node(): + xc_test_case = XcTestNode( + name="testSkip()", + node_type=XcTestNodeType.TEST_CASE, + result=XcTestResult.SKIPPED, + children=[ + XcTestNode( + name="Test skipped - skipping on purpose", + node_type=XcTestNodeType.SKIP_MESSAGE, + ), + ], + ) + assert Xcode16XcResultConverter._get_test_case_skipped(xc_test_case) == Skipped( + message="Test skipped - skipping on purpose", + ) diff --git a/tests/models/xctests/xcresult/test_xcresult.py b/tests/models/xctests/xcresult/test_xcresult.py index 6137793c..f3b6d748 100644 --- a/tests/models/xctests/xcresult/test_xcresult.py +++ b/tests/models/xctests/xcresult/test_xcresult.py @@ -10,6 +10,18 @@ from codemagic.models.xctests.xcresult.xcresult import XcTestStatistic +def test_parse_expected_failure_node_type(): + node = XcTestNode.from_dict({"nodeType": "Expected Failure", "name": "known bad assertion"}) + assert node.node_type is XcTestNodeType.EXPECTED_FAILURE + assert node.name == "known bad assertion" + + +def test_parse_skip_message_node_type(): + node = XcTestNode.from_dict({"nodeType": "Skip Message", "name": "Test skipped - skipping on purpose"}) + assert node.node_type is XcTestNodeType.SKIP_MESSAGE + assert node.name == "Test skipped - skipping on purpose" + + def test_load_test_results_summary(test_results_summary_dict): test_results_summary = XcSummary.from_dict(test_results_summary_dict) diff --git a/uv.lock b/uv.lock index 1dfb3c04..114adf13 100644 --- a/uv.lock +++ b/uv.lock @@ -550,7 +550,7 @@ wheels = [ [[package]] name = "codemagic-cli-tools" -version = "0.69.0" +version = "0.69.1" source = { editable = "." } dependencies = [ { name = "cryptography" }, From 80adf0a0545528bbfd6606c84ab1c7129d8630df Mon Sep 17 00:00:00 2001 From: Artemii Date: Mon, 5 Oct 2026 12:16:36 +0200 Subject: [PATCH 2/6] Fix: dedupe skipped message sources --- src/codemagic/models/xctests/converter.py | 3 ++- .../converter/test_xcode_16_converter.py | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/codemagic/models/xctests/converter.py b/src/codemagic/models/xctests/converter.py index 750e6891..fb25c1ff 100644 --- a/src/codemagic/models/xctests/converter.py +++ b/src/codemagic/models/xctests/converter.py @@ -247,7 +247,8 @@ def _get_test_case_skipped(cls, xc_test_case: XcTestNode) -> Optional[Skipped]: skip_message_nodes = cls._iter_nodes(xc_test_case, XcTestNodeType.SKIP_MESSAGE) skipped_messages.extend(node.name for node in skip_message_nodes if node.name) - return Skipped(message="\n".join(skipped_messages)) + unique_skipped_messages = dict.fromkeys(skipped_messages) + return Skipped(message="\n".join(unique_skipped_messages)) @classmethod def parse_xcresult_test_node_duration_value(cls, xc_duration: str) -> float: diff --git a/tests/models/xctests/converter/test_xcode_16_converter.py b/tests/models/xctests/converter/test_xcode_16_converter.py index 6540ee1c..fe96dfa4 100644 --- a/tests/models/xctests/converter/test_xcode_16_converter.py +++ b/tests/models/xctests/converter/test_xcode_16_converter.py @@ -171,3 +171,24 @@ def test_get_test_case_skipped_from_skip_message_node(): assert Xcode16XcResultConverter._get_test_case_skipped(xc_test_case) == Skipped( message="Test skipped - skipping on purpose", ) + + +def test_get_test_case_skipped_deduplicates_identical_messages(): + message = "Test skipped - skipping on purpose" + xc_test_case = XcTestNode( + name="testSkip()", + node_type=XcTestNodeType.TEST_CASE, + result=XcTestResult.SKIPPED, + children=[ + XcTestNode( + name=message, + node_type=XcTestNodeType.FAILURE_MESSAGE, + result=XcTestResult.SKIPPED, + ), + XcTestNode( + name=message, + node_type=XcTestNodeType.SKIP_MESSAGE, + ), + ], + ) + assert Xcode16XcResultConverter._get_test_case_skipped(xc_test_case) == Skipped(message=message) From d1143c96a1cead5cdcf7b55b23eb588d7f5b043a Mon Sep 17 00:00:00 2001 From: Artemii Date: Mon, 5 Oct 2026 14:21:38 +0200 Subject: [PATCH 3/6] Fix: exclude expected failure errors --- src/codemagic/models/xctests/converter.py | 22 +++++++++--- .../converter/test_xcode_16_converter.py | 34 +++++++++++++++++++ 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/codemagic/models/xctests/converter.py b/src/codemagic/models/xctests/converter.py index fb25c1ff..6e2afe27 100644 --- a/src/codemagic/models/xctests/converter.py +++ b/src/codemagic/models/xctests/converter.py @@ -182,12 +182,17 @@ def convert(self) -> TestSuites: class Xcode16XcResultConverter(XcResultConverter): @classmethod - def _iter_nodes(cls, root_node: XcTestNode, node_type: XcTestNodeType) -> Iterator[XcTestNode]: + def _iter_nodes( + cls, + root_node: XcTestNode, + node_type: XcTestNodeType, + skip_subtree_node_type: Optional[XcTestNodeType] = None, + ) -> Iterator[XcTestNode]: if root_node.node_type is node_type: yield root_node - else: + elif root_node.node_type is not skip_subtree_node_type: for child in root_node.children: - yield from cls._iter_nodes(child, node_type) + yield from cls._iter_nodes(child, node_type, skip_subtree_node_type) @classmethod def _get_run_destination(cls, root_node: XcTestNode) -> Optional[XcDevice]: @@ -225,8 +230,15 @@ def _get_test_case_error(cls, xc_test_case: XcTestNode) -> Optional[Error]: if xc_test_case.result is not XcTestResult.FAILED: return None - 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] + failure_messages_nodes = cls._iter_nodes( + xc_test_case, + XcTestNodeType.FAILURE_MESSAGE, + skip_subtree_node_type=XcTestNodeType.EXPECTED_FAILURE, + ) + unexpected_failure_message_nodes = ( + node for node in failure_messages_nodes if node.result is not XcTestResult.EXPECTED_FAILURE + ) + failure_messages = [node.name for node in unexpected_failure_message_nodes if node.name] return Error( message=failure_messages[0] if failure_messages else "", type="Error" if any("caught error" in m for m in failure_messages) else "Failure", diff --git a/tests/models/xctests/converter/test_xcode_16_converter.py b/tests/models/xctests/converter/test_xcode_16_converter.py index fe96dfa4..69313d67 100644 --- a/tests/models/xctests/converter/test_xcode_16_converter.py +++ b/tests/models/xctests/converter/test_xcode_16_converter.py @@ -192,3 +192,37 @@ def test_get_test_case_skipped_deduplicates_identical_messages(): ], ) assert Xcode16XcResultConverter._get_test_case_skipped(xc_test_case) == Skipped(message=message) + + +def test_get_test_case_error_excludes_expected_failures(): + real_failure = 'XCTAssertEqual failed: ("3") is not equal to ("4") - should be real failure' + xc_test_case = XcTestNode( + name="testExpectedFailureThenRealFailure()", + node_type=XcTestNodeType.TEST_CASE, + result=XcTestResult.FAILED, + children=[ + XcTestNode( + name=real_failure, + node_type=XcTestNodeType.FAILURE_MESSAGE, + ), + XcTestNode( + name="known bad assertion", + node_type=XcTestNodeType.EXPECTED_FAILURE, + children=[ + XcTestNode( + name="nested expected failure message", + node_type=XcTestNodeType.FAILURE_MESSAGE, + ), + ], + ), + XcTestNode( + name="expected failure via result", + node_type=XcTestNodeType.FAILURE_MESSAGE, + result=XcTestResult.EXPECTED_FAILURE, + ), + ], + ) + assert Xcode16XcResultConverter._get_test_case_error(xc_test_case) == Error( + message=real_failure, + type="Failure", + ) From 21c7f238d61150d05cdc959e4171d3c4dbf1f170 Mon Sep 17 00:00:00 2001 From: Artemii Date: Mon, 5 Oct 2026 14:33:35 +0200 Subject: [PATCH 4/6] Emit expected failure system-out --- CHANGELOG.md | 2 + src/codemagic/models/junit/definitions.py | 4 ++ src/codemagic/models/xctests/converter.py | 12 ++++++ tests/models/junit/test_xml_generation.py | 20 ++++++++++ .../converter/test_xcode_16_converter.py | 37 +++++++++++++++++++ 5 files changed, 75 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6007011c..15299549 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ Version 0.69.1 - `xcode-project junit-test-results`. Also restore skip messages in JUnit output when Apple emits `Skip Message` children instead of `Failure Message` with result `Skipped`. +- Emit `XCTExpectFailure` reason text in JUnit `` for test cases with + `status="Expected Failure"`. Version 0.69.0 ------------- diff --git a/src/codemagic/models/junit/definitions.py b/src/codemagic/models/junit/definitions.py index 48bba132..c12848a0 100644 --- a/src/codemagic/models/junit/definitions.py +++ b/src/codemagic/models/junit/definitions.py @@ -159,6 +159,7 @@ class TestCase: error: Optional[Error] = None failure: Optional[Failure] = None skipped: Optional[Skipped] = None + system_out: Optional[str] = None __test__ = False # Tell Pytest not to collect this class as test @@ -190,6 +191,9 @@ def as_xml(self) -> Element: element.append(self.failure.as_xml()) if self.skipped: element.append(self.skipped.as_xml()) + if self.system_out: + system_out = SubElement(element, "system-out") + system_out.text = self.system_out return element diff --git a/src/codemagic/models/xctests/converter.py b/src/codemagic/models/xctests/converter.py index 6e2afe27..9ab2585e 100644 --- a/src/codemagic/models/xctests/converter.py +++ b/src/codemagic/models/xctests/converter.py @@ -262,6 +262,17 @@ def _get_test_case_skipped(cls, xc_test_case: XcTestNode) -> Optional[Skipped]: unique_skipped_messages = dict.fromkeys(skipped_messages) return Skipped(message="\n".join(unique_skipped_messages)) + @classmethod + def _get_test_case_system_out(cls, xc_test_case: XcTestNode) -> Optional[str]: + if xc_test_case.result is not XcTestResult.EXPECTED_FAILURE: + return None + + expected_failure_nodes = cls._iter_nodes(xc_test_case, XcTestNodeType.EXPECTED_FAILURE) + reasons = [node.name for node in expected_failure_nodes if node.name] + if not reasons: + return None + return "\n".join(dict.fromkeys(reasons)) + @classmethod def parse_xcresult_test_node_duration_value(cls, xc_duration: str) -> float: duration = timedelta() @@ -310,6 +321,7 @@ def _get_test_case(cls, xc_test_case: XcTestNode, xc_test_suite: XcTestNode) -> 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), + system_out=cls._get_test_case_system_out(xc_test_case), ) @classmethod diff --git a/tests/models/junit/test_xml_generation.py b/tests/models/junit/test_xml_generation.py index d1b0ba79..14cb13a0 100644 --- a/tests/models/junit/test_xml_generation.py +++ b/tests/models/junit/test_xml_generation.py @@ -351,3 +351,23 @@ def test_xml(temp_dir, _testsuites, expected_xml_path): generated_xml = ElementTree.parse(xml_path) expected_xml = ElementTree.parse(expected_xml_path) _assert_elements_are_equal(generated_xml.getroot(), expected_xml.getroot()) + + +def test_testcase_emits_system_out(): + testcase = TestCase( + classname="ExpectedFailureProbeTests", + name="testExpectedFailure()", + status="Expected Failure", + time=0.001, + system_out="known bad assertion", + ) + xml = testcase.as_xml() + system_out_elements = [child for child in xml if child.tag == "system-out"] + assert len(system_out_elements) == 1 + assert system_out_elements[0].text == "known bad assertion" + + +def test_testcase_omits_system_out_when_unset(): + testcase = TestCase(classname="Suite", name="testPass()", status="Success") + xml = testcase.as_xml() + assert all(child.tag != "system-out" for child in xml) diff --git a/tests/models/xctests/converter/test_xcode_16_converter.py b/tests/models/xctests/converter/test_xcode_16_converter.py index 69313d67..683b8548 100644 --- a/tests/models/xctests/converter/test_xcode_16_converter.py +++ b/tests/models/xctests/converter/test_xcode_16_converter.py @@ -226,3 +226,40 @@ def test_get_test_case_error_excludes_expected_failures(): message=real_failure, type="Failure", ) + + +def test_get_test_case_system_out_from_expected_failure_children(): + xc_test_case = XcTestNode( + name="testExpectedFailure()", + node_type=XcTestNodeType.TEST_CASE, + node_identifier="ExpectedFailureProbeTests/testExpectedFailure()", + result=XcTestResult.EXPECTED_FAILURE, + children=[ + XcTestNode( + name="known bad assertion", + node_type=XcTestNodeType.EXPECTED_FAILURE, + ), + ], + ) + xc_test_suite = XcTestNode(name="ExpectedFailureProbeTests", node_type=XcTestNodeType.TEST_SUITE) + + test_case = Xcode16XcResultConverter._get_test_case(xc_test_case, xc_test_suite) + + assert test_case == TestCase( + name="testExpectedFailure()", + classname="ExpectedFailureProbeTests", + error=None, + time=0.0, + status="Expected Failure", + skipped=None, + system_out="known bad assertion", + ) + + +def test_get_test_case_system_out_absent_for_success(): + xc_test_case = XcTestNode( + name="testPass()", + node_type=XcTestNodeType.TEST_CASE, + result=XcTestResult.PASSED, + ) + assert Xcode16XcResultConverter._get_test_case_system_out(xc_test_case) is None From ab1db6768de379a61a01c1a2383197e263608fe6 Mon Sep 17 00:00:00 2001 From: Artemii <40743076+artemii-yanushevskyi@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:08:18 +0200 Subject: [PATCH 5/6] Add mixed expected-failure support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Priit Lätt --- CHANGELOG.md | 10 +++------- src/codemagic/models/xctests/converter.py | 10 +++++++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15299549..d0256239 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,16 +2,12 @@ Version 0.69.1 ------------- **Bugfixes** -- Support `Expected Failure` and `Skip Message` test node types for XCTest results - which were added in `xcresulttool` test-results schema 0.2.0 (default on Xcode 27). - Fixes actions +- Support `Expected Failure` and `Skip Message` test node types for XCTest results which were added in `xcresulttool` test-results schema 0.2.0 (default on Xcode 27). Fixes actions - `xcode-project run-tests`, - `xcode-project test-summary`, - `xcode-project junit-test-results`. - Also restore skip messages in JUnit output when Apple emits `Skip Message` - children instead of `Failure Message` with result `Skipped`. -- Emit `XCTExpectFailure` reason text in JUnit `` for test cases with - `status="Expected Failure"`. + Also restore skip messages in JUnit output when Apple emits `Skip Message` children instead of `Failure Message` with result `Skipped`. +- Emit `XCTExpectFailure` reason text in JUnit `` for test cases with expected failures, including failed test cases that also have expected failures. Version 0.69.0 ------------- diff --git a/src/codemagic/models/xctests/converter.py b/src/codemagic/models/xctests/converter.py index 9ab2585e..3a70397f 100644 --- a/src/codemagic/models/xctests/converter.py +++ b/src/codemagic/models/xctests/converter.py @@ -264,11 +264,15 @@ def _get_test_case_skipped(cls, xc_test_case: XcTestNode) -> Optional[Skipped]: @classmethod def _get_test_case_system_out(cls, xc_test_case: XcTestNode) -> Optional[str]: - if xc_test_case.result is not XcTestResult.EXPECTED_FAILURE: - return None - expected_failure_nodes = cls._iter_nodes(xc_test_case, XcTestNodeType.EXPECTED_FAILURE) reasons = [node.name for node in expected_failure_nodes if node.name] + + failure_messages_nodes = cls._iter_nodes(xc_test_case, XcTestNodeType.FAILURE_MESSAGE) + expected_failure_message_nodes = ( + node for node in failure_messages_nodes if node.result is XcTestResult.EXPECTED_FAILURE + ) + reasons.extend(node.name for node in expected_failure_message_nodes if node.name) + if not reasons: return None return "\n".join(dict.fromkeys(reasons)) From 52365ef17996c5d507966655c248ea58b8b97bea Mon Sep 17 00:00:00 2001 From: Artemii Date: Mon, 5 Oct 2026 17:10:57 +0200 Subject: [PATCH 6/6] Test: mixed expected-failure system-out --- .../converter/test_xcode_16_converter.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/models/xctests/converter/test_xcode_16_converter.py b/tests/models/xctests/converter/test_xcode_16_converter.py index 683b8548..5c2367c0 100644 --- a/tests/models/xctests/converter/test_xcode_16_converter.py +++ b/tests/models/xctests/converter/test_xcode_16_converter.py @@ -263,3 +263,42 @@ def test_get_test_case_system_out_absent_for_success(): result=XcTestResult.PASSED, ) assert Xcode16XcResultConverter._get_test_case_system_out(xc_test_case) is None + + +def test_get_test_case_system_out_on_failed_case_with_expected_failures(): + real_failure = 'XCTAssertEqual failed: ("3") is not equal to ("4") - should be real failure' + xc_test_case = XcTestNode( + name="testExpectedFailureThenRealFailure()", + node_type=XcTestNodeType.TEST_CASE, + node_identifier="ExpectedFailureProbeTests/testExpectedFailureThenRealFailure()", + result=XcTestResult.FAILED, + children=[ + XcTestNode( + name=real_failure, + node_type=XcTestNodeType.FAILURE_MESSAGE, + ), + XcTestNode( + name="known bad assertion", + node_type=XcTestNodeType.EXPECTED_FAILURE, + children=[ + XcTestNode( + name="nested expected failure message", + node_type=XcTestNodeType.FAILURE_MESSAGE, + ), + ], + ), + XcTestNode( + name="expected failure via result", + node_type=XcTestNodeType.FAILURE_MESSAGE, + result=XcTestResult.EXPECTED_FAILURE, + ), + ], + ) + xc_test_suite = XcTestNode(name="ExpectedFailureProbeTests", node_type=XcTestNodeType.TEST_SUITE) + + test_case = Xcode16XcResultConverter._get_test_case(xc_test_case, xc_test_suite) + + assert test_case.error == Error(message=real_failure, type="Failure") + assert test_case.system_out == "known bad assertion\nexpected failure via result" + assert test_case.status == "Failed" + assert test_case.skipped is None