FBRetainCycleDetectorTests/FBRetainCycleDetectorTests.mm (560 lines of code) (raw):

/** * Copyright (c) 2016-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. */ #import <UIKit/UIKit.h> #import <XCTest/XCTest.h> #import <FBRetainCycleDetector/FBObjectiveCBlock.h> #import <FBRetainCycleDetector/FBObjectiveCGraphElement+Internal.h> #import <FBRetainCycleDetector/FBObjectiveCObject.h> #import <FBRetainCycleDetector/FBRetainCycleDetector+Internal.h> typedef void (^_RCDTestBlockType)(); typedef struct { id<NSObject> model; __weak id<NSObject> weakModel; } _RCDTestStruct; @interface _RCDTestClass : NSObject @property (nonatomic, strong) NSObject *object; @property (nonatomic, strong) NSObject *secondObject; @property (nonatomic, copy) NSArray *array; @property (nonatomic, weak) NSObject *weakObject; @property (nonatomic, strong) _RCDTestBlockType block; @property (nonatomic, assign) _RCDTestStruct someStruct; @end @implementation _RCDTestClass @end @interface _RCDTestSubclass : _RCDTestClass @end @implementation _RCDTestSubclass @end @interface _RCDTestGraphElement : FBObjectiveCGraphElement - (instancetype)initWithObject:(id)object fakedAddress:(size_t)address className:(NSString *)className; @end @implementation _RCDTestGraphElement { size_t _address; FBObjectiveCObject *_object; NSString *_className; } - (instancetype)initWithObject:(id)object fakedAddress:(size_t)address className:(NSString *)className { if (self = [super init]) { _address = address; _object = [[FBObjectiveCObject alloc] initWithObject:object]; _className = className; } return self; } - (NSSet *)allRetainedObjects { return [_object allRetainedObjects]; } - (size_t)objectAddress { return _address; } - (NSString *)classNameOrNull { return _className; } - (Class)objectClass { return nil; } @end @interface FBRetainCycleDetectorTests : XCTestCase @end @implementation FBRetainCycleDetectorTests #if _INTERNAL_RCD_ENABLED - (void)testThatDetectorWillFindNoCyclesInEmptyObject { _RCDTestClass *testObject = [_RCDTestClass new]; FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; [detector addCandidate:testObject]; NSSet *retainCycles = [detector findRetainCycles]; XCTAssertEqual([retainCycles count], 0); } - (void)testThatDetectorWillFindCycleCreatedByOneObjectWithItself { _RCDTestClass *testObject = [_RCDTestClass new]; testObject.object = testObject; FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; [detector addCandidate:testObject]; NSSet *retainCycles = [detector findRetainCycles]; NSSet *expectedSet = [NSSet setWithObject:@[[[FBObjectiveCObject alloc] initWithObject:testObject]]]; XCTAssertEqualObjects(retainCycles, expectedSet); } - (void)testThatDetectorWillFindNoCycleIfOneIsUsingWeakProperty { _RCDTestClass *testObject = [_RCDTestClass new]; testObject.weakObject = testObject; FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; [detector addCandidate:testObject]; NSSet *retainCycles = [detector findRetainCycles]; XCTAssertEqual([retainCycles count], 0); } /** 1 -> 2 -> 3 ^ | \_________/ */ - (void)testThatDetectorWillFindCycleBetweenThreeElements { _RCDTestClass *testObject1 = [_RCDTestClass new]; _RCDTestClass *testObject2 = [_RCDTestClass new]; _RCDTestClass *testObject3 = [_RCDTestClass new]; testObject1.object = testObject2; testObject2.object = testObject3; testObject3.object = testObject1; FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; [detector addCandidate:testObject1]; NSSet *retainCycles = [detector findRetainCycles]; NSSet *expectedSet = [NSSet setWithObject:[detector _shiftToUnifiedCycle: @[[[FBObjectiveCObject alloc] initWithObject:testObject1], [[FBObjectiveCObject alloc] initWithObject:testObject2], [[FBObjectiveCObject alloc] initWithObject:testObject3], ]]]; XCTAssertEqualObjects(retainCycles, expectedSet); } /** The following example could technically be tricky with illed implemented DFS. 1 / \ / \ 2 <-- 3 */ - (void)testThatDetectorWillFindNoCycleIfObjectsWithCommonParentReferThemselves { _RCDTestClass *testObject1 = [_RCDTestClass new]; _RCDTestClass *testObject2 = [_RCDTestClass new]; _RCDTestClass *testObject3 = [_RCDTestClass new]; testObject1.object = testObject2; testObject1.secondObject = testObject3; testObject3.object = testObject2; FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; [detector addCandidate:testObject1]; NSSet *retainCycles = [detector findRetainCycles]; XCTAssertEqual([retainCycles count], 0); } - (void)testThatDetectorWillFindCycleIfArrayIsPartOfIt { _RCDTestClass *testObject = [_RCDTestClass new]; NSArray *array = @[testObject]; testObject.array = array; FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; [detector addCandidate:testObject]; NSSet *retainCycles = [detector findRetainCycles]; NSSet *expectedSet = [NSSet setWithObject:[detector _shiftToUnifiedCycle: @[[[FBObjectiveCObject alloc] initWithObject:testObject], [[FBObjectiveCObject alloc] initWithObject:array]]]]; XCTAssertEqualObjects(retainCycles, expectedSet); } - (void)testThatDetectorWillFindCycleIfCandidateIsNotPartOfTheCycle { _RCDTestClass *testObjectThatWontBePartOfRetainCycle = [_RCDTestClass new]; _RCDTestClass *testObject1 = [_RCDTestClass new]; _RCDTestClass *testObject2 = [_RCDTestClass new]; _RCDTestClass *testObject3 = [_RCDTestClass new]; testObjectThatWontBePartOfRetainCycle.object = testObject1; testObject1.object = testObject2; testObject2.object = testObject3; testObject3.object = testObject1; FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; [detector addCandidate:testObjectThatWontBePartOfRetainCycle]; NSSet *retainCycles = [detector findRetainCycles]; NSSet *expectedSet = [NSSet setWithObject:[detector _shiftToUnifiedCycle: @[[[FBObjectiveCObject alloc] initWithObject:testObject1], [[FBObjectiveCObject alloc] initWithObject:testObject2], [[FBObjectiveCObject alloc] initWithObject:testObject3]]]]; XCTAssertEqualObjects(retainCycles, expectedSet); } - (void)testThatDetectorWillFindCycleIfDictionaryIsPartOfIt { _RCDTestClass *testObject = [_RCDTestClass new]; NSDictionary *dictionary = @{@"irrelevantKey": testObject}; testObject.object = dictionary; FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; [detector addCandidate:testObject]; NSSet *retainCycles = [detector findRetainCycles]; NSSet *expectedSet = [NSSet setWithObject:[detector _shiftToUnifiedCycle: @[[[FBObjectiveCObject alloc] initWithObject:testObject], [[FBObjectiveCObject alloc] initWithObject:dictionary]]]]; XCTAssertEqualObjects(retainCycles, expectedSet); } - (void)testThatDetectorWillFindAllCyclesIfCandidateIsPartOfMoreThanOneCycle { _RCDTestClass *testObject1 = [_RCDTestClass new]; _RCDTestClass *testObject2 = [_RCDTestClass new]; _RCDTestClass *testObject3 = [_RCDTestClass new]; testObject1.object = testObject2; testObject1.secondObject = testObject3; testObject2.object = testObject1; testObject3.object = testObject1; FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; [detector addCandidate:testObject1]; NSSet *retainCycles = [detector findRetainCycles]; NSArray *firstCycle = [detector _shiftToUnifiedCycle: @[[[FBObjectiveCObject alloc] initWithObject:testObject1], [[FBObjectiveCObject alloc] initWithObject:testObject2]]]; NSArray *secondCycle = [detector _shiftToUnifiedCycle: @[[[FBObjectiveCObject alloc] initWithObject:testObject1], [[FBObjectiveCObject alloc] initWithObject:testObject3]]]; XCTAssertTrue([retainCycles containsObject:firstCycle]); XCTAssertTrue([retainCycles containsObject:secondCycle]); } /** /---> 1 <----\ | / \ | | / 3 -->| | 2 <- | / \ \ |/ \ \ 4 5 \ / \ | / \| 6 7 3 cycles: 1-2-4-1 2-5-7-2 1-3-1 */ - (void)testThatDetectorWillFindAllCyclesInMoreComplicatedObjectGraph { _RCDTestClass *testObject1 = [_RCDTestClass new]; _RCDTestClass *testObject2 = [_RCDTestClass new]; _RCDTestClass *testObject3 = [_RCDTestClass new]; _RCDTestClass *testObject4 = [_RCDTestClass new]; _RCDTestClass *testObject5 = [_RCDTestClass new]; _RCDTestClass *testObject6 = [_RCDTestClass new]; _RCDTestClass *testObject7 = [_RCDTestClass new]; testObject1.object = testObject2; testObject1.secondObject = testObject3; testObject2.object = testObject4; testObject2.secondObject = testObject5; testObject5.object = testObject6; testObject5.secondObject = testObject7; // Cycling testObject3.object = testObject1; testObject4.object = testObject1; testObject7.object = testObject2; FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; [detector addCandidate:testObject1]; NSSet *retainCycles = [detector findRetainCycles]; NSArray *firstCycle = [detector _shiftToUnifiedCycle: @[[[FBObjectiveCObject alloc] initWithObject:testObject1], [[FBObjectiveCObject alloc] initWithObject:testObject2], [[FBObjectiveCObject alloc] initWithObject:testObject4]]]; NSArray *secondCycle = [detector _shiftToUnifiedCycle: @[[[FBObjectiveCObject alloc] initWithObject:testObject2], [[FBObjectiveCObject alloc] initWithObject:testObject5], [[FBObjectiveCObject alloc] initWithObject:testObject7]]]; NSArray *thirdCycle = [detector _shiftToUnifiedCycle: @[[[FBObjectiveCObject alloc] initWithObject:testObject1], [[FBObjectiveCObject alloc] initWithObject:testObject3]]]; XCTAssertTrue([retainCycles containsObject:firstCycle]); XCTAssertTrue([retainCycles containsObject:secondCycle]); XCTAssertTrue([retainCycles containsObject:thirdCycle]); } /** 1 5<- / \ /^\| 2 3 | 6 |/ 4 */ - (void)testThatDetectorWillCatchCyclesWithoutRetraversing { _RCDTestClass *testObject1 = [_RCDTestClass new]; _RCDTestClass *testObject2 = [_RCDTestClass new]; _RCDTestClass *testObject3 = [_RCDTestClass new]; _RCDTestClass *testObject4 = [_RCDTestClass new]; _RCDTestClass *testObject5 = [_RCDTestClass new]; _RCDTestClass *testObject6 = [_RCDTestClass new]; testObject1.object = testObject2; testObject1.secondObject = testObject3; testObject5.object = testObject3; testObject5.secondObject = testObject6; testObject3.object = testObject4; testObject4.object = testObject5; testObject6.object = testObject5; FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; [detector addCandidate:testObject1]; [detector addCandidate:testObject2]; NSSet *retainCycles = [detector findRetainCycles]; NSArray *firstCycle = [detector _shiftToUnifiedCycle: @[[[FBObjectiveCObject alloc] initWithObject:testObject3], [[FBObjectiveCObject alloc] initWithObject:testObject4], [[FBObjectiveCObject alloc] initWithObject:testObject5]]]; NSArray *secondCycle = [detector _shiftToUnifiedCycle: @[[[FBObjectiveCObject alloc] initWithObject:testObject5], [[FBObjectiveCObject alloc] initWithObject:testObject6]]]; XCTAssertTrue([retainCycles containsObject:firstCycle]); XCTAssertTrue([retainCycles containsObject:secondCycle]); } - (void)testThatDetectorWillFindCycleWithFewCollectionsMixedInIt { _RCDTestClass *testObject = [_RCDTestClass new]; NSArray *array = @[testObject]; NSSet *set = [NSSet setWithObject:array]; NSDictionary *dictionary = @{@"set": set}; testObject.object = dictionary; FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; [detector addCandidate:set]; NSSet *retainCycles = [detector findRetainCycles]; NSSet *expectedSet = [NSSet setWithObject:[detector _shiftToUnifiedCycle: @[[[FBObjectiveCObject alloc] initWithObject:set], [[FBObjectiveCObject alloc] initWithObject:array], [[FBObjectiveCObject alloc] initWithObject:testObject], [[FBObjectiveCObject alloc] initWithObject:dictionary]]]]; XCTAssertEqualObjects(retainCycles, expectedSet); } - (void)testThatDetectorWillFindAllCyclesThatArrayIsPartOf { _RCDTestClass *testObject1 = [_RCDTestClass new]; _RCDTestClass *testObject2 = [_RCDTestClass new]; NSArray *array = @[testObject1, testObject2]; testObject1.object = array; testObject2.object = array; FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; [detector addCandidate:testObject1]; NSSet *retainCycles = [detector findRetainCycles]; NSArray *firstCycle = [detector _shiftToUnifiedCycle: @[[[FBObjectiveCObject alloc] initWithObject:testObject1], [[FBObjectiveCObject alloc] initWithObject:array]]]; NSArray *secondCycle = [detector _shiftToUnifiedCycle: @[[[FBObjectiveCObject alloc] initWithObject:array], [[FBObjectiveCObject alloc] initWithObject:testObject2]]]; XCTAssertTrue([retainCycles containsObject:firstCycle]); XCTAssertTrue([retainCycles containsObject:secondCycle]); } - (void)testThatDetectorWillFindOnlyOneRetainCycleIfArrayRefersToSameObjectTwice { _RCDTestClass *testObject = [_RCDTestClass new]; NSArray *array = @[testObject, testObject]; testObject.object = array; FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; [detector addCandidate:testObject]; NSSet *retainCycles = [detector findRetainCycles]; NSSet *expectedSet = [NSSet setWithObject:[detector _shiftToUnifiedCycle: @[[[FBObjectiveCObject alloc] initWithObject:testObject], [[FBObjectiveCObject alloc] initWithObject:array]]]]; XCTAssertEqualObjects(retainCycles, expectedSet); } // Negative Tests /** 1 / \ 2 3 \ / 4 */ - (void)testThatDetectorWillNotFindCycleInDiamondDAG { _RCDTestClass *testObject1 = [_RCDTestClass new]; _RCDTestClass *testObject2 = [_RCDTestClass new]; _RCDTestClass *testObject3 = [_RCDTestClass new]; _RCDTestClass *testObject4 = [_RCDTestClass new]; testObject1.object = testObject2; testObject1.secondObject = testObject3; testObject2.object = testObject4; testObject3.object = testObject4; FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; [detector addCandidate:testObject1]; NSSet *retainCycles = [detector findRetainCycles]; XCTAssertEqual([retainCycles count], 0); } - (void)testThatDetectorWillNotFindCycleInCommonDelegationPattern { _RCDTestClass *testObject1 = [_RCDTestClass new]; _RCDTestClass *testObject2 = [_RCDTestClass new]; testObject1.object = testObject2; testObject2.weakObject = testObject1; FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; [detector addCandidate:testObject1]; NSSet *retainCycles = [detector findRetainCycles]; XCTAssertEqual([retainCycles count], 0); } - (void)testThatDetectorWillNotFindCycleWhenObjectIsWrappedInNSValue { _RCDTestClass *testObject = [_RCDTestClass new]; NSValue *value = [NSValue valueWithNonretainedObject:testObject]; testObject.object = value; FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; [detector addCandidate:testObject]; NSSet *retainCycles = [detector findRetainCycles]; XCTAssertEqual([retainCycles count], 0); } // Blocks - (void)testThatDetectorWillNotFindCycleInBlockIfItHoldsWeakReferenceToObject { _RCDTestClass *testObject = [_RCDTestClass new]; __weak NSObject *weakTestObject = testObject; __block NSObject *unretainedObject; _RCDTestBlockType block = ^{ unretainedObject = weakTestObject; }; testObject.block = block; FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; [detector addCandidate:testObject]; NSSet *retainCycles = [detector findRetainCycles]; XCTAssertEqual([retainCycles count], 0); } - (void)testThatDetectorWillFindCycleBetweenBlockAndObject { _RCDTestClass *testObject = [_RCDTestClass new]; __block NSObject *unretainedObject; _RCDTestBlockType block = ^{ unretainedObject = testObject; }; block = [block copy]; testObject.block = block; FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; [detector addCandidate:testObject]; NSSet *retainCycles = [detector findRetainCycles]; NSSet *expectedSet = [NSSet setWithObject:[detector _shiftToUnifiedCycle: @[[[FBObjectiveCObject alloc] initWithObject:testObject], [[FBObjectiveCBlock alloc] initWithObject:block]]]]; XCTAssertEqualObjects(retainCycles, expectedSet); } - (void)testThatDetectorWillFindCycleBetweenBlockAndObjectHeldByArray { _RCDTestClass *testObject = [_RCDTestClass new]; NSArray *array = @[testObject]; __block NSObject *unretainedObject; _RCDTestBlockType block = ^{ unretainedObject = array; }; block = [block copy]; testObject.block = block; FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; [detector addCandidate:testObject]; NSSet *retainCycles = [detector findRetainCycles]; NSSet *expectedSet = [NSSet setWithObject:[detector _shiftToUnifiedCycle: @[[[FBObjectiveCObject alloc] initWithObject:testObject], [[FBObjectiveCBlock alloc] initWithObject:block], [[FBObjectiveCObject alloc] initWithObject:array]]]]; XCTAssertEqualObjects(retainCycles, expectedSet); } - (void)testThatDetectorWillFindCycleIfMultipleBlocksArePartOfIt { _RCDTestClass *testObject1 = [_RCDTestClass new]; _RCDTestClass *testObject2 = [_RCDTestClass new]; __block NSObject *unretainedObject; _RCDTestBlockType block1 = ^{unretainedObject = testObject1;}; _RCDTestBlockType block2 = ^{unretainedObject = testObject2;}; block1 = [block1 copy]; block2 = [block2 copy]; testObject1.block = block2; testObject2.block = block1; FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; [detector addCandidate:block1]; NSSet *retainCycles = [detector findRetainCycles]; NSSet *expectedSet = [NSSet setWithObject:[detector _shiftToUnifiedCycle: @[[[FBObjectiveCBlock alloc] initWithObject:block1], [[FBObjectiveCObject alloc] initWithObject:testObject1], [[FBObjectiveCBlock alloc] initWithObject:block2], [[FBObjectiveCObject alloc] initWithObject:testObject2]]]]; XCTAssertEqualObjects(retainCycles, expectedSet); } - (void)testThatDetectorWillFindCycleIfParentClassPropertyIsAReasonForCycle { _RCDTestSubclass *testObject = [_RCDTestSubclass new]; testObject.object = testObject; FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; [detector addCandidate:testObject]; NSSet *retainCycles = [detector findRetainCycles]; NSSet *expectedSet = [NSSet setWithObject:@[[[FBObjectiveCObject alloc] initWithObject:testObject]]]; XCTAssertEqualObjects(retainCycles, expectedSet); } - (void)testThatDetectorWillFindCycleBetweenObjectAndItsSubclass { _RCDTestSubclass *testObject = [_RCDTestSubclass new]; _RCDTestClass *testObject2 = [_RCDTestClass new]; testObject.object = testObject2; testObject2.object = testObject; FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; [detector addCandidate:testObject]; NSSet *retainCycles = [detector findRetainCycles]; NSSet *expectedSet = [NSSet setWithObject:[detector _shiftToUnifiedCycle: @[[[FBObjectiveCObject alloc] initWithObject:testObject], [[FBObjectiveCObject alloc] initWithObject:testObject2]]]]; XCTAssertEqualObjects(retainCycles, expectedSet); } - (void)testThatDetectorWillFindCycleIfPartOfItIsElementOfStruct { _RCDTestClass *testObject1 = [_RCDTestClass new]; _RCDTestClass *testObject2 = [_RCDTestClass new]; testObject1.someStruct = _RCDTestStruct { .model = testObject2 }; testObject2.object = testObject1; FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; [detector addCandidate:testObject1]; NSSet *retainCycles = [detector findRetainCycles]; NSSet *expectedSet = [NSSet setWithObject:[detector _shiftToUnifiedCycle: @[[[FBObjectiveCObject alloc] initWithObject:testObject1], [[FBObjectiveCObject alloc] initWithObject:testObject2]]]]; XCTAssertEqualObjects(retainCycles, expectedSet); } - (void)testThatDetectorWillNotFindCycleIfItGoesThroughStructsWeakReference { _RCDTestClass *testObject1 = [_RCDTestClass new]; _RCDTestClass *testObject2 = [_RCDTestClass new]; testObject1.someStruct = _RCDTestStruct { .weakModel = testObject2 }; testObject2.object = testObject1; FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; [detector addCandidate:testObject1]; NSSet *retainCycles = [detector findRetainCycles]; XCTAssertEqual([retainCycles count], 0); } - (void)testRetainCycleInStandardViewControllerDelegation { UIViewController<UITableViewDelegate> *vc = [UIViewController<UITableViewDelegate> new]; UITableView *tv = [UITableView new]; tv.delegate = vc; FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; [detector addCandidate:vc]; NSSet *retainCycles = [detector findRetainCycles]; XCTAssertEqual([retainCycles count], 0); } - (void)testThatDetectorWillNotFindCyclesDeeperThanItsStackDepth { _RCDTestClass *testObject1 = [_RCDTestClass new]; _RCDTestClass *testObject2 = [_RCDTestClass new]; _RCDTestClass *testObject3 = [_RCDTestClass new]; testObject1.object = testObject2; testObject2.object = testObject3; testObject3.object = testObject1; FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; [detector addCandidate:testObject1]; NSSet *retainCycles = [detector findRetainCyclesWithMaxCycleLength:2]; XCTAssertEqual([retainCycles count], 0); } - (void)testThatDetectorWillShiftCycleToLowestAddressFirstWhenItsPlacedInTheMiddle { _RCDTestClass *testObject1 = [_RCDTestClass new]; _RCDTestClass *testObject2 = [_RCDTestClass new]; _RCDTestClass *testObject3 = [_RCDTestClass new]; _RCDTestClass *testObject4 = [_RCDTestClass new]; _RCDTestClass *testObject5 = [_RCDTestClass new]; _RCDTestGraphElement *wrapped1 = [[_RCDTestGraphElement alloc] initWithObject:testObject1 fakedAddress:0 className:@"3"]; _RCDTestGraphElement *wrapped2 = [[_RCDTestGraphElement alloc] initWithObject:testObject2 fakedAddress:1 className:@"2"]; _RCDTestGraphElement *wrapped3 = [[_RCDTestGraphElement alloc] initWithObject:testObject3 fakedAddress:4 className:@"0"]; _RCDTestGraphElement *wrapped4 = [[_RCDTestGraphElement alloc] initWithObject:testObject4 fakedAddress:7 className:@"7"]; _RCDTestGraphElement *wrapped5 = [[_RCDTestGraphElement alloc] initWithObject:testObject5 fakedAddress:8 className:@"4"]; FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; NSArray *shifted = [detector _shiftToUnifiedCycle:@[wrapped1, wrapped2, wrapped3, wrapped4, wrapped5]]; NSArray *expectedShift = @[wrapped3, wrapped4, wrapped5, wrapped1, wrapped2]; XCTAssertEqualObjects(shifted, expectedShift); } /** A | B / \ C D \ / E <-\ | | F --/ It could register duplicate since we can get to E from two different paths. */ - (void)testThatDetectorWillRemoveDuplicateCycles { _RCDTestClass *testObject1 = [_RCDTestClass new]; _RCDTestClass *testObject2 = [_RCDTestClass new]; _RCDTestClass *testObject3 = [_RCDTestClass new]; _RCDTestClass *testObject4 = [_RCDTestClass new]; _RCDTestClass *testObject5 = [_RCDTestClass new]; _RCDTestClass *testObject6 = [_RCDTestClass new]; testObject1.object = testObject2; testObject2.object = testObject3; testObject2.secondObject = testObject4; testObject3.object = testObject5; testObject4.object = testObject5; testObject5.object = testObject6; testObject6.object = testObject5; FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; [detector addCandidate:testObject1]; NSSet *retainCycles = [detector findRetainCycles]; NSSet *expectedSet = [NSSet setWithObject:[detector _shiftToUnifiedCycle: @[[[FBObjectiveCObject alloc] initWithObject:testObject5], [[FBObjectiveCObject alloc] initWithObject:testObject6]]]]; XCTAssertEqualObjects(retainCycles, expectedSet); } - (void)testThatDetectorWillRemoveDuplicatesIfOneOfThemIsCyclicShiftOfOther { _RCDTestClass *testObject1 = [_RCDTestClass new]; _RCDTestClass *testObject2 = [_RCDTestClass new]; testObject1.object = testObject2; testObject2.object = testObject1; FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; [detector addCandidate:testObject1]; [detector addCandidate:testObject2]; NSSet *retainCycles = [detector findRetainCycles]; NSSet *expectedSet = [NSSet setWithObject:[detector _shiftToUnifiedCycle: @[[[FBObjectiveCObject alloc] initWithObject:testObject1], [[FBObjectiveCObject alloc] initWithObject:testObject2]]]]; XCTAssertEqualObjects(retainCycles, expectedSet); } #endif //_INTERNAL_RCD_ENABLED @end