blob: bdf22f16f0b1320a818746f8a35036b65dddd577 [file] [log] [blame] [edit]
//
// RACStreamExamples.m
// ReactiveCocoa
//
// Created by Justin Spahr-Summers on 2012-11-01.
// Copyright (c) 2012 GitHub, Inc. All rights reserved.
//
#import "RACStreamExamples.h"
#import "RACStream.h"
#import "RACUnit.h"
#import "RACTuple.h"
NSString * const RACStreamExamples = @"RACStreamExamples";
NSString * const RACStreamExamplesClass = @"RACStreamExamplesClass";
NSString * const RACStreamExamplesInfiniteStream = @"RACStreamExamplesInfiniteStream";
NSString * const RACStreamExamplesVerifyValuesBlock = @"RACStreamExamplesVerifyValuesBlock";
SharedExampleGroupsBegin(RACStreamExamples)
sharedExamplesFor(RACStreamExamples, ^(NSDictionary *data) {
__block Class streamClass;
__block void (^verifyValues)(RACStream *, NSArray *);
__block RACStream *infiniteStream;
__block RACStream *(^streamWithValues)(NSArray *);
before(^{
streamClass = data[RACStreamExamplesClass];
verifyValues = data[RACStreamExamplesVerifyValuesBlock];
infiniteStream = data[RACStreamExamplesInfiniteStream];
streamWithValues = [^(NSArray *values) {
RACStream *stream = [streamClass empty];
for (id value in values) {
stream = [stream concat:[streamClass return:value]];
}
return stream;
} copy];
});
it(@"should return an empty stream", ^{
RACStream *stream = [streamClass empty];
verifyValues(stream, @[]);
});
it(@"should lift a value into a stream", ^{
RACStream *stream = [streamClass return:RACUnit.defaultUnit];
verifyValues(stream, @[ RACUnit.defaultUnit ]);
});
describe(@"-concat:", ^{
it(@"should concatenate two streams", ^{
RACStream *stream = [[streamClass return:@0] concat:[streamClass return:@1]];
verifyValues(stream, @[ @0, @1 ]);
});
it(@"should concatenate three streams", ^{
RACStream *stream = [[[streamClass return:@0] concat:[streamClass return:@1]] concat:[streamClass return:@2]];
verifyValues(stream, @[ @0, @1, @2 ]);
});
it(@"should concatenate around an empty stream", ^{
RACStream *stream = [[[streamClass return:@0] concat:[streamClass empty]] concat:[streamClass return:@2]];
verifyValues(stream, @[ @0, @2 ]);
});
});
it(@"should flatten", ^{
RACStream *stream = [[streamClass return:[streamClass return:RACUnit.defaultUnit]] flatten];
verifyValues(stream, @[ RACUnit.defaultUnit ]);
});
describe(@"-bind:", ^{
it(@"should return the result of binding a single value", ^{
RACStream *stream = [[streamClass return:@0] bind:^{
return ^(NSNumber *value, BOOL *stop) {
NSNumber *newValue = @(value.integerValue + 1);
return [streamClass return:newValue];
};
}];
verifyValues(stream, @[ @1 ]);
});
it(@"should concatenate the result of binding multiple values", ^{
RACStream *baseStream = streamWithValues(@[ @0, @1 ]);
RACStream *stream = [baseStream bind:^{
return ^(NSNumber *value, BOOL *stop) {
NSNumber *newValue = @(value.integerValue + 1);
return [streamClass return:newValue];
};
}];
verifyValues(stream, @[ @1, @2 ]);
});
it(@"should concatenate with an empty result from binding a value", ^{
RACStream *baseStream = streamWithValues(@[ @0, @1, @2 ]);
RACStream *stream = [baseStream bind:^{
return ^(NSNumber *value, BOOL *stop) {
if (value.integerValue == 1) return [streamClass empty];
NSNumber *newValue = @(value.integerValue + 1);
return [streamClass return:newValue];
};
}];
verifyValues(stream, @[ @1, @3 ]);
});
it(@"should terminate immediately when returning nil", ^{
RACStream *stream = [infiniteStream bind:^{
return ^ id (id _, BOOL *stop) {
return nil;
};
}];
verifyValues(stream, @[]);
});
it(@"should terminate after one value when setting 'stop'", ^{
RACStream *stream = [infiniteStream bind:^{
return ^ id (id value, BOOL *stop) {
*stop = YES;
return [streamClass return:value];
};
}];
verifyValues(stream, @[ RACUnit.defaultUnit ]);
});
it(@"should terminate immediately when returning nil and setting 'stop'", ^{
RACStream *stream = [infiniteStream bind:^{
return ^ id (id _, BOOL *stop) {
*stop = YES;
return nil;
};
}];
verifyValues(stream, @[]);
});
it(@"should be restartable even with block state", ^{
NSArray *values = @[ @0, @1, @2 ];
RACStream *baseStream = streamWithValues(values);
RACStream *countingStream = [baseStream bind:^{
__block NSUInteger counter = 0;
return ^(id x, BOOL *stop) {
return [streamClass return:@(counter++)];
};
}];
verifyValues(countingStream, @[ @0, @1, @2 ]);
verifyValues(countingStream, @[ @0, @1, @2 ]);
});
it(@"should be interleavable even with block state", ^{
NSArray *values = @[ @0, @1, @2 ];
RACStream *baseStream = streamWithValues(values);
RACStream *countingStream = [baseStream bind:^{
__block NSUInteger counter = 0;
return ^(id x, BOOL *stop) {
return [streamClass return:@(counter++)];
};
}];
// Just so +zip:reduce: thinks this is a unique stream.
RACStream *anotherStream = [[streamClass empty] concat:countingStream];
RACStream *zipped = [streamClass zip:@[ countingStream, anotherStream ] reduce:^(NSNumber *v1, NSNumber *v2) {
return @(v1.integerValue + v2.integerValue);
}];
verifyValues(zipped, @[ @0, @2, @4 ]);
});
});
describe(@"-flattenMap:", ^{
it(@"should return a single mapped result", ^{
RACStream *stream = [[streamClass return:@0] flattenMap:^(NSNumber *value) {
NSNumber *newValue = @(value.integerValue + 1);
return [streamClass return:newValue];
}];
verifyValues(stream, @[ @1 ]);
});
it(@"should concatenate the results of mapping multiple values", ^{
RACStream *baseStream = streamWithValues(@[ @0, @1 ]);
RACStream *stream = [baseStream flattenMap:^(NSNumber *value) {
NSNumber *newValue = @(value.integerValue + 1);
return [streamClass return:newValue];
}];
verifyValues(stream, @[ @1, @2 ]);
});
it(@"should concatenate with an empty result from mapping a value", ^{
RACStream *baseStream = streamWithValues(@[ @0, @1, @2 ]);
RACStream *stream = [baseStream flattenMap:^(NSNumber *value) {
if (value.integerValue == 1) return [streamClass empty];
NSNumber *newValue = @(value.integerValue + 1);
return [streamClass return:newValue];
}];
verifyValues(stream, @[ @1, @3 ]);
});
it(@"should treat nil streams like empty streams", ^{
RACStream *baseStream = streamWithValues(@[ @0, @1, @2 ]);
RACStream *stream = [baseStream flattenMap:^ RACStream * (NSNumber *value) {
if (value.integerValue == 1) return nil;
NSNumber *newValue = @(value.integerValue + 1);
return [streamClass return:newValue];
}];
verifyValues(stream, @[ @1, @3 ]);
});
});
it(@"should map", ^{
RACStream *baseStream = streamWithValues(@[ @0, @1, @2 ]);
RACStream *stream = [baseStream map:^(NSNumber *value) {
return @(value.integerValue + 1);
}];
verifyValues(stream, @[ @1, @2, @3 ]);
});
it(@"should map and replace", ^{
RACStream *baseStream = streamWithValues(@[ @0, @1, @2 ]);
RACStream *stream = [baseStream mapReplace:RACUnit.defaultUnit];
verifyValues(stream, @[ RACUnit.defaultUnit, RACUnit.defaultUnit, RACUnit.defaultUnit ]);
});
it(@"should filter", ^{
RACStream *baseStream = streamWithValues(@[ @0, @1, @2, @3, @4, @5, @6 ]);
RACStream *stream = [baseStream filter:^ BOOL (NSNumber *value) {
return value.integerValue % 2 == 0;
}];
verifyValues(stream, @[ @0, @2, @4, @6 ]);
});
describe(@"-ignore:", ^{
it(@"should ignore a value", ^{
RACStream *baseStream = streamWithValues(@[ @0, @1, @2, @3, @4, @5, @6 ]);
RACStream *stream = [baseStream ignore:@1];
verifyValues(stream, @[ @0, @2, @3, @4, @5, @6 ]);
});
it(@"should ignore based on object equality", ^{
RACStream *baseStream = streamWithValues(@[ @"0", @"1", @"2", @"3", @"4", @"5", @"6" ]);
NSMutableString *valueToIgnore = [[NSMutableString alloc] init];
[valueToIgnore appendString:@"1"];
RACStream *stream = [baseStream ignore:valueToIgnore];
verifyValues(stream, @[ @"0", @"2", @"3", @"4", @"5", @"6" ]);
});
});
it(@"should start with a value", ^{
RACStream *stream = [[streamClass return:@1] startWith:@0];
verifyValues(stream, @[ @0, @1 ]);
});
describe(@"-skip:", ^{
__block NSArray *values;
__block RACStream *stream;
before(^{
values = @[ @0, @1, @2 ];
stream = streamWithValues(values);
});
it(@"should skip any valid number of values", ^{
for (NSUInteger i = 0; i < values.count; i++) {
verifyValues([stream skip:i], [values subarrayWithRange:NSMakeRange(i, values.count - i)]);
}
});
it(@"should return an empty stream when skipping too many values", ^{
verifyValues([stream skip:4], @[]);
});
});
describe(@"-take:", ^{
describe(@"with three values", ^{
__block NSArray *values;
__block RACStream *stream;
before(^{
values = @[ @0, @1, @2 ];
stream = streamWithValues(values);
});
it(@"should take any valid number of values", ^{
for (NSUInteger i = 0; i < values.count; i++) {
verifyValues([stream take:i], [values subarrayWithRange:NSMakeRange(0, i)]);
}
});
it(@"should return the same stream when taking too many values", ^{
verifyValues([stream take:4], values);
});
});
it(@"should take and terminate from an infinite stream", ^{
verifyValues([infiniteStream take:0], @[]);
verifyValues([infiniteStream take:1], @[ RACUnit.defaultUnit ]);
verifyValues([infiniteStream take:2], @[ RACUnit.defaultUnit, RACUnit.defaultUnit ]);
});
it(@"should take and terminate from a single-item stream", ^{
NSArray *values = @[ RACUnit.defaultUnit ];
RACStream *stream = streamWithValues(values);
verifyValues([stream take:1], values);
});
});
describe(@"zip stream creation methods", ^{
__block NSArray *valuesOne;
__block RACStream *streamOne;
__block RACStream *streamTwo;
__block RACStream *streamThree;
__block NSArray *threeStreams;
__block NSArray *oneStreamTuples;
__block NSArray *twoStreamTuples;
__block NSArray *threeStreamTuples;
before(^{
valuesOne = @[ @"Ada", @"Bob", @"Dea" ];
NSArray *valuesTwo = @[ @"eats", @"cooks", @"jumps" ];
NSArray *valuesThree = @[ @"fish", @"bear", @"rock" ];
streamOne = streamWithValues(valuesOne);
streamTwo = streamWithValues(valuesTwo);
streamThree = streamWithValues(valuesThree);
threeStreams = @[ streamOne, streamTwo, streamThree ];
oneStreamTuples = @[
RACTuplePack(valuesOne[0]),
RACTuplePack(valuesOne[1]),
RACTuplePack(valuesOne[2]),
];
twoStreamTuples = @[
RACTuplePack(valuesOne[0], valuesTwo[0]),
RACTuplePack(valuesOne[1], valuesTwo[1]),
RACTuplePack(valuesOne[2], valuesTwo[2]),
];
threeStreamTuples = @[
RACTuplePack(valuesOne[0], valuesTwo[0], valuesThree[0]),
RACTuplePack(valuesOne[1], valuesTwo[1], valuesThree[1]),
RACTuplePack(valuesOne[2], valuesTwo[2], valuesThree[2]),
];
});
describe(@"-zipWith:", ^{
it(@"should make a stream of tuples", ^{
RACStream *stream = [streamOne zipWith:streamTwo];
verifyValues(stream, twoStreamTuples);
});
it(@"should truncate streams", ^{
RACStream *shortStream = streamWithValues(@[ @"now", @"later" ]);
RACStream *stream = [streamOne zipWith:shortStream];
verifyValues(stream, @[
RACTuplePack(valuesOne[0], @"now"),
RACTuplePack(valuesOne[1], @"later")
]);
});
it(@"should work on infinite streams", ^{
RACStream *stream = [streamOne zipWith:infiniteStream];
verifyValues(stream, @[
RACTuplePack(valuesOne[0], RACUnit.defaultUnit),
RACTuplePack(valuesOne[1], RACUnit.defaultUnit),
RACTuplePack(valuesOne[2], RACUnit.defaultUnit)
]);
});
it(@"should handle multiples of the same stream", ^{
RACStream *stream = [streamOne zipWith:streamOne];
verifyValues(stream, @[
RACTuplePack(valuesOne[0], valuesOne[0]),
RACTuplePack(valuesOne[1], valuesOne[1]),
RACTuplePack(valuesOne[2], valuesOne[2]),
]);
});
});
describe(@"+zip:reduce:", ^{
it(@"should reduce values", ^{
RACStream *stream = [streamClass zip:threeStreams reduce:^ NSString * (id x, id y, id z) {
return [NSString stringWithFormat:@"%@ %@ %@", x, y, z];
}];
verifyValues(stream, @[ @"Ada eats fish", @"Bob cooks bear", @"Dea jumps rock" ]);
});
it(@"should truncate streams", ^{
RACStream *shortStream = streamWithValues(@[ @"now", @"later" ]);
NSArray *streams = [threeStreams arrayByAddingObject:shortStream];
RACStream *stream = [streamClass zip:streams reduce:^ NSString * (id w, id x, id y, id z) {
return [NSString stringWithFormat:@"%@ %@ %@ %@", w, x, y, z];
}];
verifyValues(stream, @[ @"Ada eats fish now", @"Bob cooks bear later" ]);
});
it(@"should work on infinite streams", ^{
NSArray *streams = [threeStreams arrayByAddingObject:infiniteStream];
RACStream *stream = [streamClass zip:streams reduce:^ NSString * (id w, id x, id y, id z) {
return [NSString stringWithFormat:@"%@ %@ %@", w, x, y];
}];
verifyValues(stream, @[ @"Ada eats fish", @"Bob cooks bear", @"Dea jumps rock" ]);
});
it(@"should handle multiples of the same stream", ^{
NSArray *streams = @[ streamOne, streamOne, streamTwo, streamThree, streamTwo, streamThree ];
RACStream *stream = [streamClass zip:streams reduce:^ NSString * (id x1, id x2, id y1, id z1, id y2, id z2) {
return [NSString stringWithFormat:@"%@ %@ %@ %@ %@ %@", x1, x2, y1, z1, y2, z2];
}];
verifyValues(stream, @[ @"Ada Ada eats fish eats fish", @"Bob Bob cooks bear cooks bear", @"Dea Dea jumps rock jumps rock" ]);
});
});
describe(@"+zip:", ^{
it(@"should make a stream of tuples out of single value", ^{
RACStream *stream = [streamClass zip:@[ streamOne ]];
verifyValues(stream, oneStreamTuples);
});
it(@"should make a stream of tuples out of an array of streams", ^{
RACStream *stream = [streamClass zip:threeStreams];
verifyValues(stream, threeStreamTuples);
});
it(@"should make an empty stream if given an empty array", ^{
RACStream *stream = [streamClass zip:@[]];
verifyValues(stream, @[]);
});
it(@"should make a stream of tuples out of an enumerator of streams", ^{
RACStream *stream = [streamClass zip:threeStreams.objectEnumerator];
verifyValues(stream, threeStreamTuples);
});
it(@"should make an empty stream if given an empty enumerator", ^{
RACStream *stream = [streamClass zip:@[].objectEnumerator];
verifyValues(stream, @[]);
});
});
});
describe(@"+concat:", ^{
__block NSArray *streams = nil;
__block NSArray *result = nil;
before(^{
RACStream *a = [streamClass return:@0];
RACStream *b = [streamClass empty];
RACStream *c = streamWithValues(@[ @1, @2, @3 ]);
RACStream *d = [streamClass return:@4];
RACStream *e = [streamClass return:@5];
RACStream *f = [streamClass empty];
RACStream *g = [streamClass empty];
RACStream *h = streamWithValues(@[ @6, @7 ]);
streams = @[ a, b, c, d, e, f, g, h ];
result = @[ @0, @1, @2, @3, @4, @5, @6, @7 ];
});
it(@"should concatenate an array of streams", ^{
RACStream *stream = [streamClass concat:streams];
verifyValues(stream, result);
});
it(@"should concatenate an enumerator of streams", ^{
RACStream *stream = [streamClass concat:streams.objectEnumerator];
verifyValues(stream, result);
});
});
it(@"should scan", ^{
RACStream *stream = streamWithValues(@[ @1, @2, @3, @4 ]);
RACStream *scanned = [stream scanWithStart:@0 reduce:^(NSNumber *running, NSNumber *next) {
return @(running.integerValue + next.integerValue);
}];
verifyValues(scanned, @[ @1, @3, @6, @10 ]);
});
describe(@"taking with a predicate", ^{
NSArray *values = @[ @0, @1, @2, @3, @0, @2, @4 ];
__block RACStream *stream;
before(^{
stream = streamWithValues(values);
});
it(@"should take until a predicate is true", ^{
RACStream *taken = [stream takeUntilBlock:^ BOOL (NSNumber *x) {
return x.integerValue >= 3;
}];
verifyValues(taken, @[ @0, @1, @2 ]);
});
it(@"should take while a predicate is true", ^{
RACStream *taken = [stream takeWhileBlock:^ BOOL (NSNumber *x) {
return x.integerValue <= 1;
}];
verifyValues(taken, @[ @0, @1 ]);
});
it(@"should take a full stream", ^{
RACStream *taken = [stream takeWhileBlock:^ BOOL (NSNumber *x) {
return x.integerValue <= 10;
}];
verifyValues(taken, values);
});
it(@"should return an empty stream", ^{
RACStream *taken = [stream takeWhileBlock:^ BOOL (NSNumber *x) {
return x.integerValue < 0;
}];
verifyValues(taken, @[]);
});
it(@"should terminate an infinite stream", ^{
RACStream *infiniteCounter = [infiniteStream scanWithStart:@0 reduce:^(NSNumber *running, id _) {
return @(running.unsignedIntegerValue + 1);
}];
RACStream *taken = [infiniteCounter takeWhileBlock:^ BOOL (NSNumber *x) {
return x.integerValue <= 5;
}];
verifyValues(taken, @[ @1, @2, @3, @4, @5 ]);
});
});
describe(@"skipping with a predicate", ^{
NSArray *values = @[ @0, @1, @2, @3, @0, @2, @4 ];
__block RACStream *stream;
before(^{
stream = streamWithValues(values);
});
it(@"should skip until a predicate is true", ^{
RACStream *taken = [stream skipUntilBlock:^ BOOL (NSNumber *x) {
return x.integerValue >= 3;
}];
verifyValues(taken, @[ @3, @0, @2, @4 ]);
});
it(@"should skip while a predicate is true", ^{
RACStream *taken = [stream skipWhileBlock:^ BOOL (NSNumber *x) {
return x.integerValue <= 1;
}];
verifyValues(taken, @[ @2, @3, @0, @2, @4 ]);
});
it(@"should skip a full stream", ^{
RACStream *taken = [stream skipWhileBlock:^ BOOL (NSNumber *x) {
return x.integerValue <= 10;
}];
verifyValues(taken, @[]);
});
it(@"should finish skipping immediately", ^{
RACStream *taken = [stream skipWhileBlock:^ BOOL (NSNumber *x) {
return x.integerValue < 0;
}];
verifyValues(taken, values);
});
});
describe(@"-combinePreviousWithStart:reduce:", ^{
NSArray *values = @[ @1, @2, @3 ];
__block RACStream *stream;
beforeEach(^{
stream = streamWithValues(values);
});
it(@"should pass the previous next into the reduce block", ^{
NSMutableArray *previouses = [NSMutableArray array];
RACStream *mapped = [stream combinePreviousWithStart:nil reduce:^(id previous, id next) {
[previouses addObject:previous ?: RACTupleNil.tupleNil];
return next;
}];
verifyValues(mapped, @[ @1, @2, @3 ]);
NSArray *expected = @[ RACTupleNil.tupleNil, @1, @2 ];
expect(previouses).to.equal(expected);
});
it(@"should send the combined value", ^{
RACStream *mapped = [stream combinePreviousWithStart:@1 reduce:^(NSNumber *previous, NSNumber *next) {
return [NSString stringWithFormat:@"%lu - %lu", (unsigned long)previous.unsignedIntegerValue, (unsigned long)next.unsignedIntegerValue];
}];
verifyValues(mapped, @[ @"1 - 1", @"1 - 2", @"2 - 3" ]);
});
});
it(@"should reduce tuples", ^{
RACStream *stream = streamWithValues(@[
RACTuplePack(@"foo", @"bar"),
RACTuplePack(@"buzz", @"baz"),
RACTuplePack(@"", @"_")
]);
RACStream *reduced = [stream reduceEach:^(NSString *a, NSString *b) {
return [a stringByAppendingString:b];
}];
verifyValues(reduced, @[ @"foobar", @"buzzbaz", @"_" ]);
});
});
SharedExampleGroupsEnd