Make several improvements to how tests run under RN
These changes include timeouts when waiting on notifications.
This commit is contained in:
parent
23a7c5b88d
commit
1f030a0618
|
@ -33,8 +33,7 @@ static NSString * const RJSModuleLoaderErrorDomain = @"RJSModuleLoaderErrorDomai
|
||||||
self.globalModules[name] = [JSValue valueWithObject:object inContext:self.context];
|
self.globalModules[name] = [JSValue valueWithObject:object inContext:self.context];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (JSValue *)loadModule:(NSString *)name relativeToURL:(NSURL *)baseURL error:(NSError **)error
|
- (JSValue *)loadModule:(NSString *)name relativeToURL:(NSURL *)baseURL error:(NSError **)error {
|
||||||
{
|
|
||||||
if (![name hasPrefix:@"./"] && ![name hasPrefix:@"../"]) {
|
if (![name hasPrefix:@"./"] && ![name hasPrefix:@"../"]) {
|
||||||
return [self loadGlobalModule:name relativeToURL:baseURL error:error];
|
return [self loadGlobalModule:name relativeToURL:baseURL error:error];
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,23 +26,15 @@
|
||||||
[moduleLoader addGlobalModuleObject:realmConstructor forName:@"realm"];
|
[moduleLoader addGlobalModuleObject:realmConstructor forName:@"realm"];
|
||||||
|
|
||||||
NSError *error;
|
NSError *error;
|
||||||
JSValue *testObjects = [moduleLoader loadModuleFromURL:scriptURL error:&error];
|
JSValue *testObject = [moduleLoader loadModuleFromURL:scriptURL error:&error];
|
||||||
|
NSAssert(testObject, @"%@", error);
|
||||||
|
|
||||||
if (!testObjects) {
|
NSDictionary *testCaseNames = [[testObject invokeMethod:@"getTestNames" withArguments:nil] toDictionary];
|
||||||
NSLog(@"%@", error);
|
NSAssert(testCaseNames.count, @"No test names were provided by the JS");
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
NSDictionary *testCaseNames = [[testObjects invokeMethod:@"getTestNames" withArguments:nil] toDictionary];
|
|
||||||
|
|
||||||
if (!testCaseNames.count) {
|
|
||||||
NSLog(@"No test case names from getTestNames() JS method!");
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (XCTestSuite *testSuite in [self testSuitesFromDictionary:testCaseNames]) {
|
for (XCTestSuite *testSuite in [self testSuitesFromDictionary:testCaseNames]) {
|
||||||
for (RealmJSCoreTests *test in testSuite.tests) {
|
for (RealmJSCoreTests *test in testSuite.tests) {
|
||||||
test.testObject = testObjects[testSuite.name];
|
test.testObject = testObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
[suite addTest:testSuite];
|
[suite addTest:testSuite];
|
||||||
|
@ -57,15 +49,10 @@
|
||||||
|
|
||||||
- (void)invokeMethod:(NSString *)method {
|
- (void)invokeMethod:(NSString *)method {
|
||||||
JSValue *testObject = self.testObject;
|
JSValue *testObject = self.testObject;
|
||||||
|
|
||||||
if (![testObject hasProperty:method]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
JSContext *context = testObject.context;
|
JSContext *context = testObject.context;
|
||||||
context.exception = nil;
|
context.exception = nil;
|
||||||
|
|
||||||
[testObject invokeMethod:method withArguments:nil];
|
[testObject invokeMethod:@"runTest" withArguments:@[NSStringFromClass(self.class), method]];
|
||||||
|
|
||||||
JSValue *exception = context.exception;
|
JSValue *exception = context.exception;
|
||||||
if (exception) {
|
if (exception) {
|
||||||
|
|
|
@ -10,12 +10,17 @@
|
||||||
|
|
||||||
+ (NSArray *)testSuitesFromDictionary:(NSDictionary *)testCaseNames {
|
+ (NSArray *)testSuitesFromDictionary:(NSDictionary *)testCaseNames {
|
||||||
NSMutableArray *testSuites = [[NSMutableArray alloc] init];
|
NSMutableArray *testSuites = [[NSMutableArray alloc] init];
|
||||||
|
NSSet *specialNames = [NSSet setWithObjects:@"beforeEach", @"afterEach", nil];
|
||||||
|
|
||||||
for (NSString *suiteName in testCaseNames) {
|
for (NSString *suiteName in testCaseNames) {
|
||||||
XCTestSuite *testSuite = [[XCTestSuite alloc] initWithName:suiteName];
|
XCTestSuite *testSuite = [[XCTestSuite alloc] initWithName:suiteName];
|
||||||
Class testClass = objc_allocateClassPair(self, suiteName.UTF8String, 0);
|
Class testClass = objc_allocateClassPair(self, suiteName.UTF8String, 0);
|
||||||
|
|
||||||
for (NSString *testName in testCaseNames[suiteName]) {
|
for (NSString *testName in testCaseNames[suiteName]) {
|
||||||
|
if ([specialNames containsObject:testName]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
XCTestCase *testCase = [[testClass alloc] initWithTestName:testName];
|
XCTestCase *testCase = [[testClass alloc] initWithTestName:testName];
|
||||||
[testSuite addTest:testCase];
|
[testSuite addTest:testCase];
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,11 +14,6 @@ exports.extend = function(object) {
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.defineProperties(prototype, {
|
Object.defineProperties(prototype, {
|
||||||
// TODO: Remove once missing undefined check is fixed inside RCTContextExecutor.
|
|
||||||
beforeEach: {
|
|
||||||
value: function() {}
|
|
||||||
},
|
|
||||||
|
|
||||||
afterEach: {
|
afterEach: {
|
||||||
value: function() {
|
value: function() {
|
||||||
Realm.clearTestState();
|
Realm.clearTestState();
|
||||||
|
|
|
@ -4,25 +4,24 @@
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
exports.ListTests = require('./list-tests');
|
var TESTS = {
|
||||||
exports.ObjectTests = require('./object-tests');
|
ListTests: require('./list-tests'),
|
||||||
exports.RealmTests = require('./realm-tests');
|
ObjectTests: require('./object-tests'),
|
||||||
exports.ResultsTests = require('./results-tests');
|
RealmTests: require('./realm-tests'),
|
||||||
exports.QueryTests = require('./query-tests');
|
ResultsTests: require('./results-tests'),
|
||||||
|
QueryTests: require('./query-tests'),
|
||||||
|
};
|
||||||
|
|
||||||
var SPECIAL_METHODS = {
|
var SPECIAL_METHODS = {
|
||||||
beforeEach: true,
|
beforeEach: true,
|
||||||
afterEach: true,
|
afterEach: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only the test suites should be iterable members of exports.
|
exports.getTestNames = function() {
|
||||||
Object.defineProperties(exports, {
|
|
||||||
getTestNames: {
|
|
||||||
value: function() {
|
|
||||||
var testNames = {};
|
var testNames = {};
|
||||||
|
|
||||||
for (var suiteName in exports) {
|
for (var suiteName in TESTS) {
|
||||||
var testSuite = exports[suiteName];
|
var testSuite = TESTS[suiteName];
|
||||||
|
|
||||||
testNames[suiteName] = Object.keys(testSuite).filter(function(testName) {
|
testNames[suiteName] = Object.keys(testSuite).filter(function(testName) {
|
||||||
return !(testName in SPECIAL_METHODS) && typeof testSuite[testName] == 'function';
|
return !(testName in SPECIAL_METHODS) && typeof testSuite[testName] == 'function';
|
||||||
|
@ -30,11 +29,15 @@ Object.defineProperties(exports, {
|
||||||
}
|
}
|
||||||
|
|
||||||
return testNames;
|
return testNames;
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.runTest = function(suiteName, testName) {
|
||||||
|
var testSuite = TESTS[suiteName];
|
||||||
|
var testMethod = testSuite && testSuite[testName];
|
||||||
|
|
||||||
|
if (testMethod) {
|
||||||
|
testMethod.call(testSuite);
|
||||||
|
} else if (!testSuite || !(testName in SPECIAL_METHODS)) {
|
||||||
|
throw new Error('Missing test: ' + suiteName + '.' + testName);
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
runTest: {
|
|
||||||
value: function(suiteName, testName) {
|
|
||||||
exports[suiteName][testName]();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
#import "RCTBridge.h"
|
#import "RCTBridge.h"
|
||||||
#import "RCTDevMenu.h"
|
#import "RCTDevMenu.h"
|
||||||
#import "RCTEventDispatcher.h"
|
#import "RCTEventDispatcher.h"
|
||||||
|
#import "RCTJavaScriptLoader.h"
|
||||||
|
#import "RCTLog.h"
|
||||||
|
|
||||||
@import RealmReact;
|
@import RealmReact;
|
||||||
|
|
||||||
|
@ -29,6 +31,10 @@ extern NSMutableArray *RCTGetModuleClasses(void);
|
||||||
+ (void)load {
|
+ (void)load {
|
||||||
NSMutableArray *moduleClasses = RCTGetModuleClasses();
|
NSMutableArray *moduleClasses = RCTGetModuleClasses();
|
||||||
[moduleClasses removeObject:[RCTDevMenu class]];
|
[moduleClasses removeObject:[RCTDevMenu class]];
|
||||||
|
|
||||||
|
RCTAddLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) {
|
||||||
|
NSAssert(level < RCTLogLevelError, RCTFormatLog(nil, level, fileName, lineNumber, message));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
+ (Class)executorClass {
|
+ (Class)executorClass {
|
||||||
|
@ -84,10 +90,7 @@ extern NSMutableArray *RCTGetModuleClasses(void);
|
||||||
}
|
}
|
||||||
|
|
||||||
NSDictionary *testCaseNames = [self waitForEvent:@"realm-test-names"];
|
NSDictionary *testCaseNames = [self waitForEvent:@"realm-test-names"];
|
||||||
if (!testCaseNames.count) {
|
NSAssert(testCaseNames.count, @"No test names were provided by the JS");
|
||||||
NSLog(@"ERROR: No test names were provided by the JS");
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
NSString *nameSuffix = [self classNameSuffix];
|
NSString *nameSuffix = [self classNameSuffix];
|
||||||
if (nameSuffix.length) {
|
if (nameSuffix.length) {
|
||||||
|
@ -115,25 +118,37 @@ extern NSMutableArray *RCTGetModuleClasses(void);
|
||||||
notification = note;
|
notification = note;
|
||||||
}];
|
}];
|
||||||
|
|
||||||
[self waitForCondition:&condition];
|
@try {
|
||||||
|
[self waitForCondition:&condition description:notificationName];
|
||||||
|
} @finally {
|
||||||
[nc removeObserver:token];
|
[nc removeObserver:token];
|
||||||
|
}
|
||||||
|
|
||||||
return notification;
|
return notification;
|
||||||
}
|
}
|
||||||
|
|
||||||
+ (void)waitForCondition:(BOOL *)condition {
|
+ (void)waitForCondition:(BOOL *)condition description:(NSString *)description {
|
||||||
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
|
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
|
||||||
|
NSDate *timeout = [NSDate dateWithTimeIntervalSinceNow:10.0];
|
||||||
|
|
||||||
while (!*condition) {
|
while (!*condition) {
|
||||||
|
if ([timeout timeIntervalSinceNow] < 0) {
|
||||||
|
@throw [NSException exceptionWithName:@"ConditionTimeout"
|
||||||
|
reason:[NSString stringWithFormat:@"Timed out waiting for: %@", description]
|
||||||
|
userInfo:nil];
|
||||||
|
}
|
||||||
|
|
||||||
@autoreleasepool {
|
@autoreleasepool {
|
||||||
[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
|
[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
|
||||||
|
[runLoop runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
|
||||||
|
[NSThread sleepForTimeInterval:0.01]; // Bad things may happen without some sleep.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
+ (id)waitForEvent:(NSString *)eventName {
|
+ (id)waitForEvent:(NSString *)eventName {
|
||||||
__weak RealmReact *realmModule = [[self currentBridge] moduleForClass:[RealmReact class]];
|
__weak RealmReact *realmModule = [[self currentBridge] moduleForClass:[RealmReact class]];
|
||||||
assert(realmModule);
|
NSAssert(realmModule, @"RealmReact module not found");
|
||||||
|
|
||||||
__block BOOL condition = NO;
|
__block BOOL condition = NO;
|
||||||
__block id result;
|
__block id result;
|
||||||
|
@ -148,10 +163,28 @@ extern NSMutableArray *RCTGetModuleClasses(void);
|
||||||
|
|
||||||
[realmModule addListenerForEvent:eventName handler:handler];
|
[realmModule addListenerForEvent:eventName handler:handler];
|
||||||
|
|
||||||
[self waitForCondition:&condition];
|
[self waitForCondition:&condition description:eventName];
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)invokeTest {
|
||||||
|
RCTLogFunction logFunction = RCTGetLogFunction();
|
||||||
|
|
||||||
|
// Fail when React Native logs an error.
|
||||||
|
RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) {
|
||||||
|
RCTDefaultLogFunction(level, source, fileName, lineNumber, message);
|
||||||
|
|
||||||
|
if (level >= RCTLogLevelError) {
|
||||||
|
NSString *type = (source == RCTLogSourceJavaScript) ? @"JS" : @"Native";
|
||||||
|
XCTFail(@"%@ Error: %@", type, RCTFormatLog(nil, level, fileName, lineNumber, message));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
[super invokeTest];
|
||||||
|
|
||||||
|
RCTSetLogFunction(logFunction);
|
||||||
|
}
|
||||||
|
|
||||||
- (void)invokeMethod:(NSString *)method {
|
- (void)invokeMethod:(NSString *)method {
|
||||||
NSString *module = NSStringFromClass(self.class);
|
NSString *module = NSStringFromClass(self.class);
|
||||||
NSString *suffix = [self.class classNameSuffix];
|
NSString *suffix = [self.class classNameSuffix];
|
||||||
|
@ -163,7 +196,13 @@ extern NSMutableArray *RCTGetModuleClasses(void);
|
||||||
RCTBridge *bridge = [self.class currentBridge];
|
RCTBridge *bridge = [self.class currentBridge];
|
||||||
[bridge.eventDispatcher sendAppEventWithName:@"realm-run-test" body:@{@"suite": module, @"name": method}];
|
[bridge.eventDispatcher sendAppEventWithName:@"realm-run-test" body:@{@"suite": module, @"name": method}];
|
||||||
|
|
||||||
id error = [self.class waitForEvent:@"realm-test-finished"];
|
id error;
|
||||||
|
@try {
|
||||||
|
error = [self.class waitForEvent:@"realm-test-finished"];
|
||||||
|
} @catch (id exception) {
|
||||||
|
error = exception;
|
||||||
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
[self recordFailureWithDescription:[error description] inFile:@(__FILE__) atLine:__LINE__ expected:YES];
|
[self recordFailureWithDescription:[error description] inFile:@(__FILE__) atLine:__LINE__ expected:YES];
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue