Skip to content

Commit b6b4c3d

Browse files
authored
feat(testmanagerd): add XCTestConfiguration encoder for XCTest session setup (#151)
1 parent 40c7477 commit b6b4c3d

4 files changed

Lines changed: 584 additions & 30 deletions

File tree

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ export type {
7777
} from './lib/types.js';
7878
export { PowerAssertionType } from './lib/types.js';
7979
export { NetworkMessageType } from './services/ios/dvt/instruments/network-monitor.js';
80+
export { XCTestConfigurationEncoder } from './services/ios/testmanagerd/xctestconfiguration.js';
81+
export type { XCTestConfigurationParams } from './services/ios/testmanagerd/xctestconfiguration.js';
8082
export {
8183
STRONGBOX_CONTAINER_NAME,
8284
createUsbmux,
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import crypto from 'node:crypto';
2+
3+
import { PlistUID } from '../../../lib/plist/index.js';
4+
import { TestmanagerdEncoder } from './testmanagerd-encoder.js';
5+
6+
/**
7+
* Extended encoder that supports XCTestConfiguration-specific types:
8+
* NSURL and XCTestConfiguration itself.
9+
*
10+
* Inherits NSUUID and XCTCapabilities support from TestmanagerdEncoder.
11+
*/
12+
export class XCTestConfigurationEncoder extends TestmanagerdEncoder {
13+
/**
14+
* Encode an XCTestConfiguration into NSKeyedArchiver format
15+
*/
16+
encodeXCTestConfiguration(config: XCTestConfigurationParams): any {
17+
const configObj = this.buildConfigObject(config);
18+
return this.encode(configObj);
19+
}
20+
21+
protected override archiveObject(value: any): number {
22+
if (value === null || value === undefined) {
23+
return 0;
24+
}
25+
26+
if (value && typeof value === 'object') {
27+
if (value.__type === 'NSURL') {
28+
return this.archiveNSURL(value.base, value.relative);
29+
}
30+
if (value.__type === 'XCTestConfiguration') {
31+
return this.archiveXCTestConfiguration(value.fields);
32+
}
33+
}
34+
35+
return super.archiveObject(value);
36+
}
37+
38+
private archiveNSURL(base: string | null, relative: string): number {
39+
const index = this.objects.length;
40+
this.objects.push(null); // Placeholder
41+
42+
const baseIndex = base ? this.archiveObject(base) : 0;
43+
const relativeIndex = this.archiveObject(relative);
44+
45+
const classUid = this.getClassUid('NSURL', 'NSObject');
46+
47+
this.objects[index] = {
48+
'NS.base': new PlistUID(baseIndex),
49+
'NS.relative': new PlistUID(relativeIndex),
50+
$class: new PlistUID(classUid),
51+
};
52+
53+
return index;
54+
}
55+
56+
private archiveXCTestConfiguration(fields: Record<string, any>): number {
57+
const index = this.objects.length;
58+
this.objects.push(null); // Placeholder
59+
60+
const archivedFields: Record<string, any> = {};
61+
for (const [key, value] of Object.entries(fields)) {
62+
if (value === undefined) {
63+
continue;
64+
}
65+
if (value === null) {
66+
// Null values must be encoded as $null references (index 0).
67+
// NSKeyedUnarchiver expects all keys to be present.
68+
archivedFields[key] = new PlistUID(0);
69+
} else if (typeof value === 'boolean' || typeof value === 'number') {
70+
// Booleans and numbers are the ONLY types stored inline.
71+
archivedFields[key] = value;
72+
} else if (value instanceof PlistUID) {
73+
// PlistUID values (e.g. formatVersion = UID(2)) must be stored as
74+
// separate $objects entries. In bpylist2, plistlib.UID is a
75+
// "primitive_type" (not inline) — archive() adds it to $objects
76+
// and returns a UID reference. If we inline UID(2), NSKeyedUnarchiver
77+
// would dereference it to $objects[2] instead of treating it as the
78+
// raw UID value 2.
79+
const uidIndex = this.objects.length;
80+
this.objects.push(value);
81+
archivedFields[key] = new PlistUID(uidIndex);
82+
} else {
83+
// All other types (strings, buffers, objects, arrays, custom markers)
84+
// are archived as separate $objects entries and referenced by PlistUID.
85+
archivedFields[key] = new PlistUID(this.archiveObject(value));
86+
}
87+
}
88+
89+
const classUid = this.getClassUid('XCTestConfiguration', 'NSObject');
90+
91+
this.objects[index] = {
92+
...archivedFields,
93+
$class: new PlistUID(classUid),
94+
};
95+
96+
return index;
97+
}
98+
99+
private buildConfigObject(
100+
config: XCTestConfigurationParams,
101+
): Record<string, any> {
102+
const sessionId = config.sessionIdentifier || crypto.randomUUID();
103+
104+
return {
105+
__type: 'XCTestConfiguration',
106+
fields: {
107+
testBundleURL: {
108+
__type: 'NSURL',
109+
base: null,
110+
relative: config.testBundleURL,
111+
},
112+
sessionIdentifier: {
113+
__type: 'NSUUID',
114+
uuid: sessionId,
115+
},
116+
// formatVersion MUST be a PlistUID, not a plain integer
117+
formatVersion: new PlistUID(2),
118+
treatMissingBaselinesAsFailures:
119+
config.treatMissingBaselinesAsFailures ?? false,
120+
targetApplicationBundleID: config.targetApplicationBundleID || null,
121+
targetApplicationPath:
122+
config.targetApplicationPath || '/tmp/XCTestTargetApp.app',
123+
reportResultsToIDE: config.reportResultsToIDE ?? true,
124+
automationFrameworkPath:
125+
config.automationFrameworkPath ||
126+
'/Developer/Library/PrivateFrameworks/XCTAutomationSupport.framework',
127+
testsMustRunOnMainThread: config.testsMustRunOnMainThread ?? true,
128+
initializeForUITesting: config.initializeForUITesting ?? true,
129+
reportActivities: config.reportActivities ?? true,
130+
testsToSkip: config.testsToSkip || null,
131+
testsToRun: config.testsToRun || null,
132+
productModuleName: config.productModuleName || null,
133+
testBundleRelativePath: config.testBundleRelativePath || null,
134+
aggregateStatisticsBeforeCrash: {
135+
XCSuiteRecordsKey: {},
136+
},
137+
baselineFileRelativePath: null,
138+
baselineFileURL: null,
139+
defaultTestExecutionTimeAllowance: null,
140+
disablePerformanceMetrics: false,
141+
emitOSLogs: false,
142+
gatherLocalizableStringsData: false,
143+
maximumTestExecutionTimeAllowance: null,
144+
randomExecutionOrderingSeed: null,
145+
systemAttachmentLifetime: 2,
146+
targetApplicationArguments: config.targetApplicationArguments ?? [],
147+
targetApplicationEnvironment:
148+
config.targetApplicationEnvironment ?? null,
149+
testApplicationDependencies: {},
150+
testApplicationUserOverrides: null,
151+
testExecutionOrdering: 0,
152+
testTimeoutsEnabled: false,
153+
testsDrivenByIDE: false,
154+
userAttachmentLifetime: 1,
155+
},
156+
};
157+
}
158+
}
159+
160+
/**
161+
* Parameters for creating an XCTestConfiguration
162+
*/
163+
export interface XCTestConfigurationParams {
164+
/** URL to the test bundle (e.g., file:///path/to/Runner.xctest) */
165+
testBundleURL: string;
166+
/** Session identifier UUID string. Auto-generated if not provided. */
167+
sessionIdentifier?: string;
168+
/** Target application bundle ID */
169+
targetApplicationBundleID?: string;
170+
/** Target application path */
171+
targetApplicationPath?: string;
172+
/** Whether to treat missing baselines as failures */
173+
treatMissingBaselinesAsFailures?: boolean;
174+
/** Whether to report results to IDE */
175+
reportResultsToIDE?: boolean;
176+
/** Path to automation framework */
177+
automationFrameworkPath?: string;
178+
/** Whether tests must run on main thread */
179+
testsMustRunOnMainThread?: boolean;
180+
/** Whether to initialize for UI testing */
181+
initializeForUITesting?: boolean;
182+
/** Whether to report activities */
183+
reportActivities?: boolean;
184+
/** Set of tests to skip */
185+
testsToSkip?: string[] | null;
186+
/** Set of tests to run */
187+
testsToRun?: string[] | null;
188+
/** Product module name */
189+
productModuleName?: string | null;
190+
/** Relative path to test bundle */
191+
testBundleRelativePath?: string | null;
192+
/** Arguments to pass to the target application */
193+
targetApplicationArguments?: string[];
194+
/** Environment variables for the target application */
195+
targetApplicationEnvironment?: Record<string, string> | null;
196+
}
197+
198+
export interface NSUUIDMarker {
199+
__type: 'NSUUID';
200+
uuid: string;
201+
}
202+
203+
export interface NSURLMarker {
204+
__type: 'NSURL';
205+
base: string | null;
206+
relative: string;
207+
}
208+
209+
/**
210+
* Helper to create an NSUUID marker object
211+
*/
212+
export function createNSUUID(uuid: string): NSUUIDMarker {
213+
return { __type: 'NSUUID', uuid };
214+
}
215+
216+
/**
217+
* Helper to create an NSURL marker object
218+
*/
219+
export function createNSURL(
220+
relative: string,
221+
base: string | null = null,
222+
): NSURLMarker {
223+
return { __type: 'NSURL', base, relative };
224+
}

0 commit comments

Comments
 (0)