blob: 1dae11378b5971d290122312612a55eb3493f715 [file] [log] [blame] [edit]
//
// 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