diff --git a/src/utils/__tests__/test-common.test.ts b/src/utils/__tests__/test-common.test.ts
index 991cb85a..a95a552d 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 8dab5285..123c9169 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 2287cf1c..c061fd45 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) {