Skip to content

Commit 532bcb6

Browse files
authored
fix: Capture Swift Testing custom #expect comments (#62)
* fix: Capture Swift Testing custom #expect comments (#61) When #expect has a custom comment, Swift Testing outputs it on a separate "details" line (􀄵 on macOS, ↳ on Linux). Use look-ahead in parse() to join the comment to the failed test message. * docs: Add multi-line context pattern and Swift Testing symbols to CLAUDE.md
1 parent 3fcb6e9 commit 532bcb6

File tree

3 files changed

+108
-0
lines changed

3 files changed

+108
-0
lines changed

CLAUDE.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,12 @@ The codebase follows a modular architecture:
264264
3. Parsed data → `BuildResult` struct
265265
4. Output formatting (JSON or TOON) → stdout
266266

267+
### Multi-line Context Pattern
268+
- `parseLine()` processes each line in isolation (no access to neighboring lines)
269+
- For multi-line context, use **look-ahead/look-back** in the `parse()` loop (has access to `lines` array by index)
270+
- Existing examples: look-back for `PhaseScriptExecution` context, look-ahead for Swift Testing `#expect` comments
271+
- `failedTests` are deduplicated by normalized test name — duplicate names get merged, not appended
272+
267273
### Key Features
268274
- **Error/Warning Parsing**: Multiple regex patterns handle various Xcode error formats
269275
- **Runtime Warning Parsing**: Parses SwiftUI and custom runtime warnings (e.g., from swift-issue-reporting)
@@ -274,6 +280,11 @@ The codebase follows a modular architecture:
274280
- Included with `--warnings` flag (no separate flag needed)
275281
- **Linker Error Parsing**: Captures undefined symbols, missing frameworks/libraries, architecture mismatches, and duplicate symbols (with structured conflicting file paths)
276282
- **Test Failure Detection**: XCUnit assertion failures and general test failures
283+
- **Swift Testing console symbols**: SF Symbols from Private Use Area (macOS) with Unicode fallbacks (Linux)
284+
- `details` line: `􀄵` U+100135 (macOS) / `` U+21B3 (Linux) — carries `#expect` custom comments
285+
- `fail` line: `􀢄` U+100884 (macOS) / `` U+2718 (Linux)
286+
- `default`/started: `􀟈` U+1007C8 (macOS) / `` U+25C7 (Linux)
287+
- Source: [apple/swift-testing Event.Symbol.swift](https://github.com/apple/swift-testing/blob/main/Sources/Testing/Events/Recorder/Event.Symbol.swift)
277288
- **Standard format**: `Test Case 'X.test()' passed/failed (0.123 seconds)`
278289
- **Parallel testing format**: `Test case 'X.test()' passed/failed on 'Device Name' (0.123 seconds)`
279290
- Parallel format uses lowercase 'case' and includes device name before duration

Sources/OutputParser.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,30 @@ class OutputParser {
358358
for (index, line) in lines.enumerated() {
359359
parseLine(line)
360360

361+
// Swift Testing: append custom comment from next line if present
362+
// macOS: 􀄵 (U+100135, SF Symbol "arrow.turn.down.right")
363+
// Linux: ↳ (U+21B3, fallback)
364+
if line.contains("recorded an issue"),
365+
index + 1 < lines.count
366+
{
367+
let nextLine = lines[index + 1].trimmingCharacters(in: .whitespaces)
368+
if nextLine.hasPrefix("􀄵") || nextLine.hasPrefix("") {
369+
let comment = String(
370+
nextLine.drop(while: { $0 != " " }).drop(while: { $0 == " " })
371+
)
372+
if !comment.isEmpty, let lastIdx = failedTests.indices.last {
373+
let existing = failedTests[lastIdx]
374+
failedTests[lastIdx] = FailedTest(
375+
test: existing.test,
376+
message: existing.message + ": " + comment,
377+
file: existing.file,
378+
line: existing.line,
379+
duration: existing.duration
380+
)
381+
}
382+
}
383+
}
384+
361385
// Handle PhaseScriptExecution failures with context from preceding lines
362386
if line.contains("Command PhaseScriptExecution failed with a nonzero exit") {
363387
// Look back for relevant context (skip unrelated warnings and metadata)

Tests/ParsingTests.swift

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1539,4 +1539,77 @@ final class ParsingTests: XCTestCase {
15391539
XCTAssertEqual(result.status, "success")
15401540
XCTAssertEqual(result.summary.passedTests, 1)
15411541
}
1542+
1543+
// MARK: - Swift Testing Custom Comment (#61)
1544+
1545+
func testSwiftTestingCustomComment() {
1546+
let parser = OutputParser()
1547+
let input = """
1548+
􀢄 Test "Domain stays free" recorded an issue at File.swift:16:17: Expectation failed: !(forbiddenImports.contains(import.name))
1549+
􀄵 Domain must not import SwiftData
1550+
􀢄 Test "Domain stays free" failed after 0.986 seconds with 1 issue.
1551+
"""
1552+
1553+
let result = parser.parse(input: input)
1554+
1555+
XCTAssertEqual(result.failedTests.count, 1)
1556+
XCTAssertTrue(
1557+
result.failedTests[0].message.contains("Domain must not import SwiftData"),
1558+
"Expected message to contain the custom comment, got: \(result.failedTests[0].message)"
1559+
)
1560+
XCTAssertEqual(result.failedTests[0].file, "File.swift")
1561+
XCTAssertEqual(result.failedTests[0].line, 16)
1562+
}
1563+
1564+
func testSwiftTestingCustomCommentLinuxFallback() {
1565+
let parser = OutputParser()
1566+
let input = """
1567+
✘ Test "Domain stays free" recorded an issue at File.swift:16:17: Expectation failed: !(forbiddenImports.contains(import.name))
1568+
↳ Domain must not import SwiftData
1569+
✘ Test "Domain stays free" failed after 0.986 seconds with 1 issue.
1570+
"""
1571+
1572+
let result = parser.parse(input: input)
1573+
1574+
XCTAssertEqual(result.failedTests.count, 1)
1575+
XCTAssertTrue(
1576+
result.failedTests[0].message.contains("Domain must not import SwiftData"),
1577+
"Expected message to contain the custom comment, got: \(result.failedTests[0].message)"
1578+
)
1579+
}
1580+
1581+
func testSwiftTestingNoComment() {
1582+
let parser = OutputParser()
1583+
let input = """
1584+
􀢄 Test shouldFail() recorded an issue at File.swift:9:5: Expectation failed: Bool(false)
1585+
􀢄 Test shouldFail() failed after 0.001 seconds with 1 issue.
1586+
"""
1587+
1588+
let result = parser.parse(input: input)
1589+
1590+
XCTAssertEqual(result.failedTests.count, 1)
1591+
XCTAssertEqual(result.failedTests[0].message, "Expectation failed: Bool(false)")
1592+
}
1593+
1594+
func testSwiftTestingMultipleIssuesWithComments() {
1595+
let parser = OutputParser()
1596+
let input = """
1597+
􀢄 Test "test A" recorded an issue at File.swift:10:5: Expectation failed: A
1598+
􀄵 Comment A
1599+
􀢄 Test "test B" recorded an issue at File.swift:20:5: Expectation failed: B
1600+
􀄵 Comment B
1601+
"""
1602+
1603+
let result = parser.parse(input: input)
1604+
1605+
XCTAssertEqual(result.failedTests.count, 2)
1606+
XCTAssertTrue(
1607+
result.failedTests[0].message.contains("Comment A"),
1608+
"First message should contain Comment A, got: \(result.failedTests[0].message)"
1609+
)
1610+
XCTAssertTrue(
1611+
result.failedTests[1].message.contains("Comment B"),
1612+
"Second message should contain Comment B, got: \(result.failedTests[1].message)"
1613+
)
1614+
}
15421615
}

0 commit comments

Comments
 (0)