Skip to content

Commit 5747dda

Browse files
hannojgV3RON
andauthored
feat: allow for external xctest file (#116)
## Description <!-- Provide a general summary of your changes --> Harness default behaviour on iOS is to build and run an xctest project. This allows injecting an internal build, which implies to also skip doing a build on its own. ## Related Issue <!--- This project only accepts pull requests related to open issues --> <!--- If suggesting a new feature or change, please discuss it in an issue first --> <!--- If fixing a bug, there should be an issue describing it with steps to reproduce --> <!--- Please link to the issue here: --> / ## Context <!-- Why this feature was implemented in this particular way? --> <!-- Is there anything reviewer needs to know before conducting code review? --> In environments such as AWS we can't sign on the host runner. So, we have to build beforehand, then upload the XCTest to AWS which will then resign it internally. This means we need a way to inject an externally build and signed xctest. ## Testing <!-- Please describe how you tested your changes --> --------- Co-authored-by: Szymon Chmal <szymon@chmal.it>
1 parent 7589bc7 commit 5747dda

3 files changed

Lines changed: 146 additions & 4 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
__default__: patch
3+
---
4+
5+
Harness on iOS can now start from an externally prepared `.xctestrun` and derived data directory, so you can run XCTest-based flows on hosted device infrastructure without rebuilding the agent on the runner.

packages/platform-ios/src/__tests__/xctest-agent.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ const originalCwd = process.cwd();
7575
const simulatorRuntime = 'com.apple.CoreSimulator.SimRuntime.iOS-26-0';
7676
const simulatorSdkVersion = '26.0';
7777
const xcodeVersion = 'Xcode 26.0\nBuild version 17A123';
78+
const originalExternalXCTestRunFile = process.env.HARNESS_IOS_XCTESTRUN_FILE;
79+
const originalExternalDerivedDataPath =
80+
process.env.HARNESS_IOS_XCTEST_DERIVED_DATA_PATH;
7881

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

232235
afterEach(() => {
236+
restoreEnvVar(
237+
'HARNESS_IOS_XCTESTRUN_FILE',
238+
originalExternalXCTestRunFile
239+
);
240+
restoreEnvVar(
241+
'HARNESS_IOS_XCTEST_DERIVED_DATA_PATH',
242+
originalExternalDerivedDataPath
243+
);
233244
rmBuildRoot();
234245
process.chdir(originalCwd);
235246
fs.rmSync(tempProjectRoot, { recursive: true, force: true });
@@ -614,6 +625,44 @@ describe('xctest-agent orchestration', () => {
614625
expect(mocks.spawn).toHaveBeenCalledTimes(2);
615626
});
616627

628+
it('skips building when an external xctestrun file is provided', async () => {
629+
const externalDerivedDataPath = path.join(tempProjectRoot, 'external-derived');
630+
const externalXCTestRunFilePath = path.join(
631+
tempProjectRoot,
632+
'external.xctestrun'
633+
);
634+
635+
fs.mkdirSync(externalDerivedDataPath, { recursive: true });
636+
fs.writeFileSync(externalXCTestRunFilePath, 'external xctestrun');
637+
process.env.HARNESS_IOS_XCTESTRUN_FILE = externalXCTestRunFilePath;
638+
process.env.HARNESS_IOS_XCTEST_DERIVED_DATA_PATH = externalDerivedDataPath;
639+
640+
const controller = createXCTestAgentController({
641+
port: 49152,
642+
target: {
643+
kind: 'simulator',
644+
id: 'sim-123',
645+
},
646+
});
647+
648+
await controller.prepare();
649+
await controller.ensureStarted();
650+
651+
expect(mocks.spawn).toHaveBeenCalledTimes(1);
652+
expect(mocks.spawn).toHaveBeenNthCalledWith(
653+
1,
654+
'xcodebuild',
655+
expect.arrayContaining([
656+
'test-without-building',
657+
'-xctestrun',
658+
externalXCTestRunFilePath,
659+
'-derivedDataPath',
660+
externalDerivedDataPath,
661+
]),
662+
expect.any(Object)
663+
);
664+
});
665+
617666
it('fails fast when the checked-in xcode project is missing', async () => {
618667
const projectPath = path.join(projectRoot, 'HarnessXCTestAgent.xcodeproj');
619668
const hiddenProjectPath = path.join(
@@ -666,6 +715,15 @@ const rmBuildRoot = () => {
666715
});
667716
};
668717

718+
const restoreEnvVar = (name: string, value: string | undefined) => {
719+
if (value === undefined) {
720+
delete process.env[name];
721+
return;
722+
}
723+
724+
process.env[name] = value;
725+
};
726+
669727
const getCurrentInputsHash = (): string => {
670728
const hash = createHash('sha256');
671729

packages/platform-ios/src/xctest-agent.ts

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ const XCTEST_AGENT_SCHEME_NAME = 'HarnessXCTestAgent';
2727
const XCTEST_AGENT_PORT_ENV = 'HARNESS_XCTEST_AGENT_PORT';
2828
const XCTEST_AGENT_TARGET_BUNDLE_ID_ENV =
2929
'HARNESS_XCTEST_AGENT_TARGET_BUNDLE_ID';
30+
const XCTEST_AGENT_XCTESTRUN_FILE_ENV = 'HARNESS_IOS_XCTESTRUN_FILE';
31+
const XCTEST_AGENT_DERIVED_DATA_PATH_ENV =
32+
'HARNESS_IOS_XCTEST_DERIVED_DATA_PATH';
3033
const XCTEST_AGENT_STARTUP_TIMEOUT_MS = 120_000;
3134
const XCTEST_AGENT_SHUTDOWN_TIMEOUT_MS = 5_000;
3235
const XCTEST_AGENT_STARTUP_POLL_INTERVAL_MS = 250;
@@ -101,6 +104,11 @@ type SimulatorXCTestAgentCacheContext = Omit<
101104
'artifactName' | 'destinationKind' | 'schemaVersion' | 'xctestrunRelativePath'
102105
>;
103106

107+
type ExternalXCTestConfiguration = {
108+
derivedDataPath: string;
109+
xctestrunFilePath: string;
110+
};
111+
104112
export type XCTestAgentController = {
105113
prepare: () => Promise<void>;
106114
ensureStarted: () => Promise<void>;
@@ -146,6 +154,51 @@ const getXCTestAgentDerivedDataPath = (
146154
return path.join(getXCTestAgentBuildRoot(projectRoot), destination);
147155
};
148156

157+
const getEnvironmentPath = (name: string): string | null => {
158+
const value = process.env[name]?.trim();
159+
160+
if (!value) {
161+
return null;
162+
}
163+
164+
return value;
165+
};
166+
167+
const getExternalXCTestRunFilePath = (): string | null => {
168+
return getEnvironmentPath(XCTEST_AGENT_XCTESTRUN_FILE_ENV);
169+
};
170+
171+
const getExternalDerivedDataPath = (): string | null => {
172+
return getEnvironmentPath(XCTEST_AGENT_DERIVED_DATA_PATH_ENV);
173+
};
174+
175+
const assertExternalXCTestRunFileExists = (filePath: string) => {
176+
if (fs.existsSync(filePath)) {
177+
return;
178+
}
179+
180+
throw new Error(
181+
`Missing external XCTest run file at ${filePath}. Check ${XCTEST_AGENT_XCTESTRUN_FILE_ENV}.`
182+
);
183+
};
184+
185+
const getExternalXCTestConfiguration = (
186+
fallbackDerivedDataPath: string
187+
): ExternalXCTestConfiguration | null => {
188+
const xctestrunFilePath = getExternalXCTestRunFilePath();
189+
190+
if (!xctestrunFilePath) {
191+
return null;
192+
}
193+
194+
assertExternalXCTestRunFileExists(xctestrunFilePath);
195+
196+
return {
197+
derivedDataPath: getExternalDerivedDataPath() ?? fallbackDerivedDataPath,
198+
xctestrunFilePath,
199+
};
200+
};
201+
149202
const getXCTestAgentBuildManifestPath = (derivedDataPath: string): string =>
150203
path.join(derivedDataPath, 'build-manifest.json');
151204

@@ -877,6 +930,7 @@ export const createXCTestAgentController = (options: {
877930
'xcodebuild.log'
878931
);
879932
let preparedDerivedDataPath = getXCTestAgentDerivedDataPath(target.kind);
933+
let preparedXCTestRunFilePath: string | null = null;
880934
let prepared = false;
881935
let agentProcess: Subprocess | null = null;
882936
let agentClient: ReturnType<typeof createXCTestAgentClient> | null = null;
@@ -912,6 +966,24 @@ export const createXCTestAgentController = (options: {
912966
return;
913967
}
914968

969+
const externalXCTestConfiguration = getExternalXCTestConfiguration(
970+
preparedDerivedDataPath
971+
);
972+
973+
if (externalXCTestConfiguration) {
974+
preparedDerivedDataPath = externalXCTestConfiguration.derivedDataPath;
975+
preparedXCTestRunFilePath =
976+
externalXCTestConfiguration.xctestrunFilePath;
977+
978+
prepared = true;
979+
xctestAgentLogger.info(
980+
'Using external XCTest run file for %s target: %s',
981+
target.kind,
982+
preparedXCTestRunFilePath
983+
);
984+
return;
985+
}
986+
915987
let signing: XCTestAgentBuildSigning | undefined;
916988

917989
if (target.kind === 'device') {
@@ -924,6 +996,7 @@ export const createXCTestAgentController = (options: {
924996
});
925997

926998
preparedDerivedDataPath = buildResult.derivedDataPath;
999+
preparedXCTestRunFilePath = null;
9271000
prepared = true;
9281001
};
9291002

@@ -943,12 +1016,18 @@ export const createXCTestAgentController = (options: {
9431016
target.kind
9441017
);
9451018
xctestAgentLogger.debug('Using XCTest agent port %d', port);
1019+
const xcodebuildRunInputArgs = preparedXCTestRunFilePath
1020+
? ['-xctestrun', preparedXCTestRunFilePath]
1021+
: [
1022+
'-project',
1023+
getXCTestAgentProjectFilePath(),
1024+
'-scheme',
1025+
XCTEST_AGENT_SCHEME_NAME,
1026+
];
1027+
9461028
const xcodebuildArgs = [
9471029
'test-without-building',
948-
'-project',
949-
getXCTestAgentProjectFilePath(),
950-
'-scheme',
951-
XCTEST_AGENT_SCHEME_NAME,
1030+
...xcodebuildRunInputArgs,
9521031
'-destination',
9531032
getXCTestAgentRunDestination(target),
9541033
'-parallel-testing-enabled',

0 commit comments

Comments
 (0)