| // | |
| // NSObjectRACPropertySubscribingExamples.m | |
| // ReactiveCocoa | |
| // | |
| // Created by Josh Vera on 4/10/13. | |
| // Copyright (c) 2013 GitHub, Inc. All rights reserved. | |
| // | |
| #import "RACTestObject.h" | |
| #import "NSObjectRACPropertySubscribingExamples.h" | |
| #import "EXTScope.h" | |
| #import "NSObject+RACDeallocating.h" | |
| #import "NSObject+RACPropertySubscribing.h" | |
| #import "RACCompoundDisposable.h" | |
| #import "RACDisposable.h" | |
| #import "RACSignal.h" | |
| NSString * const RACPropertySubscribingExamples = @"RACPropertySubscribingExamples"; | |
| NSString * const RACPropertySubscribingExamplesSetupBlock = @"RACPropertySubscribingExamplesSetupBlock"; | |
| SharedExamplesBegin(NSObjectRACPropertySubscribingExamples) | |
| sharedExamples(RACPropertySubscribingExamples, ^(NSDictionary *data) { | |
| __block RACSignal *(^signalBlock)(RACTestObject *object, NSString *keyPath, id observer); | |
| before(^{ | |
| signalBlock = data[RACPropertySubscribingExamplesSetupBlock]; | |
| }); | |
| it(@"should send the current value once on subscription", ^{ | |
| RACTestObject *object = [[RACTestObject alloc] init]; | |
| RACSignal *signal = signalBlock(object, @keypath(object, objectValue), self); | |
| NSMutableArray *values = [NSMutableArray array]; | |
| object.objectValue = @0; | |
| [signal subscribeNext:^(id x) { | |
| [values addObject:x]; | |
| }]; | |
| expect(values).to.equal((@[ @0 ])); | |
| }); | |
| it(@"should send the new value when it changes", ^{ | |
| RACTestObject *object = [[RACTestObject alloc] init]; | |
| RACSignal *signal = signalBlock(object, @keypath(object, objectValue), self); | |
| NSMutableArray *values = [NSMutableArray array]; | |
| object.objectValue = @0; | |
| [signal subscribeNext:^(id x) { | |
| [values addObject:x]; | |
| }]; | |
| expect(values).to.equal((@[ @0 ])); | |
| object.objectValue = @1; | |
| expect(values).to.equal((@[ @0, @1 ])); | |
| }); | |
| it(@"should stop observing when disposed", ^{ | |
| RACTestObject *object = [[RACTestObject alloc] init]; | |
| RACSignal *signal = signalBlock(object, @keypath(object, objectValue), self); | |
| NSMutableArray *values = [NSMutableArray array]; | |
| object.objectValue = @0; | |
| RACDisposable *disposable = [signal subscribeNext:^(id x) { | |
| [values addObject:x]; | |
| }]; | |
| object.objectValue = @1; | |
| NSArray *expected = @[ @0, @1 ]; | |
| expect(values).to.equal(expected); | |
| [disposable dispose]; | |
| object.objectValue = @2; | |
| expect(values).to.equal(expected); | |
| }); | |
| it(@"shouldn't send any more values after the observer is gone", ^{ | |
| __block BOOL observerDealloced = NO; | |
| RACTestObject *object = [[RACTestObject alloc] init]; | |
| NSMutableArray *values = [NSMutableArray array]; | |
| @autoreleasepool { | |
| RACTestObject *observer __attribute__((objc_precise_lifetime)) = [[RACTestObject alloc] init]; | |
| [observer.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{ | |
| observerDealloced = YES; | |
| }]]; | |
| RACSignal *signal = signalBlock(object, @keypath(object, objectValue), observer); | |
| object.objectValue = @1; | |
| [signal subscribeNext:^(id x) { | |
| [values addObject:x]; | |
| }]; | |
| } | |
| expect(observerDealloced).to.beTruthy(); | |
| NSArray *expected = @[ @1 ]; | |
| expect(values).to.equal(expected); | |
| object.objectValue = @2; | |
| expect(values).to.equal(expected); | |
| }); | |
| it(@"shouldn't keep either object alive unnaturally long", ^{ | |
| __block BOOL objectDealloced = NO; | |
| __block BOOL scopeObjectDealloced = NO; | |
| @autoreleasepool { | |
| RACTestObject *object __attribute__((objc_precise_lifetime)) = [[RACTestObject alloc] init]; | |
| [object.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{ | |
| objectDealloced = YES; | |
| }]]; | |
| RACTestObject *scopeObject __attribute__((objc_precise_lifetime)) = [[RACTestObject alloc] init]; | |
| [scopeObject.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{ | |
| scopeObjectDealloced = YES; | |
| }]]; | |
| RACSignal *signal = signalBlock(object, @keypath(object, objectValue), scopeObject); | |
| [signal subscribeNext:^(id _) { | |
| }]; | |
| } | |
| expect(objectDealloced).to.beTruthy(); | |
| expect(scopeObjectDealloced).to.beTruthy(); | |
| }); | |
| it(@"shouldn't keep the signal alive past the lifetime of the object", ^{ | |
| __block BOOL objectDealloced = NO; | |
| __block BOOL signalDealloced = NO; | |
| @autoreleasepool { | |
| RACTestObject *object __attribute__((objc_precise_lifetime)) = [[RACTestObject alloc] init]; | |
| [object.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{ | |
| objectDealloced = YES; | |
| }]]; | |
| RACSignal *signal = [signalBlock(object, @keypath(object, objectValue), self) map:^(id value) { | |
| return value; | |
| }]; | |
| [signal.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{ | |
| signalDealloced = YES; | |
| }]]; | |
| [signal subscribeNext:^(id _) { | |
| }]; | |
| } | |
| expect(signalDealloced).will.beTruthy(); | |
| expect(objectDealloced).to.beTruthy(); | |
| }); | |
| it(@"should not resurrect a deallocated object upon subscription", ^{ | |
| dispatch_queue_t queue = dispatch_queue_create(NULL, DISPATCH_QUEUE_CONCURRENT); | |
| @onExit { | |
| dispatch_release(queue); | |
| }; | |
| dispatch_set_target_queue(queue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)); | |
| // Fuzz out race conditions. | |
| for (unsigned i = 0; i < 100; i++) { | |
| dispatch_suspend(queue); | |
| __block CFTypeRef object; | |
| __block BOOL deallocated; | |
| RACSignal *signal; | |
| @autoreleasepool { | |
| RACTestObject *testObject = [[RACTestObject alloc] init]; | |
| [testObject.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{ | |
| deallocated = YES; | |
| }]]; | |
| signal = signalBlock(testObject, @keypath(testObject, objectValue), nil); | |
| object = CFBridgingRetain(testObject); | |
| } | |
| dispatch_block_t testSubscription = ^{ | |
| RACDisposable *disposable = [signal subscribeCompleted:^{}]; | |
| expect(disposable).notTo.beNil(); | |
| }; | |
| unsigned beforeCount = arc4random_uniform(20); | |
| for (unsigned j = 0; j < beforeCount; j++) { | |
| dispatch_async(queue, testSubscription); | |
| } | |
| dispatch_async(queue, ^{ | |
| CFRelease(object); | |
| // expect() is a bit finicky on background threads. | |
| STAssertTrue(deallocated, @"Object did not deallocate after being released"); | |
| }); | |
| unsigned afterCount = arc4random_uniform(20); | |
| for (unsigned j = 0; j < afterCount; j++) { | |
| dispatch_async(queue, testSubscription); | |
| } | |
| dispatch_barrier_async(queue, testSubscription); | |
| // Start everything and wait for it all to complete. | |
| dispatch_resume(queue); | |
| expect(deallocated).will.beTruthy(); | |
| dispatch_barrier_sync(queue, ^{}); | |
| } | |
| }); | |
| it(@"shouldn't crash when the value is changed on a different queue", ^{ | |
| __block id value; | |
| @autoreleasepool { | |
| RACTestObject *object __attribute__((objc_precise_lifetime)) = [[RACTestObject alloc] init]; | |
| RACSignal *signal = signalBlock(object, @keypath(object, objectValue), self); | |
| [signal subscribeNext:^(id x) { | |
| value = x; | |
| }]; | |
| NSOperationQueue *queue = [[NSOperationQueue alloc] init]; | |
| [queue addOperationWithBlock:^{ | |
| object.objectValue = @1; | |
| }]; | |
| [queue waitUntilAllOperationsAreFinished]; | |
| } | |
| expect(value).will.equal(@1); | |
| }); | |
| describe(@"mutating collections", ^{ | |
| __block RACTestObject *object; | |
| __block NSMutableOrderedSet *lastValue; | |
| __block NSMutableOrderedSet *proxySet; | |
| before(^{ | |
| object = [[RACTestObject alloc] init]; | |
| object.objectValue = [NSMutableOrderedSet orderedSetWithObject:@1]; | |
| NSString *keyPath = @keypath(object, objectValue); | |
| [signalBlock(object, keyPath, self) subscribeNext:^(NSMutableOrderedSet *x) { | |
| lastValue = x; | |
| }]; | |
| proxySet = [object mutableOrderedSetValueForKey:keyPath]; | |
| }); | |
| it(@"sends the newest object when inserting values into an observed object", ^{ | |
| NSMutableOrderedSet *expected = [NSMutableOrderedSet orderedSetWithObjects: @1, @2, nil]; | |
| [proxySet addObject:@2]; | |
| expect(lastValue).to.equal(expected); | |
| }); | |
| it(@"sends the newest object when removing values in an observed object", ^{ | |
| NSMutableOrderedSet *expected = [NSMutableOrderedSet orderedSet]; | |
| [proxySet removeAllObjects]; | |
| expect(lastValue).to.equal(expected); | |
| }); | |
| it(@"sends the newest object when replacing values in an observed object", ^{ | |
| NSMutableOrderedSet *expected = [NSMutableOrderedSet orderedSetWithObjects: @2, nil]; | |
| [proxySet replaceObjectAtIndex:0 withObject:@2]; | |
| expect(lastValue).to.equal(expected); | |
| }); | |
| }); | |
| }); | |
| SharedExamplesEnd |