XCTestHTMLReport turns an xcresult bundle into an Xcode style HTML report
Xcode-like HTML report for Unit and UI Tests
At a glance
- What is it?
- XCTestHTMLReport is a Swift command line tool that reads the result bundle produced by xcodebuild and renders a browsable HTML report with attachments, activity logs and device information, plus JUnit and JSON output for CI. Its 4.0 release replaces the deprecated xcresulttool legacy output with a documented, versioned schema.
- Who is it for?
- XCTestHTMLReport fits a team whose CI produces xcresult bundles and whose engineers want a shareable HTML report instead of a log tail, and it fits anyone who needs JUnit or JSON output for a test aggregator without adding a dependency on the Xcode toolchain itself. It does not fit a project that does not run xcodebuild on macOS, and it does not fit a pipeline that cannot accept a 4.0 report, since the JSON output schema changed with that release.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 10 days ago.
- What is it written in?
- Mainly Swift, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The input is an xcresult bundle, not a test run
XCTestHTMLReport is an Xcode-like HTML report for unit and UI tests. The way it works is that it does not run the tests. It reads the result bundle that xcodebuild already produced, and turns that into a web page.
The usage section is explicit about the requirement: run your UI tests using xcodebuild without forgetting to specify the resultBundlePath. The example given is a simulator destination with a named device and OS version, and a result bundle path of TestResults.
$ xcodebuild test -workspace XCTestHTMLReport.xcworkspace -scheme SampleApp -destination 'platform=iOS Simulator,name=iPhone 14,OS=16.0' -resultBundlePath TestResultsThe tool then consumes that bundle:
$ xchtmlreport TestResults.xcresult
Report successfully created at ./index.htmlThat separation is the design. The report generator is a post-processing step that can run anywhere the bundle can be copied to, including a CI runner or a colleague's machine, and it can be run again on the same bundle without re-running the tests. A bundle can also be passed more than once, so several result bundles can be merged into a single HTML report, which is how a project that shards its test matrix across machines gets one page instead of several.
The report itself is published for every merge to the main branch, rendered from the repository's own sample test run, and past releases are kept at a /v/ path, which makes it easy to see what the tool produced at a given version without building anything.
The report is either one portable HTML file or a directory of resources
There are two output shapes, and the choice is made with the -i flag.
The default is a bundle, a directory of resources that the index.html refers to. Adding -i inlines all resources so the output is a single standalone HTML file. The README is honest about the trade: the inlined file is much heavier but much more portable.
That trade matters more than it sounds. A single file can be attached to a ticket or dropped in a chat message, while a bundle has to be served or archived as a directory. The tool also has a feature to shrink bundle size by removing unattached files, so a bundle is not necessarily larger than it needs to be.
The report is a page, not an application, and the feature list describes navigation in browser terms. You can navigate through the report with the keyboard's arrow keys, you can filter out successful, failed, skipped or mixed-result tests, and the report displays information about the target device and activity logs. Filtering by result rather than scrolling a list of a thousand tests is what makes a large run usable.
The last push to the repository was on 2026-10-13 and the project is not archived, so the report format is still moving, which is worth remembering before you build a permanent link to a specific report in your internal tooling.
Attachments decide the size, and the tool manages them
Attachments are the feature that makes the report look like Xcode rather than a text dump, and they are also what makes the output large.
The supported attachment types are .png, .jpeg, .heic, .txt, .log, .mp4 and .gif. A screenshot on a failed UI test, a video recording of a failing interaction, a log file: these are the artefacts a developer actually needs, and the report displays them.
The tool does three things about them that a naive converter would not. It automatically converts heic images to a browser-friendly format, since HEIC is what current simulators produce and most browsers will not render it. It can downsize image attachments. And it can shrink the bundle by removing files that were never attached to anything, which is the difference between a report you can email and one you cannot.
The progress output in the README gives a sense of the cost. A run of 2531 attachments took 4.8 seconds to export, reading the result bundle took 18.2 seconds, rendering 1284 tests took 12.4 seconds and writing the report took 0.9 seconds, producing an index.html of 412 MB. Those numbers are from the authors' own example, so treat them as an illustration of the shape rather than a measurement of your bundle.
One design decision in that output is worth copying. Progress goes to standard error, so a pipeline that reads the report path off standard output is unaffected, and the progress display is off when standard error is not a terminal, so CI logs do not change. There are --progress and --quiet flags to force the behaviour either way.
JUnit and JSON output exist for machines, and 4.0 changed the JSON shape
Two flags produce machine readable output alongside the HTML. The -j flag generates a JUnit report, and the --json flag generates a JSON report.
$ xchtmlreport -j TestResults1
Report successfully created at ./index.html
JUnit report successfully created at report.junit$ xchtmlreport --json TestResults1
Report successfully created at ./index.html
JSON report successfully created at ./report.jsonThe JSON output is where the 4.0 release bites. Starting in 4.0, report.json is the project's own documented, versioned schema, and docs/json-schema.md is the contract. The schema is identical whichever result reader produced it, with the same keys, the same nesting and the same schemaVersion field. Earlier versions dumped the xcresulttool legacy object graph verbatim, and the README explains why that had to change: that graph is Apple's internal shape and it disappears together with the legacy commands, so 4.0 replaces it once and deliberately.
The change is visible immediately. Before, the file was an array of Apple internal records. After, it is an object with a runs array and a schemaVersion of 1.0.0.
The instruction for consumers is to read schemaVersion first and follow the version policy in the contract document. That is the right instruction for anyone whose CI parses report.json, because the old format was never a schema, it was a dump.
Two result readers exist because the legacy xcresulttool commands are going away
The most consequential engineering decision in the project is how it reads result bundles at all.
The xcresulttool legacy API, the way every version before 4.0 read result bundles, is deprecated and will be removed from Xcode. Rather than pick one and break on the other toolchains, xchtmlreport has two readers and picks one per run, with the choice exposed through the --result-reader flag.
$ xchtmlreport --result-reader auto TestResults.xcresult # the defaultThe auto mode prefers legacy while the toolchain still offers the legacy commands, and falls back to modern once they are gone, or when the probe cannot tell. The modern value forces the new format reader on any toolchain, and legacy forces the legacy reader, where the behaviour is strict: if the toolchain no longer provides the legacy commands that is an error, never a silent substitution.
The XCHR_RESULT_READER environment variable sets the default when the flag is absent, which is the setting a CI job would use to pin an entire pipeline to one reader.
The confidence behind that split comes from a differential test suite that holds reports from the two readers byte-identical, up to a short declared list of differences the new format cannot avoid. The README starts listing them, and the first two are worth knowing about in advance. The new format does not expose the user-supplied XCTAttachment name, so the modern reader labels attachments by type, such as Screenshot, Video or File. And the failure title prefixes differ, with the legacy rendering beginning with an assertion style prefix that the modern reader does not have.
So the migration risk is not in the report layout but in two specific fields, and both are documented rather than discovered.
Install with Homebrew or Mint, then run xcodebuild with a result bundle path
Installation is a single command through one of two package managers.
Homebrew is the recommended route, and the stable version is:
brew install xctesthtmlreportThe HEAD variant installs the latest from the main branch:
brew install xctesthtmlreport --HEADMint is the alternative, with the same stable and branch distinction:
mint install XCTestHTMLReport/XCTestHTMLReportmint install XCTestHTMLReport/XCTestHTMLReport@mainThe project is a Swift package, and the repository contains Package.swift and Package.resolved alongside Sources/, Tests/, a sample app directory named XCTestHTMLReportSampleApp, a prepareTestResults.sh script and a mise.toml for toolchain management.
The first run is the two step sequence from the usage section: xcodebuild with a resultBundlePath, then xchtmlreport on the resulting bundle. The one thing the README flags as easily forgotten is the result bundle path, without which there is nothing for the report tool to read.
There is also a live report published from the repository's own sample run, which is the fastest way to see what the output looks like before you run anything, and past releases of that report are kept so you can compare versions side by side.
MIT licensed, and the 4.0 release is still a release candidate
The licence is MIT, with the terms in the LICENSE file, which is the permissive choice you want for a tool that runs inside a build pipeline rather than shipping as part of a product.
The project is written in Swift and distributed as a Swift package, which is the natural packaging for a command line tool that has to run on the same macOS as the Xcode it reports on. The repository carries the usual hygiene for a Swift project: a .swiftlint.yml and a .swiftformat configuration, a Package.resolved so the dependency versions are pinned, a codecov.yml with a coverage badge, a GitHub Actions workflow badge for ci.yml, a CONTRIBUTING.md, a CODE_OF_CONDUCT.md and a docs directory that holds the JSON schema contract.
The release timeline is the thing to note. Three tags were published in August 2026: 3.0.0 on the 7th, 3.0.1rc1 on the 11th and 4.0.0rc1 on the 14th. The 4.0 release is therefore still a release candidate, and it is the release that changes the JSON schema and introduces the two reader modes. Before you take the 4.0 path, wait for a final tag or pin to 3.0.x.
The last push was on 2026-10-13, so the project is being worked on, and the schema policy in docs/json-schema.md is the document to follow if anything in your pipeline consumes report.json.
Editorial conclusion
XCTestHTMLReport fits a team whose CI produces xcresult bundles and whose engineers want a shareable HTML report instead of a log tail, and it fits anyone who needs JUnit or JSON output for a test aggregator without adding a dependency on the Xcode toolchain itself. It does not fit a project that does not run xcodebuild on macOS, and it does not fit a pipeline that cannot accept a 4.0 report, since the JSON output schema changed with that release. Before you upgrade, run xchtmlreport --result-reader auto against one of your bundles and diff the report against the previous version, read docs/json-schema.md if anything downstream consumes report.json, and check the schemaVersion field before you trust the file, since the schema is the contract rather than the format.
Frequently asked questions
How do I generate an XCTestHTMLReport?
Run your tests with xcodebuild and specify a resultBundlePath, then run xchtmlreport on the resulting .xcresult bundle. The tool then writes the report to ./index.html.
What is the difference between the --result-reader auto, modern and legacy modes?
auto is the default and prefers the legacy reader while the toolchain still offers the legacy xcresulttool commands, falling back to modern once they are gone. modern forces the new format reader, and legacy forces the legacy reader, erroring out rather than silently substituting if the toolchain no longer provides those commands.
What changed in the JSON output in version 4.0?
report.json became the project's own documented and versioned schema, with docs/json-schema.md as the contract and a schemaVersion field to read first. Before 4.0 the file dumped the xcresulttool legacy object graph verbatim, which was Apple's internal shape and disappears with the legacy commands.
Can I get a single portable HTML file instead of a bundle?
Yes. The -i flag inlines all resources so the output is a single standalone HTML file, which the README notes is much heavier but much more portable. The tool can also remove unattached files to shrink a bundle.
How do I install XCTestHTMLReport?
With Homebrew using brew install xctesthtmlreport, or with Mint using mint install XCTestHTMLReport/XCTestHTMLReport. Both can install the latest from the main branch, and the project is also published as a Swift package.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/xctesthtmlreport-xctesthtmlreport)Was this useful?