|
25 | 25 | @import XCTest; |
26 | 26 | #import "SFSDKSafeMutableSet.h" |
27 | 27 |
|
| 28 | +// Blocking -hash pauses inside NSMutableSet's mutation so publication timing is deterministic. |
| 29 | +@interface SFSDKBlockingHashObject : NSObject |
| 30 | +@property (nonatomic, strong) dispatch_semaphore_t hashStarted; |
| 31 | +@property (nonatomic, strong) dispatch_semaphore_t allowHashToReturn; |
| 32 | +@property (atomic, assign) BOOL shouldBlockHash; |
| 33 | +@end |
| 34 | + |
| 35 | +@implementation SFSDKBlockingHashObject |
| 36 | + |
| 37 | +- (NSUInteger)hash { |
| 38 | + if (self.shouldBlockHash) { |
| 39 | + self.shouldBlockHash = NO; |
| 40 | + dispatch_semaphore_signal(self.hashStarted); |
| 41 | + dispatch_semaphore_wait(self.allowHashToReturn, |
| 42 | + dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC))); |
| 43 | + } |
| 44 | + return 1; |
| 45 | +} |
| 46 | + |
| 47 | +@end |
| 48 | + |
28 | 49 | @interface SFSDKSafeMutableSetTests : XCTestCase |
29 | 50 | @end |
30 | 51 |
|
@@ -76,6 +97,58 @@ - (void)testConcurrentWrites { |
76 | 97 | }]; |
77 | 98 | } |
78 | 99 |
|
| 100 | +- (void)testAddObjectPublishesMutationBeforeReturning { |
| 101 | + SFSDKSafeMutableSet *set = [SFSDKSafeMutableSet set]; |
| 102 | + SFSDKBlockingHashObject *object = [SFSDKBlockingHashObject new]; |
| 103 | + object.hashStarted = dispatch_semaphore_create(0); |
| 104 | + object.allowHashToReturn = dispatch_semaphore_create(0); |
| 105 | + object.shouldBlockHash = YES; |
| 106 | + dispatch_semaphore_t addReturned = dispatch_semaphore_create(0); |
| 107 | + |
| 108 | + dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ |
| 109 | + [set addObject:object]; |
| 110 | + dispatch_semaphore_signal(addReturned); |
| 111 | + }); |
| 112 | + |
| 113 | + XCTAssertEqual(dispatch_semaphore_wait(object.hashStarted, |
| 114 | + dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC))), 0, |
| 115 | + @"set mutation should begin"); |
| 116 | + XCTAssertNotEqual(dispatch_semaphore_wait(addReturned, DISPATCH_TIME_NOW), 0, |
| 117 | + @"addObject: must not return before its mutation is published"); |
| 118 | + |
| 119 | + dispatch_semaphore_signal(object.allowHashToReturn); |
| 120 | + XCTAssertEqual(dispatch_semaphore_wait(addReturned, |
| 121 | + dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC))), 0, |
| 122 | + @"addObject: should return after publishing the mutation"); |
| 123 | + XCTAssertTrue([set containsObject:object]); |
| 124 | +} |
| 125 | + |
| 126 | +- (void)testEnumerationCallbackCanMutateSet { |
| 127 | + SFSDKSafeMutableSet *set = [SFSDKSafeMutableSet set]; |
| 128 | + [set addObjectsFromArray:@[@"one", @"two"]]; |
| 129 | + NSMutableSet *enumeratedObjects = [NSMutableSet set]; |
| 130 | + XCTestExpectation *enumerationCompleted = [self expectationWithDescription:@"enumerationCompleted"]; |
| 131 | + |
| 132 | + dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ |
| 133 | + [set enumerateObjectsUsingBlock:^(id obj, BOOL *stop) { |
| 134 | + [enumeratedObjects addObject:obj]; |
| 135 | + [set addObject:@"addedFromCallback"]; |
| 136 | + }]; |
| 137 | + [enumerationCompleted fulfill]; |
| 138 | + }); |
| 139 | + |
| 140 | + XCTWaiterResult result = [XCTWaiter waitForExpectations:@[enumerationCompleted] timeout:2.0]; |
| 141 | + XCTAssertEqual(result, XCTWaiterResultCompleted, |
| 142 | + @"enumeration callbacks must be able to mutate the set without deadlocking"); |
| 143 | + if (result != XCTWaiterResultCompleted) { |
| 144 | + return; |
| 145 | + } |
| 146 | + |
| 147 | + NSSet *expectedObjects = [NSSet setWithObjects:@"one", @"two", nil]; |
| 148 | + XCTAssertEqualObjects(enumeratedObjects, expectedObjects); |
| 149 | + XCTAssertTrue([set containsObject:@"addedFromCallback"]); |
| 150 | +} |
| 151 | + |
79 | 152 | - (void)testConcurrentReadWrites { |
80 | 153 | NSArray *inputs = @[@"Test1", @"Test2", @"Test3", @"Test4", @"Test5"]; |
81 | 154 |
|
|
0 commit comments