Created
November 7, 2013 15:16
-
-
Save depth42/7356279 to your computer and use it in GitHub Desktop.
Workaround for XCTest not reporting failing NSAssertions.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#import "PWAssertionHandler.h" | |
#import "PWLog.h" | |
#import "Intercept_objc_exception_throw.h" | |
#import <objc/runtime.h> | |
#import "PWDispatch.h" | |
#ifndef NDEBUG | |
@interface NSAssertionHandler (PWExtensions) | |
@property (nonatomic, readonly) BOOL isPWHandler; | |
@end | |
#pragma mark - | |
@implementation NSAssertionHandler (PWExtensions) | |
// The load method of any class could be used to install our intercept whenever PWFoundation is loaded. | |
+ (void) load | |
{ | |
intercept_objc_exception_throw(); | |
[self patchCurrentHandler]; | |
} | |
+ (NSAssertionHandler*) patched_currentHandler | |
{ | |
// this is not a recursion because of exchanged implementations | |
NSAssertionHandler* handler = [self patched_currentHandler]; | |
// Create and install our handler on each thread for which it is requested. | |
if (!handler.isPWHandler) { | |
handler = [[PWAssertionHandler alloc] init]; | |
NSThread.currentThread.threadDictionary[NSAssertionHandlerKey] = handler; | |
} | |
return handler; | |
} | |
+ (void) patchCurrentHandler | |
{ | |
Method original = class_getClassMethod (self, @selector(currentHandler)); | |
NSAssert (original, nil); | |
Method patched = class_getClassMethod (self, @selector(patched_currentHandler)); | |
NSAssert (patched, nil); | |
method_exchangeImplementations (original, patched); | |
} | |
- (BOOL) isPWHandler | |
{ | |
return NO; | |
} | |
@end | |
#pragma mark - | |
@implementation PWAssertionHandler | |
{ | |
BOOL dontThrow_; | |
} | |
- (BOOL) isPWHandler | |
{ | |
return YES; | |
} | |
- (void) handleFailureInMethod:(SEL)selector | |
object:(id)object | |
file:(NSString*)fileName | |
lineNumber:(NSInteger)line | |
description:(NSString*)format,... | |
{ | |
NSString* description; | |
if (format) { | |
va_list argList; | |
va_start (argList, format); | |
description = [[NSString alloc] initWithFormat:format arguments:argList]; | |
va_end (argList); | |
} | |
Class objectClass = object_getClass (object); | |
BOOL isClassMethod = class_isMetaClass (objectClass); | |
[self logFakeTestCaseAroundBlock:^{ | |
PWLog (@"*** %@ in %c[%@ %@], %@:%li\n", | |
description ? description : @"Assertion failure", | |
isClassMethod ? '+' : '-', isClassMethod ? object : objectClass, NSStringFromSelector (selector), | |
fileName, line); | |
}]; | |
[self raiseExceptionWithDescription:description]; | |
} | |
- (void) handleFailureInFunction:(NSString*)functionName | |
file:(NSString*)fileName | |
lineNumber:(NSInteger)line | |
description:(NSString*)format,... | |
{ | |
NSString* description; | |
if (format) { | |
va_list argList; | |
va_start (argList, format); | |
description = [[NSString alloc] initWithFormat:format arguments:argList]; | |
va_end (argList); | |
} | |
[self logFakeTestCaseAroundBlock:^{ | |
PWLog (@"*** %@ in %@, %@:%li\n", | |
description ? description : @"Assertion failure", | |
functionName, fileName, line); | |
}]; | |
[self raiseExceptionWithDescription:description]; | |
} | |
- (void) raiseExceptionWithDescription:(NSString*)description | |
{ | |
// To support moving the programm counter behind the throw in the debugger, the throw must not be the only possible | |
// exit of the method. So far this is the only reason for dontThrow_. | |
if (!dontThrow_) { | |
PWAssertionException* exception = [[PWAssertionException alloc] initWithReason:description userInfo:nil]; | |
// Very important: under -fobjc-arc-exceptions -[NSException raise] does not work, the exception object is | |
// released and deallocated before being thrown (probably by the exception cleanup code). | |
// The class raise-methods are not affected: with them ARC never sees the exception object. | |
// Speculation: when seeing @throw the compiler knows what happens and does the right thing. | |
// Set a breakpoint on this statement to intercept ASSERTs before the exception is thrown. Can even move the | |
// programm counter to the end of the method to continue without throwing. | |
@throw exception; | |
} | |
} | |
// When an exception occurs outside of the synchronous performing of a XCTestCase (even during -setup or -tearDown) | |
// the performing of the test suite is aborted without any report of a failed test. | |
// To avoid missing such a situation in continous integration tools, | |
// we simulated the output of a failed test case containig the exception description. | |
- (void)logFakeTestCaseAroundBlock:(PWDispatchBlock)block | |
{ | |
NSParameterAssert(block); | |
if(NSClassFromString(@"XCTestCase")) | |
{ | |
printf("%s", [[NSString stringWithFormat:@"Test Suite 'PWAssertionHandlerTests' started at %@\n", [NSDate date]] UTF8String]); | |
printf("Test Case '-[PWAssertionHandler testAssertionFailed]' started.\n"); | |
printf("PWAssertionHandler.m:1: error: -[PWAssertionHandler testAssertionFailed] : "); | |
block(); | |
printf("Test Case '-[PWAssertionHandler testAssertionFailed]' failed (0.000 seconds).\n"); | |
printf("%s", [[NSString stringWithFormat:@"Test Suite 'PWAssertionHandlerTests' finished at %@\n", [NSDate date]] UTF8String]); | |
} | |
else | |
block(); | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment