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];
}
- (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:@"../"]) {
return [self loadGlobalModule:name relativeToURL:baseURL error:error];
}

View File

@ -26,23 +26,15 @@
[moduleLoader addGlobalModuleObject:realmConstructor forName:@"realm"];
NSError *error;
JSValue *testObjects = [moduleLoader loadModuleFromURL:scriptURL error:&error];
JSValue *testObject = [moduleLoader loadModuleFromURL:scriptURL error:&error];
NSAssert(testObject, @"%@", error);
if (!testObjects) {
NSLog(@"%@", error);
exit(1);
}
NSDictionary *testCaseNames = [[testObjects invokeMethod:@"getTestNames" withArguments:nil] toDictionary];
if (!testCaseNames.count) {
NSLog(@"No test case names from getTestNames() JS method!");
exit(1);
}
NSDictionary *testCaseNames = [[testObject invokeMethod:@"getTestNames" withArguments:nil] toDictionary];
NSAssert(testCaseNames.count, @"No test names were provided by the JS");
for (XCTestSuite *testSuite in [self testSuitesFromDictionary:testCaseNames]) {
for (RealmJSCoreTests *test in testSuite.tests) {
test.testObject = testObjects[testSuite.name];
test.testObject = testObject;
}
[suite addTest:testSuite];
@ -57,15 +49,10 @@
- (void)invokeMethod:(NSString *)method {
JSValue *testObject = self.testObject;
if (![testObject hasProperty:method]) {
return;
}
JSContext *context = testObject.context;
context.exception = nil;
[testObject invokeMethod:method withArguments:nil];
[testObject invokeMethod:@"runTest" withArguments:@[NSStringFromClass(self.class), method]];
JSValue *exception = context.exception;
if (exception) {

View File

@ -10,12 +10,17 @@
+ (NSArray *)testSuitesFromDictionary:(NSDictionary *)testCaseNames {
NSMutableArray *testSuites = [[NSMutableArray alloc] init];
NSSet *specialNames = [NSSet setWithObjects:@"beforeEach", @"afterEach", nil];
for (NSString *suiteName in testCaseNames) {
XCTestSuite *testSuite = [[XCTestSuite alloc] initWithName:suiteName];
Class testClass = objc_allocateClassPair(self, suiteName.UTF8String, 0);
for (NSString *testName in testCaseNames[suiteName]) {
if ([specialNames containsObject:testName]) {
continue;
}
XCTestCase *testCase = [[testClass alloc] initWithTestName:testName];
[testSuite addTest:testCase];
}

View File

@ -14,11 +14,6 @@ exports.extend = function(object) {
};
Object.defineProperties(prototype, {
// TODO: Remove once missing undefined check is fixed inside RCTContextExecutor.
beforeEach: {
value: function() {}
},
afterEach: {
value: function() {
Realm.clearTestState();

View File

@ -4,37 +4,40 @@
'use strict';
exports.ListTests = require('./list-tests');
exports.ObjectTests = require('./object-tests');
exports.RealmTests = require('./realm-tests');
exports.ResultsTests = require('./results-tests');
exports.QueryTests = require('./query-tests');
var TESTS = {
ListTests: require('./list-tests'),
ObjectTests: require('./object-tests'),
RealmTests: require('./realm-tests'),
ResultsTests: require('./results-tests'),
QueryTests: require('./query-tests'),
};
var SPECIAL_METHODS = {
beforeEach: true,
afterEach: true,
};
// Only the test suites should be iterable members of exports.
Object.defineProperties(exports, {
getTestNames: {
value: function() {
var testNames = {};
exports.getTestNames = function() {
var testNames = {};
for (var suiteName in exports) {
var testSuite = exports[suiteName];
for (var suiteName in TESTS) {
var testSuite = TESTS[suiteName];
testNames[suiteName] = Object.keys(testSuite).filter(function(testName) {
return !(testName in SPECIAL_METHODS) && typeof testSuite[testName] == 'function';
});
}
testNames[suiteName] = Object.keys(testSuite).filter(function(testName) {
return !(testName in SPECIAL_METHODS) && typeof testSuite[testName] == 'function';
});
}
return testNames;
}
},
runTest: {
value: function(suiteName, testName) {
exports[suiteName][testName]();
}
},
});
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);
}
};

View File

@ -7,6 +7,8 @@
#import "RCTBridge.h"
#import "RCTDevMenu.h"
#import "RCTEventDispatcher.h"
#import "RCTJavaScriptLoader.h"
#import "RCTLog.h"
@import RealmReact;
@ -29,6 +31,10 @@ extern NSMutableArray *RCTGetModuleClasses(void);
+ (void)load {
NSMutableArray *moduleClasses = RCTGetModuleClasses();
[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 {
@ -84,10 +90,7 @@ extern NSMutableArray *RCTGetModuleClasses(void);
}
NSDictionary *testCaseNames = [self waitForEvent:@"realm-test-names"];
if (!testCaseNames.count) {
NSLog(@"ERROR: No test names were provided by the JS");
exit(1);
}
NSAssert(testCaseNames.count, @"No test names were provided by the JS");
NSString *nameSuffix = [self classNameSuffix];
if (nameSuffix.length) {
@ -115,25 +118,37 @@ extern NSMutableArray *RCTGetModuleClasses(void);
notification = note;
}];
[self waitForCondition:&condition];
[nc removeObserver:token];
@try {
[self waitForCondition:&condition description:notificationName];
} @finally {
[nc removeObserver:token];
}
return notification;
}
+ (void)waitForCondition:(BOOL *)condition {
+ (void)waitForCondition:(BOOL *)condition description:(NSString *)description {
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
NSDate *timeout = [NSDate dateWithTimeIntervalSinceNow:10.0];
while (!*condition) {
if ([timeout timeIntervalSinceNow] < 0) {
@throw [NSException exceptionWithName:@"ConditionTimeout"
reason:[NSString stringWithFormat:@"Timed out waiting for: %@", description]
userInfo:nil];
}
@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 {
__weak RealmReact *realmModule = [[self currentBridge] moduleForClass:[RealmReact class]];
assert(realmModule);
NSAssert(realmModule, @"RealmReact module not found");
__block BOOL condition = NO;
__block id result;
@ -148,10 +163,28 @@ extern NSMutableArray *RCTGetModuleClasses(void);
[realmModule addListenerForEvent:eventName handler:handler];
[self waitForCondition:&condition];
[self waitForCondition:&condition description:eventName];
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 {
NSString *module = NSStringFromClass(self.class);
NSString *suffix = [self.class classNameSuffix];
@ -163,7 +196,13 @@ extern NSMutableArray *RCTGetModuleClasses(void);
RCTBridge *bridge = [self.class currentBridge];
[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) {
[self recordFailureWithDescription:[error description] inFile:@(__FILE__) atLine:__LINE__ expected:YES];
}