From 2af4a2fa88ac51ec1b4cb3c1fc0eb4ec4d40eab3 Mon Sep 17 00:00:00 2001 From: Joel Klabo Date: Mon, 28 Apr 2025 19:17:03 -0700 Subject: [PATCH 1/4] Adding Unit Testing Tool for iOS --- src/index.ts | 6 ++ src/tools/test_ios_simulator.ts | 186 ++++++++++++++++++++++++++++++++ src/types/common.ts | 1 + src/utils/build-utils.ts | 1 + 4 files changed, 194 insertions(+) create mode 100644 src/tools/test_ios_simulator.ts diff --git a/src/index.ts b/src/index.ts index 78411354..a12e7815 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; @@ -142,6 +145,9 @@ async function main(): Promise { // Register log capture tools registerStartSimulatorLogCaptureTool(server); registerStopAndGetSimulatorLogTool(server); + + // Register test tools + registerIOSSimulatorTestTools(server); // Start the server await startServer(server); diff --git a/src/tools/test_ios_simulator.ts b/src/tools/test_ios_simulator.ts new file mode 100644 index 00000000..66f0152e --- /dev/null +++ b/src/tools/test_ios_simulator.ts @@ -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 { + 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 \((.*)\)/)!; + 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', + { + 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 + }) + ); +} diff --git a/src/types/common.ts b/src/types/common.ts index 77234d0e..42dfe135 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -35,6 +35,7 @@ export interface ToolResponse { content: ToolResponseContent[]; isError?: boolean; _meta?: Record; + rawOutput?: string; // Raw output from command execution [key: string]: unknown; // Index signature to match CallToolResult } diff --git a/src/utils/build-utils.ts b/src/utils/build-utils.ts index 574fb5f5..63617649 100644 --- a/src/utils/build-utils.ts +++ b/src/utils/build-utils.ts @@ -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 : ''), }; // Only add additional info if we have any From 3d3b1377818b7d708374956e769b1e8763042707 Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Wed, 30 Apr 2025 20:46:28 +0100 Subject: [PATCH 2/4] Add tests to example projects --- .../iOS/MCPTest.xcodeproj/project.pbxproj | 114 +----------------- .../xcshareddata/xcschemes/MCPTest.xcscheme | 83 +++++++++++++ example_projects/iOS/MCPTest.xctestplan | 29 +++++ .../iOS/MCPTestTests/MCPTestTests.swift | 5 +- .../iOS/MCPTestUITests/MCPTestUITests.swift | 43 ------- .../MCPTestUITestsLaunchTests.swift | 33 ----- .../macOS/MCPTest.xcodeproj/project.pbxproj | 112 +---------------- .../xcshareddata/xcschemes/MCPTest.xcscheme | 107 ++++++++++++++++ example_projects/macOS/MCPTest.xctestplan | 24 ++++ .../macOS/MCPTestTests/MCPTestTests.swift | 5 +- .../macOS/MCPTestUITests/MCPTestUITests.swift | 43 ------- .../MCPTestUITestsLaunchTests.swift | 33 ----- 12 files changed, 255 insertions(+), 376 deletions(-) create mode 100644 example_projects/iOS/MCPTest.xcodeproj/xcshareddata/xcschemes/MCPTest.xcscheme create mode 100644 example_projects/iOS/MCPTest.xctestplan delete mode 100644 example_projects/iOS/MCPTestUITests/MCPTestUITests.swift delete mode 100644 example_projects/iOS/MCPTestUITests/MCPTestUITestsLaunchTests.swift create mode 100644 example_projects/macOS/MCPTest.xcodeproj/xcshareddata/xcschemes/MCPTest.xcscheme create mode 100644 example_projects/macOS/MCPTest.xctestplan delete mode 100644 example_projects/macOS/MCPTestUITests/MCPTestUITests.swift delete mode 100644 example_projects/macOS/MCPTestUITests/MCPTestUITestsLaunchTests.swift diff --git a/example_projects/iOS/MCPTest.xcodeproj/project.pbxproj b/example_projects/iOS/MCPTest.xcodeproj/project.pbxproj index 18ac598a..22ec36c2 100644 --- a/example_projects/iOS/MCPTest.xcodeproj/project.pbxproj +++ b/example_projects/iOS/MCPTest.xcodeproj/project.pbxproj @@ -14,19 +14,12 @@ remoteGlobalIDString = 8BA9F7E92D62A14300C22D5D; remoteInfo = MCPTest; }; - 8BA9F8052D62A14500C22D5D /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 8BA9F7E22D62A14300C22D5D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 8BA9F7E92D62A14300C22D5D; - remoteInfo = MCPTest; - }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ 8BA9F7EA2D62A14300C22D5D /* MCPTest.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MCPTest.app; sourceTree = BUILT_PRODUCTS_DIR; }; 8BA9F7FA2D62A14500C22D5D /* MCPTestTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MCPTestTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 8BA9F8042D62A14500C22D5D /* MCPTestUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MCPTestUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 8BAA686D2DC2AEFB006D34CC /* MCPTest.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = MCPTest.xctestplan; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -40,11 +33,6 @@ path = MCPTestTests; sourceTree = ""; }; - 8BA9F8072D62A14500C22D5D /* MCPTestUITests */ = { - isa = PBXFileSystemSynchronizedRootGroup; - path = MCPTestUITests; - sourceTree = ""; - }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -62,13 +50,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 8BA9F8012D62A14500C22D5D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -77,7 +58,7 @@ children = ( 8BA9F7EC2D62A14300C22D5D /* MCPTest */, 8BA9F7FD2D62A14500C22D5D /* MCPTestTests */, - 8BA9F8072D62A14500C22D5D /* MCPTestUITests */, + 8BAA686D2DC2AEFB006D34CC /* MCPTest.xctestplan */, 8BA9F7EB2D62A14300C22D5D /* Products */, ); sourceTree = ""; @@ -87,7 +68,6 @@ children = ( 8BA9F7EA2D62A14300C22D5D /* MCPTest.app */, 8BA9F7FA2D62A14500C22D5D /* MCPTestTests.xctest */, - 8BA9F8042D62A14500C22D5D /* MCPTestUITests.xctest */, ); name = Products; sourceTree = ""; @@ -140,29 +120,6 @@ productReference = 8BA9F7FA2D62A14500C22D5D /* MCPTestTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; - 8BA9F8032D62A14500C22D5D /* MCPTestUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 8BA9F8142D62A14500C22D5D /* Build configuration list for PBXNativeTarget "MCPTestUITests" */; - buildPhases = ( - 8BA9F8002D62A14500C22D5D /* Sources */, - 8BA9F8012D62A14500C22D5D /* Frameworks */, - 8BA9F8022D62A14500C22D5D /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 8BA9F8062D62A14500C22D5D /* PBXTargetDependency */, - ); - fileSystemSynchronizedGroups = ( - 8BA9F8072D62A14500C22D5D /* MCPTestUITests */, - ); - name = MCPTestUITests; - packageProductDependencies = ( - ); - productName = MCPTestUITests; - productReference = 8BA9F8042D62A14500C22D5D /* MCPTestUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -180,10 +137,6 @@ CreatedOnToolsVersion = 16.2; TestTargetID = 8BA9F7E92D62A14300C22D5D; }; - 8BA9F8032D62A14500C22D5D = { - CreatedOnToolsVersion = 16.2; - TestTargetID = 8BA9F7E92D62A14300C22D5D; - }; }; }; buildConfigurationList = 8BA9F7E52D62A14300C22D5D /* Build configuration list for PBXProject "MCPTest" */; @@ -202,7 +155,6 @@ targets = ( 8BA9F7E92D62A14300C22D5D /* MCPTest */, 8BA9F7F92D62A14500C22D5D /* MCPTestTests */, - 8BA9F8032D62A14500C22D5D /* MCPTestUITests */, ); }; /* End PBXProject section */ @@ -222,13 +174,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 8BA9F8022D62A14500C22D5D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -246,13 +191,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 8BA9F8002D62A14500C22D5D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -261,11 +199,6 @@ target = 8BA9F7E92D62A14300C22D5D /* MCPTest */; targetProxy = 8BA9F7FB2D62A14500C22D5D /* PBXContainerItemProxy */; }; - 8BA9F8062D62A14500C22D5D /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 8BA9F7E92D62A14300C22D5D /* MCPTest */; - targetProxy = 8BA9F8052D62A14500C22D5D /* PBXContainerItemProxy */; - }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -484,40 +417,6 @@ }; name = Release; }; - 8BA9F8152D62A14500C22D5D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = BR6WD3M6ZD; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.cameroncooke.MCPTestUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = MCPTest; - }; - name = Debug; - }; - 8BA9F8162D62A14500C22D5D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = BR6WD3M6ZD; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.cameroncooke.MCPTestUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = MCPTest; - }; - name = Release; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -548,15 +447,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 8BA9F8142D62A14500C22D5D /* Build configuration list for PBXNativeTarget "MCPTestUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 8BA9F8152D62A14500C22D5D /* Debug */, - 8BA9F8162D62A14500C22D5D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ }; rootObject = 8BA9F7E22D62A14300C22D5D /* Project object */; diff --git a/example_projects/iOS/MCPTest.xcodeproj/xcshareddata/xcschemes/MCPTest.xcscheme b/example_projects/iOS/MCPTest.xcodeproj/xcshareddata/xcschemes/MCPTest.xcscheme new file mode 100644 index 00000000..04c3d526 --- /dev/null +++ b/example_projects/iOS/MCPTest.xcodeproj/xcshareddata/xcschemes/MCPTest.xcscheme @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example_projects/iOS/MCPTest.xctestplan b/example_projects/iOS/MCPTest.xctestplan new file mode 100644 index 00000000..8d70f1f1 --- /dev/null +++ b/example_projects/iOS/MCPTest.xctestplan @@ -0,0 +1,29 @@ +{ + "configurations" : [ + { + "id" : "03F30AE4-48A7-4967-B8BE-2A15CC349E34", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + "targetForVariableExpansion" : { + "containerPath" : "container:MCPTest.xcodeproj", + "identifier" : "8BA9F7E92D62A14300C22D5D", + "name" : "MCPTest" + } + }, + "testTargets" : [ + { + "parallelizable" : true, + "target" : { + "containerPath" : "container:MCPTest.xcodeproj", + "identifier" : "8BA9F7F92D62A14500C22D5D", + "name" : "MCPTestTests" + } + } + ], + "version" : 1 +} diff --git a/example_projects/iOS/MCPTestTests/MCPTestTests.swift b/example_projects/iOS/MCPTestTests/MCPTestTests.swift index f244a4e0..c58cd849 100644 --- a/example_projects/iOS/MCPTestTests/MCPTestTests.swift +++ b/example_projects/iOS/MCPTestTests/MCPTestTests.swift @@ -11,7 +11,10 @@ import Testing struct MCPTestTests { @Test func example() async throws { - // Write your test here and use APIs like `#expect(...)` to check expected conditions. + #expect(1 == 1) } + @Test func example3() async throws { + #expect(1 == 2) + } } diff --git a/example_projects/iOS/MCPTestUITests/MCPTestUITests.swift b/example_projects/iOS/MCPTestUITests/MCPTestUITests.swift deleted file mode 100644 index ecc9df3e..00000000 --- a/example_projects/iOS/MCPTestUITests/MCPTestUITests.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// MCPTestUITests.swift -// MCPTestUITests -// -// Created by Cameron on 16/02/2025. -// - -import XCTest - -final class MCPTestUITests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - - // In UI tests it is usually best to stop immediately when a failure occurs. - continueAfterFailure = false - - // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - @MainActor - func testExample() throws { - // UI tests must launch the application that they test. - let app = XCUIApplication() - app.launch() - - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - @MainActor - func testLaunchPerformance() throws { - if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { - // This measures how long it takes to launch your application. - measure(metrics: [XCTApplicationLaunchMetric()]) { - XCUIApplication().launch() - } - } - } -} diff --git a/example_projects/iOS/MCPTestUITests/MCPTestUITestsLaunchTests.swift b/example_projects/iOS/MCPTestUITests/MCPTestUITestsLaunchTests.swift deleted file mode 100644 index cfc46046..00000000 --- a/example_projects/iOS/MCPTestUITests/MCPTestUITestsLaunchTests.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// MCPTestUITestsLaunchTests.swift -// MCPTestUITests -// -// Created by Cameron on 16/02/2025. -// - -import XCTest - -final class MCPTestUITestsLaunchTests: XCTestCase { - - override class var runsForEachTargetApplicationUIConfiguration: Bool { - true - } - - override func setUpWithError() throws { - continueAfterFailure = false - } - - @MainActor - func testLaunch() throws { - let app = XCUIApplication() - app.launch() - - // Insert steps here to perform after app launch but before taking a screenshot, - // such as logging into a test account or navigating somewhere in the app - - let attachment = XCTAttachment(screenshot: app.screenshot()) - attachment.name = "Launch Screen" - attachment.lifetime = .keepAlways - add(attachment) - } -} diff --git a/example_projects/macOS/MCPTest.xcodeproj/project.pbxproj b/example_projects/macOS/MCPTest.xcodeproj/project.pbxproj index 6b3331a6..50ae4053 100644 --- a/example_projects/macOS/MCPTest.xcodeproj/project.pbxproj +++ b/example_projects/macOS/MCPTest.xcodeproj/project.pbxproj @@ -14,19 +14,12 @@ remoteGlobalIDString = 8BA9F81F2D62A17D00C22D5D; remoteInfo = MCPTest; }; - 8BA9F83C2D62A18100C22D5D /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 8BA9F8182D62A17D00C22D5D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 8BA9F81F2D62A17D00C22D5D; - remoteInfo = MCPTest; - }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 8B8AB1C52DC2B48100A69AD3 /* MCPTest.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = MCPTest.xctestplan; sourceTree = ""; }; 8BA9F8202D62A17D00C22D5D /* MCPTest.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MCPTest.app; sourceTree = BUILT_PRODUCTS_DIR; }; 8BA9F8312D62A18100C22D5D /* MCPTestTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MCPTestTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 8BA9F83B2D62A18100C22D5D /* MCPTestUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MCPTestUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -40,11 +33,6 @@ path = MCPTestTests; sourceTree = ""; }; - 8BA9F83E2D62A18100C22D5D /* MCPTestUITests */ = { - isa = PBXFileSystemSynchronizedRootGroup; - path = MCPTestUITests; - sourceTree = ""; - }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -62,13 +50,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 8BA9F8382D62A18100C22D5D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -77,7 +58,7 @@ children = ( 8BA9F8222D62A17D00C22D5D /* MCPTest */, 8BA9F8342D62A18100C22D5D /* MCPTestTests */, - 8BA9F83E2D62A18100C22D5D /* MCPTestUITests */, + 8B8AB1C52DC2B48100A69AD3 /* MCPTest.xctestplan */, 8BA9F8212D62A17D00C22D5D /* Products */, ); sourceTree = ""; @@ -87,7 +68,6 @@ children = ( 8BA9F8202D62A17D00C22D5D /* MCPTest.app */, 8BA9F8312D62A18100C22D5D /* MCPTestTests.xctest */, - 8BA9F83B2D62A18100C22D5D /* MCPTestUITests.xctest */, ); name = Products; sourceTree = ""; @@ -140,29 +120,6 @@ productReference = 8BA9F8312D62A18100C22D5D /* MCPTestTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; - 8BA9F83A2D62A18100C22D5D /* MCPTestUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 8BA9F84B2D62A18100C22D5D /* Build configuration list for PBXNativeTarget "MCPTestUITests" */; - buildPhases = ( - 8BA9F8372D62A18100C22D5D /* Sources */, - 8BA9F8382D62A18100C22D5D /* Frameworks */, - 8BA9F8392D62A18100C22D5D /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 8BA9F83D2D62A18100C22D5D /* PBXTargetDependency */, - ); - fileSystemSynchronizedGroups = ( - 8BA9F83E2D62A18100C22D5D /* MCPTestUITests */, - ); - name = MCPTestUITests; - packageProductDependencies = ( - ); - productName = MCPTestUITests; - productReference = 8BA9F83B2D62A18100C22D5D /* MCPTestUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -180,10 +137,6 @@ CreatedOnToolsVersion = 16.2; TestTargetID = 8BA9F81F2D62A17D00C22D5D; }; - 8BA9F83A2D62A18100C22D5D = { - CreatedOnToolsVersion = 16.2; - TestTargetID = 8BA9F81F2D62A17D00C22D5D; - }; }; }; buildConfigurationList = 8BA9F81B2D62A17D00C22D5D /* Build configuration list for PBXProject "MCPTest" */; @@ -202,7 +155,6 @@ targets = ( 8BA9F81F2D62A17D00C22D5D /* MCPTest */, 8BA9F8302D62A18100C22D5D /* MCPTestTests */, - 8BA9F83A2D62A18100C22D5D /* MCPTestUITests */, ); }; /* End PBXProject section */ @@ -222,13 +174,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 8BA9F8392D62A18100C22D5D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -246,13 +191,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 8BA9F8372D62A18100C22D5D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -261,11 +199,6 @@ target = 8BA9F81F2D62A17D00C22D5D /* MCPTest */; targetProxy = 8BA9F8322D62A18100C22D5D /* PBXContainerItemProxy */; }; - 8BA9F83D2D62A18100C22D5D /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 8BA9F81F2D62A17D00C22D5D /* MCPTest */; - targetProxy = 8BA9F83C2D62A18100C22D5D /* PBXContainerItemProxy */; - }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -477,38 +410,6 @@ }; name = Release; }; - 8BA9F84C2D62A18100C22D5D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = BR6WD3M6ZD; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.cameroncooke.MCPTestUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TEST_TARGET_NAME = MCPTest; - }; - name = Debug; - }; - 8BA9F84D2D62A18100C22D5D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = BR6WD3M6ZD; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.cameroncooke.MCPTestUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TEST_TARGET_NAME = MCPTest; - }; - name = Release; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -539,15 +440,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 8BA9F84B2D62A18100C22D5D /* Build configuration list for PBXNativeTarget "MCPTestUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 8BA9F84C2D62A18100C22D5D /* Debug */, - 8BA9F84D2D62A18100C22D5D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ }; rootObject = 8BA9F8182D62A17D00C22D5D /* Project object */; diff --git a/example_projects/macOS/MCPTest.xcodeproj/xcshareddata/xcschemes/MCPTest.xcscheme b/example_projects/macOS/MCPTest.xcodeproj/xcshareddata/xcschemes/MCPTest.xcscheme new file mode 100644 index 00000000..b4b85d69 --- /dev/null +++ b/example_projects/macOS/MCPTest.xcodeproj/xcshareddata/xcschemes/MCPTest.xcscheme @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example_projects/macOS/MCPTest.xctestplan b/example_projects/macOS/MCPTest.xctestplan new file mode 100644 index 00000000..f8432115 --- /dev/null +++ b/example_projects/macOS/MCPTest.xctestplan @@ -0,0 +1,24 @@ +{ + "configurations" : [ + { + "id" : "BF7C7C33-E738-44B3-B3DB-F6A58286B6F9", + "name" : "Configuration 1", + "options" : { + + } + } + ], + "defaultOptions" : { + + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:MCPTest.xcodeproj", + "identifier" : "8BA9F8302D62A18100C22D5D", + "name" : "MCPTestTests" + } + } + ], + "version" : 1 +} diff --git a/example_projects/macOS/MCPTestTests/MCPTestTests.swift b/example_projects/macOS/MCPTestTests/MCPTestTests.swift index f244a4e0..c58cd849 100644 --- a/example_projects/macOS/MCPTestTests/MCPTestTests.swift +++ b/example_projects/macOS/MCPTestTests/MCPTestTests.swift @@ -11,7 +11,10 @@ import Testing struct MCPTestTests { @Test func example() async throws { - // Write your test here and use APIs like `#expect(...)` to check expected conditions. + #expect(1 == 1) } + @Test func example3() async throws { + #expect(1 == 2) + } } diff --git a/example_projects/macOS/MCPTestUITests/MCPTestUITests.swift b/example_projects/macOS/MCPTestUITests/MCPTestUITests.swift deleted file mode 100644 index ecc9df3e..00000000 --- a/example_projects/macOS/MCPTestUITests/MCPTestUITests.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// MCPTestUITests.swift -// MCPTestUITests -// -// Created by Cameron on 16/02/2025. -// - -import XCTest - -final class MCPTestUITests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - - // In UI tests it is usually best to stop immediately when a failure occurs. - continueAfterFailure = false - - // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - @MainActor - func testExample() throws { - // UI tests must launch the application that they test. - let app = XCUIApplication() - app.launch() - - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - @MainActor - func testLaunchPerformance() throws { - if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { - // This measures how long it takes to launch your application. - measure(metrics: [XCTApplicationLaunchMetric()]) { - XCUIApplication().launch() - } - } - } -} diff --git a/example_projects/macOS/MCPTestUITests/MCPTestUITestsLaunchTests.swift b/example_projects/macOS/MCPTestUITests/MCPTestUITestsLaunchTests.swift deleted file mode 100644 index cfc46046..00000000 --- a/example_projects/macOS/MCPTestUITests/MCPTestUITestsLaunchTests.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// MCPTestUITestsLaunchTests.swift -// MCPTestUITests -// -// Created by Cameron on 16/02/2025. -// - -import XCTest - -final class MCPTestUITestsLaunchTests: XCTestCase { - - override class var runsForEachTargetApplicationUIConfiguration: Bool { - true - } - - override func setUpWithError() throws { - continueAfterFailure = false - } - - @MainActor - func testLaunch() throws { - let app = XCUIApplication() - app.launch() - - // Insert steps here to perform after app launch but before taking a screenshot, - // such as logging into a test account or navigating somewhere in the app - - let attachment = XCTAttachment(screenshot: app.screenshot()) - attachment.name = "Launch Screen" - attachment.lifetime = .keepAlways - add(attachment) - } -} From 42088c75a2d1878af38770f86cd64f01102b0246 Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Wed, 30 Apr 2025 20:47:05 +0100 Subject: [PATCH 3/4] Remove unused imported symbols --- src/tools/test_ios_simulator.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/tools/test_ios_simulator.ts b/src/tools/test_ios_simulator.ts index 66f0152e..b4ae1b5d 100644 --- a/src/tools/test_ios_simulator.ts +++ b/src/tools/test_ios_simulator.ts @@ -13,10 +13,9 @@ */ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { XcodePlatform, executeXcodeCommand } from '../utils/xcode.js'; +import { XcodePlatform } 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, From e3fea04ea2fa07cd6ace72d100128624267b6902 Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Wed, 30 Apr 2025 20:48:07 +0100 Subject: [PATCH 4/4] Run linter and formater --- src/index.ts | 2 +- src/tools/test_ios_simulator.ts | 74 ++++++++++++++++----------------- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/index.ts b/src/index.ts index a12e7815..e990f2c8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -145,7 +145,7 @@ async function main(): Promise { // Register log capture tools registerStartSimulatorLogCaptureTool(server); registerStopAndGetSimulatorLogTool(server); - + // Register test tools registerIOSSimulatorTestTools(server); diff --git a/src/tools/test_ios_simulator.ts b/src/tools/test_ios_simulator.ts index b4ae1b5d..1fa83b12 100644 --- a/src/tools/test_ios_simulator.ts +++ b/src/tools/test_ios_simulator.ts @@ -1,10 +1,10 @@ /** * 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 @@ -17,17 +17,17 @@ import { XcodePlatform } from '../utils/xcode.js'; import { executeXcodeBuild } from '../utils/build-utils.js'; import { log } from '../utils/logger.js'; import { ToolResponse, ToolResponseContent } from '../types/common.js'; -import { +import { registerTool, workspacePathSchema, projectPathSchema, schemeSchema, - configurationSchema, + configurationSchema, derivedDataPathSchema, extraArgsSchema, simulatorNameSchema, simulatorIdSchema, - useLatestOSSchema + useLatestOSSchema, } from './common.js'; // --- internal logic --- @@ -64,25 +64,21 @@ async function _handleIOSSimulatorTestLogic(params: { const raw = buildResult.rawOutput ?? ''; const failures = raw .split('\n') - .filter(l => /Test Case .* failed/.test(l)) - .map(l => { + .filter((l) => /Test Case .* failed/.test(l)) + .map((l) => { const m = l.match(/Test Case '(.*)' failed \((.*)\)/)!; return { testCase: m[1], reason: m[2] }; }); - const summary = failures.length - ? `❌ ${failures.length} test(s) failed` - : '✅ All tests passed'; + const summary = failures.length ? `❌ ${failures.length} test(s) failed` : '✅ All tests passed'; + + const content: ToolResponseContent[] = [{ type: 'text', text: summary }]; - 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')}` + content.push({ + type: 'text', + text: `Test failures:\n${failures.map((f) => `- ${f.testCase}: ${f.reason}`).join('\n')}`, }); } @@ -113,11 +109,12 @@ export function registerIOSSimulatorTestTools(server: McpServer): void { extraArgs: extraArgsSchema, useLatestOS: useLatestOSSchema, }, - (params: any) => _handleIOSSimulatorTestLogic({ - ...params, - configuration: params.configuration || defaults.configuration, - useLatestOS: params.useLatestOS ?? defaults.useLatestOS - }) + (params: any) => + _handleIOSSimulatorTestLogic({ + ...params, + configuration: params.configuration || defaults.configuration, + useLatestOS: params.useLatestOS ?? defaults.useLatestOS, + }), ); // 2) project + name @@ -134,11 +131,12 @@ export function registerIOSSimulatorTestTools(server: McpServer): void { extraArgs: extraArgsSchema, useLatestOS: useLatestOSSchema, }, - (params: any) => _handleIOSSimulatorTestLogic({ - ...params, - configuration: params.configuration || defaults.configuration, - useLatestOS: params.useLatestOS ?? defaults.useLatestOS - }) + (params: any) => + _handleIOSSimulatorTestLogic({ + ...params, + configuration: params.configuration || defaults.configuration, + useLatestOS: params.useLatestOS ?? defaults.useLatestOS, + }), ); // 3) workspace + id @@ -155,11 +153,12 @@ export function registerIOSSimulatorTestTools(server: McpServer): void { extraArgs: extraArgsSchema, useLatestOS: useLatestOSSchema, }, - (params: any) => _handleIOSSimulatorTestLogic({ - ...params, - configuration: params.configuration || defaults.configuration, - useLatestOS: params.useLatestOS ?? defaults.useLatestOS - }) + (params: any) => + _handleIOSSimulatorTestLogic({ + ...params, + configuration: params.configuration || defaults.configuration, + useLatestOS: params.useLatestOS ?? defaults.useLatestOS, + }), ); // 4) project + id @@ -176,10 +175,11 @@ export function registerIOSSimulatorTestTools(server: McpServer): void { extraArgs: extraArgsSchema, useLatestOS: useLatestOSSchema, }, - (params: any) => _handleIOSSimulatorTestLogic({ - ...params, - configuration: params.configuration || defaults.configuration, - useLatestOS: params.useLatestOS ?? defaults.useLatestOS - }) + (params: any) => + _handleIOSSimulatorTestLogic({ + ...params, + configuration: params.configuration || defaults.configuration, + useLatestOS: params.useLatestOS ?? defaults.useLatestOS, + }), ); }