Skip to main content
Glama
drewster99

xcode-mcp-server (drewster99)

by drewster99
run_project_tests.py14.3 kB
#!/usr/bin/env python3 """run_project_tests tool - Run Xcode project tests""" import os import sys import time import json from typing import Optional, List from xcode_mcp_server.server import mcp from xcode_mcp_server.config_manager import apply_config from xcode_mcp_server.security import validate_and_normalize_project_path from xcode_mcp_server.exceptions import InvalidParameterError from xcode_mcp_server.utils.applescript import ( escape_applescript_string, run_applescript, show_notification, show_result_notification, show_error_notification, show_warning_notification ) from xcode_mcp_server.utils.xcresult import find_xcresult_bundle, extract_test_results_from_xcresult # TODO: Implement selective test execution with xcodebuild # AppleScript's 'test' command doesn't support -only-testing: flags, so we need # to use xcodebuild directly for running specific tests. However, xcodebuild also # requires specifying a run destination (-destination flag), which we need to # extract from Xcode's active run destination before implementing this feature. # # def _get_active_scheme(project_path: str) -> str: # """Get the active scheme for a project using AppleScript""" # escaped_path = escape_applescript_string(project_path) # script = f''' # tell application "Xcode" # open "{escaped_path}" # delay 1 # set workspaceDoc to first workspace document whose path is "{escaped_path}" # set activeScheme to active scheme of workspaceDoc # return name of activeScheme # end tell # ''' # success, output = run_applescript(script) # if success: # return output.strip() # raise InvalidParameterError(f"Could not determine active scheme: {output}") # # # def _run_tests_with_xcodebuild(project_path: str, tests_to_run: List[str], # scheme: Optional[str], max_wait_seconds: int) -> str: # """Run specific tests using xcodebuild (AppleScript doesn't support -only-testing:)""" # # # Determine if this is a workspace or project # is_workspace = project_path.endswith('.xcworkspace') # project_flag = '-workspace' if is_workspace else '-project' # # # Get scheme if not provided # if not scheme: # scheme = _get_active_scheme(project_path) # # # Build xcodebuild command # cmd = [ # 'xcodebuild', # 'test', # project_flag, # project_path, # '-scheme', # scheme # ] # # # Add -only-testing: arguments # for test_id in tests_to_run: # cmd.extend(['-only-testing', test_id]) # # print(f"DEBUG: Running xcodebuild command: {' '.join(cmd)}", file=sys.stderr) # # # Run xcodebuild # if max_wait_seconds == 0: # # Start in background and return immediately # subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) # return "✅ Tests have been started. Use get_latest_test_results to check results later." # # # Run and wait for completion # try: # result = subprocess.run( # cmd, # capture_output=True, # text=True, # timeout=max_wait_seconds # ) # # # Wait a moment for xcresult to be written # time.sleep(2) # # # Get results from xcresult bundle # xcresult_path = find_xcresult_bundle(project_path) # # if xcresult_path: # print(f"DEBUG: Found xcresult bundle at {xcresult_path}", file=sys.stderr) # # try: # xcresult = subprocess.run( # ['xcrun', 'xcresulttool', 'get', 'test-results', 'tests', '--path', xcresult_path], # capture_output=True, # text=True, # timeout=10 # ) # # if xcresult.returncode == 0: # # Parse to show notification # try: # test_data = json.loads(xcresult.stdout) # failure_count = 0 # if 'tests' in test_data and '_values' in test_data['tests']: # for test in test_data['tests']['_values']: # if test.get('testStatus', '') == 'Failure': # failure_count += 1 # # if failure_count == 0: # show_result_notification("All tests PASSED") # else: # show_error_notification(f"{failure_count} test{'s' if failure_count != 1 else ''} FAILED") # except: # pass # # return xcresult.stdout # except Exception as e: # print(f"DEBUG: Exception getting xcresult data: {e}", file=sys.stderr) # # # Fallback to xcodebuild output # if result.returncode == 0: # show_result_notification("Tests PASSED") # return "✅ Tests passed" # else: # show_error_notification("Tests FAILED") # # Return last 50 lines of output # lines = result.stdout.split('\n') # return "❌ Tests failed\n\n" + '\n'.join(lines[-50:]) # # except subprocess.TimeoutExpired: # show_result_notification(f"Tests timeout ({max_wait_seconds}s)") # return f"⏳ Tests did not complete within {max_wait_seconds} seconds" @mcp.tool() @apply_config def run_project_tests(project_path: str, scheme: Optional[str] = None) -> str: """ Run tests for the specified Xcode project or workspace. Tests will run for up to 10 minutes before timing out. This timeout is hardcoded to prevent issues with test runs hanging indefinitely. Args: project_path: Path to Xcode project/workspace directory scheme: Optional scheme to test (uses active scheme if not specified) Returns: JSON with test results if tests complete, otherwise plain text status message. Success format: { "xcresult_path": "...", "summary": {"total_tests": N, "passed": M, "failed": K, "skipped": L}, "failed_tests": [{"test_name": "...", "failure_message": "...", ...}] } Timeout: Plain text message indicating timeout """ # Validate and normalize the project path project_path = validate_and_normalize_project_path(project_path, "run_project_tests") # Show notification show_notification("Drew's Xcode MCP", subtitle=os.path.basename(project_path), message="Running tests") # TODO: Selective test execution - commented out until we can get active run destination # # Handle various forms of empty/invalid tests_to_run parameter # # This works around MCP client issues with optional list parameters # if tests_to_run is not None: # # Handle string inputs that might come from the client # if isinstance(tests_to_run, str): # tests_to_run = tests_to_run.strip() # if not tests_to_run or tests_to_run in ['[]', 'null', 'undefined', '']: # tests_to_run = None # else: # # Try to parse as a comma-separated list # tests_to_run = [t.strip() for t in tests_to_run.split(',') if t.strip()] # elif not tests_to_run: # Empty list or other falsy value # tests_to_run = None # # # If specific tests are requested, use xcodebuild (AppleScript doesn't support -only-testing:) # if tests_to_run: # return _run_tests_with_xcodebuild(project_path, tests_to_run, scheme, max_wait_seconds) # For running all tests, use AppleScript escaped_path = escape_applescript_string(project_path) test_command = 'test workspaceDoc' # Build the wait section with 10 minute (600 second) timeout wait_section = f'''set waitTime to 0 repeat while waitTime < 600 if completed of testResult is true then exit repeat end if delay 1 set waitTime to waitTime + 1 end repeat -- Get results set testStatus to status of testResult as string set testCompleted to completed of testResult -- Get failures if any with full details set failureMessages to "" set failureCount to 0 try set failures to test failures of testResult set failureCount to count of failures if failureCount > 0 then repeat with failure in failures set failureMsg to "" set failurePath to "" set failureLine to "" try set failureMsg to message of failure on error set failureMsg to "Unknown test failure" end try try set failurePath to file path of failure end try try set failureLine to starting line number of failure as string end try set failureMessages to failureMessages & "FAILURE: " & failureMsg & "\\n" if failurePath is not "" and failurePath is not missing value then set failureMessages to failureMessages & "FILE: " & failurePath & "\\n" end if if failureLine is not "" and failureLine is not "missing value" then set failureMessages to failureMessages & "LINE: " & failureLine & "\\n" end if set failureMessages to failureMessages & "---\\n" end repeat else -- No test failures in collection, but status might still be failed -- This happens when tests fail but the failures collection is empty -- We'll parse the build log later to extract actual failure details if testStatus is "failed" or testStatus contains "fail" then set failureMessages to "PARSE_FROM_LOG" & "\\n" end if end if on error errMsg -- Could not access test failures if testStatus is "failed" or testStatus contains "fail" then set failureMessages to "PARSE_FROM_LOG" & "\\n" end if end try -- Get build log for statistics set buildLog to "" try set buildLog to build log of testResult end try return "Status: " & testStatus & "\\n" & ¬ "Completed: " & testCompleted & "\\n" & ¬ "FailureCount: " & (failureCount as string) & "\\n" & ¬ "Failures:\\n" & failureMessages & "\\n" & ¬ "---LOG---\\n" & buildLog''' script = f''' set projectPath to "{escaped_path}" tell application "Xcode" -- Wait for any modal dialogs to be dismissed delay 0.5 -- Open and get the workspace document open projectPath delay 2 -- Get the workspace document set workspaceDoc to first workspace document whose path is projectPath -- Wait for workspace to load set loadWaitTime to 0 repeat while loadWaitTime < 60 if loaded of workspaceDoc is true then exit repeat end if delay 0.5 set loadWaitTime to loadWaitTime + 0.5 end repeat if loaded of workspaceDoc is false then error "Workspace failed to load within timeout" end if -- Set scheme if specified {f'set active scheme of workspaceDoc to scheme "{escape_applescript_string(scheme)}" of workspaceDoc' if scheme else ''} -- Start the test set testResult to {test_command} -- Wait for completion (up to 10 minutes) {wait_section} end tell ''' success, output = run_applescript(script) if not success: show_error_notification("Failed to run tests", os.path.basename(project_path)) return f"Failed to run tests: {output}" # Debug: Log raw output to see what we're getting if os.environ.get('XCODE_MCP_DEBUG'): print(f"DEBUG: Raw test output:\n{output}\n", file=sys.stderr) # Parse the AppleScript output to get test status lines = output.split('\n') status = "" completed = False for line in lines: if line.startswith("Status: "): status = line.replace("Status: ", "").strip() elif line.startswith("Completed: "): completed = line.replace("Completed: ", "").strip().lower() == "true" # Format the output output_lines = [] if not completed: output_lines.append(f"⏳ Tests did not complete within 10 minutes") output_lines.append(f"Status: {status}") show_warning_notification(f"Tests timeout (10 min)") return '\n'.join(output_lines) # If tests completed, get detailed results from xcresult # Wait a moment for xcresult to be written time.sleep(2) xcresult_path = find_xcresult_bundle(project_path) if xcresult_path: print(f"DEBUG: Found xcresult bundle at {xcresult_path}", file=sys.stderr) # Extract and parse test results success, test_results = extract_test_results_from_xcresult(xcresult_path) if success: # Parse JSON to show notification try: result_data = json.loads(test_results) summary = result_data.get('summary', {}) failed = summary.get('failed', 0) if failed == 0: show_result_notification("All tests PASSED") else: show_error_notification(f"{failed} test{'s' if failed != 1 else ''} FAILED") except: pass # Return the parsed JSON return test_results else: print(f"DEBUG: Failed to parse xcresult data: {test_results}", file=sys.stderr) # Fallback if we couldn't get xcresult data print(f"DEBUG: No xcresult bundle found for {project_path}", file=sys.stderr) if status == "succeeded": show_result_notification("All tests PASSED") return "✅ All tests passed" elif status == "failed": show_error_notification("Tests FAILED") return "❌ Tests failed\n\nNo detailed test results available - xcresult bundle not found" else: show_warning_notification(f"Tests: {status}") return f"Test run status: {status}"

Latest Blog Posts

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/drewster99/xcode-mcp-server'

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