// // RACSequenceSpec.m // ReactiveCocoa // // Created by Justin Spahr-Summers on 2012-11-01. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import #import #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" QuickSpecBegin(RACSequenceSpec) qck_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; }]; qck_itBehavesLike(RACStreamExamples, ^{ return @{ RACStreamExamplesClass: RACSequence.class, RACStreamExamplesVerifyValuesBlock: verifyValues, RACStreamExamplesInfiniteStream: infiniteSequence }; }); }); qck_describe(@"+sequenceWithHeadBlock:tailBlock:", ^{ __block RACSequence *sequence; __block BOOL headInvoked; __block BOOL tailInvoked; qck_beforeEach(^{ headInvoked = NO; tailInvoked = NO; sequence = [RACSequence sequenceWithHeadBlock:^{ headInvoked = YES; return @0; } tailBlock:^{ tailInvoked = YES; return [RACSequence return:@1]; }]; expect(sequence).notTo(beNil()); }); qck_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()); }); qck_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()); }); qck_context(@"behaves like a sequence", ^{ qck_itBehavesLike(RACSequenceExamples, ^{ return @{ RACSequenceExampleSequence: sequence, RACSequenceExampleExpectedValues: @[ @0, @1 ] }; }); }); }); qck_describe(@"empty sequences", ^{ qck_itBehavesLike(RACSequenceExamples, ^{ return @{ RACSequenceExampleSequence: [RACSequence empty], RACSequenceExampleExpectedValues: @[] }; }); }); qck_describe(@"non-empty sequences", ^{ qck_itBehavesLike(RACSequenceExamples, ^{ return @{ RACSequenceExampleSequence: [[[RACSequence return:@0] concat:[RACSequence return:@1]] concat:[RACSequence return:@2]], RACSequenceExampleExpectedValues: @[ @0, @1, @2 ] }; }); }); qck_describe(@"eager sequences", ^{ __block RACSequence *lazySequence; __block BOOL headInvoked; __block BOOL tailInvoked; NSArray *values = @[ @0, @1 ]; qck_beforeEach(^{ headInvoked = NO; tailInvoked = NO; lazySequence = [RACSequence sequenceWithHeadBlock:^{ headInvoked = YES; return @0; } tailBlock:^{ tailInvoked = YES; return [RACSequence return:@1]; }]; expect(lazySequence).notTo(beNil()); }); qck_itBehavesLike(RACSequenceExamples, ^{ return @{ RACSequenceExampleSequence: lazySequence.eagerSequence, RACSequenceExampleExpectedValues: values }; }); qck_it(@"should evaluate all values immediately", ^{ RACSequence *eagerSequence = lazySequence.eagerSequence; expect(@(headInvoked)).to(beTruthy()); expect(@(tailInvoked)).to(beTruthy()); expect(eagerSequence.array).to(equal(values)); }); }); qck_describe(@"-take:", ^{ qck_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)); }); }); qck_describe(@"-bind:", ^{ qck_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()); }); }); qck_describe(@"-objectEnumerator", ^{ qck_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()); }); qck_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)).toEventually(beTruthy()); @autoreleasepool { expect([enumerator nextObject]).to(equal(@3)); } expect(@(secondSequenceDeallocd)).toEventually(beTruthy()); @autoreleasepool { expect([enumerator nextObject]).to(beNil()); } expect(@(thirdSequenceDeallocd)).toEventually(beTruthy()); }); }); qck_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 { (void)[[values.rac_sequence map:^(id value) { return value; }] array]; } finished = YES; }); expect(@(finished)).toEventually(beTruthy()); }); qck_describe(@"-foldLeftWithStart:reduce:", ^{ qck_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)); }); qck_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)); }); }); qck_describe(@"-foldRightWithStart:reduce:", ^{ qck_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()); }); qck_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)); }); qck_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)); }); }); qck_describe(@"-any", ^{ __block RACSequence *sequence; qck_beforeEach(^{ sequence = [[[RACSequence return:@0] concat:[RACSequence return:@1]] concat:[RACSequence return:@2]]; }); qck_it(@"should return true when at least one exists", ^{ BOOL result = [sequence any:^ BOOL (NSNumber *value) { return value.integerValue > 0; }]; expect(@(result)).to(beTruthy()); }); qck_it(@"should return false when no such thing exists", ^{ BOOL result = [sequence any:^ BOOL (NSNumber *value) { return value.integerValue == 3; }]; expect(@(result)).to(beFalsy()); }); }); qck_describe(@"-all", ^{ __block RACSequence *sequence; qck_beforeEach(^{ sequence = [[[RACSequence return:@0] concat:[RACSequence return:@1]] concat:[RACSequence return:@2]]; }); qck_it(@"should return true when all values pass", ^{ BOOL result = [sequence all:^ BOOL (NSNumber *value) { return value.integerValue >= 0; }]; expect(@(result)).to(beTruthy()); }); qck_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()); }); }); qck_describe(@"-objectPassingTest:", ^{ __block RACSequence *sequence; qck_beforeEach(^{ sequence = [[[RACSequence return:@0] concat:[RACSequence return:@1]] concat:[RACSequence return:@2]]; }); qck_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)); }); qck_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()); }); }); QuickSpecEnd