Skip to content

Commit 6d44c3b

Browse files
committed
Add temp-simulator CLI integration tests
1 parent b25db21 commit 6d44c3b

19 files changed

Lines changed: 1697 additions & 305 deletions

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,6 @@ jobs:
3737

3838
- name: Lint, build, and test
3939
run: npm run ci
40+
41+
- name: CLI simulator integration tests
42+
run: npm run test:integration:cli

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ simdeck pasteboard set <udid> "hello"
115115
simdeck pasteboard get <udid>
116116
simdeck screenshot <udid> --output screen.png
117117
simdeck describe-ui <udid>
118+
simdeck describe-ui <udid> --format agent --max-depth 4
118119
simdeck describe-ui <udid> --point 120,240
119120
simdeck tap <udid> 120 240
120121
simdeck tap <udid> --label "Continue" --wait-timeout-ms 5000
@@ -140,8 +141,10 @@ simdeck chrome-profile <udid>
140141
simdeck logs <udid> --seconds 30 --limit 200
141142
```
142143

143-
`describe-ui` uses the built-in private CoreSimulator accessibility bridge and
144-
does not shell out to AXe. Coordinate commands accept screen coordinates from
144+
`describe-ui` can use the running local SimDeck service to prefer NativeScript or
145+
UIKit in-app inspectors, then falls back to the built-in private CoreSimulator
146+
accessibility bridge. Use `--format agent` or `--format compact-json` for
147+
lower-token hierarchy dumps. Coordinate commands accept screen coordinates from
145148
the accessibility tree by default; pass `--normalized` to send `0.0..1.0`
146149
coordinates directly. The CLI intentionally does not implement screenshot-based
147150
video streaming, MJPEG output, or screen recording; the live visual path remains

cli/DFPrivateSimulatorDisplayBridge.m

Lines changed: 8 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1123,7 +1123,7 @@ static BOOL __attribute__((unused)) DFCallSwiftUnitAngleMeasurementGetter(id sel
11231123
return DFCallSwiftUnitAngleMeasurementGetterByFunction(selfObject, function, measurement);
11241124
}
11251125

1126-
static BOOL DFCallSwiftUnitAngleMeasurementGetterByPattern(id selfObject, const char *prefix, const char *suffix, const char *role, DFUnitAngleMeasurement *measurement) {
1126+
static BOOL __attribute__((unused)) DFCallSwiftUnitAngleMeasurementGetterByPattern(id selfObject, const char *prefix, const char *suffix, const char *role, DFUnitAngleMeasurement *measurement) {
11271127
if (selfObject == nil || measurement == NULL) return NO;
11281128
void *function = DFResolveSwiftSymbol(prefix, suffix, role);
11291129
return DFCallSwiftUnitAngleMeasurementGetterByFunction(selfObject, function, measurement);
@@ -1391,7 +1391,7 @@ static BOOL DFSendDeviceOrientationEvent(id device, NSInteger orientationValue)
13911391
return NO;
13921392
}
13931393

1394-
static BOOL DFSetDisplayRotationMeasurement(id object, DFUnitAngleMeasurement measurement, const char *prefix, const char *role) {
1394+
static BOOL __attribute__((unused)) DFSetDisplayRotationMeasurement(id object, DFUnitAngleMeasurement measurement, const char *prefix, const char *role) {
13951395
if (object == nil || prefix == NULL) {
13961396
return NO;
13971397
}
@@ -2735,54 +2735,14 @@ - (BOOL)rotateByDegrees:(double)deltaDegrees error:(NSError * _Nullable __autore
27352735
__block NSError *dispatchError = nil;
27362736

27372737
dispatch_block_t work = ^{
2738-
// Resolve `<View>.deviceRotation` getter/setter ObjC-thunks by stable
2739-
// mangled prefix; the Foundation.Measurement type tail in the middle is
2740-
// what shifts across Xcodes.
2741-
static const char *displayViewPrefix = "$s12SimulatorKit14SimDisplayViewC14deviceRotation";
2742-
static const char *chromePrefix = "$s12SimulatorKit20SimDisplayChromeViewC14deviceRotation";
2743-
static const char *chromeRenderPrefix = "$s12SimulatorKit26SimDisplayChromeRenderViewC14deviceRotation";
2744-
static const char *digitizerPrefix = "$s12SimulatorKit21SimDigitizerInputViewC14deviceRotation";
2745-
static const char *getterSuffix = "vgTj";
2746-
2747-
// Seed the next target rotation from whatever SimulatorKit exposes (if anything).
2748-
// If every getter is missing on this Xcode/macOS build, fall back to our locally
2749-
// tracked ivar so the device still rotates even when we can't read SimulatorKit's
2750-
// internal rotation state.
2738+
// Avoid mutating SimulatorKit/AppKit view rotation directly here. Newer
2739+
// macOS builds can trap if those private views are changed during an
2740+
// AppKit window transaction. The simulator orientation event is the
2741+
// stable control path; display geometry is refreshed by the next frame.
27512742
__block DFUnitAngleMeasurement measurement = { [NSUnitAngle degrees], self->_deviceRotationDegrees };
2752-
__block BOOL readFromSimulatorKit = NO;
27532743
__block BOOL viewsUpdated = NO;
2754-
2755-
DFRunOnMainSync(^{
2756-
DFConfigureDisplayGeometry(self->_displayView, self->_displayPixelSize);
2757-
2758-
id chromeView = self->_displayView != nil
2759-
? object_getIvar(self->_displayView, DFGetIvar(self->_displayView, "chromeView"))
2760-
: nil;
2761-
id chromeRenderView = chromeView != nil
2762-
? object_getIvar(chromeView, DFGetIvar(chromeView, "_renderView"))
2763-
: nil;
2764-
2765-
DFUnitAngleMeasurement readMeasurement = { [NSUnitAngle degrees], 0 };
2766-
if (DFCallSwiftUnitAngleMeasurementGetterByPattern(self->_displayView, displayViewPrefix, getterSuffix, "SimDisplayView.deviceRotation.getter", &readMeasurement) ||
2767-
DFCallSwiftUnitAngleMeasurementGetterByPattern(chromeView, chromePrefix, getterSuffix, "SimDisplayChromeView.deviceRotation.getter", &readMeasurement) ||
2768-
DFCallSwiftUnitAngleMeasurementGetterByPattern(self->_digitizerInputView, digitizerPrefix, getterSuffix, "SimDigitizerInputView.deviceRotation.getter", &readMeasurement)) {
2769-
readFromSimulatorKit = YES;
2770-
measurement = readMeasurement;
2771-
if (measurement.unit == nil) {
2772-
measurement.unit = [NSUnitAngle degrees];
2773-
}
2774-
}
2775-
2776-
measurement.value = DFNormalizedDegrees(measurement.value + deltaDegrees);
2777-
self->_deviceRotationDegrees = measurement.value;
2778-
2779-
if (readFromSimulatorKit) {
2780-
viewsUpdated |= DFSetDisplayRotationMeasurement(self->_displayView, measurement, displayViewPrefix, "SimDisplayView.deviceRotation.setter");
2781-
viewsUpdated |= DFSetDisplayRotationMeasurement(chromeView, measurement, chromePrefix, "SimDisplayChromeView.deviceRotation.setter");
2782-
viewsUpdated |= DFSetDisplayRotationMeasurement(chromeRenderView, measurement, chromeRenderPrefix, "SimDisplayChromeRenderView.deviceRotation.setter");
2783-
viewsUpdated |= DFSetDisplayRotationMeasurement(self->_digitizerInputView, measurement, digitizerPrefix, "SimDigitizerInputView.deviceRotation.setter");
2784-
}
2785-
});
2744+
measurement.value = DFNormalizedDegrees(measurement.value + deltaDegrees);
2745+
self->_deviceRotationDegrees = measurement.value;
27862746

27872747
NSInteger orientationValue = DFOrientationEquivalentValueForMeasurement(measurement);
27882748
BOOL propagatedOrientation = DFSendDeviceOrientationEvent(self->_device, orientationValue);

cli/XCWProcessRunner.m

Lines changed: 72 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -18,28 +18,6 @@ static void XCWCloseFD(int *fd) {
1818
}
1919
}
2020

21-
static NSData *XCWReadAllAndCloseFD(int fd) {
22-
if (fd < 0) {
23-
return [NSData data];
24-
}
25-
26-
NSMutableData *data = [NSMutableData data];
27-
uint8_t buffer[16384];
28-
for (;;) {
29-
ssize_t count = read(fd, buffer, sizeof(buffer));
30-
if (count > 0) {
31-
[data appendBytes:buffer length:(NSUInteger)count];
32-
continue;
33-
}
34-
if (count < 0 && errno == EINTR) {
35-
continue;
36-
}
37-
break;
38-
}
39-
close(fd);
40-
return data;
41-
}
42-
4321
static void XCWWriteAllAndCloseFD(int fd, NSData *data) {
4422
if (fd < 0) {
4523
return;
@@ -68,6 +46,33 @@ static void XCWWriteAllAndCloseFD(int fd, NSData *data) {
6846
userInfo:@{ NSLocalizedDescriptionKey: description ?: @"Process failed." }];
6947
}
7048

49+
static int XCWCreateTemporaryOutputFile(NSString **path, NSError * _Nullable __autoreleasing *error) {
50+
NSString *templatePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"simdeck-process-XXXXXX"];
51+
char *fileTemplate = strdup(templatePath.fileSystemRepresentation);
52+
if (fileTemplate == NULL) {
53+
if (error != NULL) {
54+
*error = XCWProcessRunnerError(6, @"Failed to allocate temporary output path.");
55+
}
56+
return -1;
57+
}
58+
59+
int fd = mkstemp(fileTemplate);
60+
if (fd < 0) {
61+
if (error != NULL) {
62+
*error = XCWProcessRunnerError(7, [NSString stringWithFormat:@"Failed to create temporary output file: %s", strerror(errno)]);
63+
}
64+
free(fileTemplate);
65+
return -1;
66+
}
67+
68+
if (path != NULL) {
69+
*path = [[NSFileManager defaultManager] stringWithFileSystemRepresentation:fileTemplate
70+
length:strlen(fileTemplate)];
71+
}
72+
free(fileTemplate);
73+
return fd;
74+
}
75+
7176
@implementation XCWProcessResult
7277

7378
- (instancetype)initWithTerminationStatus:(int)terminationStatus
@@ -94,51 +99,58 @@ + (XCWProcessResult *)runLaunchPath:(NSString *)launchPath
9499
arguments:(NSArray<NSString *> *)arguments
95100
inputData:(NSData *)inputData
96101
error:(NSError * _Nullable __autoreleasing *)error {
97-
int stdoutPipe[2] = { -1, -1 };
98-
int stderrPipe[2] = { -1, -1 };
102+
int stdoutFD = -1;
103+
int stderrFD = -1;
99104
int stdinPipe[2] = { -1, -1 };
105+
NSString *stdoutPath = nil;
106+
NSString *stderrPath = nil;
100107
posix_spawn_file_actions_t fileActions;
101108
BOOL fileActionsInitialized = NO;
102109
char **argv = NULL;
103110

104-
if (pipe(stdoutPipe) != 0 || pipe(stderrPipe) != 0 || (inputData != nil && pipe(stdinPipe) != 0)) {
111+
NSError *creationError = nil;
112+
stdoutFD = XCWCreateTemporaryOutputFile(&stdoutPath, &creationError);
113+
stderrFD = XCWCreateTemporaryOutputFile(&stderrPath, &creationError);
114+
if (stdoutFD < 0 || stderrFD < 0 || (inputData != nil && pipe(stdinPipe) != 0)) {
105115
if (error != NULL) {
106-
*error = XCWProcessRunnerError(1, [NSString stringWithFormat:@"Failed to create process pipes: %s", strerror(errno)]);
116+
*error = creationError ?: XCWProcessRunnerError(1, [NSString stringWithFormat:@"Failed to create process pipes: %s", strerror(errno)]);
107117
}
108-
XCWCloseFD(&stdoutPipe[0]);
109-
XCWCloseFD(&stdoutPipe[1]);
110-
XCWCloseFD(&stderrPipe[0]);
111-
XCWCloseFD(&stderrPipe[1]);
118+
XCWCloseFD(&stdoutFD);
119+
XCWCloseFD(&stderrFD);
112120
XCWCloseFD(&stdinPipe[0]);
113121
XCWCloseFD(&stdinPipe[1]);
122+
if (stdoutPath != nil) {
123+
[[NSFileManager defaultManager] removeItemAtPath:stdoutPath error:nil];
124+
}
125+
if (stderrPath != nil) {
126+
[[NSFileManager defaultManager] removeItemAtPath:stderrPath error:nil];
127+
}
114128
return nil;
115129
}
116130

117131
if (posix_spawn_file_actions_init(&fileActions) != 0) {
118132
if (error != NULL) {
119133
*error = XCWProcessRunnerError(2, [NSString stringWithFormat:@"Failed to initialize spawn actions: %s", strerror(errno)]);
120134
}
121-
XCWCloseFD(&stdoutPipe[0]);
122-
XCWCloseFD(&stdoutPipe[1]);
123-
XCWCloseFD(&stderrPipe[0]);
124-
XCWCloseFD(&stderrPipe[1]);
135+
XCWCloseFD(&stdoutFD);
136+
XCWCloseFD(&stderrFD);
125137
XCWCloseFD(&stdinPipe[0]);
126138
XCWCloseFD(&stdinPipe[1]);
139+
[[NSFileManager defaultManager] removeItemAtPath:stdoutPath error:nil];
140+
[[NSFileManager defaultManager] removeItemAtPath:stderrPath error:nil];
127141
return nil;
128142
}
129143
fileActionsInitialized = YES;
130144

131-
posix_spawn_file_actions_adddup2(&fileActions, stdoutPipe[1], STDOUT_FILENO);
132-
posix_spawn_file_actions_adddup2(&fileActions, stderrPipe[1], STDERR_FILENO);
145+
posix_spawn_file_actions_adddup2(&fileActions, stdoutFD, STDOUT_FILENO);
146+
posix_spawn_file_actions_adddup2(&fileActions, stderrFD, STDERR_FILENO);
133147
if (inputData != nil) {
134148
posix_spawn_file_actions_adddup2(&fileActions, stdinPipe[0], STDIN_FILENO);
135149
} else {
136150
posix_spawn_file_actions_addopen(&fileActions, STDIN_FILENO, "/dev/null", O_RDONLY, 0);
137151
}
138-
posix_spawn_file_actions_addclose(&fileActions, stdoutPipe[0]);
139-
posix_spawn_file_actions_addclose(&fileActions, stdoutPipe[1]);
140-
posix_spawn_file_actions_addclose(&fileActions, stderrPipe[0]);
141-
posix_spawn_file_actions_addclose(&fileActions, stderrPipe[1]);
152+
posix_spawn_file_actions_addclose(&fileActions, stdoutFD);
153+
posix_spawn_file_actions_addclose(&fileActions, stderrFD);
142154
if (inputData != nil) {
143155
posix_spawn_file_actions_addclose(&fileActions, stdinPipe[0]);
144156
posix_spawn_file_actions_addclose(&fileActions, stdinPipe[1]);
@@ -151,12 +163,12 @@ + (XCWProcessResult *)runLaunchPath:(NSString *)launchPath
151163
*error = XCWProcessRunnerError(3, @"Failed to allocate process arguments.");
152164
}
153165
posix_spawn_file_actions_destroy(&fileActions);
154-
XCWCloseFD(&stdoutPipe[0]);
155-
XCWCloseFD(&stdoutPipe[1]);
156-
XCWCloseFD(&stderrPipe[0]);
157-
XCWCloseFD(&stderrPipe[1]);
166+
XCWCloseFD(&stdoutFD);
167+
XCWCloseFD(&stderrFD);
158168
XCWCloseFD(&stdinPipe[0]);
159169
XCWCloseFD(&stdinPipe[1]);
170+
[[NSFileManager defaultManager] removeItemAtPath:stdoutPath error:nil];
171+
[[NSFileManager defaultManager] removeItemAtPath:stderrPath error:nil];
160172
return nil;
161173
}
162174
argv[0] = (char *)launchPath.fileSystemRepresentation;
@@ -173,39 +185,23 @@ + (XCWProcessResult *)runLaunchPath:(NSString *)launchPath
173185
}
174186
posix_spawn_file_actions_destroy(&fileActions);
175187
free(argv);
176-
XCWCloseFD(&stdoutPipe[0]);
177-
XCWCloseFD(&stdoutPipe[1]);
178-
XCWCloseFD(&stderrPipe[0]);
179-
XCWCloseFD(&stderrPipe[1]);
188+
XCWCloseFD(&stdoutFD);
189+
XCWCloseFD(&stderrFD);
180190
XCWCloseFD(&stdinPipe[0]);
181191
XCWCloseFD(&stdinPipe[1]);
192+
[[NSFileManager defaultManager] removeItemAtPath:stdoutPath error:nil];
193+
[[NSFileManager defaultManager] removeItemAtPath:stderrPath error:nil];
182194
return nil;
183195
}
184196

185-
__block NSData *stdoutData = [NSData data];
186-
__block NSData *stderrData = [NSData data];
187-
dispatch_group_t readGroup = dispatch_group_create();
188-
dispatch_queue_t readQueue = dispatch_get_global_queue(QOS_CLASS_UTILITY, 0);
189-
int stdoutReadFD = stdoutPipe[0];
190-
int stderrReadFD = stderrPipe[0];
191-
stdoutPipe[0] = -1;
192-
stderrPipe[0] = -1;
193-
194-
dispatch_group_async(readGroup, readQueue, ^{
195-
stdoutData = XCWReadAllAndCloseFD(stdoutReadFD);
196-
});
197-
198-
dispatch_group_async(readGroup, readQueue, ^{
199-
stderrData = XCWReadAllAndCloseFD(stderrReadFD);
200-
});
201-
202-
XCWCloseFD(&stdoutPipe[1]);
203-
XCWCloseFD(&stderrPipe[1]);
197+
XCWCloseFD(&stdoutFD);
198+
XCWCloseFD(&stderrFD);
199+
dispatch_group_t writeGroup = inputData != nil ? dispatch_group_create() : nil;
204200
if (inputData != nil) {
205201
XCWCloseFD(&stdinPipe[0]);
206202
int stdinWriteFD = stdinPipe[1];
207203
stdinPipe[1] = -1;
208-
dispatch_group_async(readGroup, readQueue, ^{
204+
dispatch_group_async(writeGroup, dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
209205
XCWWriteAllAndCloseFD(stdinWriteFD, inputData);
210206
});
211207
}
@@ -215,7 +211,9 @@ + (XCWProcessResult *)runLaunchPath:(NSString *)launchPath
215211
do {
216212
waitResult = waitpid(pid, &waitStatus, 0);
217213
} while (waitResult < 0 && errno == EINTR);
218-
dispatch_group_wait(readGroup, DISPATCH_TIME_FOREVER);
214+
if (writeGroup != nil) {
215+
dispatch_group_wait(writeGroup, DISPATCH_TIME_FOREVER);
216+
}
219217
int terminationStatus = 1;
220218
if (waitResult < 0) {
221219
if (error != NULL) {
@@ -232,6 +230,11 @@ + (XCWProcessResult *)runLaunchPath:(NSString *)launchPath
232230
}
233231
free(argv);
234232

233+
NSData *stdoutData = [NSData dataWithContentsOfFile:stdoutPath] ?: [NSData data];
234+
NSData *stderrData = [NSData dataWithContentsOfFile:stderrPath] ?: [NSData data];
235+
[[NSFileManager defaultManager] removeItemAtPath:stdoutPath error:nil];
236+
[[NSFileManager defaultManager] removeItemAtPath:stderrPath error:nil];
237+
235238
return [[XCWProcessResult alloc] initWithTerminationStatus:terminationStatus
236239
stdoutData:stdoutData
237240
stderrData:stderrData];

cli/XCWSimctl.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ - (BOOL)openURL:(NSString *)urlString simulatorUDID:(NSString *)udid error:(NSEr
266266
}
267267

268268
- (BOOL)launchBundleID:(NSString *)bundleID simulatorUDID:(NSString *)udid error:(NSError * _Nullable __autoreleasing *)error {
269-
XCWProcessResult *result = [self.class runSimctl:@[@"launch", udid, bundleID] error:error];
269+
XCWProcessResult *result = [self.class runSimctl:@[@"launch", @"--stdout=/dev/null", @"--stderr=/dev/null", udid, bundleID] error:error];
270270
if (result == nil) {
271271
return NO;
272272
}

cli/native/XCWNativeBridge.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ bool xcw_native_send_key(const char * _Nonnull udid, uint16_t key_code, uint32_t
4949
bool xcw_native_send_key_event(const char * _Nonnull udid, uint16_t key_code, bool down, char * _Nullable * _Nullable error_message);
5050
bool xcw_native_press_home(const char * _Nonnull udid, char * _Nullable * _Nullable error_message);
5151
bool xcw_native_press_button(const char * _Nonnull udid, const char * _Nonnull button_name, uint32_t duration_ms, char * _Nullable * _Nullable error_message);
52+
bool xcw_native_rotate_right(const char * _Nonnull udid, char * _Nullable * _Nullable error_message);
53+
bool xcw_native_rotate_left(const char * _Nonnull udid, char * _Nullable * _Nullable error_message);
5254
bool xcw_native_erase_simulator(const char * _Nonnull udid, char * _Nullable * _Nullable error_message);
5355
bool xcw_native_install_app(const char * _Nonnull udid, const char * _Nonnull app_path, char * _Nullable * _Nullable error_message);
5456
bool xcw_native_uninstall_app(const char * _Nonnull udid, const char * _Nonnull bundle_id, char * _Nullable * _Nullable error_message);

0 commit comments

Comments
 (0)