resurrect.m   [plain text]


/*
 * Copyright (c) 2011 Apple Inc. All rights reserved.
 *
 * @APPLE_APACHE_LICENSE_HEADER_START@
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * @APPLE_APACHE_LICENSE_HEADER_END@
 */
//
//  resurrect.m
//  Copyright (c) 2009-2011 Apple Inc. All rights reserved.
//

#import "BlackBoxTest.h"
#import <objc/objc-auto.h>
#import <objc/runtime.h>

@interface GlobalResurrectionContainer : NSObject
{
    @public
    id object;
}
@end

@implementation GlobalResurrectionContainer
@end

static id global;
static __weak id global_weak;
static GlobalResurrectionContainer *ivarTester;

/*
 There are several different resurrection tests, which are all TestCase subclasses.
 Each also has a test class with a -finalize that implements the resurrection.
 In general, each test instanciates a test class object, runs a collection, an verifies
 that the correct resurrection behavior happens.
*/

/* These are the helper test classes. */

@interface ResurrectMemmoveOkTester : TestFinalizer {
@public
    id srcObject;
    id dstObject;
}
@end

@interface ResurrectAssociatedTester : TestFinalizer
@end

@interface ResurrectCASTester : TestFinalizer
@end

@interface ResurrectGlobalTester : TestFinalizer
@end

@interface ResurrectGlobalIvarTester : TestFinalizer
@end

@interface ResurrectGlobalMemmoveTester : TestFinalizer
@end

@interface ResurrectGlobalWeakTester : TestFinalizer
@end

@interface ResurrectThreadedTester : TestFinalizer
@end

@interface ResurrectRetainTester : TestFinalizer
@end

/* 
 These are the TestCase classes. We have a base class that is common to all the resurrection tests. 
 The base class verifies that the test object was actually finalized.
 */

@interface Resurrection : BlackBoxTest
{
    uint _finalizedCount;
    vm_address_t _localTester;
    vm_address_t _globalTester;
    NSMutableArray *_expectedMessages;
}
@end

@interface ResurrectMemmoveOk : Resurrection
@end

@interface ResurrectAssociated : Resurrection
@end

@interface ResurrectCAS : Resurrection
@end

@interface ResurrectGlobal : Resurrection
@end

@interface ResurrectGlobalIvar : Resurrection
@end

@interface ResurrectGlobalMemmove : Resurrection
@end

@interface ResurrectGlobalWeak : Resurrection
@end

@interface ResurrectThreaded : Resurrection
@end

@interface ResurrectRetain : Resurrection
@end

/* helper implementations */
 
@implementation ResurrectMemmoveOkTester
- init {
    self = [super init];
    if (self) {
        srcObject = [[NSObject alloc] init];
    }
    return self;
}

- (void)finalize {
    // verify objc_memmove_collectable into dead ivar gives no resurrection warning
    if (!objc_is_finalized(srcObject)) {
        [[TestCase currentTestCase] fail:@"inner object not garbage"];
    }
    objc_memmove_collectable(&dstObject, &self->srcObject, sizeof(void *));
    [super finalize];
}

@end


@implementation ResurrectAssociatedTester
// associate a garbage object (self) with a non-garbage object
- (void)finalize {
    objc_setAssociatedObject(
                             [TestCase currentTestCase],     // target
                             self,       // use self as key
                             self,       // use self (garbage) as value
                             OBJC_ASSOCIATION_RETAIN_NONATOMIC);      
    objc_setAssociatedObject(
                             [TestCase currentTestCase],     // target
                             self,       // use self as key
                             nil,
                             OBJC_ASSOCIATION_RETAIN_NONATOMIC);      
    [super finalize];
}
@end


@implementation ResurrectCASTester
// store thread local and global objects into global variable using atomic primitive, test for resurrection warning
- (void)finalize {
    objc_atomicCompareAndSwapGlobal(global, self, &global);
    objc_atomicCompareAndSwapGlobal(global, nil, &global);
    [super finalize];
}
@end

@implementation ResurrectGlobalTester
// store the garbage object in a global
- (void)finalize
{
    global = self;
    global = nil;
    [super finalize];
}
@end

@implementation ResurrectGlobalIvarTester
// store the garbage object in a global object's ivar
- (void)finalize
{
    ivarTester->object = self;
    ivarTester->object = nil;
    [super finalize];
}
@end

@implementation ResurrectGlobalMemmoveTester
// store the garbage object in a global object's ivar
- (void)finalize
{
    // verify objc_memmove_collectable into live ivar gives resurrection warning
    objc_memmove_collectable(&ivarTester->object, &self, sizeof(void *));
    [super finalize];
}
@end

@implementation ResurrectGlobalWeakTester
- (void)finalize
{
    // verify a store into a global weak produces a warning
    global_weak = self;
    global_weak = nil;
    [super finalize];
}
@end

@implementation ResurrectThreadedTester

static void *otherThreadResurrect(void *arg)
{
    objc_registerThreadWithCollector();
    global = arg;
    global = nil;
    return NULL;
}

- (void)finalize
{
    pthread_t pthread;
    pthread_create(&pthread, NULL, otherThreadResurrect, self);
    pthread_join(pthread, NULL);
    
    [super finalize];
}
@end

@implementation ResurrectRetainTester
- (void)finalize
{
    CFRetain(self);
    [super finalize];
}
@end

/* Resurrection base class implementation */
@implementation Resurrection

+ (void)initialize
{
    if (!ivarTester) ivarTester = [GlobalResurrectionContainer new];
}

+ (BOOL)isConcreteTestCase
{
    if (self == [Resurrection class])
        return NO;
    return [super isConcreteTestCase];
}

- (Class)testClass
{
    // construct the test class name from our class name
    NSString *testName = [[self className] stringByAppendingString:@"Tester"];
    return NSClassFromString(testName);
}

// by default all tests are run with both a local and a global test object
- (BOOL)doGlobal
{
    return YES;
}
- (BOOL)doLocal
{
    return YES;
}

// convenience method for returning an empty output list
- (NSString **)emptyErrorOutput
{
    static NSString *errMsgs[] = {
        nil
    };
    return errMsgs;
}

// convenience method for returning common output list
- (NSString **)standardErrorOutput
{
    static NSString *errMsgs[] = {
        @"resurrection error for object",
        @"garbage pointer stored into reachable memory, break on auto_zone_resurrection_error to debug",
        @"**resurrected**",
        nil
    };
    return errMsgs;
}

// convenience method for returning common output list
- (NSString **)storeErrorOutput
{
    static NSString *errMsgs[] = {
        @"storing an already collected object",
        @"resurrection error for object",
        @"garbage pointer stored into reachable memory, break on auto_zone_resurrection_error to debug",
        @"**resurrected**",
        nil
    };
    return errMsgs;
}

// subclasses override to return customized error messages
- (NSString **)expectedErrorOutput
{
    return [self standardErrorOutput];
}

// subclasses override to return customized error messages for the thread local test block
- (NSString **)expectedLocalErrorOutput
{
    return [self expectedErrorOutput];
}

// subclasses override to return customized error messages for the global test block
- (NSString **)expectedGlobalErrorOutput
{
    return [self expectedErrorOutput];
}

- (NSArray *)localErrorOutput
{
    int c = 0;
    NSString **messages = [self expectedLocalErrorOutput];
    while (messages[c]) c++;
    return [NSArray arrayWithObjects:messages count:c];
}

- (NSArray *)globalErrorOutput
{
    int c = 0;
    NSString **messages = [self expectedGlobalErrorOutput];
    while (messages[c]) c++;
    return [NSArray arrayWithObjects:messages count:c];
}

- (BOOL)isFatalResurrection
{
    return NO;
}

- (NSString *)shouldSkip
{
    if ([self isFatalResurrection] && [self resurrectionIsFatal])
        return @"Resurrection is fatal";
    return [super shouldSkip];
}

- (void)processOutputLine:(NSString *)line
{
    NSString *expected = nil;
    if ([_expectedMessages count])
        expected = [_expectedMessages objectAtIndex:0];
    NSRange errRange;
    if (expected) 
        errRange = [line rangeOfString:expected];
    else
        errRange.location = NSNotFound;
    
    if (errRange.location == NSNotFound) {
        [super processOutputLine:line];
    } else {
        [_expectedMessages removeObjectAtIndex:0];
    }
}

- (void)checkResult
{
    if (_localTester)
        [self fail:@"did not finalize local test object"];
    if (_globalTester)
        [self fail:@"did not finalize global test object"];
    
    // run another collection to clean up the resurrected objects
    [self requestFullCollectionWithCompletionCallback:^{ [self testFinished]; }];
}

// done as a separate method to avoid keeping a reference on the stack
- (void)allocateLocal
{
    _localTester = [self disguise:[[self testClass] new]];
}

- (void)performGlobalTest
{
    if ([self doGlobal]) {
        [_expectedMessages addObjectsFromArray:[self localErrorOutput]];
        global = [[self testClass] new];
        _globalTester = [self disguise:global];
        global = nil;
    }
    [self requestFullCollectionWithCompletionCallback:^{ [self checkResult]; }];
}

- (void)performTest
{
    _expectedMessages = [NSMutableArray array];
    if ([self doLocal]) {
        [self allocateLocal];
        [self clearStack];
        [_expectedMessages addObjectsFromArray:[self localErrorOutput]];
        [self runThreadLocalCollection];
        // Must run 2 collections now. The first will clear the resurrected local block out of
        // the zombie list. The 2nd will cause it to finalize and generate a log.
        [self requestFullCollectionWithCompletionCallback:^{ 
            [self requestFullCollectionWithCompletionCallback: ^{ [self performGlobalTest]; }];
        }];
    } else {
        [self performGlobalTest];
    }
}

- (void)didFinalize:(TestFinalizer *)finalizer
{
    if (finalizer == [self undisguise:_localTester])
        _localTester = 0;
    if (finalizer == [self undisguise:_globalTester])
        _globalTester = 0;
}

- (void)outputComplete
{
    if (![self failed]) {
        if (_expectedMessages == nil || [_expectedMessages count] == 0)
            [self passed];
        else
            [self fail:@"did not emit expected error messages"];
    }
    [super outputComplete];
}
@end


/* Concrete TestCase subclasses */

@implementation ResurrectMemmoveOk
- (NSString **)expectedErrorOutput
{
    return [self emptyErrorOutput];
}
@end

@implementation ResurrectAssociated
@end

@implementation ResurrectCAS
@end

@implementation ResurrectGlobal
- (NSString **)expectedErrorOutput
{
    return [self storeErrorOutput];
}
@end

@implementation ResurrectGlobalIvar
@end

@implementation ResurrectGlobalMemmove
- (NSString *)shouldSkip
{
    return @"no resurrection check in auto_zone_write_barrier_memmove()";
}
@end

@implementation ResurrectGlobalWeak
@end

@implementation ResurrectThreaded
- (BOOL)doLocal
{
    return NO;
}
- (NSString **)expectedErrorOutput
{
    return [self storeErrorOutput];
}
@end

@implementation ResurrectRetain
- (BOOL)isFatalResurrection
{
    return YES;
}

- (NSString **)expectedErrorOutput
{
    static NSString *errMsgs[] = {
        @"was over-retained during finalization, refcount = 1",
        @"This could be an unbalanced CFRetain(), or CFRetain() balanced with -release.",
        @"Break on auto_zone_resurrection_error() to debug.",
        nil
    };
    return errMsgs;
}

@end