Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import {
registerIOSSimulatorBuildAndRunTools,
} from './tools/build_ios_simulator.js';

// Import iOS simulator test tools
import { registerIOSSimulatorTestTools } from './tools/test_ios_simulator.js';

// Import iOS device build tools
import { registerIOSDeviceBuildTools } from './tools/build_ios_device.js';

Expand Down Expand Up @@ -142,6 +145,9 @@ async function main(): Promise<void> {
// Register log capture tools
registerStartSimulatorLogCaptureTool(server);
registerStopAndGetSimulatorLogTool(server);

// Register test tools
registerIOSSimulatorTestTools(server);

// Start the server
await startServer(server);
Expand Down
186 changes: 186 additions & 0 deletions src/tools/test_ios_simulator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/**
* iOS Simulator Test Tools - Tools for running tests on iOS applications in simulators
*
* This module provides specialized tools for running tests on iOS applications in simulators
* using xcodebuild test. It supports both workspace and project-based testing with simulator targeting
* by name or UUID, and includes test failure parsing.
*
* Responsibilities:
* - Running tests on iOS applications in simulators from project files and workspaces
* - Supporting simulator targeting by name or UUID
* - Parsing and summarizing test failure results
* - Handling test configuration and derived data paths
*/

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { XcodePlatform, executeXcodeCommand } from '../utils/xcode.js';
import { executeXcodeBuild } from '../utils/build-utils.js';
import { log } from '../utils/logger.js';
import { createTextResponse } from '../utils/validation.js';
import { ToolResponse, ToolResponseContent } from '../types/common.js';
import {
registerTool,
workspacePathSchema,
projectPathSchema,
schemeSchema,
configurationSchema,
derivedDataPathSchema,
extraArgsSchema,
simulatorNameSchema,
simulatorIdSchema,
useLatestOSSchema
} from './common.js';

// --- internal logic ---
async function _handleIOSSimulatorTestLogic(params: {
workspacePath?: string;
projectPath?: string;
scheme: string;
configuration: string;
simulatorName?: string;
simulatorId?: string;
useLatestOS: boolean;
derivedDataPath?: string;
extraArgs?: string[];
}): Promise<ToolResponse> {
log('info', `Starting iOS Simulator tests for scheme ${params.scheme} (internal)`);

const buildResult = await executeXcodeBuild(
{
...params,
},
{
platform: XcodePlatform.iOSSimulator,
simulatorName: params.simulatorName,
simulatorId: params.simulatorId,
useLatestOS: params.useLatestOS,
logPrefix: 'iOS Simulator Test',
},
'test',
);

if (buildResult.isError) return buildResult;

// --- Parse failures ---
const raw = buildResult.rawOutput ?? '';
const failures = raw
.split('\n')
.filter(l => /Test Case .* failed/.test(l))
.map(l => {
const m = l.match(/Test Case '(.*)' failed \((.*)\)/)!;
Copy link

Copilot AI Apr 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a non-null assertion with the regex match may cause a runtime error if the log format ever changes; consider performing a safe check before destructuring the match result.

Copilot uses AI. Check for mistakes.
return { testCase: m[1], reason: m[2] };
});

const summary = failures.length
? `❌ ${failures.length} test(s) failed`
: '✅ All tests passed';

const content: ToolResponseContent[] = [
{ type: 'text', text: summary }
];

// Add failures as formatted text if any exist
if (failures.length > 0) {
content.push({
type: 'text',
text: `Test failures:\n${failures.map(f => `- ${f.testCase}: ${f.reason}`).join('\n')}`
});
}

return { content };
}

/**
* Register all iOS Simulator test tools with the MCP server
*/
export function registerIOSSimulatorTestTools(server: McpServer): void {
// Common default values
const defaults = {
configuration: 'Debug',
useLatestOS: true,
};

// 1) workspace + name
registerTool(
server,
'ios_simulator_test_by_name_workspace',
'Run tests for an iOS app on a simulator specified by name using a workspace',
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cameroncooke I think I should improve these if you have any tips. I see now that your other tools have much more detail here.

{
workspacePath: workspacePathSchema,
scheme: schemeSchema,
simulatorName: simulatorNameSchema,
configuration: configurationSchema,
derivedDataPath: derivedDataPathSchema,
extraArgs: extraArgsSchema,
useLatestOS: useLatestOSSchema,
},
(params: any) => _handleIOSSimulatorTestLogic({
...params,
configuration: params.configuration || defaults.configuration,
useLatestOS: params.useLatestOS ?? defaults.useLatestOS
})
);

// 2) project + name
registerTool(
server,
'ios_simulator_test_by_name_project',
'Run tests for an iOS app on a simulator specified by name using a project file',
{
projectPath: projectPathSchema,
scheme: schemeSchema,
simulatorName: simulatorNameSchema,
configuration: configurationSchema,
derivedDataPath: derivedDataPathSchema,
extraArgs: extraArgsSchema,
useLatestOS: useLatestOSSchema,
},
(params: any) => _handleIOSSimulatorTestLogic({
...params,
configuration: params.configuration || defaults.configuration,
useLatestOS: params.useLatestOS ?? defaults.useLatestOS
})
);

// 3) workspace + id
registerTool(
server,
'ios_simulator_test_by_id_workspace',
'Run tests for an iOS app on a simulator specified by ID using a workspace',
{
workspacePath: workspacePathSchema,
scheme: schemeSchema,
simulatorId: simulatorIdSchema,
configuration: configurationSchema,
derivedDataPath: derivedDataPathSchema,
extraArgs: extraArgsSchema,
useLatestOS: useLatestOSSchema,
},
(params: any) => _handleIOSSimulatorTestLogic({
...params,
configuration: params.configuration || defaults.configuration,
useLatestOS: params.useLatestOS ?? defaults.useLatestOS
})
);

// 4) project + id
registerTool(
server,
'ios_simulator_test_by_id_project',
'Run tests for an iOS app on a simulator specified by ID using a project file',
{
projectPath: projectPathSchema,
scheme: schemeSchema,
simulatorId: simulatorIdSchema,
configuration: configurationSchema,
derivedDataPath: derivedDataPathSchema,
extraArgs: extraArgsSchema,
useLatestOS: useLatestOSSchema,
},
(params: any) => _handleIOSSimulatorTestLogic({
...params,
configuration: params.configuration || defaults.configuration,
useLatestOS: params.useLatestOS ?? defaults.useLatestOS
})
);
}
1 change: 1 addition & 0 deletions src/types/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface ToolResponse {
content: ToolResponseContent[];
isError?: boolean;
_meta?: Record<string, unknown>;
rawOutput?: string; // Raw output from command execution
[key: string]: unknown; // Index signature to match CallToolResult
}

Expand Down
1 change: 1 addition & 0 deletions src/utils/build-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ When done capturing logs, use: stop_and_get_simulator_log({ logSessionId: 'SESSI
text: `✅ ${platformOptions.logPrefix} ${buildAction} succeeded for scheme ${params.scheme}.`,
},
],
rawOutput: result.output + (result.error ? '\n' + result.error : ''),
Copy link

Copilot AI Apr 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider trimming trailing newlines from result.output before concatenation to avoid unintended extra newline characters in rawOutput.

Suggested change
rawOutput: result.output + (result.error ? '\n' + result.error : ''),
rawOutput: result.output.trimEnd() + (result.error ? '\n' + result.error : ''),

Copilot uses AI. Check for mistakes.
};

// Only add additional info if we have any
Expand Down