Skip to content

Commit 40f6bd6

Browse files
authored
Merge pull request #1 from diogot/feature/phase1-implementation
Implement Phase 1: XCResult parser library
2 parents e860f01 + 989eeb1 commit 40f6bd6

File tree

1,662 files changed

+2992
-1
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

1,662 files changed

+2992
-1
lines changed

.github/workflows/test.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: Tests
2+
on:
3+
push:
4+
branches:
5+
- main
6+
pull_request:
7+
branches:
8+
- main
9+
10+
jobs:
11+
test:
12+
runs-on: macos-26
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- name: Check Swift version
17+
run: swift --version
18+
19+
- name: Build
20+
run: swift build
21+
22+
- name: Run Tests
23+
run: swift test

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [1.0.0] - Unreleased
9+
10+
### Added
11+
12+
- Initial release
13+
- `XCResultParser` for parsing xcresult bundles
14+
- Build results parsing (warnings, errors, analyzer warnings)
15+
- Test results parsing with failure extraction
16+
- Source location parsing from both build issues and test failures
17+
- Resilient enum decoding for forward compatibility with future Xcode versions
18+
- Comprehensive test suite with fixtures

CLAUDE.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
Swift library for parsing Xcode `.xcresult` bundles. Extracts build warnings/errors and test failures in a structured, type-safe format. Zero external dependencies (uses only `xcrun xcresulttool` from Xcode).
8+
9+
**Requirements:** Swift 6.2+, macOS 15+, Xcode 16+
10+
11+
**Status:** Phase 1 implemented. Design document at `swift-xcresult-parser-plan.md`.
12+
13+
## Build & Test Commands
14+
15+
```bash
16+
swift build # Build the package
17+
swift test # Run unit tests
18+
swift test --filter IntegrationTests # Integration tests (CI only)
19+
```
20+
21+
## Architecture
22+
23+
Three-layer design:
24+
25+
1. **XCResultParser** (public API) - Main entry point for parsing xcresult bundles
26+
2. **XCResultTool** (internal actor) - Wraps `xcrun xcresulttool` commands
27+
3. **Models** - Strongly-typed Swift structs/enums with resilient decoding
28+
29+
Data flow:
30+
```
31+
XCResultParser.parse(path:)
32+
→ XCResultTool (executes xcrun xcresulttool get build-results/test-results)
33+
→ JSON decoding into models
34+
→ XCResult (unified container)
35+
```
36+
37+
## Key Design Principles
38+
39+
- All types are `Sendable` for concurrency safety
40+
- Resilient enum decoding: unknown values captured as `.unknown(String)` for forward compatibility with future Xcode versions
41+
- Source location normalization from two xcresulttool formats:
42+
- Build issues: `file:///path/File.swift#LineNumber`
43+
- Test failures: parsed from failure message (`File.swift:42: message`)
44+
- Zero external dependencies (Foundation only)
45+
46+
## Main Types
47+
48+
| Type | Purpose |
49+
|------|---------|
50+
| `XCResult` | Combined build + test results container |
51+
| `BuildResults` / `BuildIssue` | Build warnings, errors, analyzer warnings |
52+
| `TestResults` / `TestNode` / `TestFailure` | Test execution tree and flattened failures |
53+
| `SourceLocation` | Normalized file location with path relativization |
54+
| `TestResult`, `TestNodeType`, `Severity` | Enums with resilient decoding |
55+
56+
## Testing Strategy
57+
58+
- Unit tests with mock JSON fixtures in `Tests/XCResultParserTests/Fixtures/`
59+
- Integration tests with real xcresult bundles (CI environment only)
60+
- Resilient decoding tests for unknown enum values and schema drift

Package.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// swift-tools-version: 6.2
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "swift-xcresult-parser",
6+
platforms: [.macOS(.v15)],
7+
products: [
8+
.library(name: "XCResultParser", targets: ["XCResultParser"])
9+
],
10+
targets: [
11+
.target(name: "XCResultParser"),
12+
.testTarget(
13+
name: "XCResultParserTests",
14+
dependencies: ["XCResultParser"],
15+
resources: [.copy("Fixtures")]
16+
)
17+
]
18+
)

README.md

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,135 @@
1-
# swift-xcresult-parser
1+
# swift-xcresult-parser
2+
3+
A type-safe Swift library for parsing Xcode `.xcresult` bundles and extracting build warnings, errors, and test failures in a structured format.
4+
5+
[![Swift](https://img.shields.io/badge/Swift-6.2+-orange.svg)](https://swift.org)
6+
[![Platform](https://img.shields.io/badge/Platform-macOS%2015+-blue.svg)](https://developer.apple.com/macos)
7+
[![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
8+
9+
## Features
10+
11+
- **Parse xcresult bundles**: Extract structured data from Xcode result bundles
12+
- **Zero external dependencies**: Uses only `xcrun xcresulttool` (ships with Xcode)
13+
- **Type-safe output**: Strongly-typed Swift models with resilient decoding
14+
- **Async/await support**: Modern Swift concurrency
15+
- **Forward compatible**: Handles unknown enum values gracefully for future Xcode versions
16+
17+
## Requirements
18+
19+
- Swift 6.2+
20+
- macOS 15+
21+
- Xcode 16+
22+
23+
## Installation
24+
25+
### Swift Package Manager
26+
27+
Add the following to your `Package.swift`:
28+
29+
```swift
30+
dependencies: [
31+
.package(url: "https://github.com/diogot/swift-xcresult-parser.git", from: "1.0.0")
32+
]
33+
```
34+
35+
Then add `XCResultParser` to your target dependencies:
36+
37+
```swift
38+
.target(name: "YourTarget", dependencies: ["XCResultParser"])
39+
```
40+
41+
## Usage
42+
43+
### Basic Usage
44+
45+
```swift
46+
import XCResultParser
47+
48+
let parser = XCResultParser(path: "path/to/test.xcresult")
49+
let result = try await parser.parse()
50+
51+
// Access build issues
52+
for issue in result.buildResults?.allIssues ?? [] {
53+
print("\(issue.severity): \(issue.message)")
54+
if let loc = issue.sourceLocation {
55+
print(" at \(loc.file):\(loc.line)")
56+
}
57+
}
58+
59+
// Access test failures
60+
for failure in result.testResults?.failures ?? [] {
61+
print("FAIL: \(failure.testClass).\(failure.testName)")
62+
print(" \(failure.message)")
63+
}
64+
```
65+
66+
### Parse Only Build Results
67+
68+
```swift
69+
let parser = XCResultParser(path: "path/to/test.xcresult")
70+
let buildResults = try await parser.parseBuildResults()
71+
72+
print("Warnings: \(buildResults.warningCount)")
73+
print("Errors: \(buildResults.errorCount)")
74+
```
75+
76+
### Parse Only Test Results
77+
78+
```swift
79+
let parser = XCResultParser(path: "path/to/test.xcresult")
80+
let testResults = try await parser.parseTestResults()
81+
82+
let summary = testResults.summary
83+
print("Total: \(summary.totalCount)")
84+
print("Passed: \(summary.passedCount)")
85+
print("Failed: \(summary.failedCount)")
86+
```
87+
88+
### Source Location Handling
89+
90+
The library normalizes source locations from two different xcresulttool formats:
91+
92+
```swift
93+
// Build issues provide absolute paths
94+
if let location = buildIssue.sourceLocation {
95+
let relativePath = location.relativePath(from: "/path/to/repo")
96+
print("\(relativePath):\(location.line)")
97+
}
98+
99+
// Test failures provide filename and line
100+
for failure in testResults.failures {
101+
if let location = failure.sourceLocation {
102+
print("\(location.file):\(location.line)")
103+
}
104+
}
105+
```
106+
107+
## API Reference
108+
109+
### XCResultParser
110+
111+
```swift
112+
public final class XCResultParser: Sendable {
113+
public init(path: String)
114+
public func parse() async throws -> XCResult
115+
public func parseBuildResults() async throws -> BuildResults
116+
public func parseTestResults() async throws -> TestResults
117+
}
118+
```
119+
120+
### Models
121+
122+
- `XCResult` - Combined build and test results
123+
- `BuildResults` - Build action results with warnings, errors, analyzer warnings
124+
- `BuildIssue` - Individual build warning or error with source location
125+
- `TestResults` - Test execution results with device info and test node tree
126+
- `TestNode` - Hierarchical test node (test plan, bundle, suite, case, etc.)
127+
- `TestFailure` - Flattened test failure for easy reporting
128+
- `SourceLocation` - File path, line, and optional column
129+
- `TestResult` - Test outcome (passed, failed, skipped, expectedFailure)
130+
- `TestNodeType` - Node type in test hierarchy
131+
- `Severity` - Issue severity level (notice, warning, failure)
132+
133+
## License
134+
135+
MIT License - see [LICENSE](LICENSE) for details.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/// A build warning, error, or analyzer warning
2+
public struct BuildIssue: Sendable, Codable, Equatable {
3+
public let issueType: String
4+
public let message: String
5+
public let targetName: String?
6+
public let sourceURL: String?
7+
public let className: String?
8+
9+
public init(
10+
issueType: String,
11+
message: String,
12+
targetName: String? = nil,
13+
sourceURL: String? = nil,
14+
className: String? = nil
15+
) {
16+
self.issueType = issueType
17+
self.message = message
18+
self.targetName = targetName
19+
self.sourceURL = sourceURL
20+
self.className = className
21+
}
22+
23+
/// Parsed source location from sourceURL
24+
public var sourceLocation: SourceLocation? {
25+
guard let sourceURL else { return nil }
26+
return SourceLocation.fromSourceURL(sourceURL)
27+
}
28+
29+
/// Severity level for annotations based on issue type
30+
public var severity: Severity {
31+
let lowercased = issueType.lowercased()
32+
if lowercased.contains("error") {
33+
return .failure
34+
} else if lowercased.contains("warning") {
35+
return .warning
36+
} else {
37+
return .notice
38+
}
39+
}
40+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/// Build action results from xcresulttool
2+
public struct BuildResults: Sendable, Codable, Equatable {
3+
public let actionTitle: String?
4+
public let status: String?
5+
public let warningCount: Int
6+
public let errorCount: Int
7+
public let analyzerWarningCount: Int
8+
public let warnings: [BuildIssue]
9+
public let errors: [BuildIssue]
10+
public let analyzerWarnings: [BuildIssue]
11+
12+
public init(
13+
actionTitle: String? = nil,
14+
status: String? = nil,
15+
warningCount: Int = 0,
16+
errorCount: Int = 0,
17+
analyzerWarningCount: Int = 0,
18+
warnings: [BuildIssue] = [],
19+
errors: [BuildIssue] = [],
20+
analyzerWarnings: [BuildIssue] = []
21+
) {
22+
self.actionTitle = actionTitle
23+
self.status = status
24+
self.warningCount = warningCount
25+
self.errorCount = errorCount
26+
self.analyzerWarningCount = analyzerWarningCount
27+
self.warnings = warnings
28+
self.errors = errors
29+
self.analyzerWarnings = analyzerWarnings
30+
}
31+
32+
/// All issues combined (errors, warnings, analyzer warnings)
33+
public var allIssues: [BuildIssue] {
34+
errors + warnings + analyzerWarnings
35+
}
36+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/// Device information from test results
2+
public struct Device: Sendable, Codable, Equatable {
3+
public let architecture: String?
4+
public let deviceId: String?
5+
public let deviceName: String?
6+
public let modelName: String?
7+
public let osBuildNumber: String?
8+
public let osVersion: String?
9+
public let platform: String?
10+
11+
public init(
12+
architecture: String? = nil,
13+
deviceId: String? = nil,
14+
deviceName: String? = nil,
15+
modelName: String? = nil,
16+
osBuildNumber: String? = nil,
17+
osVersion: String? = nil,
18+
platform: String? = nil
19+
) {
20+
self.architecture = architecture
21+
self.deviceId = deviceId
22+
self.deviceName = deviceName
23+
self.modelName = modelName
24+
self.osBuildNumber = osBuildNumber
25+
self.osVersion = osVersion
26+
self.platform = platform
27+
}
28+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/// Issue/annotation severity level
2+
public enum Severity: Sendable, Equatable {
3+
case notice
4+
case warning
5+
case failure
6+
}

0 commit comments

Comments
 (0)