From 35da174339968689b5db2f1a135da7c86a35fa39 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Tue, 1 Mar 2016 09:44:05 -0800 Subject: [PATCH] Added unit tests for module init Summary: The module initialization process is complex and full of race conditions. This diff adds a set of unit tests that verify that modules setup happens in the correct order, and enforces all the various conditions for main/background init. Reviewed By: javache Differential Revision: D2994145 fb-gh-sync-id: 92ea84508cdeeb280ff0fb9e9b2dffa8dbc37e66 shipit-source-id: 92ea84508cdeeb280ff0fb9e9b2dffa8dbc37e66 --- .../UIExplorer.xcodeproj/project.pbxproj | 10 +- .../UIExplorerUnitTests/RCTBridgeTests.m | 12 +- .../RCTModuleInitNotificationRaceTests.m | 142 ++++++++++ .../UIExplorerUnitTests/RCTModuleInitTests.m | 254 ++++++++++++++++++ React/Base/RCTBatchedBridge.m | 2 +- React/Base/RCTBridge+Private.h | 5 + React/Base/RCTBridge.m | 42 +-- React/Base/RCTLog.m | 7 +- React/Base/RCTModuleData.m | 5 + React/Modules/RCTDevMenu.m | 2 +- 10 files changed, 452 insertions(+), 29 deletions(-) create mode 100644 Examples/UIExplorer/UIExplorerUnitTests/RCTModuleInitNotificationRaceTests.m create mode 100644 Examples/UIExplorer/UIExplorerUnitTests/RCTModuleInitTests.m diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj index b2608f0fc..95f9215ce 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 1300627F1B59179B0043FE5A /* RCTGzipTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1300627E1B59179B0043FE5A /* RCTGzipTests.m */; }; + 13129DD41C85F87C007D611C /* RCTModuleInitNotificationRaceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 13129DD31C85F87C007D611C /* RCTModuleInitNotificationRaceTests.m */; }; 1323F1891C04AB9F0091BED0 /* bunny.png in Resources */ = {isa = PBXBuildFile; fileRef = 1323F1851C04AB9F0091BED0 /* bunny.png */; }; 1323F18A1C04AB9F0091BED0 /* flux@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 1323F1861C04AB9F0091BED0 /* flux@3x.png */; }; 1323F18B1C04AB9F0091BED0 /* hawk.png in Resources */ = {isa = PBXBuildFile; fileRef = 1323F1871C04AB9F0091BED0 /* hawk.png */; }; @@ -18,6 +19,7 @@ 1341802C1AA9178B003F314A /* libRCTNetwork.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1341802B1AA91779003F314A /* libRCTNetwork.a */; }; 134454601AAFCABD003F0779 /* libRCTAdSupport.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1344545A1AAFCAAE003F0779 /* libRCTAdSupport.a */; }; 134A8A2A1AACED7A00945AAE /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 134A8A251AACED6A00945AAE /* libRCTGeolocation.a */; }; + 134CB92A1C85A38800265FA6 /* RCTModuleInitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 134CB9291C85A38800265FA6 /* RCTModuleInitTests.m */; }; 138D6A181B53CD440074A87E /* RCTShadowViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 138D6A161B53CD440074A87E /* RCTShadowViewTests.m */; }; 138DEE241B9EDFB6007F4EA5 /* libRCTCameraRoll.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 138DEE091B9EDDDB007F4EA5 /* libRCTCameraRoll.a */; }; 1393D0381B68CD1300E1B601 /* RCTModuleMethodTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1393D0371B68CD1300E1B601 /* RCTModuleMethodTests.m */; }; @@ -178,6 +180,7 @@ /* Begin PBXFileReference section */ 004D289E1AAF61C70097A701 /* UIExplorerUnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UIExplorerUnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 1300627E1B59179B0043FE5A /* RCTGzipTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTGzipTests.m; sourceTree = ""; }; + 13129DD31C85F87C007D611C /* RCTModuleInitNotificationRaceTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTModuleInitNotificationRaceTests.m; sourceTree = ""; }; 1323F1851C04AB9F0091BED0 /* bunny.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = bunny.png; sourceTree = ""; }; 1323F1861C04AB9F0091BED0 /* flux@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "flux@3x.png"; sourceTree = ""; }; 1323F1871C04AB9F0091BED0 /* hawk.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = hawk.png; sourceTree = ""; }; @@ -188,6 +191,7 @@ 134180261AA91779003F314A /* RCTNetwork.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTNetwork.xcodeproj; path = ../../Libraries/Network/RCTNetwork.xcodeproj; sourceTree = ""; }; 134454551AAFCAAE003F0779 /* RCTAdSupport.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTAdSupport.xcodeproj; path = ../../Libraries/AdSupport/RCTAdSupport.xcodeproj; sourceTree = ""; }; 134A8A201AACED6A00945AAE /* RCTGeolocation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTGeolocation.xcodeproj; path = ../../Libraries/Geolocation/RCTGeolocation.xcodeproj; sourceTree = ""; }; + 134CB9291C85A38800265FA6 /* RCTModuleInitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTModuleInitTests.m; sourceTree = ""; }; 138D6A161B53CD440074A87E /* RCTShadowViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTShadowViewTests.m; sourceTree = ""; }; 138DEE021B9EDDDB007F4EA5 /* RCTCameraRoll.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTCameraRoll.xcodeproj; path = ../../Libraries/CameraRoll/RCTCameraRoll.xcodeproj; sourceTree = ""; }; 1393D0371B68CD1300E1B601 /* RCTModuleMethodTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTModuleMethodTests.m; sourceTree = ""; }; @@ -422,6 +426,8 @@ 144D21231B2204C5006DB32B /* RCTImageUtilTests.m */, 13DB03471B5D2ED500C27245 /* RCTJSONTests.m */, 13DF61B51B67A45000EDB188 /* RCTMethodArgumentTests.m */, + 134CB9291C85A38800265FA6 /* RCTModuleInitTests.m */, + 13129DD31C85F87C007D611C /* RCTModuleInitNotificationRaceTests.m */, 1393D0371B68CD1300E1B601 /* RCTModuleMethodTests.m */, 138D6A161B53CD440074A87E /* RCTShadowViewTests.m */, 1497CFAB1B21F5E400C1F8F2 /* RCTUIManagerTests.m */, @@ -882,7 +888,9 @@ 1300627F1B59179B0043FE5A /* RCTGzipTests.m in Sources */, 1497CFAF1B21F5E400C1F8F2 /* RCTConvert_NSURLTests.m in Sources */, 1497CFAE1B21F5E400C1F8F2 /* RCTJSCExecutorTests.m in Sources */, + 13129DD41C85F87C007D611C /* RCTModuleInitNotificationRaceTests.m in Sources */, 1497CFAD1B21F5E400C1F8F2 /* RCTBridgeTests.m in Sources */, + 134CB92A1C85A38800265FA6 /* RCTModuleInitTests.m in Sources */, 1497CFB11B21F5E400C1F8F2 /* RCTEventDispatcherTests.m in Sources */, 1497CFB31B21F5E400C1F8F2 /* RCTUIManagerTests.m in Sources */, 13DB03481B5D2ED500C27245 /* RCTJSONTests.m in Sources */, @@ -1276,4 +1284,4 @@ /* End XCConfigurationList section */ }; rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; -} \ No newline at end of file +} diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTBridgeTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTBridgeTests.m index f415f6da6..08a1a1e37 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTBridgeTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTBridgeTests.m @@ -41,6 +41,8 @@ @implementation TestExecutor +@synthesize valid = _valid; + RCT_EXPORT_MODULE() - (void)setUp {} @@ -55,7 +57,7 @@ RCT_EXPORT_MODULE() - (BOOL)isValid { - return YES; + return _valid; } - (void)flushedQueue:(RCTJavaScriptCallback)onComplete @@ -98,7 +100,10 @@ RCT_EXPORT_MODULE() onComplete(nil); } -- (void)invalidate {} +- (void)invalidate +{ + _valid = NO; +} @end @@ -132,7 +137,7 @@ RCT_EXPORT_MODULE(TestModule) [_bridge invalidate]; [_bridge setUp]; - _jsExecutor = [_bridge.batchedBridge valueForKey:@"javaScriptExecutor"]; + _jsExecutor = _bridge.batchedBridge.javaScriptExecutor; XCTAssertNotNil(_jsExecutor); } @@ -143,6 +148,7 @@ RCT_EXPORT_MODULE(TestModule) _testMethodCalled = NO; [_bridge invalidate]; + RUN_RUNLOOP_WHILE(_jsExecutor.isValid); _bridge = nil; } diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleInitNotificationRaceTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleInitNotificationRaceTests.m new file mode 100644 index 000000000..39650db27 --- /dev/null +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleInitNotificationRaceTests.m @@ -0,0 +1,142 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import +#import + +#import "RCTBridge.h" +#import "RCTBridge+Private.h" +#import "RCTBridgeModule.h" +#import "RCTUtils.h" +#import "RCTUIManager.h" +#import "RCTViewManager.h" + +#define RUN_RUNLOOP_WHILE(CONDITION) \ +{ \ + NSDate *timeout = [NSDate dateWithTimeIntervalSinceNow:5]; \ + while ((CONDITION)) { \ + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; \ + if ([timeout timeIntervalSinceNow] <= 0) { \ + XCTFail(@"Runloop timed out before condition was met"); \ + break; \ + } \ + } \ +} + +// Must be declared before RCTTestCustomSetBridgeModule in order to trigger the +// race condition that we are testing for - namely that the +// RCTDidInitializeModuleNotification for RCTTestViewManager gets sent before +// setBridge: is called on RCTTestCustomSetBridgeModule +@interface RCTTestViewManager : RCTViewManager +@end + +@implementation RCTTestViewManager + +@synthesize bridge = _bridge; +@synthesize methodQueue = _methodQueue; + +RCT_EXPORT_MODULE() + +- (void)setBridge:(RCTBridge *)bridge +{ + _bridge = bridge; + (void)[_bridge uiManager]; // Needed to trigger a race condition +} + +- (NSArray *)customDirectEventTypes +{ + return @[@"foo"]; +} + +@end + + +@interface RCTNotificationObserverModule : NSObject + +@property (nonatomic, assign) BOOL didDetectViewManagerInit; + +@end + +@implementation RCTNotificationObserverModule + +@synthesize bridge = _bridge; + +RCT_EXPORT_MODULE() + +- (void)setBridge:(RCTBridge *)bridge +{ + _bridge = bridge; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didInitViewManager:) name:RCTDidInitializeModuleNotification object:nil]; +} + +- (void)didInitViewManager:(NSNotification *)note +{ + id module = note.userInfo[@"module"]; + if ([module isKindOfClass:[RCTTestViewManager class]]) { + _didDetectViewManagerInit = YES; + } +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +@end + + +@interface RCTModuleInitNotificationRaceTests : XCTestCase +{ + RCTBridge *_bridge; + RCTNotificationObserverModule *_notificationObserver; +} +@end + +@implementation RCTModuleInitNotificationRaceTests + +- (NSURL *)sourceURLForBridge:(__unused RCTBridge *)bridge +{ + return nil; +} + +- (NSArray *)extraModulesForBridge:(__unused RCTBridge *)bridge +{ + return @[_notificationObserver]; +} + +- (void)setUp +{ + [super setUp]; + + _notificationObserver = [RCTNotificationObserverModule new]; + _bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:nil]; +} + +- (void)tearDown +{ + [super tearDown]; + + _notificationObserver = nil; + id jsExecutor = _bridge.batchedBridge.javaScriptExecutor; + [_bridge invalidate]; + RUN_RUNLOOP_WHILE(jsExecutor.isValid); + _bridge = nil; +} + +- (void)testViewManagerNotInitializedBeforeSetBridgeModule +{ + RUN_RUNLOOP_WHILE(!_notificationObserver.didDetectViewManagerInit); +} + +@end diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleInitTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleInitTests.m new file mode 100644 index 000000000..a0289b2e4 --- /dev/null +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleInitTests.m @@ -0,0 +1,254 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import +#import + +#import "RCTBridge.h" +#import "RCTBridge+Private.h" +#import "RCTBridgeModule.h" +#import "RCTUtils.h" + +#define RUN_RUNLOOP_WHILE(CONDITION) \ +{ \ + NSDate *timeout = [NSDate dateWithTimeIntervalSinceNow:5]; \ + while ((CONDITION)) { \ + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; \ + if ([timeout timeIntervalSinceNow] <= 0) { \ + XCTFail(@"Runloop timed out before condition was met"); \ + break; \ + } \ + } \ +} + + +@interface RCTTestInjectedModule : NSObject +@end + +@implementation RCTTestInjectedModule + +@synthesize bridge = _bridge; +@synthesize methodQueue = _methodQueue; + +RCT_EXPORT_MODULE() + +@end + + +@interface RCTTestCustomInitModule : NSObject + +@property (nonatomic, assign) BOOL initializedOnMainThread; + +@end + +@implementation RCTTestCustomInitModule + +@synthesize bridge = _bridge; +@synthesize methodQueue = _methodQueue; + +RCT_EXPORT_MODULE() + +- (id)init +{ + if ((self = [super init])) { + _initializedOnMainThread = [NSThread isMainThread]; + } + return self; +} + +@end + + +@interface RCTTestCustomSetBridgeModule : NSObject + +@property (nonatomic, assign) BOOL setBridgeOnMainThread; + +@end + +@implementation RCTTestCustomSetBridgeModule + +@synthesize bridge = _bridge; +@synthesize methodQueue = _methodQueue; + +RCT_EXPORT_MODULE() + +- (void)setBridge:(RCTBridge *)bridge +{ + _bridge = bridge; + _setBridgeOnMainThread = [NSThread isMainThread]; +} + +@end + + +@interface RCTTestExportConstantsModule : NSObject + +@property (nonatomic, assign) BOOL exportedConstants; +@property (nonatomic, assign) BOOL exportedConstantsOnMainThread; + +@end + +@implementation RCTTestExportConstantsModule + +@synthesize bridge = _bridge; +@synthesize methodQueue = _methodQueue; + +RCT_EXPORT_MODULE() + +- (NSDictionary *)constantsToExport +{ + _exportedConstants = YES; + _exportedConstantsOnMainThread = [NSThread isMainThread]; + return @{ @"foo": @"bar" }; +} + +@end + + +@interface RCTLazyInitModule : NSObject +@end + +@implementation RCTLazyInitModule + +@synthesize bridge = _bridge; +@synthesize methodQueue = _methodQueue; + +RCT_EXPORT_MODULE() + +@end + + +@interface RCTModuleInitTests : XCTestCase +{ + RCTBridge *_bridge; + BOOL _injectedModuleInitNotificationSent; + BOOL _customInitModuleNotificationSent; + BOOL _customSetBridgeModuleNotificationSent; + BOOL _exportConstantsModuleNotificationSent; + BOOL _lazyInitModuleNotificationSent; + BOOL _lazyInitModuleNotificationSentOnMainThread; + BOOL _viewManagerModuleNotificationSent; + RCTTestInjectedModule *_injectedModule; +} +@end + +@implementation RCTModuleInitTests + +- (NSURL *)sourceURLForBridge:(__unused RCTBridge *)bridge +{ + return nil; +} + +- (NSArray *)extraModulesForBridge:(__unused RCTBridge *)bridge +{ + return @[_injectedModule]; +} + +- (void)setUp +{ + [super setUp]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(moduleDidInit:) name:RCTDidInitializeModuleNotification object:nil]; + + _injectedModuleInitNotificationSent = NO; + _customInitModuleNotificationSent = NO; + _customSetBridgeModuleNotificationSent = NO; + _exportConstantsModuleNotificationSent = NO; + _lazyInitModuleNotificationSent = NO; + _viewManagerModuleNotificationSent = NO; + _injectedModule = [RCTTestInjectedModule new]; + _bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:nil]; +} + +- (void)tearDown +{ + [super tearDown]; + + [[NSNotificationCenter defaultCenter] removeObserver:self name:RCTDidInitializeModuleNotification object:nil]; + + id jsExecutor = _bridge.batchedBridge.javaScriptExecutor; + [_bridge invalidate]; + RUN_RUNLOOP_WHILE(jsExecutor.isValid); + _bridge = nil; +} + +- (void)moduleDidInit:(NSNotification *)note +{ + id module = note.userInfo[@"module"]; + if ([module isKindOfClass:[RCTTestInjectedModule class]]) { + _injectedModuleInitNotificationSent = YES; + } else if ([module isKindOfClass:[RCTTestCustomInitModule class]]) { + _customInitModuleNotificationSent = YES; + } else if ([module isKindOfClass:[RCTTestCustomSetBridgeModule class]]) { + _customSetBridgeModuleNotificationSent = YES; + } else if ([module isKindOfClass:[RCTTestExportConstantsModule class]]) { + _exportConstantsModuleNotificationSent = YES; + } else if ([module isKindOfClass:[RCTLazyInitModule class]]) { + _lazyInitModuleNotificationSent = YES; + _lazyInitModuleNotificationSentOnMainThread = [NSThread isMainThread]; + } +} + +- (void)testInjectedModulesInitializedDuringBridgeInit +{ + XCTAssertTrue(_injectedModuleInitNotificationSent); + XCTAssertEqual(_injectedModule, [_bridge moduleForClass:[RCTTestInjectedModule class]]); + XCTAssertEqual(_injectedModule.bridge, _bridge.batchedBridge); + XCTAssertNotNil(_injectedModule.methodQueue); +} + +- (void)testCustomInitModuleInitializedAtBridgeStartup +{ + RUN_RUNLOOP_WHILE(!_customInitModuleNotificationSent); + XCTAssertTrue(_customInitModuleNotificationSent); + RCTTestCustomInitModule *module = [_bridge moduleForClass:[RCTTestCustomInitModule class]]; + XCTAssertTrue(module.initializedOnMainThread); +} + +- (void)testCustomSetBridgeModuleInitializedAtBridgeStartup +{ + RUN_RUNLOOP_WHILE(!_customSetBridgeModuleNotificationSent); + XCTAssertTrue(_customSetBridgeModuleNotificationSent); + RCTTestCustomSetBridgeModule *module = [_bridge moduleForClass:[RCTTestCustomSetBridgeModule class]]; + XCTAssertTrue(module.setBridgeOnMainThread); +} + +- (void)testExportConstantsModuleInitializedAtBridgeStartup +{ + RUN_RUNLOOP_WHILE(!_exportConstantsModuleNotificationSent); + XCTAssertTrue(_exportConstantsModuleNotificationSent); + RCTTestExportConstantsModule *module = [_bridge moduleForClass:[RCTTestExportConstantsModule class]]; + RUN_RUNLOOP_WHILE(!module.exportedConstants); + XCTAssertTrue(module.exportedConstants); + XCTAssertTrue(module.exportedConstantsOnMainThread); +} + +- (void)testLazyInitModuleNotInitializedDuringBridgeInit +{ + XCTAssertFalse(_lazyInitModuleNotificationSent); + + __block RCTLazyInitModule *module; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + module = [_bridge moduleForClass:[RCTLazyInitModule class]]; + }); + + RUN_RUNLOOP_WHILE(!module); + XCTAssertTrue(_lazyInitModuleNotificationSent); + XCTAssertFalse(_lazyInitModuleNotificationSentOnMainThread); + XCTAssertNotNil(module); + XCTAssertEqual(module.bridge, _bridge.batchedBridge); + XCTAssertNotNil(module.methodQueue); +} + +@end diff --git a/React/Base/RCTBatchedBridge.m b/React/Base/RCTBatchedBridge.m index b574d62be..3dd44ec9e 100644 --- a/React/Base/RCTBatchedBridge.m +++ b/React/Base/RCTBatchedBridge.m @@ -44,6 +44,7 @@ RCT_EXTERN NSArray *RCTGetModuleClasses(void); @interface RCTBatchedBridge : RCTBridge @property (nonatomic, weak) RCTBridge *parentBridge; +@property (nonatomic, weak) id javaScriptExecutor; @end @@ -52,7 +53,6 @@ RCT_EXTERN NSArray *RCTGetModuleClasses(void); BOOL _loading; BOOL _valid; BOOL _wasBatchActive; - __weak id _javaScriptExecutor; NSMutableArray *_pendingCalls; NSMutableDictionary *_moduleDataByName; NSArray *_moduleDataByID; diff --git a/React/Base/RCTBridge+Private.h b/React/Base/RCTBridge+Private.h index eedda8cd8..c954561ef 100644 --- a/React/Base/RCTBridge+Private.h +++ b/React/Base/RCTBridge+Private.h @@ -54,6 +54,11 @@ @interface RCTBridge (RCTBatchedBridge) +/** + * Used for unit testing, to detect when executor has been invalidated. + */ +@property (nonatomic, weak, readonly) id javaScriptExecutor; + /** * Used by RCTModuleData to register the module for frame updates after it is * lazily initialized. diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index 2c3455d7c..aa939eae6 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -68,7 +68,8 @@ void RCTRegisterModule(Class moduleClass) NSString *RCTBridgeModuleNameForClass(Class cls) { #if RCT_DEV - RCTAssert([cls conformsToProtocol:@protocol(RCTBridgeModule)], @"Bridge module classes must conform to RCTBridgeModule"); + RCTAssert([cls conformsToProtocol:@protocol(RCTBridgeModule)], + @"Bridge module `%@` does not conform to RCTBridgeModule", cls); #endif NSString *name = [cls moduleName]; @@ -104,34 +105,33 @@ dispatch_queue_t RCTJSThread; // Set up JS thread RCTJSThread = (id)kCFNull; -#if RCT_DEBUG + if (RCT_DEBUG && !RCTRunningInTestEnvironment()) { - // Set up module classes - static unsigned int classCount; - Class *classes = objc_copyClassList(&classCount); + // Set up module classes + static unsigned int classCount; + Class *classes = objc_copyClassList(&classCount); - for (unsigned int i = 0; i < classCount; i++) - { - Class cls = classes[i]; - Class superclass = cls; - while (superclass) + for (unsigned int i = 0; i < classCount; i++) { - if (class_conformsToProtocol(superclass, @protocol(RCTBridgeModule))) + Class cls = classes[i]; + Class superclass = cls; + while (superclass) { - if (![RCTModuleClasses containsObject:cls]) { - RCTLogWarn(@"Class %@ was not exported. Did you forget to use " - "RCT_EXPORT_MODULE()?", cls); + if (class_conformsToProtocol(superclass, @protocol(RCTBridgeModule))) + { + if (![RCTModuleClasses containsObject:cls]) { + RCTLogWarn(@"Class %@ was not exported. Did you forget to use " + "RCT_EXPORT_MODULE()?", cls); + } + break; } - break; + superclass = class_getSuperclass(superclass); } - superclass = class_getSuperclass(superclass); } + + free(classes); + } - - free(classes); - -#endif - }); } diff --git a/React/Base/RCTLog.m b/React/Base/RCTLog.m index efcc12659..459d8a666 100644 --- a/React/Base/RCTLog.m +++ b/React/Base/RCTLog.m @@ -16,6 +16,7 @@ #import "RCTBridge+Private.h" #import "RCTDefines.h" #import "RCTRedBox.h" +#import "RCTUtils.h" static NSString *const RCTLogFunctionStack = @"RCTLogFunctionStack"; @@ -226,8 +227,10 @@ void _RCTLogNativeInternal(RCTLogLevel level, const char *fileName, int lineNumb }); } - // Log to JS executor - [[RCTBridge currentBridge] logMessage:message level:level ? @(RCTLogLevels[level]) : @"info"]; + if (!RCTRunningInTestEnvironment()) { + // Log to JS executor + [[RCTBridge currentBridge] logMessage:message level:level ? @(RCTLogLevels[level]) : @"info"]; + } #endif diff --git a/React/Base/RCTModuleData.m b/React/Base/RCTModuleData.m index 5175fb16d..93427f52e 100644 --- a/React/Base/RCTModuleData.m +++ b/React/Base/RCTModuleData.m @@ -238,4 +238,9 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init); _methodQueue = nil; } +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@: %p; name=\"%@\">", [self class], self, self.name]; +} + @end diff --git a/React/Modules/RCTDevMenu.m b/React/Modules/RCTDevMenu.m index 5e1132a21..d9e39b702 100644 --- a/React/Modules/RCTDevMenu.m +++ b/React/Modules/RCTDevMenu.m @@ -369,7 +369,7 @@ RCT_EXPORT_MODULE() if (!sourceCodeModule.scriptURL) { if (!sourceCodeModule) { RCTLogWarn(@"RCTSourceCode module not found"); - } else { + } else if (!RCTRunningInTestEnvironment()) { RCTLogWarn(@"RCTSourceCode module scriptURL has not been set"); } } else if (!sourceCodeModule.scriptURL.fileURL) {