# HG changeset patch # User Jens Alfke # Date 1211330428 25200 # Node ID 82a37ccf6b8c4c56a53665762eb42bd7bae15f03 # Parent 823e7e74088e97b80eec74a19adacf0817309d64 Split ExceptionUtils out of Test. diff -r 823e7e74088e -r 82a37ccf6b8c ExceptionUtils.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ExceptionUtils.h Tue May 20 17:40:28 2008 -0700 @@ -0,0 +1,41 @@ +// +// ExceptionUtils.h +// MYUtilities +// +// Created by Jens Alfke on 1/5/08. +// Copyright 2008 Jens Alfke. All rights reserved. +// See BSD license at bottom of ExceptionUtils.m. +// + +#import + + +/** Edit your Info.plist to make this your app's principal class, + and most exceptions will be reported via a modal alert. + This includes exceptions caught by AppKit (i.e. uncaught ones from event handlers) + and ones you report yourself via MYReportException and @catchAndReport. */ +@interface MYExceptionReportingApplication : NSApplication +@end + + +/** A useful macro to use in code where you absolutely cannot allow an exception to + go uncaught because it would crash (e.g. in a C callback or at the top level of a thread.) + It catches the exception but makes sure it gets reported. */ +#define catchAndReport(MSG...) @catch(NSException *x) {MYReportException(x,MSG);} + + +/** Report an exception that's being caught and consumed. + Logs a warning to the console, and calls the current MYReportException target if any. */ +void MYReportException( NSException *x, NSString *where, ... ); + + +/** Sets a callback to be invoked when MYReportException is called. + In a GUI app, the callback would typically call [NSApp reportException: theException]. + The ExceptionReportingApplication class, below, sets this up automatically. */ +void MYSetExceptionReporter( void (*reporter)(NSException*) ); + + +@interface NSException (MYUtilities) +/** Returns a textual, human-readable backtrace of the point where the exception was thrown. */ +- (NSString*) my_callStack; +@end diff -r 823e7e74088e -r 82a37ccf6b8c ExceptionUtils.m --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ExceptionUtils.m Tue May 20 17:40:28 2008 -0700 @@ -0,0 +1,174 @@ +// +// ExceptionUtils.m +// MYUtilities +// +// Created by Jens Alfke on 1/5/08. +// Copyright 2008 Jens Alfke. All rights reserved. +// See BSD license at bottom of file. +// + +#import "ExceptionUtils.h" +#import + + +#ifndef Warn +#define Warn NSLog +#endif + + +static void (*sExceptionReporter)(NSException*); + +void MYSetExceptionReporter( void (*reporter)(NSException*) ) +{ + sExceptionReporter = reporter; +} + +void MYReportException( NSException *x, NSString *where, ... ) +{ + va_list args; + va_start(args,where); + where = [[NSString alloc] initWithFormat: where arguments: args]; + va_end(args); + if( sExceptionReporter ) { + Warn(@"Exception caught in %@:\n\t%@",where,x); + sExceptionReporter(x); + }else + Warn(@"Exception caught in %@:\n\t%@\n%@",where,x,x.my_callStack); + [where release]; +} + + +@implementation NSException (MooseyardUtil) + + +- (NSArray*) my_callStackReturnAddresses +{ + // On 10.5 or later, can get the backtrace: + if( [self respondsToSelector: @selector(callStackReturnAddresses)] ) + return [self valueForKey: @"callStackReturnAddresses"]; + else + return nil; +} + +- (NSArray*) my_callStackReturnAddressesSkipping: (unsigned)skip limit: (unsigned)limit +{ + NSArray *addresses = [self my_callStackReturnAddresses]; + if( addresses ) { + unsigned n = [addresses count]; + skip = MIN(skip,n); + limit = MIN(limit,n-skip); + addresses = [addresses subarrayWithRange: NSMakeRange(skip,limit)]; + } + return addresses; +} + + +- (NSString*) my_callStack +{ + NSArray *addresses = [self my_callStackReturnAddressesSkipping: 2 limit: 15]; + if (!addresses) + return nil; + + // We pipe the hex return addresses through the 'atos' tool to get symbolic names: + // Adapted from : + NSMutableString *cmd = [NSMutableString stringWithFormat: @"/usr/bin/atos -p %d", getpid()]; + NSValue *addr; + foreach(addr,addresses) { + [cmd appendFormat: @" %p", [addr pointerValue]]; + } + FILE *file = popen( [cmd UTF8String], "r" ); + if( ! file ) + return nil; + + NSMutableData *output = [NSMutableData data]; + char buffer[512]; + size_t length; + while ((length = fread( buffer, 1, sizeof( buffer ), file ) )) + [output appendBytes: buffer length: length]; + pclose( file ); + NSString *outStr = [[[NSString alloc] initWithData: output encoding: NSUTF8StringEncoding] autorelease]; + + NSMutableString *result = [NSMutableString string]; + NSString *line; + foreach( line, [outStr componentsSeparatedByString: @"\n"] ) { + // Skip frames that are part of the exception/assertion handling itself: + if( [line hasPrefix: @"-[NSAssertionHandler"] || [line hasPrefix: @"+[NSException"] + || [line hasPrefix: @"-[NSException"] || [line hasPrefix: @"_AssertFailed"] ) + continue; + if( result.length ) + [result appendString: @"\n"]; + [result appendString: @"\t"]; + [result appendString: line]; + // Don't show the "__start" frame below "main": + if( [line hasPrefix: @"main "] ) + break; + } + return result; +} + +@end + + + + +@implementation MYExceptionReportingApplication + + +static void report( NSException *x ) { + [NSApp reportException: x]; +} + +- (id) init +{ + self = [super init]; + if (self != nil) { + MYSetExceptionReporter(&report); + } + return self; +} + + +- (void)reportException:(NSException *)x +{ + [super reportException: x]; + [self performSelector: @selector(_showExceptionAlert:) withObject: x afterDelay: 0.0]; + MYSetExceptionReporter(NULL); // ignore further exceptions till alert is dismissed +} + +- (void) _showExceptionAlert: (NSException*)x +{ + NSString *stack = [x my_callStack] ?:@""; + int r = NSRunCriticalAlertPanel( @"Cloudy Internal Error!", + [NSString stringWithFormat: @"Uncaught exception: %@\n%@\n\n%@\n\n" + "Please report this bug to jens@mooseyard.com (you can copy & paste the text).", + [x name], [x reason], stack], + @"Sorry",@"Quit",nil); + if( r == NSAlertAlternateReturn ) + exit(1); + MYSetExceptionReporter(&report); +} + +@end + + +/* + Copyright (c) 2008, Jens Alfke. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, are permitted + provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions + and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + and the following disclaimer in the documentation and/or other materials provided with the + distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRI- + BUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ diff -r 823e7e74088e -r 82a37ccf6b8c Test.h --- a/Test.h Sat May 17 13:14:48 2008 -0700 +++ b/Test.h Tue May 20 17:40:28 2008 -0700 @@ -88,16 +88,6 @@ }while(0) -#pragma mark - -#pragma mark EXCEPTION UTILITIES: - -@interface NSException (MooseyardUtil) -/** Returns a textual, human-readable backtrace of the point where the exception was thrown. */ -- (NSString*) my_callStack; -@end - - - // Nasty internals ... #if DEBUG void _RunTestCase( void (*testptr)(), const char *name ); diff -r 823e7e74088e -r 82a37ccf6b8c Test.m --- a/Test.m Sat May 17 13:14:48 2008 -0700 +++ b/Test.m Tue May 20 17:40:28 2008 -0700 @@ -7,7 +7,7 @@ // #import "Test.h" -#import + #if DEBUG @@ -15,6 +15,13 @@ struct TestCaseLink *gAllTestCases; static int sPassed, sFailed; +static int sCurTestCaseExceptions; + + +static void TestCaseExceptionReporter( NSException *x ) { + sCurTestCaseExceptions++; + Log(@"XXX FAILED test case -- backtrace:\n%@\n\n", x.my_callStack); +} static BOOL RunTestCase( struct TestCaseLink *test ) { @@ -24,10 +31,20 @@ NSAutoreleasePool *pool = [NSAutoreleasePool new]; Log(@"=== Testing %s ...",test->name); @try{ - test->testptr(); - Log(@"√√√ %s passed\n\n",test->name); - test->passed = YES; - sPassed++; + sCurTestCaseExceptions = 0; + MYSetExceptionReporter(&TestCaseExceptionReporter); + + test->testptr(); //SHAZAM! + + if( sCurTestCaseExceptions == 0 ) { + Log(@"√√√ %s passed\n\n",test->name); + test->passed = YES; + sPassed++; + } else { + Log(@"XXX FAILED test case '%s' due to %i exception(s) already reported above", + test->name,sCurTestCaseExceptions); + sFailed++; + } }@catch( NSException *x ) { if( [x.name isEqualToString: @"TestCaseSkipped"] ) Log(@"... skipping test %s since %@\n\n", test->name, x.reason); @@ -131,78 +148,3 @@ description: @"%@", message]; abort(); // unreachable, but appeases compiler } - - -#pragma mark - -#pragma mark EXCEPTION BACKTRACES: - - -@implementation NSException (MooseyardUtil) - - -- (NSArray*) my_callStackReturnAddresses -{ - // On 10.5 or later, can get the backtrace: - if( [self respondsToSelector: @selector(callStackReturnAddresses)] ) - return [self valueForKey: @"callStackReturnAddresses"]; - else - return nil; -} - -- (NSArray*) my_callStackReturnAddressesSkipping: (unsigned)skip limit: (unsigned)limit -{ - NSArray *addresses = [self my_callStackReturnAddresses]; - if( addresses ) { - unsigned n = [addresses count]; - skip = MIN(skip,n); - limit = MIN(limit,n-skip); - addresses = [addresses subarrayWithRange: NSMakeRange(skip,limit)]; - } - return addresses; -} - - -- (NSString*) my_callStack -{ - NSArray *addresses = [self my_callStackReturnAddressesSkipping: 2 limit: 15]; - if (!addresses) - return nil; - - // We pipe the hex return addresses through the 'atos' tool to get symbolic names: - // Adapted from : - NSMutableString *cmd = [NSMutableString stringWithFormat: @"/usr/bin/atos -p %d", getpid()]; - NSValue *addr; - foreach(addr,addresses) { - [cmd appendFormat: @" %p", [addr pointerValue]]; - } - FILE *file = popen( [cmd UTF8String], "r" ); - if( ! file ) - return nil; - - NSMutableData *output = [NSMutableData data]; - char buffer[512]; - size_t length; - while ((length = fread( buffer, 1, sizeof( buffer ), file ) )) - [output appendBytes: buffer length: length]; - pclose( file ); - NSString *outStr = [[[NSString alloc] initWithData: output encoding: NSUTF8StringEncoding] autorelease]; - - NSMutableString *result = [NSMutableString string]; - NSString *line; - foreach( line, [outStr componentsSeparatedByString: @"\n"] ) { - // Skip frames that are part of the exception/assertion handling itself: - if( [line hasPrefix: @"-[NSAssertionHandler"] || [line hasPrefix: @"+[NSException"] - || [line hasPrefix: @"-[NSException"] || [line hasPrefix: @"_AssertFailed"] ) - continue; - if( result.length ) - [result appendString: @"\n"]; - [result appendString: @"\t"]; - [result appendString: line]; - // Don't show the "__start" frame below "main": - if( [line hasPrefix: @"main "] ) - break; - } - return result; -} - -@end