From 46d3b6d86bfb0342a6778e136edf5b5801cb4e72 Mon Sep 17 00:00:00 2001 From: Brian Croom and Sam Coward Date: Fri, 1 Apr 2016 11:21:38 -0400 Subject: [PATCH] New attempt at fixing test output under Xcode 7.3 while not breaking xctool - The dispatch_async approach causes Cedar to not be hooked into XCTest in time for the xctool runner to pick it up. - Instead, we are now swizzling -[XCTestObservationCenter addTestObserver:] and -_addLegacyTestObserver: and using them as an earlier hook to register Cedar's test observer with the test observation center. Some extra context around this can be found at: https://github.com/pivotal/cedar/issues/383 --- .../Headers/Project/XCTest/CDRXCTestSupport.h | 8 ++- Source/XCTest/CDRXCTestFunctions.m | 55 ++++++++++++++----- Spec/SpecBundle/SpecBundle-Info.plist | 2 + .../Support/TestObservationHelper.m | 19 ++++--- 4 files changed, 59 insertions(+), 25 deletions(-) diff --git a/Source/Headers/Project/XCTest/CDRXCTestSupport.h b/Source/Headers/Project/XCTest/CDRXCTestSupport.h index 80e11d02..cd33e08a 100644 --- a/Source/Headers/Project/XCTest/CDRXCTestSupport.h +++ b/Source/Headers/Project/XCTest/CDRXCTestSupport.h @@ -22,8 +22,12 @@ - (id)CDR_original_allTests; - (id)initWithName:(NSString *)aName; -// XCTestObservationCenter +@end + +@interface XCTestObservationCenter: NSObject + (instancetype)sharedTestObservationCenter; - (void)addTestObserver:(id)observer; - +- (void)_addLegacyTestObserver:(id)observer; +- (void)CDR_original_addTestObserver:(id)observer; +- (void)CDR_original__addLegacyTestObserver:(id)observer; @end diff --git a/Source/XCTest/CDRXCTestFunctions.m b/Source/XCTest/CDRXCTestFunctions.m index 4e74ecd3..51eb3a21 100644 --- a/Source/XCTest/CDRXCTestFunctions.m +++ b/Source/XCTest/CDRXCTestFunctions.m @@ -23,25 +23,19 @@ id CDRCreateXCTestSuite() { return [[[testSuiteSubclass alloc] initWithSpecRun:run] autorelease]; } -void CDRInjectIntoXCTestRunner() { +void CDRAddCedarTestObserver(id observationCenter) { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [observationCenter CDR_original_addTestObserver:[[CDRXCTestObserver alloc] init]]; + }); +} + +void CDRSwizzleTestSuiteAllTestsMethod() { Class testSuiteClass = NSClassFromString(@"XCTestSuite") ?: NSClassFromString(@"SenTestSuite"); if (!testSuiteClass) { [[NSException exceptionWithName:@"CedarNoTestFrameworkAvailable" reason:@"You must link against either the XCTest or SenTestingKit frameworks." userInfo:nil] raise]; } - // if possible, use the new XCTestObservation protocol available in Xcode 7 - Class observationCenterClass = NSClassFromString(@"XCTestObservationCenter"); - if (observationCenterClass && [observationCenterClass respondsToSelector:@selector(sharedTestObservationCenter)]) { - // Accessing the `sharedTestObservationCenter` too early causes XCTest console output to break when running - // on Xcode 7.3. Deferring adding the observer works around this issue. (rdar://25456276) - dispatch_async(dispatch_get_main_queue(), ^{ - id observationCenter = [observationCenterClass sharedTestObservationCenter]; - static CDRXCTestObserver *xcTestObserver; - xcTestObserver = [[CDRXCTestObserver alloc] init]; - [observationCenter addTestObserver:xcTestObserver]; - }); - } - Class testSuiteMetaClass = object_getClass(testSuiteClass); Method m = class_getClassMethod(testSuiteClass, @selector(allTests)); @@ -53,3 +47,36 @@ void CDRInjectIntoXCTestRunner() { }); class_replaceMethod(testSuiteMetaClass, @selector(allTests), newImp, method_getTypeEncoding(m)); } + +void CDRSwizzleTestObservationCenter() { + Class observationCenterClass = NSClassFromString(@"XCTestObservationCenter"); + if (observationCenterClass && [observationCenterClass respondsToSelector:@selector(sharedTestObservationCenter)]) { + // Swizzle -addTestObserver: + Method addTestObserverMethod = class_getInstanceMethod(observationCenterClass, @selector(addTestObserver:)); + class_addMethod(observationCenterClass, @selector(CDR_original_addTestObserver:), method_getImplementation(addTestObserverMethod), method_getTypeEncoding(addTestObserverMethod)); + + IMP newAddTestObserverImp = imp_implementationWithBlock(^void(id self, id observer){ + [self CDR_original_addTestObserver:observer]; + CDRAddCedarTestObserver(self); + }); + class_replaceMethod(observationCenterClass, @selector(addTestObserver:), newAddTestObserverImp, method_getTypeEncoding(addTestObserverMethod)); + + + // Swizzle -_addLegacyTestObserver: + Method addLegacyTestObserverMethod = class_getInstanceMethod(observationCenterClass, @selector(_addLegacyTestObserver:)); + if (addLegacyTestObserverMethod) { + class_addMethod(observationCenterClass, @selector(CDR_original__addLegacyTestObserver:), method_getImplementation(addLegacyTestObserverMethod), method_getTypeEncoding(addLegacyTestObserverMethod)); + + IMP newAddLegacyTestObserverImp = imp_implementationWithBlock(^void(id self, id observer){ + [self CDR_original__addLegacyTestObserver:observer]; + CDRAddCedarTestObserver(self); + }); + class_replaceMethod(observationCenterClass, @selector(_addLegacyTestObserver:), newAddLegacyTestObserverImp, method_getTypeEncoding(addLegacyTestObserverMethod)); + } + } +} + +void CDRInjectIntoXCTestRunner() { + CDRSwizzleTestSuiteAllTestsMethod(); + CDRSwizzleTestObservationCenter(); +} diff --git a/Spec/SpecBundle/SpecBundle-Info.plist b/Spec/SpecBundle/SpecBundle-Info.plist index ba72822e..abca64a6 100644 --- a/Spec/SpecBundle/SpecBundle-Info.plist +++ b/Spec/SpecBundle/SpecBundle-Info.plist @@ -20,5 +20,7 @@ ???? CFBundleVersion 1 + NSPrincipalClass + TestObservationHelper diff --git a/Spec/SpecBundle/Support/TestObservationHelper.m b/Spec/SpecBundle/Support/TestObservationHelper.m index 428847bf..58122758 100644 --- a/Spec/SpecBundle/Support/TestObservationHelper.m +++ b/Spec/SpecBundle/Support/TestObservationHelper.m @@ -7,20 +7,21 @@ + (instancetype)defaultTestSuite; @interface TestObservationHelper () @end +// This class is loaded as the NSPrincipalClass of test bundles that require it, at which point +// it registers itself as a test observer. @implementation TestObservationHelper static NSMutableArray *_knownTestSuites; -+ (void)load { - Class observationCenterClass = NSClassFromString(@"XCTestObservationCenter"); - if (observationCenterClass && [observationCenterClass respondsToSelector:@selector(sharedTestObservationCenter)]) { - _knownTestSuites = [NSMutableArray array]; - - // See comment in CDRXCTestFunctions.m for context on the dispatch_async - dispatch_async(dispatch_get_main_queue(), ^{ - [[observationCenterClass sharedTestObservationCenter] addTestObserver:(id)[TestObservationHelper new]]; - }); +- (instancetype)init { + if (self = [super init]) { + Class observationCenterClass = NSClassFromString(@"XCTestObservationCenter"); + if (observationCenterClass && [observationCenterClass respondsToSelector:@selector(sharedTestObservationCenter)]) { + _knownTestSuites = [NSMutableArray array]; + [[observationCenterClass sharedTestObservationCenter] addTestObserver:self]; + } } + return self; } + (NSArray *)knownTestSuites {