Make several improvements to how tests run under RN

These changes include timeouts when waiting on notifications.
This commit is contained in:
Scott Kyle 2016-02-15 14:22:40 -08:00
parent 23a7c5b88d
commit 1f030a0618
6 changed files with 90 additions and 62 deletions

View File

@ -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];
} }

View File

@ -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) {

View File

@ -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];
} }

View File

@ -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();

View File

@ -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]();
}
},
});

View File

@ -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];
} }