react-native-macos/RNTester/RNTesterUnitTests/RCTAllocationTests.m

208 строки
6.6 KiB
Mathematica
Исходник Обычный вид История

/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <Foundation/Foundation.h>
#import <XCTest/XCTest.h>
#import <RCTTest/RCTTestRunner.h>
#import <React/RCTBridge+Private.h>
#import <React/RCTBridge.h>
#import <React/RCTModuleMethod.h>
#import <React/RCTRootView.h>
@interface AllocationTestModule : NSObject<RCTBridgeModule, RCTInvalidating>
@property (nonatomic, assign, getter=isValid) BOOL valid;
@end
@implementation AllocationTestModule
RCT_EXPORT_MODULE();
- (instancetype)init
{
if ((self = [super init])) {
_valid = YES;
}
return self;
}
- (void)invalidate
{
_valid = NO;
}
RCT_EXPORT_METHOD(test:(__unused NSString *)a
:(__unused NSNumber *)b
:(__unused RCTResponseSenderBlock)c
:(__unused RCTResponseErrorBlock)d) {}
@end
@interface RCTAllocationTests : XCTestCase
@end
@implementation RCTAllocationTests {
NSURL *_bundleURL;
}
- (void)setUp
{
[super setUp];
NSString *bundleContents =
@"var __fbBatchedBridge = {"
" callFunctionReturnFlushedQueue: function() { return null; },"
" invokeCallbackAndReturnFlushedQueue: function() { return null; },"
" flushedQueue: function() { return null; },"
"};";
NSURL *tempDir = [NSURL fileURLWithPath:NSTemporaryDirectory() isDirectory:YES];
[[NSFileManager defaultManager] createDirectoryAtURL:tempDir withIntermediateDirectories:YES attributes:nil error:NULL];
NSString *guid = [[NSProcessInfo processInfo] globallyUniqueString];
NSString *fileName = [NSString stringWithFormat:@"rctallocationtests-bundle-%@.js", guid];
_bundleURL = [tempDir URLByAppendingPathComponent:fileName];
NSError *saveError;
if (![bundleContents writeToURL:_bundleURL atomically:YES encoding:NSUTF8StringEncoding error:&saveError]) {
XCTFail(@"Failed to save test bundle to %@, error: %@", _bundleURL, saveError);
};
}
- (void)tearDown
{
[super tearDown];
[[NSFileManager defaultManager] removeItemAtURL:_bundleURL error:NULL];
}
- (void)testBridgeIsDeallocated
{
__weak RCTBridge *weakBridge;
@autoreleasepool {
RCTRootView *view = [[RCTRootView alloc] initWithBundleURL:_bundleURL
moduleName:@""
initialProperties:nil
launchOptions:nil];
weakBridge = view.bridge;
XCTAssertNotNil(weakBridge, @"RCTBridge should have been created");
(void)view;
}
XCTAssertNil(weakBridge, @"RCTBridge should have been deallocated");
}
- (void)testModulesAreInvalidated
{
AllocationTestModule *module = [AllocationTestModule new];
@autoreleasepool {
RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:_bundleURL
moduleProvider:^{ return @[module]; }
launchOptions:nil];
XCTAssertTrue(module.isValid, @"AllocationTestModule should be valid");
(void)bridge;
}
RCT_RUN_RUNLOOP_WHILE(module.isValid)
XCTAssertFalse(module.isValid, @"AllocationTestModule should have been invalidated by the bridge");
}
- (void)testModulesAreDeallocated
{
__weak AllocationTestModule *weakModule;
@autoreleasepool {
AllocationTestModule *module = [AllocationTestModule new];
RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:_bundleURL
moduleProvider:^{ return @[module]; }
launchOptions:nil];
XCTAssertNotNil(module, @"AllocationTestModule should have been created");
weakModule = module;
(void)bridge;
}
RCT_RUN_RUNLOOP_WHILE(weakModule)
XCTAssertNil(weakModule, @"AllocationTestModule should have been deallocated");
}
- (void)testModuleMethodsAreDeallocated
{
static RCTMethodInfo methodInfo = {
.objcName = "test:(NSString *)a :(nonnull NSNumber *)b :(RCTResponseSenderBlock)c :(RCTResponseErrorBlock)d",
.jsName = "",
.isSync = false
};
__weak RCTModuleMethod *weakMethod;
@autoreleasepool {
__autoreleasing RCTModuleMethod *method = [[RCTModuleMethod alloc] initWithExportedMethod:&methodInfo
moduleClass:[AllocationTestModule class]];
XCTAssertNotNil(method, @"RCTModuleMethod should have been created");
weakMethod = method;
}
RCT_RUN_RUNLOOP_WHILE(weakMethod)
XCTAssertNil(weakMethod, @"RCTModuleMethod should have been deallocated");
}
- (void)testContentViewIsInvalidated
{
RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:_bundleURL
moduleProvider:nil
launchOptions:nil];
__weak UIView *rootContentView;
@autoreleasepool {
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@"" initialProperties:nil];
RCT_RUN_RUNLOOP_WHILE(!(rootContentView = [rootView valueForKey:@"contentView"]))
XCTAssertTrue(rootContentView.userInteractionEnabled, @"RCTContentView should be valid");
(void)rootView;
}
#if !TARGET_OS_TV // userInteractionEnabled is true for Apple TV views
XCTAssertFalse(rootContentView.userInteractionEnabled, @"RCTContentView should have been invalidated");
#endif
}
Disable flaky RNTester test Summary: `[RCTBridge setUp]` and `[RCTBridge invalidate]` execute asynchronously and concurrently. Therefore, it's not safe to call one method after the other, as we do in `[RCTBridge reload]`. In this test, we create a bridge, and immediately reload it. Initializing the bridge causes the JS bundle to execute. Invalidating the bridge causes the jsThread to be terminated. If circumstances are correct, we could end up trying to executing the JS bundle after the jsThread has been terminated, which can lead to these assertions being triggered: 1. `RCTAssert(_jsThread, @"This method must not be called before the JS thread is created");` in `ensureOnJavaScriptThread:`. 2. `RCTAssert(_jsMessageThread != nullptr, @"Cannot invoke completion without jsMessageThread");` in `enqueueApplicationScript:url:onComplete:`. ``` - (void)testUnderlyingBridgeIsDeallocated { RCTBridge *bridge; __weak id batchedBridge; autoreleasepool { bridge = [[RCTBridge alloc] initWithBundleURL:_bundleURL moduleProvider:nil launchOptions:nil]; batchedBridge = bridge.batchedBridge; XCTAssertTrue([batchedBridge isValid], @"RCTBridge impl should be valid"); [bridge reload]; } RCT_RUN_RUNLOOP_WHILE(batchedBridge != nil) XCTAssertNotNil(bridge, @"RCTBridge should not have been deallocated"); XCTAssertNil(batchedBridge, @"RCTBridge impl should have been deallocated"); // Wait to complete the test until the new bridge impl is also deallocated autoreleasepool { batchedBridge = bridge.batchedBridge; [bridge invalidate]; bridge = nil; } RCT_RUN_RUNLOOP_WHILE(batchedBridge != nil); XCTAssertNil(batchedBridge); } ``` To verify that this race is real, patch: P62410422. This adds an artificial delay in the `[RCTCxxBridge start]` method, which makes it so that the bridge is invalidated and the js thread is destroyed before we start executing the jsBundle. I think a proper solution to this problem would require some bit of restructuring of `[RCTCxxBridge invalidate]` and `[RCTCxxBridge start]` to either: 1. Force `[RCTCxxBridge invalidate]` to wait for `[RCTCxxBridge start]` to complete and vice versa. 2. Make it safe to interleave execution of `[RCTCxxBridge start]` and `[RCTCxxBridge invalidate]`. I tried the first approach using two semaphores: `_startSem(1)` and `_invalidateSem(0)`. When you start executing the code inside `[RCTCxxBridge start]`, you `semWait(_startSem)`. When you stop executing the code inside `[RCTCxxBridge start]` (which could happen in another thread at some later point in time), you `semSignal(_invalidateSem)`. Likewise, when you start executing `[RCTCxxBridge invalidate]`, you `semWait(_invalidateSem)` and when you stop executing the code inside `[RCTCxxBridge invalidate]` you `semSignal(_startSem)`. This way, invalidates always wait for starts to finish, and starts always wait for invalidates to finish. But considering all the concurrency involved in these methods, this is hard to get right. The second approach seems possible. You could keep locks for the shared data, and create critical sections whever you want to access that data. I didn't actually try to implement this approach though. Given that we're going to elminate the Bridge anyway, and that this race condition practically only occurs when you reload imediately after initializing the bridge (which can only really be done programmatically), I think it's fine to just disable the test for now. One other thing I considered was making the current thread sleep for some time after we created the bridge in the test. The reason why I'm hesitant to implement this approach is that it would slow down the execution of the test suite and still wouldn't guarantee that we don't hit this race condition. Ultimately, our infra might end up disabling these tests again. Reviewed By: shergin Differential Revision: D14909121 fbshipit-source-id: d7d441c3e2f0ad59182c8c7e23740be4ac4cf83c
2019-04-13 00:38:16 +03:00
/**
* T42930872:
*
* Both bridge invalidation and bridge setUp occur execute concurrently.
Disable flaky RNTester test Summary: `[RCTBridge setUp]` and `[RCTBridge invalidate]` execute asynchronously and concurrently. Therefore, it's not safe to call one method after the other, as we do in `[RCTBridge reload]`. In this test, we create a bridge, and immediately reload it. Initializing the bridge causes the JS bundle to execute. Invalidating the bridge causes the jsThread to be terminated. If circumstances are correct, we could end up trying to executing the JS bundle after the jsThread has been terminated, which can lead to these assertions being triggered: 1. `RCTAssert(_jsThread, @"This method must not be called before the JS thread is created");` in `ensureOnJavaScriptThread:`. 2. `RCTAssert(_jsMessageThread != nullptr, @"Cannot invoke completion without jsMessageThread");` in `enqueueApplicationScript:url:onComplete:`. ``` - (void)testUnderlyingBridgeIsDeallocated { RCTBridge *bridge; __weak id batchedBridge; autoreleasepool { bridge = [[RCTBridge alloc] initWithBundleURL:_bundleURL moduleProvider:nil launchOptions:nil]; batchedBridge = bridge.batchedBridge; XCTAssertTrue([batchedBridge isValid], @"RCTBridge impl should be valid"); [bridge reload]; } RCT_RUN_RUNLOOP_WHILE(batchedBridge != nil) XCTAssertNotNil(bridge, @"RCTBridge should not have been deallocated"); XCTAssertNil(batchedBridge, @"RCTBridge impl should have been deallocated"); // Wait to complete the test until the new bridge impl is also deallocated autoreleasepool { batchedBridge = bridge.batchedBridge; [bridge invalidate]; bridge = nil; } RCT_RUN_RUNLOOP_WHILE(batchedBridge != nil); XCTAssertNil(batchedBridge); } ``` To verify that this race is real, patch: P62410422. This adds an artificial delay in the `[RCTCxxBridge start]` method, which makes it so that the bridge is invalidated and the js thread is destroyed before we start executing the jsBundle. I think a proper solution to this problem would require some bit of restructuring of `[RCTCxxBridge invalidate]` and `[RCTCxxBridge start]` to either: 1. Force `[RCTCxxBridge invalidate]` to wait for `[RCTCxxBridge start]` to complete and vice versa. 2. Make it safe to interleave execution of `[RCTCxxBridge start]` and `[RCTCxxBridge invalidate]`. I tried the first approach using two semaphores: `_startSem(1)` and `_invalidateSem(0)`. When you start executing the code inside `[RCTCxxBridge start]`, you `semWait(_startSem)`. When you stop executing the code inside `[RCTCxxBridge start]` (which could happen in another thread at some later point in time), you `semSignal(_invalidateSem)`. Likewise, when you start executing `[RCTCxxBridge invalidate]`, you `semWait(_invalidateSem)` and when you stop executing the code inside `[RCTCxxBridge invalidate]` you `semSignal(_startSem)`. This way, invalidates always wait for starts to finish, and starts always wait for invalidates to finish. But considering all the concurrency involved in these methods, this is hard to get right. The second approach seems possible. You could keep locks for the shared data, and create critical sections whever you want to access that data. I didn't actually try to implement this approach though. Given that we're going to elminate the Bridge anyway, and that this race condition practically only occurs when you reload imediately after initializing the bridge (which can only really be done programmatically), I think it's fine to just disable the test for now. One other thing I considered was making the current thread sleep for some time after we created the bridge in the test. The reason why I'm hesitant to implement this approach is that it would slow down the execution of the test suite and still wouldn't guarantee that we don't hit this race condition. Ultimately, our infra might end up disabling these tests again. Reviewed By: shergin Differential Revision: D14909121 fbshipit-source-id: d7d441c3e2f0ad59182c8c7e23740be4ac4cf83c
2019-04-13 00:38:16 +03:00
* Therefore, it's not safe for us to create a bridge, and immediately reload on
* it. It's also unsafe to just reload the bridge, because that calls invalidate
* and then setUp. Because of these race conditions, this test may randomly
* crash. Hence, we should disable this test until we either fix the bridge
* or delete it.
*/
- (void)disabled_testUnderlyingBridgeIsDeallocated
{
RCTBridge *bridge;
__weak id batchedBridge;
@autoreleasepool {
bridge = [[RCTBridge alloc] initWithBundleURL:_bundleURL moduleProvider:nil launchOptions:nil];
batchedBridge = bridge.batchedBridge;
XCTAssertTrue([batchedBridge isValid], @"RCTBridge impl should be valid");
[bridge reload];
}
RCT_RUN_RUNLOOP_WHILE(batchedBridge != nil)
XCTAssertNotNil(bridge, @"RCTBridge should not have been deallocated");
XCTAssertNil(batchedBridge, @"RCTBridge impl should have been deallocated");
// Wait to complete the test until the new bridge impl is also deallocated
@autoreleasepool {
batchedBridge = bridge.batchedBridge;
[bridge invalidate];
bridge = nil;
}
RCT_RUN_RUNLOOP_WHILE(batchedBridge != nil);
XCTAssertNil(batchedBridge);
}
@end