Split ExceptionUtils out of Test.
authorJens Alfke <jens@mooseyard.com>
Tue May 20 17:40:28 2008 -0700 (2008-05-20)
changeset 1082a37ccf6b8c
parent 9 823e7e74088e
child 11 e5976864dfe9
Split ExceptionUtils out of Test.
ExceptionUtils.h
ExceptionUtils.m
Test.h
Test.m
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/ExceptionUtils.h	Tue May 20 17:40:28 2008 -0700
     1.3 @@ -0,0 +1,41 @@
     1.4 +//
     1.5 +//  ExceptionUtils.h
     1.6 +//  MYUtilities
     1.7 +//
     1.8 +//  Created by Jens Alfke on 1/5/08.
     1.9 +//  Copyright 2008 Jens Alfke. All rights reserved.
    1.10 +//  See BSD license at bottom of ExceptionUtils.m.
    1.11 +//
    1.12 +
    1.13 +#import <Cocoa/Cocoa.h>
    1.14 +
    1.15 +
    1.16 +/** Edit your Info.plist to make this your app's principal class,
    1.17 +    and most exceptions will be reported via a modal alert. 
    1.18 +    This includes exceptions caught by AppKit (i.e. uncaught ones from event handlers)
    1.19 +    and ones you report yourself via MYReportException and @catchAndReport. */
    1.20 +@interface MYExceptionReportingApplication : NSApplication
    1.21 +@end
    1.22 +
    1.23 +
    1.24 +/** A useful macro to use in code where you absolutely cannot allow an exception to 
    1.25 +    go uncaught because it would crash (e.g. in a C callback or at the top level of a thread.)
    1.26 +    It catches the exception but makes sure it gets reported. */
    1.27 +#define catchAndReport(MSG...) @catch(NSException *x) {MYReportException(x,MSG);}
    1.28 +
    1.29 +
    1.30 +/** Report an exception that's being caught and consumed.
    1.31 +    Logs a warning to the console, and calls the current MYReportException target if any. */
    1.32 +void MYReportException( NSException *x, NSString *where, ... );
    1.33 +
    1.34 +
    1.35 +/** Sets a callback to be invoked when MYReportException is called.
    1.36 +    In a GUI app, the callback would typically call [NSApp reportException: theException].
    1.37 +    The ExceptionReportingApplication class, below, sets this up automatically. */
    1.38 +void MYSetExceptionReporter( void (*reporter)(NSException*) );
    1.39 +
    1.40 +
    1.41 +@interface NSException (MYUtilities)
    1.42 +/** Returns a textual, human-readable backtrace of the point where the exception was thrown. */
    1.43 +- (NSString*) my_callStack;
    1.44 +@end
     2.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     2.2 +++ b/ExceptionUtils.m	Tue May 20 17:40:28 2008 -0700
     2.3 @@ -0,0 +1,174 @@
     2.4 +//
     2.5 +//  ExceptionUtils.m
     2.6 +//  MYUtilities
     2.7 +//
     2.8 +//  Created by Jens Alfke on 1/5/08.
     2.9 +//  Copyright 2008 Jens Alfke. All rights reserved.
    2.10 +//  See BSD license at bottom of file.
    2.11 +//
    2.12 +
    2.13 +#import "ExceptionUtils.h"
    2.14 +#import <unistd.h>
    2.15 +
    2.16 +
    2.17 +#ifndef Warn
    2.18 +#define Warn NSLog
    2.19 +#endif
    2.20 +
    2.21 +
    2.22 +static void (*sExceptionReporter)(NSException*);
    2.23 +
    2.24 +void MYSetExceptionReporter( void (*reporter)(NSException*) )
    2.25 +{
    2.26 +    sExceptionReporter = reporter;
    2.27 +}
    2.28 +
    2.29 +void MYReportException( NSException *x, NSString *where, ... )
    2.30 +{
    2.31 +    va_list args;
    2.32 +    va_start(args,where);
    2.33 +    where = [[NSString alloc] initWithFormat: where arguments: args];
    2.34 +    va_end(args);
    2.35 +    if( sExceptionReporter ) {
    2.36 +        Warn(@"Exception caught in %@:\n\t%@",where,x);
    2.37 +        sExceptionReporter(x);
    2.38 +    }else
    2.39 +        Warn(@"Exception caught in %@:\n\t%@\n%@",where,x,x.my_callStack);
    2.40 +    [where release];
    2.41 +}
    2.42 +
    2.43 +
    2.44 +@implementation NSException (MooseyardUtil)
    2.45 +
    2.46 +
    2.47 +- (NSArray*) my_callStackReturnAddresses
    2.48 +{
    2.49 +    // On 10.5 or later, can get the backtrace:
    2.50 +    if( [self respondsToSelector: @selector(callStackReturnAddresses)] )
    2.51 +        return [self valueForKey: @"callStackReturnAddresses"];
    2.52 +    else
    2.53 +        return nil;
    2.54 +}
    2.55 +
    2.56 +- (NSArray*) my_callStackReturnAddressesSkipping: (unsigned)skip limit: (unsigned)limit
    2.57 +{
    2.58 +    NSArray *addresses = [self my_callStackReturnAddresses];
    2.59 +    if( addresses ) {
    2.60 +        unsigned n = [addresses count];
    2.61 +        skip = MIN(skip,n);
    2.62 +        limit = MIN(limit,n-skip);
    2.63 +        addresses = [addresses subarrayWithRange: NSMakeRange(skip,limit)];
    2.64 +    }
    2.65 +    return addresses;
    2.66 +}
    2.67 +
    2.68 +
    2.69 +- (NSString*) my_callStack
    2.70 +{
    2.71 +    NSArray *addresses = [self my_callStackReturnAddressesSkipping: 2 limit: 15];
    2.72 +    if (!addresses)
    2.73 +        return nil;
    2.74 +    
    2.75 +    // We pipe the hex return addresses through the 'atos' tool to get symbolic names:
    2.76 +    // Adapted from <http://paste.lisp.org/display/47196>:
    2.77 +    NSMutableString *cmd = [NSMutableString stringWithFormat: @"/usr/bin/atos -p %d", getpid()];
    2.78 +    NSValue *addr;
    2.79 +    foreach(addr,addresses) {
    2.80 +        [cmd appendFormat: @" %p", [addr pointerValue]];
    2.81 +    }
    2.82 +    FILE *file = popen( [cmd UTF8String], "r" );
    2.83 +    if( ! file )
    2.84 +        return nil;
    2.85 +    
    2.86 +    NSMutableData *output = [NSMutableData data];
    2.87 +    char buffer[512];
    2.88 +    size_t length;
    2.89 +    while ((length = fread( buffer, 1, sizeof( buffer ), file ) ))
    2.90 +        [output appendBytes: buffer length: length];
    2.91 +    pclose( file );
    2.92 +    NSString *outStr = [[[NSString alloc] initWithData: output encoding: NSUTF8StringEncoding] autorelease];
    2.93 +    
    2.94 +    NSMutableString *result = [NSMutableString string];
    2.95 +    NSString *line;
    2.96 +    foreach( line, [outStr componentsSeparatedByString: @"\n"] ) {
    2.97 +        // Skip  frames that are part of the exception/assertion handling itself:
    2.98 +        if( [line hasPrefix: @"-[NSAssertionHandler"] || [line hasPrefix: @"+[NSException"] 
    2.99 +                || [line hasPrefix: @"-[NSException"] || [line hasPrefix: @"_AssertFailed"] )
   2.100 +            continue;
   2.101 +        if( result.length )
   2.102 +            [result appendString: @"\n"];
   2.103 +        [result appendString: @"\t"];
   2.104 +        [result appendString: line];
   2.105 +        // Don't show the "__start" frame below "main":
   2.106 +        if( [line hasPrefix: @"main "] )
   2.107 +            break;
   2.108 +    }
   2.109 +    return result;
   2.110 +}
   2.111 +
   2.112 +@end
   2.113 +
   2.114 +
   2.115 +
   2.116 +
   2.117 +@implementation MYExceptionReportingApplication
   2.118 +
   2.119 +
   2.120 +static void report( NSException *x ) {
   2.121 +    [NSApp reportException: x];
   2.122 +}
   2.123 +
   2.124 +- (id) init
   2.125 +{
   2.126 +    self = [super init];
   2.127 +    if (self != nil) {
   2.128 +        MYSetExceptionReporter(&report);
   2.129 +    }
   2.130 +    return self;
   2.131 +}
   2.132 +
   2.133 +
   2.134 +- (void)reportException:(NSException *)x
   2.135 +{
   2.136 +    [super reportException: x];
   2.137 +    [self performSelector: @selector(_showExceptionAlert:) withObject: x afterDelay: 0.0];
   2.138 +    MYSetExceptionReporter(NULL);     // ignore further exceptions till alert is dismissed
   2.139 +}
   2.140 +
   2.141 +- (void) _showExceptionAlert: (NSException*)x
   2.142 +{
   2.143 +    NSString *stack = [x my_callStack] ?:@"";
   2.144 +    int r = NSRunCriticalAlertPanel( @"Cloudy Internal Error!",
   2.145 +                            [NSString stringWithFormat: @"Uncaught exception: %@\n%@\n\n%@\n\n"
   2.146 +                             "Please report this bug to jens@mooseyard.com (you can copy & paste the text).",
   2.147 +                             [x name], [x reason], stack],
   2.148 +                            @"Sorry",@"Quit",nil);
   2.149 +    if( r == NSAlertAlternateReturn )
   2.150 +        exit(1);
   2.151 +    MYSetExceptionReporter(&report);
   2.152 +}
   2.153 +
   2.154 +@end
   2.155 +
   2.156 +
   2.157 +/*
   2.158 + Copyright (c) 2008, Jens Alfke. All rights reserved.
   2.159 + 
   2.160 + Redistribution and use in source and binary forms, with or without modification, are permitted
   2.161 + provided that the following conditions are met:
   2.162 + 
   2.163 + * Redistributions of source code must retain the above copyright notice, this list of conditions
   2.164 + and the following disclaimer.
   2.165 + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
   2.166 + and the following disclaimer in the documentation and/or other materials provided with the
   2.167 + distribution.
   2.168 + 
   2.169 + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
   2.170 + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 
   2.171 + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRI-
   2.172 + BUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
   2.173 + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
   2.174 +  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
   2.175 + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 
   2.176 + THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   2.177 + */
     3.1 --- a/Test.h	Sat May 17 13:14:48 2008 -0700
     3.2 +++ b/Test.h	Tue May 20 17:40:28 2008 -0700
     3.3 @@ -88,16 +88,6 @@
     3.4                                  }while(0)
     3.5  
     3.6  
     3.7 -#pragma mark -
     3.8 -#pragma mark EXCEPTION UTILITIES:
     3.9 -
    3.10 -@interface NSException (MooseyardUtil)
    3.11 -/** Returns a textual, human-readable backtrace of the point where the exception was thrown. */
    3.12 -- (NSString*) my_callStack;
    3.13 -@end
    3.14 -
    3.15 -
    3.16 -
    3.17  // Nasty internals ...
    3.18  #if DEBUG
    3.19  void _RunTestCase( void (*testptr)(), const char *name );
     4.1 --- a/Test.m	Sat May 17 13:14:48 2008 -0700
     4.2 +++ b/Test.m	Tue May 20 17:40:28 2008 -0700
     4.3 @@ -7,7 +7,7 @@
     4.4  //
     4.5  
     4.6  #import "Test.h"
     4.7 -#import <unistd.h>
     4.8 +
     4.9  
    4.10  #if DEBUG
    4.11  
    4.12 @@ -15,6 +15,13 @@
    4.13  
    4.14  struct TestCaseLink *gAllTestCases;
    4.15  static int sPassed, sFailed;
    4.16 +static int sCurTestCaseExceptions;
    4.17 +
    4.18 +
    4.19 +static void TestCaseExceptionReporter( NSException *x ) {
    4.20 +    sCurTestCaseExceptions++;
    4.21 +    Log(@"XXX FAILED test case -- backtrace:\n%@\n\n", x.my_callStack);
    4.22 +}
    4.23  
    4.24  static BOOL RunTestCase( struct TestCaseLink *test )
    4.25  {
    4.26 @@ -24,10 +31,20 @@
    4.27          NSAutoreleasePool *pool = [NSAutoreleasePool new];
    4.28          Log(@"=== Testing %s ...",test->name);
    4.29          @try{
    4.30 -            test->testptr();
    4.31 -            Log(@"√√√ %s passed\n\n",test->name);
    4.32 -            test->passed = YES;
    4.33 -            sPassed++;
    4.34 +            sCurTestCaseExceptions = 0;
    4.35 +            MYSetExceptionReporter(&TestCaseExceptionReporter);
    4.36 +
    4.37 +            test->testptr();    //SHAZAM!
    4.38 +            
    4.39 +            if( sCurTestCaseExceptions == 0 ) {
    4.40 +                Log(@"√√√ %s passed\n\n",test->name);
    4.41 +                test->passed = YES;
    4.42 +                sPassed++;
    4.43 +            } else {
    4.44 +                Log(@"XXX FAILED test case '%s' due to %i exception(s) already reported above",
    4.45 +                    test->name,sCurTestCaseExceptions);
    4.46 +                sFailed++;
    4.47 +            }
    4.48          }@catch( NSException *x ) {
    4.49              if( [x.name isEqualToString: @"TestCaseSkipped"] )
    4.50                  Log(@"... skipping test %s since %@\n\n", test->name, x.reason);
    4.51 @@ -131,78 +148,3 @@
    4.52                                                           description: @"%@", message];
    4.53      abort(); // unreachable, but appeases compiler
    4.54  }
    4.55 -
    4.56 -
    4.57 -#pragma mark -
    4.58 -#pragma mark EXCEPTION BACKTRACES:
    4.59 -
    4.60 -
    4.61 -@implementation NSException (MooseyardUtil)
    4.62 -
    4.63 -
    4.64 -- (NSArray*) my_callStackReturnAddresses
    4.65 -{
    4.66 -    // On 10.5 or later, can get the backtrace:
    4.67 -    if( [self respondsToSelector: @selector(callStackReturnAddresses)] )
    4.68 -        return [self valueForKey: @"callStackReturnAddresses"];
    4.69 -    else
    4.70 -        return nil;
    4.71 -}
    4.72 -
    4.73 -- (NSArray*) my_callStackReturnAddressesSkipping: (unsigned)skip limit: (unsigned)limit
    4.74 -{
    4.75 -    NSArray *addresses = [self my_callStackReturnAddresses];
    4.76 -    if( addresses ) {
    4.77 -        unsigned n = [addresses count];
    4.78 -        skip = MIN(skip,n);
    4.79 -        limit = MIN(limit,n-skip);
    4.80 -        addresses = [addresses subarrayWithRange: NSMakeRange(skip,limit)];
    4.81 -    }
    4.82 -    return addresses;
    4.83 -}
    4.84 -
    4.85 -
    4.86 -- (NSString*) my_callStack
    4.87 -{
    4.88 -    NSArray *addresses = [self my_callStackReturnAddressesSkipping: 2 limit: 15];
    4.89 -    if (!addresses)
    4.90 -        return nil;
    4.91 -    
    4.92 -    // We pipe the hex return addresses through the 'atos' tool to get symbolic names:
    4.93 -    // Adapted from <http://paste.lisp.org/display/47196>:
    4.94 -    NSMutableString *cmd = [NSMutableString stringWithFormat: @"/usr/bin/atos -p %d", getpid()];
    4.95 -    NSValue *addr;
    4.96 -    foreach(addr,addresses) {
    4.97 -        [cmd appendFormat: @" %p", [addr pointerValue]];
    4.98 -    }
    4.99 -    FILE *file = popen( [cmd UTF8String], "r" );
   4.100 -    if( ! file )
   4.101 -        return nil;
   4.102 -    
   4.103 -    NSMutableData *output = [NSMutableData data];
   4.104 -    char buffer[512];
   4.105 -    size_t length;
   4.106 -    while ((length = fread( buffer, 1, sizeof( buffer ), file ) ))
   4.107 -        [output appendBytes: buffer length: length];
   4.108 -    pclose( file );
   4.109 -    NSString *outStr = [[[NSString alloc] initWithData: output encoding: NSUTF8StringEncoding] autorelease];
   4.110 -    
   4.111 -    NSMutableString *result = [NSMutableString string];
   4.112 -    NSString *line;
   4.113 -    foreach( line, [outStr componentsSeparatedByString: @"\n"] ) {
   4.114 -        // Skip  frames that are part of the exception/assertion handling itself:
   4.115 -        if( [line hasPrefix: @"-[NSAssertionHandler"] || [line hasPrefix: @"+[NSException"] 
   4.116 -                || [line hasPrefix: @"-[NSException"] || [line hasPrefix: @"_AssertFailed"] )
   4.117 -            continue;
   4.118 -        if( result.length )
   4.119 -            [result appendString: @"\n"];
   4.120 -        [result appendString: @"\t"];
   4.121 -        [result appendString: line];
   4.122 -        // Don't show the "__start" frame below "main":
   4.123 -        if( [line hasPrefix: @"main "] )
   4.124 -            break;
   4.125 -    }
   4.126 -    return result;
   4.127 -}
   4.128 -
   4.129 -@end