Skip to content

Commit bc6267a

Browse files
committed
fix(ci): stabilize direct ffi test runs
1 parent 4e9355f commit bc6267a

6 files changed

Lines changed: 123 additions & 31 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,6 @@ jobs:
5252
IOS_BUILD_TIMEOUT_MS: "600000"
5353
IOS_TEST_TIMEOUT_MS: "600000"
5454
IOS_TEST_INACTIVITY_TIMEOUT_MS: "180000"
55+
IOS_TEST_VERBOSE_SPECS: "1"
56+
IOS_SIMCTL_QUERY_TIMEOUT_MS: "10000"
5557
run: npm run test:ios

NativeScript/ffi/shared/jsi/NativeApiJsiBridge.h

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,32 @@ std::string jsifySelector(const char* selector) {
187187
return jsifiedSelector;
188188
}
189189

190+
std::string booleanGetterSelectorForProperty(const std::string& property) {
191+
if (property.empty()) {
192+
return property;
193+
}
194+
195+
std::string selector = "is";
196+
selector += static_cast<char>(toupper(property[0]));
197+
selector += property.substr(1);
198+
return selector;
199+
}
200+
201+
std::optional<std::string> runtimeBooleanGetterSelectorForProperty(
202+
Class cls, bool staticMethod, const std::string& property) {
203+
if (cls == nil || property.empty()) {
204+
return std::nullopt;
205+
}
206+
207+
std::string selectorName = booleanGetterSelectorForProperty(property);
208+
SEL selector = sel_getUid(selectorName.c_str());
209+
if ((!staticMethod && class_getInstanceMethod(cls, selector) != nullptr) ||
210+
(staticMethod && class_getClassMethod(cls, selector) != nullptr)) {
211+
return selectorName;
212+
}
213+
return std::nullopt;
214+
}
215+
190216
std::optional<std::string> runtimeSelectorNameForProperty(
191217
Class cls, bool staticMethod, const std::string& property) {
192218
if (cls == nil || property.empty()) {
@@ -221,6 +247,11 @@ std::optional<std::string> runtimeSelectorNameForProperty(
221247
}
222248
#endif
223249

250+
if (auto selectorName =
251+
runtimeBooleanGetterSelectorForProperty(cls, staticMethod, property)) {
252+
return selectorName;
253+
}
254+
224255
Class scan = staticMethod ? object_getClass(cls) : cls;
225256
while (scan != Nil) {
226257
unsigned int methodCount = 0;

NativeScript/ffi/shared/jsi/NativeApiJsiHostObjects.h

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -390,9 +390,12 @@ class NativeApiSuperHostObject final : public HostObject {
390390
const auto& members = bridge_->membersForClass(*symbol);
391391
if (const NativeApiMember* propertyMember =
392392
selectPropertyMember(members, property, false)) {
393-
return callObjCSelector(runtime, bridge_, receiver_, false,
394-
propertyMember->selectorName, propertyMember,
395-
nullptr, 0, dispatchClass_);
393+
SEL selector = sel_getUid(propertyMember->selectorName.c_str());
394+
if (class_getInstanceMethod(dispatchClass_, selector) != nullptr) {
395+
return callObjCSelector(runtime, bridge_, receiver_, false,
396+
propertyMember->selectorName, propertyMember,
397+
nullptr, 0, dispatchClass_);
398+
}
396399
}
397400

398401
if (selectMethodMember(members, property, false, 0) != nullptr) {
@@ -845,12 +848,21 @@ class NativeApiObjectHostObject final
845848
if (const NativeApiMember* propertyMember =
846849
selectPropertyMember(members, property, false)) {
847850
SEL selector = sel_getUid(propertyMember->selectorName.c_str());
848-
if (class_getInstanceMethod(object_getClass(object_), selector) ==
849-
nullptr) {
850-
return Value::undefined();
851+
if ([object_ respondsToSelector:selector]) {
852+
return callObjectSelector(runtime, propertyMember->selectorName,
853+
propertyMember, nullptr, 0);
854+
}
855+
std::string booleanSelectorName =
856+
booleanGetterSelectorForProperty(property);
857+
if (booleanSelectorName != propertyMember->selectorName) {
858+
SEL booleanSelector = sel_getUid(booleanSelectorName.c_str());
859+
if ([object_ respondsToSelector:booleanSelector]) {
860+
NativeApiMember getterMember = *propertyMember;
861+
getterMember.selectorName = booleanSelectorName;
862+
return callObjectSelector(runtime, getterMember.selectorName,
863+
&getterMember, nullptr, 0);
864+
}
851865
}
852-
return callObjectSelector(runtime, propertyMember->selectorName,
853-
propertyMember, nullptr, 0);
854866
}
855867

856868
if (selectMethodMember(members, property, false, 0) != nullptr) {
@@ -958,7 +970,7 @@ class NativeApiObjectHostObject final
958970

959971
std::string setterSelectorName = setterSelectorForProperty(property);
960972
SEL selector = sel_getUid(setterSelectorName.c_str());
961-
if (class_getInstanceMethod(object_getClass(object_), selector) != nullptr) {
973+
if ([object_ respondsToSelector:selector]) {
962974
Value args[] = {Value(runtime, value)};
963975
callObjCSelector(runtime, bridge_, object_, false, setterSelectorName,
964976
nullptr, args, 1);
@@ -1163,12 +1175,11 @@ class NativeApiClassHostObject final : public HostObject {
11631175
runtime, "Objective-C class is not available: " + symbol.name);
11641176
}
11651177
SEL selector = sel_getUid(propertyMember->selectorName.c_str());
1166-
if (class_getClassMethod(cls, selector) == nullptr) {
1167-
return Value::undefined();
1178+
if (class_getClassMethod(cls, selector) != nullptr) {
1179+
return callObjCSelector(runtime, bridge, static_cast<id>(cls), true,
1180+
propertyMember->selectorName, propertyMember,
1181+
nullptr, 0);
11681182
}
1169-
return callObjCSelector(runtime, bridge, static_cast<id>(cls), true,
1170-
propertyMember->selectorName, propertyMember,
1171-
nullptr, 0);
11721183
}
11731184

11741185
if (selectMethodMember(members, property, true, 0) != nullptr) {

NativeScript/ffi/shared/jsi/NativeApiJsiInvocation.h

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,20 @@ Value callCFunction(Runtime& runtime,
361361
returnStorage.data());
362362
}
363363

364+
bool signatureSupportedForJsiInvocation(
365+
const std::optional<NativeApiJsiSignature>& signature) {
366+
if (!signature || !signature->prepared || signature->variadic ||
367+
unsupportedJsiType(signature->returnType)) {
368+
return false;
369+
}
370+
for (const auto& argType : signature->argumentTypes) {
371+
if (unsupportedJsiType(argType)) {
372+
return false;
373+
}
374+
}
375+
return true;
376+
}
377+
364378
Value callObjCSelector(Runtime& runtime,
365379
const std::shared_ptr<NativeApiJsiBridge>& bridge,
366380
id receiver, bool receiverIsClass,
@@ -379,7 +393,8 @@ Value callObjCSelector(Runtime& runtime,
379393
Class lookupClass = dispatchSuperClass != Nil ? dispatchSuperClass : receiverClass;
380394
Method method = receiverIsClass ? class_getClassMethod(lookupClass, selector)
381395
: class_getInstanceMethod(lookupClass, selector);
382-
if (method == nullptr) {
396+
if (method == nullptr &&
397+
(dispatchSuperClass != Nil || ![receiver respondsToSelector:selector])) {
383398
throw facebook::jsi::JSError(runtime,
384399
"Objective-C selector is not available: " +
385400
selectorName);
@@ -393,12 +408,11 @@ Value callObjCSelector(Runtime& runtime,
393408
bridge->metadata(), member->signatureOffset, 2, bridge.get(),
394409
(member->flags & metagen::mdMemberReturnOwned) != 0);
395410
}
396-
if (!signature) {
411+
if (!signatureSupportedForJsiInvocation(signature) && method != nullptr) {
397412
signature = parseObjCMethodJsiSignature(method, bridge.get());
398413
}
399414

400-
if (!signature || !signature->prepared || signature->variadic ||
401-
unsupportedJsiType(signature->returnType)) {
415+
if (!signatureSupportedForJsiInvocation(signature)) {
402416
throw facebook::jsi::JSError(
403417
runtime, "Objective-C signature is not supported by pure JSI: " +
404418
selectorName);

NativeScript/runtime/modules/timers/Timers.mm

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,18 @@ bool shouldAvoidMainQueueSyncWhileHoldingHermesLock(napi_env env) {
229229
#endif
230230
}
231231

232+
void DrainPendingJobs(napi_env env) {
233+
if (env == nullptr) {
234+
return;
235+
}
236+
237+
#ifdef ENABLE_JS_RUNTIME
238+
js_execute_pending_jobs(env);
239+
#else
240+
(void)env;
241+
#endif
242+
}
243+
232244
void AddTimerToMainRunLoop(napi_env env, NSTimer* timer) {
233245
if (timer == nil) {
234246
return;
@@ -310,9 +322,7 @@ void DisposeTimerHandle(napi_env callEnv, NSTimerHandle* handle, bool invalidate
310322
uint32_t remaining = 0;
311323
napi_reference_unref(cleanupEnv, callback, &remaining);
312324
napi_delete_reference(cleanupEnv, callback);
313-
#ifdef TARGET_ENGINE_HERMES
314-
js_execute_pending_jobs(cleanupEnv);
315-
#endif
325+
DrainPendingJobs(cleanupEnv);
316326
}
317327
}
318328

@@ -321,6 +331,11 @@ void ScheduleOneShotTimerCleanup(napi_env env, NSTimerHandle* handle) {
321331
return;
322332
}
323333

334+
if ([NSThread isMainThread]) {
335+
DisposeTimerHandle(env, handle, false);
336+
return;
337+
}
338+
324339
[handle retain];
325340
dispatch_async(dispatch_get_main_queue(), ^{
326341
DisposeTimerHandle(env, handle, false);
@@ -429,9 +444,7 @@ void ScheduleOneShotTimerCleanup(napi_env env, NSTimerHandle* handle) {
429444
napi_get_reference_value(callbackEnv, callbackRef, &callbackValue);
430445
napi_call_function(callbackEnv, global, callbackValue, 0, nullptr, nullptr);
431446
#endif
432-
#ifdef TARGET_ENGINE_HERMES
433-
js_execute_pending_jobs(callbackEnv);
434-
#endif
447+
DrainPendingJobs(callbackEnv);
435448

436449
ScheduleOneShotTimerCleanup(callbackEnv, handle);
437450
[handle release];
@@ -533,9 +546,7 @@ void ScheduleOneShotTimerCleanup(napi_env env, NSTimerHandle* handle) {
533546
napi_get_reference_value(callbackEnv, callbackRef, &callbackValue);
534547
napi_call_function(callbackEnv, global, callbackValue, 0, nullptr, nullptr);
535548
#endif
536-
#ifdef TARGET_ENGINE_HERMES
537-
js_execute_pending_jobs(callbackEnv);
538-
#endif
549+
DrainPendingJobs(callbackEnv);
539550
[handle release];
540551
}];
541552

scripts/run-tests-ios.js

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// artifacts need rebuilding. Supported: v8, hermes, quickjs, jsc. Defaults to v8.
1414
// - IOS_SWIFT_VERSION overrides default Swift version (default: 5.0).
1515
// - IOS_COMMAND_TIMEOUT_MS overrides timeout for build/install/simctl commands (default: 3 minutes).
16+
// - IOS_SIMCTL_QUERY_TIMEOUT_MS overrides timeout for polling simctl queries (default: 10 seconds).
1617
// - IOS_BUILD_TIMEOUT_MS overrides timeout for xcodebuild app build (default: IOS_COMMAND_TIMEOUT_MS).
1718
// - IOS_COMMAND_MAX_BUFFER_BYTES overrides spawnSync maxBuffer for captured command output (default: 64 MiB).
1819
// - IOS_TEST_TIMEOUT_MS overrides max test runtime (default: 10 minutes).
@@ -95,6 +96,10 @@ const commandTimeoutMs = parseTimeoutMs("IOS_COMMAND_TIMEOUT_MS", 3 * 60 * 1000)
9596
// Clean CI runners often need substantially longer for the first iOS build than
9697
// for simulator control commands like boot/install/log collection.
9798
const buildTimeoutMs = parseTimeoutMs("IOS_BUILD_TIMEOUT_MS", 10 * 60 * 1000);
99+
const simctlQueryTimeoutMs = parseTimeoutMs(
100+
"IOS_SIMCTL_QUERY_TIMEOUT_MS",
101+
Math.min(commandTimeoutMs, 10 * 1000)
102+
);
98103
const commandMaxBufferBytes = parsePositiveInt("IOS_COMMAND_MAX_BUFFER_BYTES", 64 * 1024 * 1024);
99104
const testTimeoutMs = Number(process.env.IOS_TEST_TIMEOUT_MS || 10 * 60 * 1000);
100105
const inactivityTimeoutMs = Number(process.env.IOS_TEST_INACTIVITY_TIMEOUT_MS || 2 * 60 * 1000);
@@ -634,13 +639,25 @@ function buildTestRunnerApp(destination, swiftVersion) {
634639
return { appPath, reusedBuild: canReuseBuild };
635640
}
636641

642+
const appContainerPathCache = new Map();
643+
637644
function getAppContainerPath(udid, containerType) {
638-
const result = run("xcrun", ["simctl", "get_app_container", udid, bundleId, containerType]);
645+
const cacheKey = `${udid}:${containerType}`;
646+
if (appContainerPathCache.has(cacheKey)) {
647+
return appContainerPathCache.get(cacheKey);
648+
}
649+
650+
const result = run("xcrun", ["simctl", "get_app_container", udid, bundleId, containerType], {
651+
timeout: simctlQueryTimeoutMs
652+
});
639653
if (result.status !== 0) {
640654
return null;
641655
}
642656

643657
const out = (result.stdout || "").trim();
658+
if (out) {
659+
appContainerPathCache.set(cacheKey, out);
660+
}
644661
return out || null;
645662
}
646663

@@ -882,7 +899,8 @@ async function waitForCompletedJunitOrLaunchExit(udid, launchProcess, timeoutMs,
882899

883900
if (Date.now() - state.lastActivityAt >= inactivityTimeoutMs) {
884901
const launchPid = extractLaunchPid(state.logs);
885-
if (!isAppProcessRunning(udid, launchPid)) {
902+
const appRunning = isAppProcessRunning(udid, launchPid);
903+
if (appRunning === false) {
886904
return { junitResult: null, launchResult, timedOut: true, inactive: true };
887905
}
888906
}
@@ -1002,9 +1020,11 @@ function readJunitFileState(udid) {
10021020
}
10031021

10041022
function collectSimulatorProcessSnapshot(udid) {
1005-
const result = run("xcrun", ["simctl", "spawn", udid, "ps", "-axo", "pid,ppid,stat,etime,command"]);
1023+
const result = run("xcrun", ["simctl", "spawn", udid, "ps", "-axo", "pid,ppid,stat,etime,command"], {
1024+
timeout: simctlQueryTimeoutMs
1025+
});
10061026
if (result.status !== 0) {
1007-
return "";
1027+
return null;
10081028
}
10091029

10101030
const lines = (result.stdout || "")
@@ -1016,6 +1036,9 @@ function collectSimulatorProcessSnapshot(udid) {
10161036

10171037
function isAppProcessRunning(udid, pid) {
10181038
const snapshot = collectSimulatorProcessSnapshot(udid);
1039+
if (snapshot == null) {
1040+
return null;
1041+
}
10191042
if (!snapshot) {
10201043
return false;
10211044
}

0 commit comments

Comments
 (0)