From 54a6684a7bcebb3612c51ee5e9cc092b47851c6f Mon Sep 17 00:00:00 2001 From: Victor Solano Date: Tue, 8 Sep 2026 15:59:26 +0200 Subject: [PATCH] fix(test): support mixed-platform simulator test plans --- src/utils/__tests__/test-common.test.ts | 45 +++++++++++++++++++++++++ src/utils/test-common.ts | 41 ++++++++++++++++------ src/utils/test-products-path.ts | 16 +++++++++ 3 files changed, 92 insertions(+), 10 deletions(-) diff --git a/src/utils/__tests__/test-common.test.ts b/src/utils/__tests__/test-common.test.ts index 991cb85a5..a95a552d0 100644 --- a/src/utils/__tests__/test-common.test.ts +++ b/src/utils/__tests__/test-common.test.ts @@ -197,6 +197,51 @@ describe('createTestExecutor', () => { expect(runTestsIndex).toBeGreaterThan(-1); expect(finalSummaryIndex).toBeGreaterThan(runTestsIndex); }); + it('uses the source test phase for prepared products containing multiple simulator platforms', async () => { + const commands: string[][] = []; + const executor: CommandExecutor = async (command) => { + commands.push(command); + if (command.at(-1) === 'build-for-testing') { + const testProductsIndex = command.indexOf('-testProductsPath'); + const testProductsPath = command[testProductsIndex + 1]!; + mkdirSync(join(testProductsPath, 'Tests', '0'), { recursive: true }); + writeFileSync( + join(testProductsPath, 'Tests', '0', 'Weather.xctestrun'), + `TestConfigurationsTestTargetsDependentProductPaths__TESTROOT__/Debug-iphonesimulator/Weather.app__TESTROOT__/Debug-watchsimulator/WatchTests.xctest`, + ); + } + return createSuccessfulCommandResponse(); + }; + + const executeTest = createTestExecutor(executor, { + preflight: createPreflight(), + toolName: 'test_sim', + target: 'simulator', + request: { + scheme: 'Weather', + projectPath: 'Weather.xcodeproj', + configuration: 'Debug', + platform: XcodePlatform.iOSSimulator, + }, + }); + + await executeTest( + { + projectPath: 'Weather.xcodeproj', + scheme: 'Weather', + configuration: 'Debug', + simulatorId: 'A2C64636-37E9-4B68-B872-E7F0A82A5670', + platform: XcodePlatform.iOSSimulator, + }, + new DefaultStreamingExecutionContext(), + ); + + expect(commands).toHaveLength(2); + expect(commands[1]).toContain('-project'); + expect(commands[1]).toContain('-scheme'); + expect(commands[1]).toContain('-derivedDataPath'); + expect(commands[1]).not.toContain('-testProductsPath'); + }); it('injects a workspace-scoped default result bundle path for macOS test commands', async () => { const commands: string[][] = []; diff --git a/src/utils/test-common.ts b/src/utils/test-common.ts index 8dab52859..123c9169a 100644 --- a/src/utils/test-common.ts +++ b/src/utils/test-common.ts @@ -8,6 +8,7 @@ import * as path from 'node:path'; import { log } from './logger.ts'; import { constructDestinationString, XcodePlatform } from './xcode.ts'; import { executeXcodeBuildCommand } from './build/index.ts'; +import type { BuildCommandResult } from './build/index.ts'; import { extractTestFailuresFromXcresult } from './xcresult-test-failures.ts'; import { normalizeTestRunnerEnv } from './environment.ts'; @@ -23,6 +24,7 @@ import { } from './result-bundle-path.ts'; import { createDefaultTestProductsPath, + hasMultipleSimulatorPlatforms, markTestProductsPathCompleted, } from './test-products-path.ts'; import { resolvePathFromCwd } from './path.ts'; @@ -317,17 +319,36 @@ export function createTestExecutor( message: 'Running tests', }); - let testWithoutBuildingResult: PreparedTestCommandResult; + let testWithoutBuildingResult: PreparedTestCommandResult | BuildCommandResult; try { - testWithoutBuildingResult = await executePreparedTestCommand( - { ...params, testProductsPath }, - filterPreparedTestExtraArgs(executionPlan.testArgs), - resultBundlePath, - executor, - execOpts, - started.pipeline, - getPreparedTestDestinationArgs(executionPlan.testArgs), - ); + const usesMultipleSimulatorPlatforms = + await hasMultipleSimulatorPlatforms(testProductsPath); + testWithoutBuildingResult = usesMultipleSimulatorPlatforms + ? await executeXcodeBuildCommand( + { + ...params, + extraArgs: [ + ...filterPreparedTestExtraArgs(executionPlan.testArgs), + '-resultBundlePath', + resultBundlePath, + ], + }, + platformOptions, + params.preferXcodebuild, + 'test-without-building', + executor, + execOpts, + started.pipeline, + ) + : await executePreparedTestCommand( + { ...params, testProductsPath }, + filterPreparedTestExtraArgs(executionPlan.testArgs), + resultBundlePath, + executor, + execOpts, + started.pipeline, + getPreparedTestDestinationArgs(executionPlan.testArgs), + ); } finally { markTestProductsPathCompleted(testProductsPath); if (shouldUseDefaultResultBundlePath) { diff --git a/src/utils/test-products-path.ts b/src/utils/test-products-path.ts index 2287cf1c2..c061fd458 100644 --- a/src/utils/test-products-path.ts +++ b/src/utils/test-products-path.ts @@ -87,6 +87,22 @@ export async function findXctestrunPaths(testProductsPath: string): Promise left.localeCompare(right)); } +export async function hasMultipleSimulatorPlatforms(testProductsPath: string): Promise { + const xctestrunPaths = await findXctestrunPaths(testProductsPath); + const simulatorPlatforms = new Set(); + + for (const xctestrunPath of xctestrunPaths) { + const contents = await fs.promises.readFile(xctestrunPath, 'utf8'); + for (const match of contents.matchAll(/(?:^|[-/])([A-Za-z]+simulator)(?:[/\\])/gu)) { + simulatorPlatforms.add(match[1]!.toLowerCase()); + } + if (simulatorPlatforms.size > 1) { + return true; + } + } + + return false; +} export function markTestProductsPathCompleted(testProductsPath: string | undefined): void { if (!testProductsPath) {