diff --git a/packages/react-native/ReactCommon/react/bridgeless/iostests/RCTHostTests.mm b/packages/react-native/ReactCommon/react/bridgeless/iostests/RCTHostTests.mm index 58467be556b005..606d2d9dabd1f1 100644 --- a/packages/react-native/ReactCommon/react/bridgeless/iostests/RCTHostTests.mm +++ b/packages/react-native/ReactCommon/react/bridgeless/iostests/RCTHostTests.mm @@ -7,7 +7,10 @@ #import +#import #import +#import +#import #import #import #import @@ -15,6 +18,22 @@ #import +RCT_MOCK_REF(RCTHost, _RCTLogNativeInternal); + +RCTLogLevel gLogLevel; +int gLogCalledTimes = 0; +NSString *gLogMessage = nil; +static void RCTLogNativeInternalMock(RCTLogLevel level, const char *fileName, int lineNumber, NSString *format, ...) +{ + gLogLevel = level; + gLogCalledTimes++; + + va_list args; + va_start(args, format); + gLogMessage = [[NSString alloc] initWithFormat:format arguments:args]; + va_end(args); +} + @interface RCTHostTests : XCTestCase @end @@ -29,6 +48,8 @@ - (void)setUp { [super setUp]; + RCTAutoReleasePoolPush(); + shimmedRCTInstance = [ShimRCTInstance new]; _mockHostDelegate = OCMProtocolMock(@protocol(RCTHostDelegate)); @@ -42,16 +63,96 @@ - (void)setUp - (void)tearDown { + RCTAutoReleasePoolPop(); + + _subject = nil; + XCTAssertEqual(RCTGetRetainCount(_subject), 0); + + _mockHostDelegate = nil; + XCTAssertEqual(RCTGetRetainCount(_mockHostDelegate), 0); + [shimmedRCTInstance reset]; + gLogCalledTimes = 0; + gLogMessage = nil; [super tearDown]; } - (void)testStart { + RCT_MOCK_SET(RCTHost, _RCTLogNativeInternal, RCTLogNativeInternalMock); + + XCTAssertEqual(shimmedRCTInstance.initCount, 0); [_subject start]; OCMVerify(OCMTimes(1), [_mockHostDelegate hostDidStart:_subject]); XCTAssertEqual(shimmedRCTInstance.initCount, 1); + XCTAssertEqual(gLogCalledTimes, 0); + + XCTAssertEqual(shimmedRCTInstance.invalidateCount, 0); + [_subject start]; + XCTAssertEqual(shimmedRCTInstance.initCount, 2); + XCTAssertEqual(shimmedRCTInstance.invalidateCount, 1); + OCMVerify(OCMTimes(2), [_mockHostDelegate hostDidStart:_subject]); + XCTAssertEqual(gLogLevel, RCTLogLevelWarning); + XCTAssertEqual(gLogCalledTimes, 1); + XCTAssertEqualObjects( + gLogMessage, + @"RCTHost should not be creating a new instance if one already exists. This implies there is a bug with how/when this method is being called."); + + RCT_MOCK_RESET(RCTHost, _RCTLogNativeInternal); +} + +- (void)testCallFunctionOnJSModule +{ + [_subject start]; + + NSArray *args = @[ @"hi", @(5), @(NO) ]; + [_subject callFunctionOnJSModule:@"jsModule" method:@"method" args:args]; + + XCTAssertEqualObjects(shimmedRCTInstance.jsModuleName, @"jsModule"); + XCTAssertEqualObjects(shimmedRCTInstance.method, @"method"); + XCTAssertEqualObjects(shimmedRCTInstance.args, args); +} + +- (void)testDidReceiveErrorStack +{ + id instanceDelegate = (id)_subject; + + NSMutableArray *> *stack = [NSMutableArray array]; + + NSMutableDictionary *stackFrame0 = [NSMutableDictionary dictionary]; + stackFrame0[@"linenumber"] = @(3); + stackFrame0[@"column"] = @(4); + stackFrame0[@"methodname"] = @"method1"; + stackFrame0[@"file"] = @"file1.js"; + [stack addObject:stackFrame0]; + + NSMutableDictionary *stackFrame1 = [NSMutableDictionary dictionary]; + stackFrame0[@"linenumber"] = @(63); + stackFrame0[@"column"] = @(44); + stackFrame0[@"methodname"] = @"method2"; + stackFrame0[@"file"] = @"file2.js"; + [stack addObject:stackFrame1]; + + [instanceDelegate instance:[OCMArg any] didReceiveJSErrorStack:stack message:@"message" exceptionId:5 isFatal:YES]; + + OCMVerify( + OCMTimes(1), + [_mockHostDelegate host:_subject didReceiveJSErrorStack:stack message:@"message" exceptionId:5 isFatal:YES]); +} + +- (void)testDidInitializeRuntime +{ + id mockRuntimeDelegate = OCMProtocolMock(@protocol(RCTHostRuntimeDelegate)); + _subject.runtimeDelegate = mockRuntimeDelegate; + + auto hermesRuntime = facebook::hermes::makeHermesRuntime(); + facebook::jsi::Runtime *rt = hermesRuntime.get(); + + id instanceDelegate = (id)_subject; + [instanceDelegate instance:[OCMArg any] didInitializeRuntime:*rt]; + + OCMVerify(OCMTimes(1), [mockRuntimeDelegate host:_subject didInitializeRuntime:*rt]); } @end diff --git a/packages/react-native/ReactCommon/react/bridgeless/platform/ios/Core/RCTHost.mm b/packages/react-native/ReactCommon/react/bridgeless/platform/ios/Core/RCTHost.mm index d7d8c274af17a5..fc91d96e5420cb 100644 --- a/packages/react-native/ReactCommon/react/bridgeless/platform/ios/Core/RCTHost.mm +++ b/packages/react-native/ReactCommon/react/bridgeless/platform/ios/Core/RCTHost.mm @@ -15,9 +15,13 @@ #import #import #import +#import #import #import +RCT_MOCK_DEF(RCTHost, _RCTLogNativeInternal); +#define _RCTLogNativeInternal RCT_MOCK_USE(RCTHost, _RCTLogNativeInternal) + using namespace facebook::react; @interface RCTHost () @@ -230,28 +234,13 @@ - (void)dealloc #pragma mark - RCTInstanceDelegate -- (void)instance:(RCTInstance *)instance didReceiveErrorMap:(facebook::react::MapBuffer)errorMap +- (void)instance:(RCTInstance *)instance + didReceiveJSErrorStack:(NSArray *> *)stack + message:(NSString *)message + exceptionId:(NSUInteger)exceptionId + isFatal:(BOOL)isFatal { - NSString *message = [NSString stringWithCString:errorMap.getString(JSErrorHandlerKey::kErrorMessage).c_str() - encoding:[NSString defaultCStringEncoding]]; - std::vector frames = errorMap.getMapBufferList(JSErrorHandlerKey::kAllStackFrames); - NSMutableArray *> *stack = [NSMutableArray new]; - for (facebook::react::MapBuffer const &mapBuffer : frames) { - NSDictionary *frame = @{ - @"file" : [NSString stringWithCString:mapBuffer.getString(JSErrorHandlerKey::kFrameFileName).c_str() - encoding:[NSString defaultCStringEncoding]], - @"methodName" : [NSString stringWithCString:mapBuffer.getString(JSErrorHandlerKey::kFrameMethodName).c_str() - encoding:[NSString defaultCStringEncoding]], - @"lineNumber" : [NSNumber numberWithInt:mapBuffer.getInt(JSErrorHandlerKey::kFrameLineNumber)], - @"column" : [NSNumber numberWithInt:mapBuffer.getInt(JSErrorHandlerKey::kFrameColumnNumber)], - }; - [stack addObject:frame]; - } - [_hostDelegate host:self - didReceiveJSErrorStack:stack - message:message - exceptionId:errorMap.getInt(JSErrorHandlerKey::kExceptionId) - isFatal:errorMap.getBool(JSErrorHandlerKey::kIsFatal)]; + [_hostDelegate host:self didReceiveJSErrorStack:stack message:message exceptionId:exceptionId isFatal:isFatal]; } - (void)instance:(RCTInstance *)instance didInitializeRuntime:(facebook::jsi::Runtime &)runtime diff --git a/packages/react-native/ReactCommon/react/bridgeless/platform/ios/Core/RCTInstance.h b/packages/react-native/ReactCommon/react/bridgeless/platform/ios/Core/RCTInstance.h index 80c10803316f76..5445ca8b7c247d 100644 --- a/packages/react-native/ReactCommon/react/bridgeless/platform/ios/Core/RCTInstance.h +++ b/packages/react-native/ReactCommon/react/bridgeless/platform/ios/Core/RCTInstance.h @@ -38,7 +38,12 @@ FB_RUNTIME_PROTOCOL @protocol RCTInstanceDelegate -- (void)instance:(RCTInstance *)instance didReceiveErrorMap:(facebook::react::MapBuffer)errorMap; +- (void)instance:(RCTInstance *)instance + didReceiveJSErrorStack:(NSArray *> *)stack + message:(NSString *)message + exceptionId:(NSUInteger)exceptionId + isFatal:(BOOL)isFatal; + - (void)instance:(RCTInstance *)instance didInitializeRuntime:(facebook::jsi::Runtime &)runtime; @end diff --git a/packages/react-native/ReactCommon/react/bridgeless/platform/ios/Core/RCTInstance.mm b/packages/react-native/ReactCommon/react/bridgeless/platform/ios/Core/RCTInstance.mm index 5e23e46836c287..6c76569703cc65 100644 --- a/packages/react-native/ReactCommon/react/bridgeless/platform/ios/Core/RCTInstance.mm +++ b/packages/react-native/ReactCommon/react/bridgeless/platform/ios/Core/RCTInstance.mm @@ -413,7 +413,26 @@ - (void)_notifyEventDispatcherObserversOfEvent_DEPRECATED:(NSNotification *)noti - (void)_handleJSErrorMap:(facebook::react::MapBuffer)errorMap { - [_delegate instance:self didReceiveErrorMap:std::move(errorMap)]; + NSString *message = [NSString stringWithCString:errorMap.getString(JSErrorHandlerKey::kErrorMessage).c_str() + encoding:[NSString defaultCStringEncoding]]; + std::vector frames = errorMap.getMapBufferList(JSErrorHandlerKey::kAllStackFrames); + NSMutableArray *> *stack = [NSMutableArray new]; + for (facebook::react::MapBuffer const &mapBuffer : frames) { + NSDictionary *frame = @{ + @"file" : [NSString stringWithCString:mapBuffer.getString(JSErrorHandlerKey::kFrameFileName).c_str() + encoding:[NSString defaultCStringEncoding]], + @"methodName" : [NSString stringWithCString:mapBuffer.getString(JSErrorHandlerKey::kFrameMethodName).c_str() + encoding:[NSString defaultCStringEncoding]], + @"lineNumber" : [NSNumber numberWithInt:mapBuffer.getInt(JSErrorHandlerKey::kFrameLineNumber)], + @"column" : [NSNumber numberWithInt:mapBuffer.getInt(JSErrorHandlerKey::kFrameColumnNumber)], + }; + [stack addObject:frame]; + } + [_delegate instance:self + didReceiveJSErrorStack:stack + message:message + exceptionId:errorMap.getInt(JSErrorHandlerKey::kExceptionId) + isFatal:errorMap.getBool(JSErrorHandlerKey::kIsFatal)]; } @end diff --git a/packages/react-native/ReactCommon/react/test_utils/ios/Memory/RCTMemoryUtils.h b/packages/react-native/ReactCommon/react/test_utils/ios/Memory/RCTMemoryUtils.h new file mode 100644 index 00000000000000..e9110fa59e5456 --- /dev/null +++ b/packages/react-native/ReactCommon/react/test_utils/ios/Memory/RCTMemoryUtils.h @@ -0,0 +1,19 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +RCT_EXTERN_C_BEGIN + +int RCTGetRetainCount(id _Nullable object); + +void RCTAutoReleasePoolPush(void); +void RCTAutoReleasePoolPop(void); + +RCT_EXTERN_C_END diff --git a/packages/react-native/ReactCommon/react/test_utils/ios/Memory/RCTMemoryUtils.m b/packages/react-native/ReactCommon/react/test_utils/ios/Memory/RCTMemoryUtils.m new file mode 100644 index 00000000000000..22f41fec29fea8 --- /dev/null +++ b/packages/react-native/ReactCommon/react/test_utils/ios/Memory/RCTMemoryUtils.m @@ -0,0 +1,45 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RCTMemoryUtils.h" + +int RCTGetRetainCount(id _Nullable object) +{ + return object != nil ? CFGetRetainCount((__bridge CFTypeRef)object) - 1 : 0; +} + +OBJC_EXPORT +void *objc_autoreleasePoolPush(void) __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +OBJC_EXPORT +void objc_autoreleasePoolPop(void *context) __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +static NSString *const kAutoreleasePoolContextStackKey = @"autorelease_pool_context_stack"; + +void RCTAutoReleasePoolPush(void) +{ + assert([NSThread isMainThread]); + NSMutableDictionary *dictionary = [[NSThread currentThread] threadDictionary]; + void *context = objc_autoreleasePoolPush(); + NSMutableArray *contextStack = dictionary[kAutoreleasePoolContextStackKey]; + if (!contextStack) { + contextStack = [NSMutableArray array]; + dictionary[kAutoreleasePoolContextStackKey] = contextStack; + } + [contextStack addObject:[NSValue valueWithPointer:context]]; +} + +void RCTAutoReleasePoolPop(void) +{ + assert([NSThread isMainThread]); + NSMutableDictionary *dictionary = [[NSThread currentThread] threadDictionary]; + NSMutableArray *contextStack = dictionary[kAutoreleasePoolContextStackKey]; + assert(contextStack.count > 0); + NSValue *lastContext = contextStack.lastObject; + [contextStack removeLastObject]; + objc_autoreleasePoolPop(lastContext.pointerValue); +} diff --git a/packages/react-native/ReactCommon/react/test_utils/ios/Shims/ShimRCTInstance.h b/packages/react-native/ReactCommon/react/test_utils/ios/Shims/ShimRCTInstance.h index a0aee07975b34f..e11e88c560a72a 100644 --- a/packages/react-native/ReactCommon/react/test_utils/ios/Shims/ShimRCTInstance.h +++ b/packages/react-native/ReactCommon/react/test_utils/ios/Shims/ShimRCTInstance.h @@ -9,7 +9,12 @@ @interface ShimRCTInstance : NSObject -@property (assign) int initCount; +@property int initCount; +@property int invalidateCount; + +@property NSString *jsModuleName; +@property NSString *method; +@property NSArray *args; - (void)reset; diff --git a/packages/react-native/ReactCommon/react/test_utils/ios/Shims/ShimRCTInstance.mm b/packages/react-native/ReactCommon/react/test_utils/ios/Shims/ShimRCTInstance.mm index 017b45ac212b2c..77476f9dfd3d52 100644 --- a/packages/react-native/ReactCommon/react/test_utils/ios/Shims/ShimRCTInstance.mm +++ b/packages/react-native/ReactCommon/react/test_utils/ios/Shims/ShimRCTInstance.mm @@ -24,6 +24,9 @@ - (instancetype)init [ShimRCTInstance class], @selector(initWithDelegate: jsEngineInstance:bundleManager:turboModuleManagerDelegate:onInitialBundleLoad:moduleRegistry:)); + RCTSwizzleInstanceSelector([RCTInstance class], [ShimRCTInstance class], @selector(invalidate)); + RCTSwizzleInstanceSelector( + [RCTInstance class], [ShimRCTInstance class], @selector(callFunctionOnJSModule:method:args:)); weakShim = self; } return self; @@ -36,7 +39,11 @@ - (void)reset [ShimRCTInstance class], @selector(initWithDelegate: jsEngineInstance:bundleManager:turboModuleManagerDelegate:onInitialBundleLoad:moduleRegistry:)); + RCTSwizzleInstanceSelector([RCTInstance class], [ShimRCTInstance class], @selector(invalidate)); + RCTSwizzleInstanceSelector( + [RCTInstance class], [ShimRCTInstance class], @selector(callFunctionOnJSModule:method:args:)); _initCount = 0; + _invalidateCount = 0; } - (instancetype)initWithDelegate:(id)delegate @@ -50,4 +57,16 @@ - (instancetype)initWithDelegate:(id)delegate return self; } +- (void)invalidate +{ + weakShim.invalidateCount++; +} + +- (void)callFunctionOnJSModule:(NSString *)moduleName method:(NSString *)method args:(NSArray *)args +{ + weakShim.jsModuleName = moduleName; + weakShim.method = method; + weakShim.args = [args copy]; +} + @end