Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions packages/platform-ios/src/__tests__/xctest-agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ const originalCwd = process.cwd();
const simulatorRuntime = 'com.apple.CoreSimulator.SimRuntime.iOS-26-0';
const simulatorSdkVersion = '26.0';
const xcodeVersion = 'Xcode 26.0\nBuild version 17A123';
const originalExternalXCTestRunFile = process.env.HARNESS_IOS_XCTESTRUN_FILE;
const originalExternalDerivedDataPath =
process.env.HARNESS_IOS_XCTEST_DERIVED_DATA_PATH;

const createLongRunningSubprocess = (options?: {
ignoreSignal?: NodeJS.Signals;
Expand Down Expand Up @@ -230,6 +233,14 @@ describe('xctest-agent orchestration', () => {
});

afterEach(() => {
restoreEnvVar(
'HARNESS_IOS_XCTESTRUN_FILE',
originalExternalXCTestRunFile
);
restoreEnvVar(
'HARNESS_IOS_XCTEST_DERIVED_DATA_PATH',
originalExternalDerivedDataPath
);
rmBuildRoot();
process.chdir(originalCwd);
fs.rmSync(tempProjectRoot, { recursive: true, force: true });
Expand Down Expand Up @@ -614,6 +625,44 @@ describe('xctest-agent orchestration', () => {
expect(mocks.spawn).toHaveBeenCalledTimes(2);
});

it('skips building when an external xctestrun file is provided', async () => {
const externalDerivedDataPath = path.join(tempProjectRoot, 'external-derived');
const externalXCTestRunFilePath = path.join(
tempProjectRoot,
'external.xctestrun'
);

fs.mkdirSync(externalDerivedDataPath, { recursive: true });
fs.writeFileSync(externalXCTestRunFilePath, 'external xctestrun');
process.env.HARNESS_IOS_XCTESTRUN_FILE = externalXCTestRunFilePath;
process.env.HARNESS_IOS_XCTEST_DERIVED_DATA_PATH = externalDerivedDataPath;

const controller = createXCTestAgentController({
port: 49152,
target: {
kind: 'simulator',
id: 'sim-123',
},
});

await controller.prepare();
await controller.ensureStarted();

expect(mocks.spawn).toHaveBeenCalledTimes(1);
expect(mocks.spawn).toHaveBeenNthCalledWith(
1,
'xcodebuild',
expect.arrayContaining([
'test-without-building',
'-xctestrun',
externalXCTestRunFilePath,
'-derivedDataPath',
externalDerivedDataPath,
]),
expect.any(Object)
);
});

it('fails fast when the checked-in xcode project is missing', async () => {
const projectPath = path.join(projectRoot, 'HarnessXCTestAgent.xcodeproj');
const hiddenProjectPath = path.join(
Expand Down Expand Up @@ -666,6 +715,15 @@ const rmBuildRoot = () => {
});
};

const restoreEnvVar = (name: string, value: string | undefined) => {
if (value === undefined) {
delete process.env[name];
return;
}

process.env[name] = value;
};

const getCurrentInputsHash = (): string => {
const hash = createHash('sha256');

Expand Down
68 changes: 64 additions & 4 deletions packages/platform-ios/src/xctest-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ const XCTEST_AGENT_SCHEME_NAME = 'HarnessXCTestAgent';
const XCTEST_AGENT_PORT_ENV = 'HARNESS_XCTEST_AGENT_PORT';
const XCTEST_AGENT_TARGET_BUNDLE_ID_ENV =
'HARNESS_XCTEST_AGENT_TARGET_BUNDLE_ID';
const XCTEST_AGENT_XCTESTRUN_FILE_ENV = 'HARNESS_IOS_XCTESTRUN_FILE';
const XCTEST_AGENT_DERIVED_DATA_PATH_ENV =
'HARNESS_IOS_XCTEST_DERIVED_DATA_PATH';
const XCTEST_AGENT_STARTUP_TIMEOUT_MS = 120_000;
const XCTEST_AGENT_SHUTDOWN_TIMEOUT_MS = 5_000;
const XCTEST_AGENT_STARTUP_POLL_INTERVAL_MS = 250;
Expand Down Expand Up @@ -146,6 +149,34 @@ const getXCTestAgentDerivedDataPath = (
return path.join(getXCTestAgentBuildRoot(projectRoot), destination);
};

const getEnvironmentPath = (name: string): string | null => {
const value = process.env[name]?.trim();

if (!value) {
return null;
}

return value;
};

const getExternalXCTestRunFilePath = (): string | null => {
return getEnvironmentPath(XCTEST_AGENT_XCTESTRUN_FILE_ENV);
};

const getExternalDerivedDataPath = (): string | null => {
return getEnvironmentPath(XCTEST_AGENT_DERIVED_DATA_PATH_ENV);
};

const assertExternalXCTestRunFileExists = (filePath: string) => {
if (fs.existsSync(filePath)) {
return;
}

throw new Error(
`Missing external XCTest run file at ${filePath}. Check ${XCTEST_AGENT_XCTESTRUN_FILE_ENV}.`
);
};

const getXCTestAgentBuildManifestPath = (derivedDataPath: string): string =>
path.join(derivedDataPath, 'build-manifest.json');

Expand Down Expand Up @@ -912,6 +943,24 @@ export const createXCTestAgentController = (options: {
return;
}

const externalXCTestRunFilePath = getExternalXCTestRunFilePath();
if (externalXCTestRunFilePath) {
assertExternalXCTestRunFileExists(externalXCTestRunFilePath);

const externalDerivedDataPath = getExternalDerivedDataPath();
if (externalDerivedDataPath) {
preparedDerivedDataPath = externalDerivedDataPath;
}

prepared = true;
xctestAgentLogger.info(
'Using external XCTest run file for %s target: %s',
target.kind,
externalXCTestRunFilePath
);
return;
}

let signing: XCTestAgentBuildSigning | undefined;

if (target.kind === 'device') {
Expand Down Expand Up @@ -943,12 +992,23 @@ export const createXCTestAgentController = (options: {
target.kind
);
xctestAgentLogger.debug('Using XCTest agent port %d', port);
const externalXCTestRunFilePath = getExternalXCTestRunFilePath();
let xcodebuildRunInputArgs: string[];

if (externalXCTestRunFilePath) {
xcodebuildRunInputArgs = ['-xctestrun', externalXCTestRunFilePath];
} else {
xcodebuildRunInputArgs = [
'-project',
getXCTestAgentProjectFilePath(),
'-scheme',
XCTEST_AGENT_SCHEME_NAME,
];
}

const xcodebuildArgs = [
'test-without-building',
'-project',
getXCTestAgentProjectFilePath(),
'-scheme',
XCTEST_AGENT_SCHEME_NAME,
...xcodebuildRunInputArgs,
'-destination',
getXCTestAgentRunDestination(target),
'-parallel-testing-enabled',
Expand Down
Loading