Skip to content

Commit 1631f58

Browse files
cameroncookecodex
andcommitted
fix(test): Stream simulator test execution progress
Emit a test-running stage when two-phase simulator execution moves from build-for-testing to test-without-building so the CLI does not leave stale build status visible while UI tests are running. Keep static discovery observational, parse destination-suffixed XCTest result lines, and handle multiline Swift Testing arguments so discovery and final counts stay project-agnostic across Calculator and Weather. Fixes #384 Co-Authored-By: OpenAI Codex <noreply@openai.com>
1 parent 007d974 commit 1631f58

14 files changed

Lines changed: 727 additions & 18 deletions

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44

55
### Fixed
66

7+
- Fixed simulator test JSONL accuracy by keeping preflight discovery observational, preserving only user-supplied test selectors, discovering multiline parameterized Swift Testing tests, and parsing destination-suffixed xcodebuild test result lines.
78
- Removed stale physical-device log session status and shutdown cleanup for deprecated standalone device log capture, and corrected the device build-and-run tool description.
9+
- Fixed mixed Swift Testing and XCTest summaries so simulator test text output no longer overcounts parameterized Swift Testing results or issue lines.
810
- Fixed CLI test summaries showing false-positive compiler errors from xcodebuild NSError dump lines, and added compiler-error snapshot coverage for simulator, device, and macOS build-style flows ([#383](https://github.com/getsentry/XcodeBuildMCP/issues/383)).
911
- Fixed simulator OSLog helper cleanup so server and daemon startup reconcile same-workspace orphaned log streams without stopping helpers owned by live sessions in other workspaces ([#382](https://github.com/getsentry/XcodeBuildMCP/issues/382)).
12+
- Fixed Weather example test discovery and made CLI test progress visible while tests are running instead of leaving the last build phase displayed.
1013

1114
## [2.5.0-beta.1]
1215

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Scheme
3+
LastUpgradeVersion = "1640"
4+
version = "1.7">
5+
<BuildAction
6+
parallelizeBuildables = "YES"
7+
buildImplicitDependencies = "YES"
8+
buildArchitectures = "Automatic">
9+
<BuildActionEntries>
10+
<BuildActionEntry
11+
buildForTesting = "YES"
12+
buildForRunning = "YES"
13+
buildForProfiling = "YES"
14+
buildForArchiving = "YES"
15+
buildForAnalyzing = "YES">
16+
<BuildableReference
17+
BuildableIdentifier = "primary"
18+
BlueprintIdentifier = "8B9291402FA3FCC300B2E371"
19+
BuildableName = "Weather.app"
20+
BlueprintName = "Weather"
21+
ReferencedContainer = "container:Weather.xcodeproj">
22+
</BuildableReference>
23+
</BuildActionEntry>
24+
</BuildActionEntries>
25+
</BuildAction>
26+
<TestAction
27+
buildConfiguration = "Debug"
28+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
29+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
30+
shouldUseLaunchSchemeArgsEnv = "YES">
31+
<Testables>
32+
<TestableReference
33+
skipped = "NO"
34+
parallelizable = "YES">
35+
<BuildableReference
36+
BuildableIdentifier = "primary"
37+
BlueprintIdentifier = "8B92914D2FA3FCC400B2E371"
38+
BuildableName = "WeatherTests.xctest"
39+
BlueprintName = "WeatherTests"
40+
ReferencedContainer = "container:Weather.xcodeproj">
41+
</BuildableReference>
42+
</TestableReference>
43+
<TestableReference
44+
skipped = "NO"
45+
parallelizable = "YES">
46+
<BuildableReference
47+
BuildableIdentifier = "primary"
48+
BlueprintIdentifier = "8B9291572FA3FCC400B2E371"
49+
BuildableName = "WeatherUITests.xctest"
50+
BlueprintName = "WeatherUITests"
51+
ReferencedContainer = "container:Weather.xcodeproj">
52+
</BuildableReference>
53+
</TestableReference>
54+
</Testables>
55+
</TestAction>
56+
<LaunchAction
57+
buildConfiguration = "Debug"
58+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
59+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
60+
launchStyle = "0"
61+
useCustomWorkingDirectory = "NO"
62+
ignoresPersistentStateOnLaunch = "NO"
63+
debugDocumentVersioning = "YES"
64+
debugServiceExtension = "internal"
65+
allowLocationSimulation = "YES">
66+
<BuildableProductRunnable
67+
runnableDebuggingMode = "0">
68+
<BuildableReference
69+
BuildableIdentifier = "primary"
70+
BlueprintIdentifier = "8B9291402FA3FCC300B2E371"
71+
BuildableName = "Weather.app"
72+
BlueprintName = "Weather"
73+
ReferencedContainer = "container:Weather.xcodeproj">
74+
</BuildableReference>
75+
</BuildableProductRunnable>
76+
</LaunchAction>
77+
<ProfileAction
78+
buildConfiguration = "Release"
79+
shouldUseLaunchSchemeArgsEnv = "YES"
80+
savedToolIdentifier = ""
81+
useCustomWorkingDirectory = "NO"
82+
debugDocumentVersioning = "YES">
83+
<BuildableProductRunnable
84+
runnableDebuggingMode = "0">
85+
<BuildableReference
86+
BuildableIdentifier = "primary"
87+
BlueprintIdentifier = "8B9291402FA3FCC300B2E371"
88+
BuildableName = "Weather.app"
89+
BlueprintName = "Weather"
90+
ReferencedContainer = "container:Weather.xcodeproj">
91+
</BuildableReference>
92+
</BuildableProductRunnable>
93+
</ProfileAction>
94+
<AnalyzeAction
95+
buildConfiguration = "Debug">
96+
</AnalyzeAction>
97+
<ArchiveAction
98+
buildConfiguration = "Release"
99+
revealArchiveInOrganizer = "YES">
100+
</ArchiveAction>
101+
</Scheme>
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { createSimulatorTwoPhaseExecutionPlan } from '../simulator-test-execution.ts';
3+
import type { TestPreflightResult } from '../test-preflight.ts';
4+
5+
function createPreflight(): TestPreflightResult {
6+
return {
7+
scheme: 'CalculatorApp',
8+
configuration: 'Debug',
9+
projectPath: '/tmp/CalculatorApp.xcodeproj',
10+
destinationName: 'iPhone 17 Pro',
11+
selectors: { onlyTesting: [], skipTesting: [] },
12+
warnings: [],
13+
completeness: 'complete',
14+
totalTests: 2,
15+
targets: [
16+
{
17+
name: 'CalculatorAppTests',
18+
warnings: [],
19+
files: [
20+
{
21+
path: '/tmp/CalculatorAppTests.swift',
22+
tests: [
23+
{
24+
framework: 'xctest',
25+
targetName: 'CalculatorAppTests',
26+
typeName: 'CalculatorAppTests',
27+
methodName: 'testAddition',
28+
displayName: 'CalculatorAppTests/CalculatorAppTests/testAddition',
29+
line: 10,
30+
parameterized: false,
31+
},
32+
{
33+
framework: 'swift-testing',
34+
targetName: 'CalculatorAppTests',
35+
typeName: 'ExpressionSuite',
36+
methodName: 'evaluatesExpression',
37+
displayName: 'CalculatorAppTests/ExpressionSuite/evaluatesExpression',
38+
line: 20,
39+
parameterized: true,
40+
},
41+
],
42+
},
43+
],
44+
},
45+
],
46+
};
47+
}
48+
49+
describe('createSimulatorTwoPhaseExecutionPlan', () => {
50+
it('keeps preflight discovery observational instead of synthesizing only-testing selectors', () => {
51+
const plan = createSimulatorTwoPhaseExecutionPlan({
52+
extraArgs: ['-parallel-testing-enabled', 'YES'],
53+
preflight: createPreflight(),
54+
resultBundlePath: '/tmp/Calculator.xcresult',
55+
});
56+
57+
expect(plan.buildArgs).toEqual(['-parallel-testing-enabled', 'YES']);
58+
expect(plan.testArgs).toEqual([
59+
'-parallel-testing-enabled',
60+
'YES',
61+
'-resultBundlePath',
62+
'/tmp/Calculator.xcresult',
63+
]);
64+
expect(plan.usesExactSelectors).toBe(false);
65+
});
66+
67+
it('preserves user-supplied selector arguments in both simulator test phases', () => {
68+
const plan = createSimulatorTwoPhaseExecutionPlan({
69+
extraArgs: [
70+
'-only-testing:CalculatorAppTests/CalculatorAppTests/testAddition',
71+
'-skip-testing',
72+
'CalculatorAppTests/ExpressionSuite/evaluatesExpression',
73+
],
74+
preflight: createPreflight(),
75+
});
76+
77+
expect(plan.buildArgs).toEqual([
78+
'-only-testing:CalculatorAppTests/CalculatorAppTests/testAddition',
79+
'-skip-testing',
80+
'CalculatorAppTests/ExpressionSuite/evaluatesExpression',
81+
]);
82+
expect(plan.testArgs).toEqual(plan.buildArgs);
83+
expect(plan.usesExactSelectors).toBe(true);
84+
});
85+
86+
it('keeps resultBundlePath out of build-for-testing args and includes it for test-without-building', () => {
87+
const plan = createSimulatorTwoPhaseExecutionPlan({
88+
extraArgs: ['-resultBundlePath', '/tmp/UserProvided.xcresult'],
89+
});
90+
91+
expect(plan.buildArgs).toEqual([]);
92+
expect(plan.testArgs).toEqual(['-resultBundlePath', '/tmp/UserProvided.xcresult']);
93+
});
94+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { createMockFileSystemExecutor } from '../../test-utils/mock-executors.ts';
3+
import { discoverSwiftTestsInFiles } from '../swift-test-discovery.ts';
4+
5+
describe('discoverSwiftTestsInFiles', () => {
6+
it('discovers Swift Testing functions with multiline parameterized Test attributes', async () => {
7+
const filePath = '/tmp/CalculatorServiceTests.swift';
8+
const fileSystemExecutor = createMockFileSystemExecutor({
9+
readFile: async () => `
10+
import Testing
11+
12+
struct CalculatorServiceTests {
13+
@Test(
14+
"evaluates decimal operations",
15+
arguments: [
16+
("1 + 1", "2"),
17+
("4 / 2", "2"),
18+
]
19+
)
20+
func evaluatesDecimalOperations(expression: String, expected: String) async throws {}
21+
22+
@Test(arguments: ["+", "-", "×"])
23+
func evaluatesOperators(symbol: String) async throws {}
24+
}
25+
`,
26+
});
27+
28+
const files = await discoverSwiftTestsInFiles(
29+
'CalculatorAppFeatureTests',
30+
[filePath],
31+
fileSystemExecutor,
32+
);
33+
34+
expect(files).toHaveLength(1);
35+
expect(files[0].tests).toMatchObject([
36+
{
37+
framework: 'swift-testing',
38+
targetName: 'CalculatorAppFeatureTests',
39+
typeName: 'CalculatorServiceTests',
40+
methodName: 'evaluatesDecimalOperations',
41+
displayName: 'CalculatorAppFeatureTests/CalculatorServiceTests/evaluatesDecimalOperations',
42+
parameterized: true,
43+
},
44+
{
45+
framework: 'swift-testing',
46+
targetName: 'CalculatorAppFeatureTests',
47+
typeName: 'CalculatorServiceTests',
48+
methodName: 'evaluatesOperators',
49+
displayName: 'CalculatorAppFeatureTests/CalculatorServiceTests/evaluatesOperators',
50+
parameterized: true,
51+
},
52+
]);
53+
});
54+
});

src/utils/__tests__/test-common.test.ts

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,58 @@
11
import { afterEach, describe, expect, it, vi } from 'vitest';
2-
import { resolveTestProgressEnabled } from '../test-common.ts';
2+
import type { ChildProcess } from 'node:child_process';
3+
import { createTestExecutor, resolveTestProgressEnabled } from '../test-common.ts';
4+
import type { CommandExecutor, CommandResponse } from '../command.ts';
5+
import { DefaultStreamingExecutionContext } from '../execution/index.ts';
6+
import type { AnyFragment } from '../../types/domain-fragments.ts';
7+
import type { TestPreflightResult } from '../test-preflight.ts';
8+
import { XcodePlatform } from '../xcode.ts';
9+
10+
function createSuccessfulCommandResponse(): CommandResponse {
11+
return {
12+
success: true,
13+
output: '',
14+
process: { pid: 12345 } as ChildProcess,
15+
exitCode: 0,
16+
};
17+
}
18+
19+
function createPreflight(): TestPreflightResult {
20+
return {
21+
scheme: 'Weather',
22+
configuration: 'Debug',
23+
projectPath: 'Weather.xcodeproj',
24+
destinationName: 'iPhone 17 Pro',
25+
selectors: {
26+
onlyTesting: [],
27+
skipTesting: [],
28+
},
29+
targets: [
30+
{
31+
name: 'WeatherTests',
32+
files: [
33+
{
34+
path: 'WeatherTests/WeatherTests.swift',
35+
tests: [
36+
{
37+
framework: 'swift-testing',
38+
targetName: 'WeatherTests',
39+
typeName: 'WeatherTests',
40+
methodName: 'emptySearchReturnsNoResults',
41+
displayName: 'WeatherTests/WeatherTests/emptySearchReturnsNoResults',
42+
line: 12,
43+
parameterized: false,
44+
},
45+
],
46+
},
47+
],
48+
warnings: [],
49+
},
50+
],
51+
warnings: [],
52+
totalTests: 1,
53+
completeness: 'complete',
54+
};
55+
}
356

457
describe('resolveTestProgressEnabled', () => {
558
const originalRuntime = process.env.XCODEBUILDMCP_RUNTIME;
@@ -39,3 +92,60 @@ describe('resolveTestProgressEnabled', () => {
3992
expect(resolveTestProgressEnabled(false)).toBe(false);
4093
});
4194
});
95+
96+
describe('createTestExecutor', () => {
97+
it('emits RUN_TESTS before test-without-building starts in two-phase simulator execution', async () => {
98+
const emitted: AnyFragment[] = [];
99+
const actions: string[] = [];
100+
const executor: CommandExecutor = async (command, _logPrefix, _useShell, opts) => {
101+
const action = command.at(-1);
102+
if (action) {
103+
actions.push(action);
104+
}
105+
106+
if (action === 'build-for-testing') {
107+
opts?.onStdout?.('Ld /tmp/Weather.build/Weather normal arm64\n');
108+
}
109+
110+
return createSuccessfulCommandResponse();
111+
};
112+
113+
const executeTest = createTestExecutor(executor, {
114+
preflight: createPreflight(),
115+
toolName: 'test_sim',
116+
target: 'simulator',
117+
request: {
118+
scheme: 'Weather',
119+
projectPath: 'Weather.xcodeproj',
120+
configuration: 'Debug',
121+
platform: XcodePlatform.iOSSimulator,
122+
},
123+
});
124+
125+
await executeTest(
126+
{
127+
projectPath: 'Weather.xcodeproj',
128+
scheme: 'Weather',
129+
configuration: 'Debug',
130+
simulatorId: 'A2C64636-37E9-4B68-B872-E7F0A82A5670',
131+
platform: XcodePlatform.iOSSimulator,
132+
},
133+
new DefaultStreamingExecutionContext({
134+
onFragment: (fragment) => emitted.push(fragment),
135+
}),
136+
);
137+
138+
expect(actions).toEqual(['build-for-testing', 'test-without-building']);
139+
140+
const stageEvents = emitted.filter((event) => event.fragment === 'build-stage');
141+
expect(stageEvents.map((event) => event.stage)).toEqual(['LINKING', 'RUN_TESTS']);
142+
143+
const runTestsIndex = emitted.findIndex(
144+
(event) => event.fragment === 'build-stage' && event.stage === 'RUN_TESTS',
145+
);
146+
const finalSummaryIndex = emitted.findIndex((event) => event.fragment === 'build-summary');
147+
148+
expect(runTestsIndex).toBeGreaterThan(-1);
149+
expect(finalSummaryIndex).toBeGreaterThan(runTestsIndex);
150+
});
151+
});

0 commit comments

Comments
 (0)