blob: bfae7040e54db70622e147529b89d038ebbb108a [file] [log] [blame]
//
// GTMIPhoneUnitTestDelegate.m
//
// Copyright 2008 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy
// of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
//
#import "GTMIPhoneUnitTestDelegate.h"
#import "GTMCodeCoverageApp.h"
#import "GTMDefines.h"
#if !GTM_IPHONE_SDK
#error GTMIPhoneUnitTestDelegate for iPhone only
#endif
#import <objc/runtime.h>
#import <stdio.h>
#import <UIKit/UIKit.h>
#import "GTMSenTestCase.h"
#pragma clang diagnostic push
#if !GTM_USING_XCTEST
// Turn off the deprecated warning when GTMTestCase is still based on SenTest.
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#endif // !GTM_USING_XCTEST
@interface UIApplication (GTMIPhoneUnitTestDelegate)
// SPI that we need to exit cleanly with a value.
- (void)_terminateWithStatus:(int)status;
@end
@interface GTMIPhoneUnitTestDelegate ()
// We have cases where we are created in UIApplicationMain, but then the
// user accidentally/intentionally replaces us as a delegate in their xib file
// which means that we never get the applicationDidFinishLaunching: message.
// We can register for the notification, but when the applications delegate
// is reset, it releases us, and we get dealloced. Therefore we have retainer
// which is responsible for retaining us until we get the notification.
// We do it through this slightly roundabout route (instead of just an extra
// retain in the init) so that clang doesn't complain about a leak.
// We also check to make sure we aren't called twice with the
// applicationDidFinishLaunchingCalled flag.
@property (readwrite, retain, nonatomic) GTMIPhoneUnitTestDelegate *retainer;
- (void)runTestsAndExit:(UIApplication *)application;
@end
@implementation GTMIPhoneUnitTestDelegate
@synthesize retainer = retainer_;
- (id)init {
if ((self = [super init])) {
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self
selector:@selector(handleApplicationDidFinishLaunchingNotification:)
name:UIApplicationDidFinishLaunchingNotification
object:[UIApplication sharedApplication]];
[self setRetainer:self];
}
return self;
}
// If running in the mode were we get the notification, bridge it over to
// the method we'd get if we are the app delegate.
- (void)handleApplicationDidFinishLaunchingNotification:(NSNotification *)note {
[self applicationDidFinishLaunching:[note object]];
}
// Run through all the registered classes and run test methods on any
// that are subclasses of SenTestCase. Terminate the application upon
// test completion.
- (void)applicationDidFinishLaunching:(UIApplication *)application {
// We could get called twice once from our notification registration, and
// once if we actually still are the delegate of the application after
// it has finished launching. So we'll just return if we've been called once.
if (applicationDidFinishLaunchingCalled_) return;
applicationDidFinishLaunchingCalled_ = YES;
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc removeObserver:self
name:UIApplicationDidFinishLaunchingNotification
object:application];
// To permit UI-based tests, run the tests and exit app in a delayed selector
// call. This ensures tests are run outside of applicationDidFinishLaunching:.
[self performSelector:@selector(runTestsAndExit:)
withObject:application
afterDelay:0];
}
// Run through all tests and then exit application if required.
- (void)runTestsAndExit:(UIApplication *)application {
[self runTests];
if ([application respondsToSelector:@selector(gtm_gcov_flush)]) {
[application performSelector:@selector(gtm_gcov_flush)];
}
if (!getenv("GTM_DISABLE_TERMINATION")) {
// To help using xcodebuild, make the exit status 0/1 to signal the tests
// success/failure.
int exitStatus = (([self totalFailures] == 0U) ? 0 : 1);
// Alternative to exit(status); so it cleanly terminates the UIApplication
// and classes that depend on this signal to exit cleanly.
NSMethodSignature * terminateSignature
= [UIApplication instanceMethodSignatureForSelector:@selector(_terminateWithStatus:)];
if (terminateSignature != nil) {
NSInvocation * terminateInvocation
= [NSInvocation invocationWithMethodSignature:terminateSignature];
[terminateInvocation setTarget:application];
[terminateInvocation setSelector:@selector(_terminateWithStatus:)];
[terminateInvocation setArgument:&exitStatus atIndex:2];
[terminateInvocation invoke];
} else {
exit(exitStatus);
}
}
// Release ourself now that we're done. If we really are the application
// delegate, it will have retained us, so we'll stick around if necessary.
[self setRetainer:nil];
}
static int ClassSort(const void *a, const void *b) {
Class *classA = (Class *)a;
Class *classB = (Class *)b;
const char *nameA = class_getName(*classA);
const char *nameB = class_getName(*classB);
return strcmp(nameA, nameB);
}
// Run through all the registered classes and run test methods on any
// that are subclasses of SenTestCase. Print results and run time to
// the default output.
- (void)runTests {
int count = objc_getClassList(NULL, 0);
NSMutableData *classData
= [NSMutableData dataWithLength:sizeof(Class) * count];
Class *classes = (Class*)[classData mutableBytes];
_GTMDevAssert(classes, @"Couldn't allocate class list");
objc_getClassList(classes, count);
// Follow SenTest's lead and sort the classes. (This may only be a change
// in the iOS 5 runtime, but make sure it is always sorted just incase)
mergesort(classes, count, sizeof(Class), ClassSort);
totalFailures_ = 0;
totalSuccesses_ = 0;
NSString *suiteName = [[NSBundle mainBundle] bundlePath];
NSDate *suiteStartDate = [NSDate date];
NSString *suiteStartString
= [NSString stringWithFormat:@"Test Suite '%@' started at %@\n",
suiteName, suiteStartDate];
fputs([suiteStartString UTF8String], stderr);
fflush(stderr);
for (int i = 0; i < count; ++i) {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
Class currClass = classes[i];
if (class_respondsToSelector(currClass, @selector(conformsToProtocol:)) &&
[currClass conformsToProtocol:@protocol(SenTestCase)]) {
NSArray *invocations = [currClass testInvocations];
if ([invocations count]) {
NSDate *fixtureStartDate = [NSDate date];
NSString *fixtureName = NSStringFromClass(currClass);
NSString *fixtureStartString
= [NSString stringWithFormat:@"Test Suite '%@' started at %@\n",
fixtureName, fixtureStartDate];
int fixtureSuccesses = 0;
int fixtureFailures = 0;
fputs([fixtureStartString UTF8String], stderr);
fflush(stderr);
NSInvocation *invocation;
for (invocation in invocations) {
GTMTestCase *testCase
= [[currClass alloc] initWithInvocation:invocation];
BOOL failed = NO;
NSDate *caseStartDate = [NSDate date];
NSString *selectorName = NSStringFromSelector([invocation selector]);
NSString *caseStartString
= [NSString stringWithFormat:@"Test Case '-[%@ %@]' started.\n",
fixtureName, selectorName];
fputs([caseStartString UTF8String], stderr);
fflush(stderr);
@try {
[testCase performTest];
} @catch (NSException *exception) {
failed = YES;
}
if (failed) {
fixtureFailures += 1;
} else {
fixtureSuccesses += 1;
}
NSTimeInterval caseEndTime
= [[NSDate date] timeIntervalSinceDate:caseStartDate];
NSString *caseEndString
= [NSString stringWithFormat:@"Test Case '-[%@ %@]' %@ (%0.3f "
"seconds).\n",
fixtureName, selectorName,
failed ? @"failed" : @"passed",
caseEndTime];
fputs([caseEndString UTF8String], stderr);
fflush(stderr);
[testCase release];
}
NSDate *fixtureEndDate = [NSDate date];
NSTimeInterval fixtureEndTime
= [fixtureEndDate timeIntervalSinceDate:fixtureStartDate];
NSString *fixtureEndString
= [NSString stringWithFormat:@"Test Suite '%@' finished at %@.\n"
"Executed %d tests, with %d failures (%d "
"unexpected) in %0.3f (%0.3f) seconds\n\n",
fixtureName, fixtureEndDate,
fixtureSuccesses + fixtureFailures,
fixtureFailures, fixtureFailures,
fixtureEndTime, fixtureEndTime];
fputs([fixtureEndString UTF8String], stderr);
fflush(stderr);
totalSuccesses_ += fixtureSuccesses;
totalFailures_ += fixtureFailures;
}
}
[pool release];
}
NSDate *suiteEndDate = [NSDate date];
NSTimeInterval suiteEndTime
= [suiteEndDate timeIntervalSinceDate:suiteStartDate];
NSString *suiteEndString
= [NSString stringWithFormat:@"Test Suite '%@' finished at %@.\n"
"Executed %tu tests, with %tu failures (%tu "
"unexpected) in %0.3f (%0.3f) seconds\n\n",
suiteName, suiteEndDate,
totalSuccesses_ + totalFailures_,
totalFailures_, totalFailures_,
suiteEndTime, suiteEndTime];
fputs([suiteEndString UTF8String], stderr);
fflush(stderr);
}
- (NSUInteger)totalSuccesses {
return totalSuccesses_;
}
- (NSUInteger)totalFailures {
return totalFailures_;
}
@end
#pragma clang diagnostic pop