Skip to content

Commit ccb2ed9

Browse files
committed
fix: correct parser patterns validated against real xcodebuild output
Fixes discovered by auditing synthetic test data against real swift test and xcodebuild output: - Swift Testing verbose mode: (aka 'func()') suffix now optionally matched in result and issue parsers - Skipped test format: real output uses arrow symbol and bare function name, not diamond with quoted name. Both formats now handled. - Parameterized tests: 'with N test cases' suffix in results and 'with N argument value' in issues now matched - Build errors without line numbers: .xcodeproj path-based errors like 'Missing package product' now parsed - Stage detection: lowercase 'Test suite' from Xcode 26 now matched Tests updated to use patterns captured from real swift test and xcodebuild runs.
1 parent 8415cd7 commit ccb2ed9

4 files changed

Lines changed: 175 additions & 12 deletions

File tree

src/utils/__tests__/swift-testing-line-parsers.test.ts

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,30 @@ describe('Swift Testing line parsers', () => {
2121
});
2222
});
2323

24+
it('should parse a passed test with verbose aka suffix', () => {
25+
const result = parseSwiftTestingResultLine(
26+
"✔ Test \"String operations\" (aka 'stringTest()') passed after 0.001 seconds.",
27+
);
28+
expect(result).toEqual({
29+
status: 'passed',
30+
rawName: 'String operations',
31+
testName: 'String operations',
32+
durationText: '0.001s',
33+
});
34+
});
35+
36+
it('should parse a passed parameterized test', () => {
37+
const result = parseSwiftTestingResultLine(
38+
'✔ Test "Parameterized test" with 3 test cases passed after 0.001 seconds.',
39+
);
40+
expect(result).toEqual({
41+
status: 'passed',
42+
rawName: 'Parameterized test',
43+
testName: 'Parameterized test',
44+
durationText: '0.001s',
45+
});
46+
});
47+
2448
it('should parse a failed test', () => {
2549
const result = parseSwiftTestingResultLine(
2650
'✘ Test "Expected failure" failed after 0.001 seconds with 1 issue.',
@@ -33,7 +57,40 @@ describe('Swift Testing line parsers', () => {
3357
});
3458
});
3559

36-
it('should parse a skipped test', () => {
60+
it('should parse a failed test with verbose aka suffix', () => {
61+
const result = parseSwiftTestingResultLine(
62+
"✘ Test \"Expected failure\" (aka 'deliberateFailure()') failed after 0.001 seconds with 1 issue.",
63+
);
64+
expect(result).toEqual({
65+
status: 'failed',
66+
rawName: 'Expected failure',
67+
testName: 'Expected failure',
68+
durationText: '0.001s',
69+
});
70+
});
71+
72+
it('should parse a failed parameterized test', () => {
73+
const result = parseSwiftTestingResultLine(
74+
'✘ Test "Parameterized failure" with 3 test cases failed after 0.001 seconds with 1 issue.',
75+
);
76+
expect(result).toEqual({
77+
status: 'failed',
78+
rawName: 'Parameterized failure',
79+
testName: 'Parameterized failure',
80+
durationText: '0.001s',
81+
});
82+
});
83+
84+
it('should parse a skipped test (arrow format)', () => {
85+
const result = parseSwiftTestingResultLine('➜ Test disabledTest() skipped: "Not ready yet"');
86+
expect(result).toEqual({
87+
status: 'skipped',
88+
rawName: 'disabledTest',
89+
testName: 'disabledTest',
90+
});
91+
});
92+
93+
it('should parse a skipped test (legacy diamond format)', () => {
3794
const result = parseSwiftTestingResultLine('◇ Test "Disabled test" skipped.');
3895
expect(result).toEqual({
3996
status: 'skipped',
@@ -42,6 +99,15 @@ describe('Swift Testing line parsers', () => {
4299
});
43100
});
44101

102+
it('should parse a skipped test without reason', () => {
103+
const result = parseSwiftTestingResultLine('➜ Test disabledTest skipped');
104+
expect(result).toEqual({
105+
status: 'skipped',
106+
rawName: 'disabledTest',
107+
testName: 'disabledTest',
108+
});
109+
});
110+
45111
it('should return null for non-matching lines', () => {
46112
expect(parseSwiftTestingResultLine('◇ Test "Foo" started.')).toBeNull();
47113
expect(parseSwiftTestingResultLine('random text')).toBeNull();
@@ -61,6 +127,30 @@ describe('Swift Testing line parsers', () => {
61127
});
62128
});
63129

130+
it('should parse an issue with verbose aka suffix', () => {
131+
const result = parseSwiftTestingIssueLine(
132+
"✘ Test \"Expected failure\" (aka 'deliberateFailure()') recorded an issue at AuditTests.swift:5:5: Expectation failed: true == false",
133+
);
134+
expect(result).toEqual({
135+
rawTestName: 'Expected failure',
136+
testName: 'Expected failure',
137+
location: 'AuditTests.swift:5',
138+
message: 'Expectation failed: true == false',
139+
});
140+
});
141+
142+
it('should parse a parameterized issue with argument values', () => {
143+
const result = parseSwiftTestingIssueLine(
144+
'✘ Test "Parameterized failure" recorded an issue with 1 argument value → 0 at ParameterizedTests.swift:10:5: Expectation failed: (value → 0) > 0',
145+
);
146+
expect(result).toEqual({
147+
rawTestName: 'Parameterized failure',
148+
testName: 'Parameterized failure',
149+
location: 'ParameterizedTests.swift:10',
150+
message: 'Expectation failed: (value → 0) > 0',
151+
});
152+
});
153+
64154
it('should parse an issue without location', () => {
65155
const result = parseSwiftTestingIssueLine(
66156
'✘ Test "Some test" recorded an issue: Something went wrong',
@@ -72,6 +162,17 @@ describe('Swift Testing line parsers', () => {
72162
});
73163
});
74164

165+
it('should parse an issue without location with verbose aka suffix', () => {
166+
const result = parseSwiftTestingIssueLine(
167+
"✘ Test \"Some test\" (aka 'someFunc()') recorded an issue: Something went wrong",
168+
);
169+
expect(result).toEqual({
170+
rawTestName: 'Some test',
171+
testName: 'Some test',
172+
message: 'Something went wrong',
173+
});
174+
});
175+
75176
it('should return null for non-matching lines', () => {
76177
expect(parseSwiftTestingIssueLine('✘ Test "Foo" failed after 0.001 seconds')).toBeNull();
77178
});
@@ -100,6 +201,17 @@ describe('Swift Testing line parsers', () => {
100201
});
101202
});
102203

204+
it('should parse a summary with singular suite', () => {
205+
const result = parseSwiftTestingRunSummary(
206+
'✘ Test run with 5 tests in 1 suite failed after 0.001 seconds with 3 issues.',
207+
);
208+
expect(result).toEqual({
209+
executed: 5,
210+
failed: 3,
211+
durationText: '0.001s',
212+
});
213+
});
214+
103215
it('should return null for non-matching lines', () => {
104216
expect(parseSwiftTestingRunSummary('random text')).toBeNull();
105217
});
@@ -112,6 +224,12 @@ describe('Swift Testing line parsers', () => {
112224
);
113225
});
114226

227+
it('should parse a continuation with version info', () => {
228+
expect(parseSwiftTestingContinuationLine('↳ Testing Library Version: 1743')).toBe(
229+
'Testing Library Version: 1743',
230+
);
231+
});
232+
115233
it('should return null for non-continuation lines', () => {
116234
expect(parseSwiftTestingContinuationLine('regular line')).toBeNull();
117235
});

src/utils/swift-testing-line-parsers.ts

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,29 @@ import {
55
parseRawTestName,
66
} from './xcodebuild-line-parsers.ts';
77

8+
// Optional verbose suffix: (aka 'funcName()')
9+
// Optional parameterized suffix: with N test cases
10+
const OPTIONAL_AKA = `(?:\\s*\\(aka '[^']*'\\))?`;
11+
const OPTIONAL_PARAMETERIZED = `(?:\\s+with \\d+ test cases?)?`;
12+
813
/**
914
* Parse a Swift Testing result line (passed/failed/skipped).
1015
*
11-
* Matches:
16+
* Matches (non-verbose and verbose):
1217
* ✔ Test "Name" passed after 0.001 seconds.
18+
* ✔ Test "Name" (aka 'func()') passed after 0.001 seconds.
19+
* ✔ Test "Name" with 3 test cases passed after 0.001 seconds.
1320
* ✘ Test "Name" failed after 0.001 seconds with 1 issue.
14-
* ✘ Test "Name" failed after 0.001 seconds with 3 issues.
15-
* ◇ Test "Name" skipped.
21+
* ✘ Test "Name" (aka 'func()') failed after 0.001 seconds with 1 issue.
22+
* ➜ Test funcName() skipped: "reason"
23+
* ➜ Test funcName() skipped
1624
*/
1725
export function parseSwiftTestingResultLine(line: string): ParsedTestCase | null {
18-
const passedMatch = line.match(/^[] Test "(.+)" passed after ([\d.]+) seconds\.?$/u);
26+
const passedRegex = new RegExp(
27+
`^[✔] Test "(.+)"${OPTIONAL_AKA}${OPTIONAL_PARAMETERIZED} passed after ([\\d.]+) seconds\\.?$`,
28+
'u',
29+
);
30+
const passedMatch = line.match(passedRegex);
1931
if (passedMatch) {
2032
const [, name, duration] = passedMatch;
2133
const { suiteName, testName } = parseRawTestName(name);
@@ -28,7 +40,11 @@ export function parseSwiftTestingResultLine(line: string): ParsedTestCase | null
2840
};
2941
}
3042

31-
const failedMatch = line.match(/^[] Test "(.+)" failed after ([\d.]+) seconds/u);
43+
const failedRegex = new RegExp(
44+
`^[✘] Test "(.+)"${OPTIONAL_AKA}${OPTIONAL_PARAMETERIZED} failed after ([\\d.]+) seconds`,
45+
'u',
46+
);
47+
const failedMatch = line.match(failedRegex);
3248
if (failedMatch) {
3349
const [, name, duration] = failedMatch;
3450
const { suiteName, testName } = parseRawTestName(name);
@@ -41,7 +57,11 @@ export function parseSwiftTestingResultLine(line: string): ParsedTestCase | null
4157
};
4258
}
4359

44-
const skippedMatch = line.match(/^[] Test "(.+)" skipped/u);
60+
// Skipped: ➜ Test funcName() skipped: "reason"
61+
// Also handle legacy format: ◇ Test "Name" skipped
62+
const skippedMatch =
63+
line.match(/^[] Test (\S+?)(?:\(\))? skipped/u) ??
64+
line.match(/^[] Test "(.+)" skipped/u);
4565
if (skippedMatch) {
4666
const rawName = skippedMatch[1];
4767
const { suiteName, testName } = parseRawTestName(rawName);
@@ -59,12 +79,19 @@ export function parseSwiftTestingResultLine(line: string): ParsedTestCase | null
5979
/**
6080
* Parse a Swift Testing issue line.
6181
*
62-
* Matches:
82+
* Matches (non-verbose and verbose, including parameterized):
6383
* ✘ Test "Name" recorded an issue at File.swift:48:5: Expectation failed: ...
84+
* ✘ Test "Name" (aka 'func()') recorded an issue at File.swift:48:5: msg
85+
* ✘ Test "Name" recorded an issue with 1 argument value → 0 at File.swift:10:5: msg
6486
* ✘ Test "Name" recorded an issue: message
6587
*/
6688
export function parseSwiftTestingIssueLine(line: string): ParsedFailureDiagnostic | null {
67-
const locationMatch = line.match(/^[] Test "(.+)" recorded an issue at (.+?):(\d+):\d+: (.+)$/u);
89+
// Match with location -- handle both aka suffix and parameterized argument values before "at"
90+
const locationRegex = new RegExp(
91+
`^[✘] Test "(.+)"${OPTIONAL_AKA} recorded an issue(?:\\s+with \\d+ argument values?[^:]*?)? at (.+?):(\\d+):\\d+: (.+)$`,
92+
'u',
93+
);
94+
const locationMatch = line.match(locationRegex);
6895
if (locationMatch) {
6996
const [, rawTestName, filePath, lineNumber, message] = locationMatch;
7097
const { suiteName, testName } = parseRawTestName(rawTestName);
@@ -77,7 +104,12 @@ export function parseSwiftTestingIssueLine(line: string): ParsedFailureDiagnosti
77104
};
78105
}
79106

80-
const simpleMatch = line.match(/^[] Test "(.+)" recorded an issue: (.+)$/u);
107+
// Match without location
108+
const simpleRegex = new RegExp(
109+
`^[✘] Test "(.+)"${OPTIONAL_AKA} recorded an issue: (.+)$`,
110+
'u',
111+
);
112+
const simpleMatch = line.match(simpleRegex);
81113
if (simpleMatch) {
82114
const [, rawTestName, message] = simpleMatch;
83115
const { suiteName, testName } = parseRawTestName(rawTestName);

src/utils/xcodebuild-event-parser.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ function resolveStageFromLine(line: string): XcodebuildStage | null {
3333
}
3434
if (
3535
/^Testing started$/u.test(line) ||
36-
/^Test Suite .+ started/u.test(line) ||
36+
/^Test [Ss]uite .+ started/u.test(line) ||
3737
/^[] Test run started/u.test(line)
3838
) {
3939
return 'RUN_TESTS';
@@ -373,7 +373,7 @@ export function createXcodebuildEventParser(options: EventParserOptions): Xcodeb
373373
return;
374374
}
375375

376-
if (/^Test Suite /u.test(line)) {
376+
if (/^Test [Ss]uite /u.test(line)) {
377377
return;
378378
}
379379

src/utils/xcodebuild-line-parsers.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ export function parseDurationMs(durationText?: string): number | undefined {
141141
}
142142

143143
export function parseBuildErrorDiagnostic(line: string): ParsedBuildError | null {
144+
// File path with line number: /path/to/File.swift:42:10: error: message
144145
const locationMatch = line.match(/^(.*?):(\d+)(?::\d+)?: (?:fatal error|error): (.+)$/u);
145146
if (locationMatch) {
146147
const [, filePath, lineNumber, message] = locationMatch;
@@ -151,6 +152,18 @@ export function parseBuildErrorDiagnostic(line: string): ParsedBuildError | null
151152
};
152153
}
153154

155+
// Path-based error without line number: /path/to/Project.xcodeproj: error: message
156+
const pathErrorMatch = line.match(/^(\/[^:]+): (?:fatal error|error): (.+)$/u);
157+
if (pathErrorMatch) {
158+
const [, filePath, message] = pathErrorMatch;
159+
return {
160+
location: filePath,
161+
message,
162+
renderedLine: line,
163+
};
164+
}
165+
166+
// Prefixed error: xcodebuild: error: message / error: message
154167
const rawMatch = line.match(/^(?:[\w-]+:\s+)?(?:fatal error|error): (.+)$/u);
155168
if (!rawMatch) {
156169
return null;

0 commit comments

Comments
 (0)