Skip to main content
Glama

XcodeBuildMCP

test-common.ts8.34 kB
/** * Common Test Utilities - Shared logic for test tools * * This module provides shared functionality for all test-related tools across different platforms. * It includes common test execution logic, xcresult parsing, and utility functions used by * platform-specific test tools. * * Responsibilities: * - Parsing xcresult bundles into human-readable format * - Shared test execution logic with platform-specific handling * - Common error handling and cleanup for test operations * - Temporary directory management for xcresult files */ import { promisify } from 'util'; import { exec } from 'child_process'; import { mkdtemp, rm } from 'fs/promises'; import { tmpdir } from 'os'; import { join } from 'path'; import { log } from './logger.ts'; import { XcodePlatform } from './xcode.ts'; import { executeXcodeBuildCommand } from './build/index.ts'; import { createTextResponse, consolidateContentForClaudeCode } from './validation.ts'; import { normalizeTestRunnerEnv } from './environment.ts'; import { ToolResponse } from '../types/common.ts'; import { CommandExecutor, CommandExecOptions, getDefaultCommandExecutor } from './command.ts'; /** * Type definition for test summary structure from xcresulttool */ interface TestSummary { title?: string; result?: string; totalTestCount?: number; passedTests?: number; failedTests?: number; skippedTests?: number; expectedFailures?: number; environmentDescription?: string; devicesAndConfigurations?: Array<{ device?: { deviceName?: string; platform?: string; osVersion?: string; }; }>; testFailures?: Array<{ testName?: string; targetName?: string; failureText?: string; }>; topInsights?: Array<{ impact?: string; text?: string; }>; } /** * Parse xcresult bundle using xcrun xcresulttool */ export async function parseXcresultBundle(resultBundlePath: string): Promise<string> { try { const execAsync = promisify(exec); const { stdout } = await execAsync( `xcrun xcresulttool get test-results summary --path "${resultBundlePath}"`, ); // Parse JSON response and format as human-readable const summary = JSON.parse(stdout) as TestSummary; return formatTestSummary(summary); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); log('error', `Error parsing xcresult bundle: ${errorMessage}`); throw error; } } /** * Format test summary JSON into human-readable text */ function formatTestSummary(summary: TestSummary): string { const lines: string[] = []; lines.push(`Test Summary: ${summary.title ?? 'Unknown'}`); lines.push(`Overall Result: ${summary.result ?? 'Unknown'}`); lines.push(''); lines.push('Test Counts:'); lines.push(` Total: ${summary.totalTestCount ?? 0}`); lines.push(` Passed: ${summary.passedTests ?? 0}`); lines.push(` Failed: ${summary.failedTests ?? 0}`); lines.push(` Skipped: ${summary.skippedTests ?? 0}`); lines.push(` Expected Failures: ${summary.expectedFailures ?? 0}`); lines.push(''); if (summary.environmentDescription) { lines.push(`Environment: ${summary.environmentDescription}`); lines.push(''); } if ( summary.devicesAndConfigurations && Array.isArray(summary.devicesAndConfigurations) && summary.devicesAndConfigurations.length > 0 ) { const device = summary.devicesAndConfigurations[0].device; if (device) { lines.push( `Device: ${device.deviceName ?? 'Unknown'} (${device.platform ?? 'Unknown'} ${device.osVersion ?? 'Unknown'})`, ); lines.push(''); } } if ( summary.testFailures && Array.isArray(summary.testFailures) && summary.testFailures.length > 0 ) { lines.push('Test Failures:'); summary.testFailures.forEach((failure, index: number) => { lines.push( ` ${index + 1}. ${failure.testName ?? 'Unknown Test'} (${failure.targetName ?? 'Unknown Target'})`, ); if (failure.failureText) { lines.push(` ${failure.failureText}`); } }); lines.push(''); } if (summary.topInsights && Array.isArray(summary.topInsights) && summary.topInsights.length > 0) { lines.push('Insights:'); summary.topInsights.forEach((insight, index: number) => { lines.push( ` ${index + 1}. [${insight.impact ?? 'Unknown'}] ${insight.text ?? 'No description'}`, ); }); } return lines.join('\n'); } /** * Internal logic for running tests with platform-specific handling */ export async function handleTestLogic( params: { workspacePath?: string; projectPath?: string; scheme: string; configuration: string; simulatorName?: string; simulatorId?: string; deviceId?: string; useLatestOS?: boolean; derivedDataPath?: string; extraArgs?: string[]; preferXcodebuild?: boolean; platform: XcodePlatform; testRunnerEnv?: Record<string, string>; }, executor?: CommandExecutor, ): Promise<ToolResponse> { log( 'info', `Starting test run for scheme ${params.scheme} on platform ${params.platform} (internal)`, ); try { // Create temporary directory for xcresult bundle const tempDir = await mkdtemp(join(tmpdir(), 'xcodebuild-test-')); const resultBundlePath = join(tempDir, 'TestResults.xcresult'); // Add resultBundlePath to extraArgs const extraArgs = [...(params.extraArgs ?? []), `-resultBundlePath`, resultBundlePath]; // Prepare execution options with TEST_RUNNER_ environment variables const execOpts: CommandExecOptions | undefined = params.testRunnerEnv ? { env: normalizeTestRunnerEnv(params.testRunnerEnv) } : undefined; // Run the test command const testResult = await executeXcodeBuildCommand( { ...params, extraArgs, }, { platform: params.platform, simulatorName: params.simulatorName, simulatorId: params.simulatorId, deviceId: params.deviceId, useLatestOS: params.useLatestOS, logPrefix: 'Test Run', }, params.preferXcodebuild, 'test', executor ?? getDefaultCommandExecutor(), execOpts, ); // Parse xcresult bundle if it exists, regardless of whether tests passed or failed // Test failures are expected and should not prevent xcresult parsing try { log('info', `Attempting to parse xcresult bundle at: ${resultBundlePath}`); // Check if the file exists try { const { stat } = await import('fs/promises'); await stat(resultBundlePath); log('info', `xcresult bundle exists at: ${resultBundlePath}`); } catch { log('warn', `xcresult bundle does not exist at: ${resultBundlePath}`); throw new Error(`xcresult bundle not found at ${resultBundlePath}`); } const testSummary = await parseXcresultBundle(resultBundlePath); log('info', 'Successfully parsed xcresult bundle'); // Clean up temporary directory await rm(tempDir, { recursive: true, force: true }); // Return combined result - preserve isError from testResult (test failures should be marked as errors) const combinedResponse: ToolResponse = { content: [ ...(testResult.content || []), { type: 'text', text: '\nTest Results Summary:\n' + testSummary, }, ], isError: testResult.isError, }; // Apply Claude Code workaround if enabled return consolidateContentForClaudeCode(combinedResponse); } catch (parseError) { // If parsing fails, return original test result log('warn', `Failed to parse xcresult bundle: ${parseError}`); // Clean up temporary directory even if parsing fails try { await rm(tempDir, { recursive: true, force: true }); } catch (cleanupError) { log('warn', `Failed to clean up temporary directory: ${cleanupError}`); } return consolidateContentForClaudeCode(testResult); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); log('error', `Error during test run: ${errorMessage}`); return consolidateContentForClaudeCode( createTextResponse(`Error during test run: ${errorMessage}`, true), ); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cameroncooke/XcodeBuildMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server