blob: 6bc175f82c4b11836c3c994d8ec7949652a91f33 [file] [log] [blame]
//
// RACSubjectSpec.m
// ReactiveCocoa
//
// Created by Josh Abernathy on 6/24/12.
// Copyright (c) 2012 GitHub, Inc. All rights reserved.
//
#import "RACSubscriberExamples.h"
#import <libkern/OSAtomic.h>
#import "EXTScope.h"
#import "RACBehaviorSubject.h"
#import "RACDisposable.h"
#import "RACReplaySubject.h"
#import "RACScheduler.h"
#import "RACSignal+Operations.h"
#import "RACSubject.h"
#import "RACUnit.h"
SpecBegin(RACSubject)
describe(@"RACSubject", ^{
__block RACSubject *subject;
__block NSMutableArray *values;
__block BOOL success;
__block NSError *error;
beforeEach(^{
values = [NSMutableArray array];
subject = [RACSubject subject];
success = YES;
error = nil;
[subject subscribeNext:^(id value) {
[values addObject:value];
} error:^(NSError *e) {
error = e;
success = NO;
} completed:^{
success = YES;
}];
});
itShouldBehaveLike(RACSubscriberExamples, ^{
return @{
RACSubscriberExampleSubscriber: subject,
RACSubscriberExampleValuesReceivedBlock: [^{ return [values copy]; } copy],
RACSubscriberExampleErrorReceivedBlock: [^{ return error; } copy],
RACSubscriberExampleSuccessBlock: [^{ return success; } copy]
};
});
});
describe(@"RACReplaySubject", ^{
__block RACReplaySubject *subject = nil;
describe(@"with a capacity of 1", ^{
beforeEach(^{
subject = [RACReplaySubject replaySubjectWithCapacity:1];
});
it(@"should send the last value", ^{
id firstValue = @"blah";
id secondValue = @"more blah";
[subject sendNext:firstValue];
[subject sendNext:secondValue];
__block id valueReceived = nil;
[subject subscribeNext:^(id x) {
valueReceived = x;
}];
expect(valueReceived).to.equal(secondValue);
});
it(@"should send the last value to new subscribers after completion", ^{
id firstValue = @"blah";
id secondValue = @"more blah";
__block id valueReceived = nil;
__block NSUInteger nextsReceived = 0;
[subject sendNext:firstValue];
[subject sendNext:secondValue];
expect(nextsReceived).to.equal(0);
expect(valueReceived).to.beNil();
[subject sendCompleted];
[subject subscribeNext:^(id x) {
valueReceived = x;
nextsReceived++;
}];
expect(nextsReceived).to.equal(1);
expect(valueReceived).to.equal(secondValue);
});
it(@"should not send any values to new subscribers if none were sent originally", ^{
[subject sendCompleted];
__block BOOL nextInvoked = NO;
[subject subscribeNext:^(id x) {
nextInvoked = YES;
}];
expect(nextInvoked).to.beFalsy();
});
it(@"should resend errors", ^{
NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:nil];
[subject sendError:error];
__block BOOL errorSent = NO;
[subject subscribeError:^(NSError *sentError) {
expect(sentError).to.equal(error);
errorSent = YES;
}];
expect(errorSent).to.beTruthy();
});
it(@"should resend nil errors", ^{
[subject sendError:nil];
__block BOOL errorSent = NO;
[subject subscribeError:^(NSError *sentError) {
expect(sentError).to.beNil();
errorSent = YES;
}];
expect(errorSent).to.beTruthy();
});
});
describe(@"with an unlimited capacity", ^{
beforeEach(^{
subject = [RACReplaySubject subject];
});
itShouldBehaveLike(RACSubscriberExamples, ^{
return @{
RACSubscriberExampleSubscriber: subject,
RACSubscriberExampleValuesReceivedBlock: [^{
NSMutableArray *values = [NSMutableArray array];
// This subscription should synchronously dump all values already
// received into 'values'.
[subject subscribeNext:^(id value) {
[values addObject:value];
}];
return values;
} copy],
RACSubscriberExampleErrorReceivedBlock: [^{
__block NSError *error = nil;
[subject subscribeError:^(NSError *x) {
error = x;
}];
return error;
} copy],
RACSubscriberExampleSuccessBlock: [^{
__block BOOL success = YES;
[subject subscribeError:^(NSError *x) {
success = NO;
}];
return success;
} copy]
};
});
it(@"should send both values to new subscribers after completion", ^{
id firstValue = @"blah";
id secondValue = @"more blah";
[subject sendNext:firstValue];
[subject sendNext:secondValue];
[subject sendCompleted];
__block BOOL completed = NO;
NSMutableArray *valuesReceived = [NSMutableArray array];
[subject subscribeNext:^(id x) {
[valuesReceived addObject:x];
} completed:^{
completed = YES;
}];
expect(valuesReceived.count).to.equal(2);
NSArray *expected = [NSArray arrayWithObjects:firstValue, secondValue, nil];
expect(valuesReceived).to.equal(expected);
expect(completed).to.beTruthy();
});
it(@"should send values in the same order live as when replaying", ^{
NSUInteger count = 49317;
// Just leak it, ain't no thang.
__unsafe_unretained volatile id *values = (__unsafe_unretained id *)calloc(count, sizeof(*values));
__block volatile int32_t nextIndex = 0;
[subject subscribeNext:^(NSNumber *value) {
int32_t indexPlusOne = OSAtomicIncrement32(&nextIndex);
values[indexPlusOne - 1] = value;
}];
dispatch_queue_t queue = dispatch_queue_create("com.github.ReactiveCocoa.RACSubjectSpec", DISPATCH_QUEUE_CONCURRENT);
@onExit {
dispatch_release(queue);
};
dispatch_suspend(queue);
for (NSUInteger i = 0; i < count; i++) {
dispatch_async(queue, ^{
[subject sendNext:@(i)];
});
}
dispatch_resume(queue);
dispatch_barrier_sync(queue, ^{
[subject sendCompleted];
});
OSMemoryBarrier();
NSArray *liveValues = [NSArray arrayWithObjects:(id *)values count:(NSUInteger)nextIndex];
expect(liveValues.count).to.equal(count);
NSArray *replayedValues = subject.toArray;
expect(replayedValues.count).to.equal(count);
// It should return the same ordering for multiple invocations too.
expect(replayedValues).to.equal(subject.toArray);
[replayedValues enumerateObjectsUsingBlock:^(id value, NSUInteger index, BOOL *stop) {
expect(liveValues[index]).to.equal(value);
}];
});
it(@"should have a current scheduler when replaying", ^{
[subject sendNext:RACUnit.defaultUnit];
__block RACScheduler *currentScheduler;
[subject subscribeNext:^(id x) {
currentScheduler = RACScheduler.currentScheduler;
}];
expect(currentScheduler).notTo.beNil();
currentScheduler = nil;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[subject subscribeNext:^(id x) {
currentScheduler = RACScheduler.currentScheduler;
}];
});
expect(currentScheduler).willNot.beNil();
});
it(@"should stop replaying when the subscription is disposed", ^{
NSMutableArray *values = [NSMutableArray array];
[subject sendNext:@0];
[subject sendNext:@1];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
__block RACDisposable *disposable = [subject subscribeNext:^(id x) {
expect(disposable).notTo.beNil();
[values addObject:x];
[disposable dispose];
}];
});
expect(values).will.equal(@[ @0 ]);
});
it(@"should finish replaying before completing", ^{
[subject sendNext:@1];
__block id received;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[subject subscribeNext:^(id x) {
received = x;
}];
[subject sendCompleted];
});
expect(received).will.equal(@1);
});
it(@"should finish replaying before erroring", ^{
[subject sendNext:@1];
__block id received;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[subject subscribeNext:^(id x) {
received = x;
}];
[subject sendError:[NSError errorWithDomain:@"blah" code:-99 userInfo:nil]];
});
expect(received).will.equal(@1);
});
it(@"should finish replaying before sending new values", ^{
[subject sendNext:@1];
NSMutableArray *received = [NSMutableArray array];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[subject subscribeNext:^(id x) {
[received addObject:x];
}];
[subject sendNext:@2];
});
NSArray *expected = @[ @1, @2 ];
expect(received).will.equal(expected);
});
});
});
SpecEnd