Tuesday, April 21, 2009

GTM test suite for the iPhone - figuring out why it was hanging on startup

[Sorry about the formatting problems. This was posted to my old blog, and I found it again when I was doing a google search for the topic. The old blog posts are lying around somewhere, but I'm lazy so I'll just cut-and-paste from the google cache...] I'm trying to use the GTM test suite, and it's just hanging. The build window shows:
    2008-11-21 09:23:46.996 UnitTests[1794:10b] CFPreferences: user home directory at /Users/jamesmoore/dev/TestPlatform/build/TestPlatform.build/Debug-iphonesimulator/UnitTests.build/iPhone Simulator User Dir is unavailable. User domains will be volatile.    2008-11-21 09:23:47.012 UnitTests[1794:10b] Unable to send CPDistributedMessagingCenter message named SBRemoteNotificationClientStartedMessage to com.apple.remotenotification.server: (ipc/send) invalid destination port
Starting under the debugger (using option-cmd-Y to skip running the test harness shell script) isn't very informative:
(gdb) bt#0  0x964954a6 in mach_msg_trap ()#1  0x9649cc9c in mach_msg ()#2  0x956fd0ce in CFRunLoopRunSpecific ()#3  0x956fdcf8 in CFRunLoopRunInMode ()#4  0x31699d38 in GSEventRunModal ()#5  0x31699dfd in GSEventRun ()#6  0x30a5dadb in -[UIApplication _run] ()#7  0x30a68ce4 in UIApplicationMain ()#8  0x00002a39 in main (argc=1, argv=0xbffff060) at /Users/jamesmoore/dev/TestPlatform/../google-toolbox-for-mac/UnitTesting/GTMIPhoneUnitTestMain.m:30(gdb)
All that looks fine. So, did the tests run at all? GTMIphoneUnitTestMain.m does:
  int retVal = UIApplicationMain(argc, argv, nil, @"GTMIPhoneUnitTestDelegate");
So I set a breakpoint on the first line of applicationDidFinishLaunching, on the call to runTests:
- (void)applicationDidFinishLaunching:(UIApplication *)application {  [self runTests];
And sure enough, it's not hit. So my next suspect is the app delegate for the regular application - inthis case, TestPlatformAppDelegate. Set a breakpoint in itsapplicationDidFinishLaunching, start up, and sure enough that's what'srunning. Why? Turns out I had added MainWindow.xib to the unit test target. For the regular (non-unit-test)application, MainWindow.xib has an instance of TestPlatformAppDelegate.The TestPlatformAppDelegate is attached to the File's Owner delegate.When the nib file is instantiated, it creates theTestPlatformAppDelegate, then sets the File's Owner delegate to be thenewly instantiated TestPlatformAppDelegate. That overwrites theinstance of GTMIPhoneUnitTestDelegate that used to be there, and no tests will run. The next question is can something spit out a warning if this happens? Off the top of my head, I don't know who gets called when nib files areloaded. I'm sitting at a debugger, so I slap in a new init method toGTMIPhoneUnitTestDelegate, set a breakpoint on it, and restart. Here'sthe stack:
#0  -[TestPlatformAppDelegate init] (self=0x44fb80, _cmd=0x96d6c858) at /Users/jamesmoore/dev/TestPlatform/Classes/TestPlatformAppDelegate.m:19#1  0x30c2ca67 in -[UIClassSwapper initWithCoder:] ()#2  0x922a4940 in _decodeObjectBinary ()#3  0x922a42b5 in _decodeObject ()#4  0x30c2c615 in -[UIRuntimeConnection initWithCoder:] ()#5  0x922a4940 in _decodeObjectBinary ()#6  0x922a63cd in -[NSKeyedUnarchiver _decodeArrayOfObjectsForKey:] ()#7  0x922a6849 in -[NSArray(NSArray) initWithCoder:] ()#8  0x922a4940 in _decodeObjectBinary ()#9  0x922a42b5 in _decodeObject ()#10 0x30c2bbeb in -[UINib instantiateWithOptions:owner:loadingResourcesFromBundle:] ()#11 0x30c2dcf8 in -[NSBundle(NSBundleAdditions) loadNibNamed:owner:options:] ()#12 0x30a5df99 in -[UIApplication _loadMainNibFile] ()#13 0x30a65f42 in -[UIApplication _runWithURL:] ()#14 0x922ce5ee in __NSFireDelayedPerform ()#15 0x956fdb45 in CFRunLoopRunSpecific ()#16 0x956fdcf8 in CFRunLoopRunInMode ()#17 0x31699d38 in GSEventRunModal ()#18 0x31699dfd in GSEventRun ()#19 0x30a5dadb in -[UIApplication _run] ()#20 0x30a68ce4 in UIApplicationMain ()#21 0x00002a39 in main (argc=1, argv=0xbffff060) at /Users/jamesmoore/dev/TestPlatform/../google-toolbox-for-mac/UnitTesting/GTMIPhoneUnitTestMain.m:30
The interesting points look like:
#10 0x30c2bbeb in -[UINib instantiateWithOptions:owner:loadingResourcesFromBundle:] ()#12 0x30a5df99 in -[UIApplication _loadMainNibFile] ()
Does one of those have something to hook into? I start reading doc. The Resource Programming Guide has lots ofinformation about nib files, but the only thing I find that seemsrelevant is the bit where it points out that nib instantiation is goingto use setValue:forKey: to set the fields. To see what happens there, I added this to GTMIphoneUnitTestMain.m:
@interface GuardedUIApplication : UIApplication@end @implementation GuardedUIApplication- (void)setValue:(id)value forKey:(NSString *)key{ NSLog(@"Current value for key |%@| is |%@|", key, value); [super setValue: value forKey: key];}@end
And changed the call to UIApplicationMain to use GuardedUIApplication instead of the default UIApplication:
  int retVal = UIApplicationMain(argc, argv, @"GuardedUIApplication", @"GTMIPhoneUnitTestDelegate");
Running it again, I see that the call I expected does in fact happen:
2008-11-21 10:43:09.966 UnitTests[2841:20b] Current value for key |delegate| is ||
Something like this might be a solution:
@interface GuardedUIApplication : UIApplication@end@implementation GuardedUIApplication- (void)setValue:(id)value forKey:(NSString *)key{ NSLog(@"Existing value for key |%@| is |%@|", key, [self valueForKey: key]); NSLog(@"New value for key |%@| is |%@|", key, value); if ([key isEqualToString: @"delegate"] && ![value isKindOfClass: [GTMIPhoneUnitTestDelegate class]]) {  NSException *e = [NSException failureInFile: [NSString stringWithCString: __FILE__]             atLine: __LINE__          withDescription: @"The app delegate must be a GTMIPhoneUnitTestDelegate.  Is MainWindow.nib attempting to use a different application delegate?"];  [e raise]; } [super setValue: value forKey: key];}@end

1 comment:

boga said...

Maybe the delegate set a timer in it's init and verify that it's still the [UIApplication sharedApplication]-s delegate...