Skip to content

[trainer] Support parsing test results from SwiftTesting #29463

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 31 commits into from
Mar 13, 2025

Conversation

AliSoftware
Copy link
Contributor

@AliSoftware AliSoftware commented Feb 24, 2025

This PR allows trainer (and by extension scan/run_tests) to support test results from Swift Testing—by parsing the .xcresult bundle using the new xcresulttool API

☑️ Checklist

  • I've run bundle exec rspec from the root directory to see all new and existing tests pass
  • I've followed the fastlane code style and run bundle exec rubocop -a to ensure the code style is valid
  • I see several green ci/circleci builds in the "All checks have passed" section of my PR (connect CircleCI to GitHub if not)
  • I've read the Contribution Guidelines
  • I've updated the documentation if necessary.
  • I've added or updated relevant unit tests.

❔ Motivation and Context

In Xcode 16b3, the xcresulttool tool—that ships with Xcode Command Line Tools to parse and extract data from .xcresult bundles—had its CLI API updated.

So far we've relied on adding the --legacy flag to our xcresulttool invocations to be able to still execute old CLI and still use the old way of calling the tool, using xcresulttool get <object> --format json / xcresulttool get <object> --id <id>. But:

  • As the name of the flag suggests, this way of calling the tool is legacy, and might become unsupported in future versions
  • That way of calling the tool seems to not support extracting information from reports from tests written using Swift Testing, and only supports tests written in XCTest.

The version of xcresulttool that ships with Xcode 16+ now expects us to call the CLI with xcresulttool get test-results test …, and generates a new JSON format (whose JSONSchema is described in xcresulttool get test-results test --help) that is easier to parse but more importantly also supports Swift Testing test results.

✍️ Description

Instead of trying to modify the implementation of xcresult.rb to adapt to the new command and JSON format—which would make the code difficult to read if we wanted to keep backwards compatibility in the same codebase—and because the new JSON format is way better structured and doesn't need the complexity of what the old XCResult parser implementation needed, I've decided to instead avoid piling up on the legacy implementation and implement the parsing of new JSON format completely separately.

I have thus:

  • Extracted the methods dedicated to parsing the legacy .xcresult format as well as the old .plist format in dedicated classes and files, to separate that legacy code clearly.
  • Renamed the class dedicated to parsing the legacy .xcresult format to LegacyXCResult
  • Created a new XCResult class instead, dedicated to parsing the new format
  • Made TestParser call the parse_xcresult method of either LegacyXCResult or XCResult class depending on the version of xcresulttool being used/installed (with a possibility to force the legacy mode if needed, to avoid breaking things accidentally if we missed some edge cases in the new implementation

The new implementation of XCResult parsing is also now split into nested classes under xcresult/*.rb, namely TestPlan, TestSuite, TestCase, and Repetitions, which make semantic sense and are easier to reason about when parsing the xcresult JSON representation.

The generation of the output JUnit file is also directly handled in the model objects (to_xml methods) rather than using ERB (via JunitGenerator) like the old implementation was using.

🧪 Testing Steps

I've added a couple of Unit Tests to test_parser_spec.rb, that use an .xcresult file that I generated from a dummy xcproject1, which contained a mix of Unit and UITests, implemented with a mix of XCTest and SwiftTesting, and using features like test skipping, arguments, and retries.

Hopefully this should cover a good set of features that those testing frameworks offer.

That being said, I'd love if more people would test this implementation against some .xcresult files of their own Xcode projects, to validate the generated JUnit .xml looks right and contains all necessary info (failure messages, etc) and so that it covers a whole range of possibilities (e.g. tests runs with or without retries enabled, custom traits, etc…)

👁️ Suggestions on how to review this PR

This PR's diff is huge, but this is mostly because:

  • A lot of code was just moved (as opposed to modified/added/deleted)
  • I included large fixtures (.junit expectation fixtures which are XMLs of >1K lines each)
  • I commited the dummy Xcode project I used for generating the .xcresult fixture, even if that Xcode project is not directly referenced by any of the rspec tests, but so that it'd be easy to add more tests in the future in we wanted to regenerate the .xcresult to cover more edge cases
  • I also had to commit the new .xcresult fixture… which is in practice a bundle containing a lot of small (mostly-binary) files. So that shows up as a lot of files in this PR's diff, while those don't need to be manually reviewed at all when reviewing that PR. [EDIT] I ended up only commiting the .xcresult/database.sqlite3 file as I discovered that it was enough for xcresulttool to operate.

I'd thus suggest the reviewers to instead focus on:

  • Changes in trainer/lib/* — keeping in mind that some of those changes are just existing code for the legacy parsers having been moved to dedicated files
  • Unit tests in trainer/spec/*_spec.rb
  • For trainer/spec/fixtures, only check the *.junit expectation fixtures that were added. You can in particular safely ignore:
    • The whole trainer/spec/fixtures/Xcode16-Mixed-XCTest-SwiftTesting.xcresult/* folder and its files, since that fixture was auto-generated (it's the bundle that Xcode generated from my dummy project)
    • The intermediate Xcode16-Mixed-XCTest-SwiftTesting.json fixture, as that fixture was also auto-generated (by running xcresulttool on the added .xcresult bundle fixture)
    • The whole trainer/spec/fixtures/Xcode16-Mixed-XCTest-SwiftTesting-Project/ folder, since this just contains the dummy Xcode project with lots of dummy tests that was used to generate the .xcresult fixture. Of course it's ok if you want to review the Xcode project itself to compare how those various ways of writing tests in Swift appear in the JUnit XML fixture files, but it's probably not worth reviewing that in GitHub directly if so, but directly in Xcode instead.

Hopefully those suggestions should help reduce the scope of the review of this PR by a lot

Footnotes

  1. Created with Cursor and AI agent, so I could quickly generate a full project with a wide range of all possible API usages and edge cases of XCTest and SwiftTesting features

Extract parsing of PLIST and Parsing of --legacy xcresulttool output each in their own dedicated file, on preparation for refactoring and introducing new xcresult parsing method for Xcode 16
To avoid storing useless large data from fixture in git, while it's not used for our test cases
To ensure that tools that parse the generated JUnit would consider those as different test cases, instead of multiple runs of the same test case.

We already have <property name="argument" value="…"> to help us distinguish, but in practice most of the tools that read JUnit XML files will now know about that property or account for it, and only look at the `<testcase classname=…name=…>` attributes to differentiate cases.
Into a dedicated file, for easier browsing of the code and smaller files to scroll through.
@fastlane-bot-helper
Copy link
Contributor

1 Warning
⚠️ Big PR

Generated by 🚫 Danger

So that even you run the tests on CI with older versions of Xcode (that don't need nor know the `--legacy` flag) would still run and pass those tests
Use Open3 to run the command instead of backticks
 - So that we can capture the error if it happens
 - So that the code is more resilient against special characters
 - So that we avoid the involvement of an intermediate shell (that would have to interpret the string into parameters before passing it to the process, requiring us to `Shellwords.escape` it in the first place… instead of just passing the parameters to the process directly without involving some unnecessary shell escaping + parsing intermediary)
@TheMetalCode
Copy link
Contributor

Happy to test this with my projects! Thank you! 👏

I had just started to figure out Swift Testing issues and had started testing with #29470, which addressed issues with test cases nested in inner classes and included some tests specific to Swift Testing, but I saw no way to handle parameterized tests (see #29174) and a more explicit/clean way to detect and remove auto-retries without using the newer, non-legacy xcresulttool output and new parsing/modeling code to match. Evidently I missed the presence of this PR until today. 🤦

@TheMetalCode
Copy link
Contributor

@AliSoftware One thing I'm seeing so far in my testing:

[16:41:04]: Called from Fastfile at line 154
[16:41:04]: ```
[16:41:04]:     152:      desc "Uses trainer to collect test results after a test run"
[16:41:04]:     153:      lane :collect_test_results do |options|
[16:41:04]:  => 154:        trainer(
[16:41:04]:     155:          path: "#{options[:test_plan]}_test_result_bundle/Test-#{options[:test_plan]}.xcresult",
[16:41:04]:     156:          output_directory: "test_results",
[16:41:04]: ```
[16:41:04]: Illegal character "\u0001" in raw string "audioPlayerReturnedForOnlyValidInput(\u0001)"

For a test that looks something like:

@Test(.serialized, arguments: (0..<128).map(UnicodeScalar.init).map(Character.init))
func audioPlayerReturnedForOnlyValidInput(asciiCharacter: Character) throws { ... }

Hopefully some kind of sanitization we can do on test names? Similar to https://github.com/fastlane/fastlane/blob/alisoftware/trainer/support-swift-testing/trainer/lib/trainer/legacy_xcresult.rb#L180?

@AliSoftware
Copy link
Contributor Author

Thanks so much @TheMetalCode for testing this PR against some more concrete tests!

Hopefully some kind of sanitization we can do on test names? Similar to https://github.com/fastlane/fastlane/blob/alisoftware/trainer/support-swift-testing/trainer/lib/trainer/legacy_xcresult.rb#L180?

Ah, good catch. Yeah, that sanitization seems like a decent plan and easy solution.

I'd like to better understand the root cause of this issue first though rather than blindly applying a workaround that was applied at the time the first trainer implementation was done, because maybe we can come up with something better / more resilient? Like, depending on at which level in the pipeline this error happens (during the execution of xcresulttool itself? During Ruby's parsing of xcresulttool's JSON stdout to a Hash object? Later in the process when trying to generate the XML?) maybe if we force the use of the right unicode encoding at the right level in the code to avoid the issue altogether? 🤔

@AliSoftware
Copy link
Contributor Author

Btw, for refs, below is the dummy Xcode project I used to generate the trainer/spec/fixtures/Xcode16-Mixed-XCTest-SwiftTesting.xcresult bundle that I use in my fixtures.

FastlaneTrainerExample.zip

Maybe I should commit it in the repo, even if the project itself is not used by the tests, so that it'd be easier to re-generate the .xcresult bundle with more tests if we want to add more test cases (like the one you made with tricky unicode characters)?

Speaking of which, feel free to add your test case from above to that dummy Xcode project then regenerate the .xcresult and .json fixtures with that added test, so we can make sure we cover such edge-case (via a future commit that'd fix this and take care of those kind of uncommon characters)

The `*.xcresult/database.sqlite3` database file contains the same data as what's serialized in the `.xcresult/Data/*` files, and is just an alternate (and more modern and disk-space-optimized) representation of the same info that the latest `xcresulttool` supports

So we might as well commit just the database instead of the whole Data/* folder and its files, not only to make it take less space on disk, but also to make the git diff and logs easier to deal with.
AliSoftware added a commit that referenced this pull request Mar 7, 2025
As apparently JUnit conventions don't allow the root <testsuites> element to contain <properties> elements, only <testsuite> child elements can

h/t @TheMetalCode for noticing in #29463 (comment)
As apparently JUnit conventions don't allow the root <testsuites> element to contain <properties> elements, only <testsuite> child elements can

h/t @TheMetalCode for noticing in #29463 (comment)
`trainer` -> _trainer_
@AliSoftware AliSoftware force-pushed the alisoftware/trainer/support-swift-testing branch from 254deb7 to cefe134 Compare March 7, 2025 22:02
Co-authored-by: Jason Hagglund <jhagglund@doximity.com>

def ensure_file_valid!(raw_json)
format_version = raw_json["FormatVersion"]
supported_versions = ["1.1", "1.2"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it mean that with every new version of Xcode, we need to check if the format has changed and support the new version if necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes and no.

To the best of my understanding, this .plist format is legacy format, even older than before .xcresult bundles and xcresulttool were introduced. This is just existing code that was initially in xcresult.rb and that I extracted in its own file just like I did for legacy_xcresult.rb so we keep that backwards compatibility, but in practice I'm not sure any modern version of Xcode produces those .plist file formats for test summaries anymore, so I don't see a new FormatVersion being added ever by now.

@@ -50,6 +50,11 @@ def self.available_options
description: "Produces class name and test name identical to xcpretty naming in junit file",
is_string: false,
default_value: false),
FastlaneCore::ConfigItem.new(key: :force_legacy_xcresulttool,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new option key is going to help to rollout this implementation with minimized regression risk 👏

@mollyIV
Copy link
Member

mollyIV commented Mar 12, 2025

Hey Ali,

Thanks a lot for this PR! Supporting SwiftTesting is a very much welcomed addition, and I truly appreciate the effort you put into it.

Also, a special shoutout for the well-written PR description. It was super informative and made reviewing this big PR much smoother. The clear steps you provided helped me a lot!

Great work, and thanks again for contributing! 🚀

@mollyIV
Copy link
Member

mollyIV commented Mar 12, 2025

even if that project is not directly used as a fixture by the rspec tests, it's useful to have it around for the future

I agree, we could easily add more test cases in the future there 👍

@AliSoftware AliSoftware requested a review from mollyIV March 12, 2025 12:23
@AliSoftware
Copy link
Contributor Author

@mollyIV I should have addressed all your suggestions, so ready for a re-review (and hopefully approval) 🙂

Copy link
Contributor

@TheMetalCode TheMetalCode left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For whatever my approval is worth: I've been continuously running this branch on two iOS projects over the last week or so, just over 10K test cases, about 60-70% of which are Swift Testing. Aside from the ASCII character parsing and junit schema issues that have already been addressed with follow-up commits, I've seen no issues and can happily report that all failing test cases, Swift Testing or otherwise, are being properly reflected in the XML report.

This is a very welcome and deeply appreciated change, thank you! 👏

Copy link
Member

@mollyIV mollyIV left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi 👋

I tested the changes on some of our projects - mix of Swift Testing and XCTests, pure XCTests and pure Swift Testing, and all were looking great! I didn't spot any issues.

Thanks again for the changes, Ali.

Let's ship it 🫡

@AliSoftware AliSoftware merged commit 392fc59 into master Mar 13, 2025
7 checks passed
@AliSoftware AliSoftware deleted the alisoftware/trainer/support-swift-testing branch March 14, 2025 10:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants