Skip to content

Commit d922fd7

Browse files
committed
refactor: add pipeline event types, xcodebuild parsers, and event builders
1 parent 46a7228 commit d922fd7

12 files changed

+2392
-0
lines changed

src/types/pipeline-events.ts

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
export type XcodebuildOperation = 'BUILD' | 'TEST';
2+
3+
export type XcodebuildStage =
4+
| 'RESOLVING_PACKAGES'
5+
| 'COMPILING'
6+
| 'LINKING'
7+
| 'PREPARING_TESTS'
8+
| 'RUN_TESTS'
9+
| 'ARCHIVING'
10+
| 'COMPLETED';
11+
12+
export const STAGE_RANK: Record<XcodebuildStage, number> = {
13+
RESOLVING_PACKAGES: 0,
14+
COMPILING: 1,
15+
LINKING: 2,
16+
PREPARING_TESTS: 3,
17+
RUN_TESTS: 4,
18+
ARCHIVING: 5,
19+
COMPLETED: 6,
20+
};
21+
22+
interface BaseEvent {
23+
timestamp: string;
24+
}
25+
26+
// --- Canonical types (used by ALL tools) ---
27+
28+
export interface HeaderEvent extends BaseEvent {
29+
type: 'header';
30+
operation: string;
31+
params: Array<{ label: string; value: string }>;
32+
}
33+
34+
export interface StatusLineEvent extends BaseEvent {
35+
type: 'status-line';
36+
level: 'success' | 'error' | 'info' | 'warning';
37+
message: string;
38+
}
39+
40+
export interface SummaryEvent extends BaseEvent {
41+
type: 'summary';
42+
operation?: string;
43+
status: 'SUCCEEDED' | 'FAILED';
44+
totalTests?: number;
45+
passedTests?: number;
46+
failedTests?: number;
47+
skippedTests?: number;
48+
durationMs?: number;
49+
}
50+
51+
export interface SectionEvent extends BaseEvent {
52+
type: 'section';
53+
title: string;
54+
icon?: 'red-circle' | 'yellow-circle' | 'green-circle' | 'checkmark' | 'cross' | 'info';
55+
lines: string[];
56+
blankLineAfterTitle?: boolean;
57+
}
58+
59+
export interface DetailTreeEvent extends BaseEvent {
60+
type: 'detail-tree';
61+
items: Array<{ label: string; value: string }>;
62+
}
63+
64+
export interface TableEvent extends BaseEvent {
65+
type: 'table';
66+
heading?: string;
67+
columns: string[];
68+
rows: Array<Record<string, string>>;
69+
}
70+
71+
export interface FileRefEvent extends BaseEvent {
72+
type: 'file-ref';
73+
label?: string;
74+
path: string;
75+
}
76+
77+
export interface NextStepsEvent extends BaseEvent {
78+
type: 'next-steps';
79+
steps: Array<{
80+
label?: string;
81+
tool?: string;
82+
workflow?: string;
83+
cliTool?: string;
84+
params?: Record<string, string | number | boolean>;
85+
}>;
86+
runtime?: 'cli' | 'daemon' | 'mcp';
87+
}
88+
89+
// --- Xcodebuild-specific types ---
90+
91+
export interface BuildStageEvent extends BaseEvent {
92+
type: 'build-stage';
93+
operation: XcodebuildOperation;
94+
stage: XcodebuildStage;
95+
message: string;
96+
}
97+
98+
export interface CompilerWarningEvent extends BaseEvent {
99+
type: 'compiler-warning';
100+
operation: XcodebuildOperation;
101+
message: string;
102+
location?: string;
103+
rawLine: string;
104+
}
105+
106+
export interface CompilerErrorEvent extends BaseEvent {
107+
type: 'compiler-error';
108+
operation: XcodebuildOperation;
109+
message: string;
110+
location?: string;
111+
rawLine: string;
112+
}
113+
114+
export interface TestDiscoveryEvent extends BaseEvent {
115+
type: 'test-discovery';
116+
operation: 'TEST';
117+
total: number;
118+
tests: string[];
119+
truncated: boolean;
120+
}
121+
122+
export interface TestProgressEvent extends BaseEvent {
123+
type: 'test-progress';
124+
operation: 'TEST';
125+
completed: number;
126+
failed: number;
127+
skipped: number;
128+
}
129+
130+
export interface TestFailureEvent extends BaseEvent {
131+
type: 'test-failure';
132+
operation: 'TEST';
133+
target?: string;
134+
suite?: string;
135+
test?: string;
136+
message: string;
137+
location?: string;
138+
durationMs?: number;
139+
}
140+
141+
// --- Union types ---
142+
143+
/** Generic UI/output events usable by any tool */
144+
export type CommonPipelineEvent =
145+
| HeaderEvent
146+
| StatusLineEvent
147+
| SummaryEvent
148+
| SectionEvent
149+
| DetailTreeEvent
150+
| TableEvent
151+
| FileRefEvent
152+
| NextStepsEvent;
153+
154+
/** Build/test-specific events (xcodebuild, swift build/test/run) */
155+
export type BuildTestPipelineEvent =
156+
| BuildStageEvent
157+
| CompilerWarningEvent
158+
| CompilerErrorEvent
159+
| TestDiscoveryEvent
160+
| TestProgressEvent
161+
| TestFailureEvent;
162+
163+
export type PipelineEvent = CommonPipelineEvent | BuildTestPipelineEvent;
164+
165+
// --- Build-run notice types (used by xcodebuild pipeline internals) ---
166+
167+
export type NoticeLevel = 'info' | 'success' | 'warning';
168+
169+
export type BuildRunStepName =
170+
| 'resolve-app-path'
171+
| 'resolve-simulator'
172+
| 'boot-simulator'
173+
| 'install-app'
174+
| 'extract-bundle-id'
175+
| 'launch-app';
176+
177+
export type BuildRunStepStatus = 'started' | 'succeeded';
178+
179+
export interface BuildRunStepNoticeData {
180+
step: BuildRunStepName;
181+
status: BuildRunStepStatus;
182+
appPath?: string;
183+
}
184+
185+
export interface BuildRunResultNoticeData {
186+
scheme: string;
187+
platform: string;
188+
target: string;
189+
appPath: string;
190+
launchState: 'requested' | 'running';
191+
bundleId?: string;
192+
appId?: string;
193+
processId?: number;
194+
buildLogPath?: string;
195+
runtimeLogPath?: string;
196+
osLogPath?: string;
197+
}
198+
199+
export type NoticeCode = 'build-run-step' | 'build-run-result';
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { describe, it, expect } from 'vitest';
2+
import {
3+
parseSwiftTestingResultLine,
4+
parseSwiftTestingIssueLine,
5+
parseSwiftTestingRunSummary,
6+
parseSwiftTestingContinuationLine,
7+
parseXcodebuildSwiftTestingLine,
8+
} from '../swift-testing-line-parsers.ts';
9+
10+
describe('Swift Testing line parsers', () => {
11+
describe('parseSwiftTestingResultLine', () => {
12+
it('should parse a passed test', () => {
13+
const result = parseSwiftTestingResultLine(
14+
'✔ Test "Basic math operations" passed after 0.001 seconds.',
15+
);
16+
expect(result).toEqual({
17+
status: 'passed',
18+
rawName: 'Basic math operations',
19+
testName: 'Basic math operations',
20+
durationText: '0.001s',
21+
});
22+
});
23+
24+
it('should parse a failed test', () => {
25+
const result = parseSwiftTestingResultLine(
26+
'✘ Test "Expected failure" failed after 0.001 seconds with 1 issue.',
27+
);
28+
expect(result).toEqual({
29+
status: 'failed',
30+
rawName: 'Expected failure',
31+
testName: 'Expected failure',
32+
durationText: '0.001s',
33+
});
34+
});
35+
36+
it('should parse a skipped test', () => {
37+
const result = parseSwiftTestingResultLine('◇ Test "Disabled test" skipped.');
38+
expect(result).toEqual({
39+
status: 'skipped',
40+
rawName: 'Disabled test',
41+
testName: 'Disabled test',
42+
});
43+
});
44+
45+
it('should return null for non-matching lines', () => {
46+
expect(parseSwiftTestingResultLine('◇ Test "Foo" started.')).toBeNull();
47+
expect(parseSwiftTestingResultLine('random text')).toBeNull();
48+
});
49+
});
50+
51+
describe('parseSwiftTestingIssueLine', () => {
52+
it('should parse an issue with location', () => {
53+
const result = parseSwiftTestingIssueLine(
54+
'✘ Test "Expected failure" recorded an issue at SimpleTests.swift:48:5: Expectation failed: true == false',
55+
);
56+
expect(result).toEqual({
57+
rawTestName: 'Expected failure',
58+
testName: 'Expected failure',
59+
location: 'SimpleTests.swift:48',
60+
message: 'Expectation failed: true == false',
61+
});
62+
});
63+
64+
it('should parse an issue without location', () => {
65+
const result = parseSwiftTestingIssueLine(
66+
'✘ Test "Some test" recorded an issue: Something went wrong',
67+
);
68+
expect(result).toEqual({
69+
rawTestName: 'Some test',
70+
testName: 'Some test',
71+
message: 'Something went wrong',
72+
});
73+
});
74+
75+
it('should return null for non-matching lines', () => {
76+
expect(parseSwiftTestingIssueLine('✘ Test "Foo" failed after 0.001 seconds')).toBeNull();
77+
});
78+
});
79+
80+
describe('parseSwiftTestingRunSummary', () => {
81+
it('should parse a failed run summary', () => {
82+
const result = parseSwiftTestingRunSummary(
83+
'✘ Test run with 6 tests in 0 suites failed after 0.001 seconds with 1 issue.',
84+
);
85+
expect(result).toEqual({
86+
executed: 6,
87+
failed: 1,
88+
durationText: '0.001s',
89+
});
90+
});
91+
92+
it('should parse a passed run summary', () => {
93+
const result = parseSwiftTestingRunSummary(
94+
'✔ Test run with 5 tests in 2 suites passed after 0.003 seconds.',
95+
);
96+
expect(result).toEqual({
97+
executed: 5,
98+
failed: 0,
99+
durationText: '0.003s',
100+
});
101+
});
102+
103+
it('should return null for non-matching lines', () => {
104+
expect(parseSwiftTestingRunSummary('random text')).toBeNull();
105+
});
106+
});
107+
108+
describe('parseSwiftTestingContinuationLine', () => {
109+
it('should parse a continuation line', () => {
110+
expect(parseSwiftTestingContinuationLine('↳ This test should fail')).toBe(
111+
'This test should fail',
112+
);
113+
});
114+
115+
it('should return null for non-continuation lines', () => {
116+
expect(parseSwiftTestingContinuationLine('regular line')).toBeNull();
117+
});
118+
});
119+
120+
describe('parseXcodebuildSwiftTestingLine', () => {
121+
it('should parse a passed test case', () => {
122+
const result = parseXcodebuildSwiftTestingLine(
123+
"Test case 'MCPTestTests/appNameIsCorrect()' passed on 'My Mac - MCPTest (78757)' (0.000 seconds)",
124+
);
125+
expect(result).toEqual({
126+
status: 'passed',
127+
rawName: 'MCPTestTests/appNameIsCorrect()',
128+
suiteName: 'MCPTestTests',
129+
testName: 'appNameIsCorrect()',
130+
durationText: '0.000s',
131+
});
132+
});
133+
134+
it('should parse a failed test case', () => {
135+
const result = parseXcodebuildSwiftTestingLine(
136+
"Test case 'MCPTestTests/deliberateFailure()' failed on 'My Mac - MCPTest (78757)' (0.000 seconds)",
137+
);
138+
expect(result).toEqual({
139+
status: 'failed',
140+
rawName: 'MCPTestTests/deliberateFailure()',
141+
suiteName: 'MCPTestTests',
142+
testName: 'deliberateFailure()',
143+
durationText: '0.000s',
144+
});
145+
});
146+
147+
it('should return null for XCTest format lines', () => {
148+
expect(
149+
parseXcodebuildSwiftTestingLine("Test Case '-[Suite test]' passed (0.001 seconds)."),
150+
).toBeNull();
151+
});
152+
153+
it('should return null for non-matching lines', () => {
154+
expect(parseXcodebuildSwiftTestingLine('random text')).toBeNull();
155+
});
156+
});
157+
});

0 commit comments

Comments
 (0)