blob: 3bf7dcab8cdf9bafb8ffd748c14d8dddc0f3f19b [file] [log] [blame]
//
// RACSequenceSpec.m
// ReactiveCocoa
//
// Created by Justin Spahr-Summers on 2012-11-01.
// Copyright (c) 2012 GitHub, Inc. All rights reserved.
//
#import "RACSequenceExamples.h"
#import "RACStreamExamples.h"
#import "NSArray+RACSequenceAdditions.h"
#import "NSObject+RACDeallocating.h"
#import "NSObject+RACPropertySubscribing.h"
#import "RACCompoundDisposable.h"
#import "RACDisposable.h"
#import "RACSequence.h"
#import "RACUnit.h"
SpecBegin(RACSequence)
describe(@"RACStream", ^{
id verifyValues = ^(RACSequence *sequence, NSArray *expectedValues) {
NSMutableArray *collectedValues = [NSMutableArray array];
while (sequence.head != nil) {
[collectedValues addObject:sequence.head];
sequence = sequence.tail;
}
expect(collectedValues).to.equal(expectedValues);
};
__block RACSequence *infiniteSequence = [RACSequence sequenceWithHeadBlock:^{
return RACUnit.defaultUnit;
} tailBlock:^{
return infiniteSequence;
}];
itShouldBehaveLike(RACStreamExamples, ^{
return @{
RACStreamExamplesClass: RACSequence.class,
RACStreamExamplesVerifyValuesBlock: verifyValues,
RACStreamExamplesInfiniteStream: infiniteSequence
};
});
});
describe(@"+sequenceWithHeadBlock:tailBlock:", ^{
__block RACSequence *sequence;
__block BOOL headInvoked;
__block BOOL tailInvoked;
before(^{
headInvoked = NO;
tailInvoked = NO;
sequence = [RACSequence sequenceWithHeadBlock:^{
headInvoked = YES;
return @0;
} tailBlock:^{
tailInvoked = YES;
return [RACSequence return:@1];
}];
expect(sequence).notTo.beNil();
});
it(@"should use the values from the head and tail blocks", ^{
expect(sequence.head).to.equal(@0);
expect(sequence.tail.head).to.equal(@1);
expect(sequence.tail.tail).to.beNil();
});
it(@"should lazily invoke head and tail blocks", ^{
expect(headInvoked).to.beFalsy();
expect(tailInvoked).to.beFalsy();
expect(sequence.head).to.equal(@0);
expect(headInvoked).to.beTruthy();
expect(tailInvoked).to.beFalsy();
expect(sequence.tail).notTo.beNil();
expect(tailInvoked).to.beTruthy();
});
after(^{
itShouldBehaveLike(RACSequenceExamples, ^{
return @{
RACSequenceExampleSequence: sequence,
RACSequenceExampleExpectedValues: @[ @0, @1 ]
};
});
});
});
describe(@"empty sequences", ^{
itShouldBehaveLike(RACSequenceExamples, ^{
return @{
RACSequenceExampleSequence: [RACSequence empty],
RACSequenceExampleExpectedValues: @[]
};
});
});
describe(@"non-empty sequences", ^{
itShouldBehaveLike(RACSequenceExamples, ^{
return @{
RACSequenceExampleSequence: [[[RACSequence return:@0] concat:[RACSequence return:@1]] concat:[RACSequence return:@2]],
RACSequenceExampleExpectedValues: @[ @0, @1, @2 ]
};
});
});
describe(@"eager sequences", ^{
__block RACSequence *lazySequence;
__block BOOL headInvoked;
__block BOOL tailInvoked;
NSArray *values = @[ @0, @1 ];
before(^{
headInvoked = NO;
tailInvoked = NO;
lazySequence = [RACSequence sequenceWithHeadBlock:^{
headInvoked = YES;
return @0;
} tailBlock:^{
tailInvoked = YES;
return [RACSequence return:@1];
}];
expect(lazySequence).notTo.beNil();
});
itShouldBehaveLike(RACSequenceExamples, ^{
return @{
RACSequenceExampleSequence: lazySequence.eagerSequence,
RACSequenceExampleExpectedValues: values
};
});
it(@"should evaluate all values immediately", ^{
RACSequence *eagerSequence = lazySequence.eagerSequence;
expect(headInvoked).to.beTruthy();
expect(tailInvoked).to.beTruthy();
expect(eagerSequence.array).to.equal(values);
});
});
describe(@"-take:", ^{
it(@"should complete take: without needing the head of the second item in the sequence", ^{
__block NSUInteger valuesTaken = 0;
__block RACSequence *sequence = [RACSequence sequenceWithHeadBlock:^{
++valuesTaken;
return RACUnit.defaultUnit;
} tailBlock:^{
return sequence;
}];
NSArray *values = [sequence take:1].array;
expect(values).to.equal(@[ RACUnit.defaultUnit ]);
expect(valuesTaken).to.equal(1);
});
});
describe(@"-bind:", ^{
it(@"should only evaluate head when the resulting sequence is evaluated", ^{
__block BOOL headInvoked = NO;
RACSequence *original = [RACSequence sequenceWithHeadBlock:^{
headInvoked = YES;
return RACUnit.defaultUnit;
} tailBlock:^ id {
return nil;
}];
RACSequence *bound = [original bind:^{
return ^(id value, BOOL *stop) {
return [RACSequence return:value];
};
}];
expect(bound).notTo.beNil();
expect(headInvoked).to.beFalsy();
expect(bound.head).to.equal(RACUnit.defaultUnit);
expect(headInvoked).to.beTruthy();
});
});
describe(@"-objectEnumerator", ^{
it(@"should only evaluate head as it's enumerated", ^{
__block BOOL firstHeadInvoked = NO;
__block BOOL secondHeadInvoked = NO;
__block BOOL thirdHeadInvoked = NO;
RACSequence *sequence = [RACSequence sequenceWithHeadBlock:^id{
firstHeadInvoked = YES;
return @1;
} tailBlock:^RACSequence *{
return [RACSequence sequenceWithHeadBlock:^id{
secondHeadInvoked = YES;
return @2;
} tailBlock:^RACSequence *{
return [RACSequence sequenceWithHeadBlock:^id{
thirdHeadInvoked = YES;
return @3;
} tailBlock:^RACSequence *{
return RACSequence.empty;
}];
}];
}];
NSEnumerator *enumerator = sequence.objectEnumerator;
expect(firstHeadInvoked).to.beFalsy();
expect(secondHeadInvoked).to.beFalsy();
expect(thirdHeadInvoked).to.beFalsy();
expect([enumerator nextObject]).to.equal(@1);
expect(firstHeadInvoked).to.beTruthy();
expect(secondHeadInvoked).to.beFalsy();
expect(thirdHeadInvoked).to.beFalsy();
expect([enumerator nextObject]).to.equal(@2);
expect(secondHeadInvoked).to.beTruthy();
expect(thirdHeadInvoked).to.beFalsy();
expect([enumerator nextObject]).to.equal(@3);
expect(thirdHeadInvoked).to.beTruthy();
expect([enumerator nextObject]).to.beNil();
});
it(@"should let the sequence dealloc as it's enumerated", ^{
__block BOOL firstSequenceDeallocd = NO;
__block BOOL secondSequenceDeallocd = NO;
__block BOOL thirdSequenceDeallocd = NO;
NSEnumerator *enumerator = nil;
@autoreleasepool {
RACSequence *thirdSequence __attribute__((objc_precise_lifetime)) = [RACSequence sequenceWithHeadBlock:^id{
return @3;
} tailBlock:^RACSequence *{
return RACSequence.empty;
}];
[thirdSequence.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{
thirdSequenceDeallocd = YES;
}]];
RACSequence *secondSequence __attribute__((objc_precise_lifetime)) = [RACSequence sequenceWithHeadBlock:^id{
return @2;
} tailBlock:^RACSequence *{
return thirdSequence;
}];
[secondSequence.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{
secondSequenceDeallocd = YES;
}]];
RACSequence *firstSequence __attribute__((objc_precise_lifetime)) = [RACSequence sequenceWithHeadBlock:^id{
return @1;
} tailBlock:^RACSequence *{
return secondSequence;
}];
[firstSequence.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{
firstSequenceDeallocd = YES;
}]];
enumerator = firstSequence.objectEnumerator;
}
@autoreleasepool {
expect([enumerator nextObject]).to.equal(@1);
}
@autoreleasepool {
expect([enumerator nextObject]).to.equal(@2);
}
expect(firstSequenceDeallocd).will.beTruthy();
@autoreleasepool {
expect([enumerator nextObject]).to.equal(@3);
}
expect(secondSequenceDeallocd).will.beTruthy();
@autoreleasepool {
expect([enumerator nextObject]).to.beNil();
}
expect(thirdSequenceDeallocd).will.beTruthy();
});
});
it(@"shouldn't overflow the stack when deallocated on a background queue", ^{
NSUInteger length = 10000;
NSMutableArray *values = [NSMutableArray arrayWithCapacity:length];
for (NSUInteger i = 0; i < length; ++i) {
[values addObject:@(i)];
}
__block BOOL finished = NO;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@autoreleasepool {
[[values.rac_sequence map:^(id value) {
return value;
}] array];
}
finished = YES;
});
NSTimeInterval oldTimeout = Expecta.asynchronousTestTimeout;
Expecta.asynchronousTestTimeout = DBL_MAX;
expect(finished).will.beTruthy();
Expecta.asynchronousTestTimeout = oldTimeout;
});
describe(@"-foldLeftWithStart:reduce:", ^{
it(@"should reduce with start first", ^{
RACSequence *sequence = [[[RACSequence return:@0] concat:[RACSequence return:@1]] concat:[RACSequence return:@2]];
NSNumber *result = [sequence foldLeftWithStart:@3 reduce:^(NSNumber *first, NSNumber *rest) {
return first;
}];
expect(result).to.equal(@3);
});
it(@"should be left associative", ^{
RACSequence *sequence = [[[RACSequence return:@1] concat:[RACSequence return:@2]] concat:[RACSequence return:@3]];
NSNumber *result = [sequence foldLeftWithStart:@0 reduce:^(NSNumber *first, NSNumber *rest) {
int difference = first.intValue - rest.intValue;
return @(difference);
}];
expect(result).to.equal(@-6);
});
});
describe(@"-foldRightWithStart:reduce:", ^{
it(@"should be lazy", ^{
__block BOOL headInvoked = NO;
__block BOOL tailInvoked = NO;
RACSequence *sequence = [RACSequence sequenceWithHeadBlock:^{
headInvoked = YES;
return @0;
} tailBlock:^{
tailInvoked = YES;
return [RACSequence return:@1];
}];
NSNumber *result = [sequence foldRightWithStart:@2 reduce:^(NSNumber *first, RACSequence *rest) {
return first;
}];
expect(result).to.equal(@0);
expect(headInvoked).to.beTruthy();
expect(tailInvoked).to.beFalsy();
});
it(@"should reduce with start last", ^{
RACSequence *sequence = [[[RACSequence return:@0] concat:[RACSequence return:@1]] concat:[RACSequence return:@2]];
NSNumber *result = [sequence foldRightWithStart:@3 reduce:^(NSNumber *first, RACSequence *rest) {
return rest.head;
}];
expect(result).to.equal(@3);
});
it(@"should be right associative", ^{
RACSequence *sequence = [[[RACSequence return:@1] concat:[RACSequence return:@2]] concat:[RACSequence return:@3]];
NSNumber *result = [sequence foldRightWithStart:@0 reduce:^(NSNumber *first, RACSequence *rest) {
int difference = first.intValue - [rest.head intValue];
return @(difference);
}];
expect(result).to.equal(@2);
});
});
describe(@"-any", ^{
__block RACSequence *sequence;
beforeEach(^{
sequence = [[[RACSequence return:@0] concat:[RACSequence return:@1]] concat:[RACSequence return:@2]];
});
it(@"should return true when at least one exists", ^{
BOOL result = [sequence any:^ BOOL (NSNumber *value) {
return value.integerValue > 0;
}];
expect(result).to.beTruthy();
});
it(@"should return false when no such thing exists", ^{
BOOL result = [sequence any:^ BOOL (NSNumber *value) {
return value.integerValue == 3;
}];
expect(result).to.beFalsy();
});
});
describe(@"-all", ^{
__block RACSequence *sequence;
beforeEach(^{
sequence = [[[RACSequence return:@0] concat:[RACSequence return:@1]] concat:[RACSequence return:@2]];
});
it(@"should return true when all values pass", ^{
BOOL result = [sequence all:^ BOOL (NSNumber *value) {
return value.integerValue >= 0;
}];
expect(result).to.beTruthy();
});
it(@"should return false when at least one value fails", ^{
BOOL result = [sequence all:^ BOOL (NSNumber *value) {
return value.integerValue < 2;
}];
expect(result).to.beFalsy();
});
});
describe(@"-objectPassingTest:", ^{
__block RACSequence *sequence;
beforeEach(^{
sequence = [[[RACSequence return:@0] concat:[RACSequence return:@1]] concat:[RACSequence return:@2]];
});
it(@"should return leftmost object that passes the test", ^{
NSNumber *result = [sequence objectPassingTest:^ BOOL (NSNumber *value) {
return value.intValue > 0;
}];
expect(result).to.equal(@1);
});
it(@"should return nil if no objects pass the test", ^{
NSNumber *result = [sequence objectPassingTest:^ BOOL (NSNumber *value) {
return value.intValue < 0;
}];
expect(result).to.beNil();
});
});
SpecEnd