| // | |
| // 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 |