diff --git a/Source/CDRExample.m b/Source/CDRExample.m index 585cba2e..a197f2d2 100644 --- a/Source/CDRExample.m +++ b/Source/CDRExample.m @@ -30,6 +30,12 @@ - (void)dealloc { [super dealloc]; } +- (id)copy { + CDRExample* example = [[CDRExample alloc] initWithText:text_ andBlock:block_]; + example.focused = self.focused; + return example; +} + #pragma mark CDRExampleBase - (CDRExampleState)state { return state_; diff --git a/Source/CDRExampleGroup.m b/Source/CDRExampleGroup.m index 34e771ef..01c11c29 100644 --- a/Source/CDRExampleGroup.m +++ b/Source/CDRExampleGroup.m @@ -24,6 +24,7 @@ - (id)initWithText:(NSString *)text isRoot:(BOOL)isRoot { beforeBlocks_ = [[NSMutableArray alloc] init]; examples_ = [[NSMutableArray alloc] init]; afterBlocks_ = [[NSMutableArray alloc] init]; + invariants_ = [[NSMutableArray alloc] init]; isRoot_ = isRoot; } return self; @@ -33,6 +34,7 @@ - (void)dealloc { [afterBlocks_ release]; [examples_ release]; [beforeBlocks_ release]; + [invariants_ release]; self.subjectActionBlock = nil; [super dealloc]; } @@ -74,6 +76,13 @@ - (void)addAfter:(CDRSpecBlock)block { [blockCopy release]; } +- (void)addInvariant:(CDRExampleBase *)inv { + CDRExampleBase * invCopy = [inv copy]; + invCopy.parent = self; + [invariants_ addObject: invCopy]; + [invCopy release]; +} + #pragma mark CDRExampleBase - (CDRExampleState)state { if (0 == [examples_ count]) { @@ -96,7 +105,10 @@ - (float)progress { for (CDRExampleBase *example in examples_) { aggregateProgress += [example progress]; } - return aggregateProgress / [examples_ count]; + for (CDRExampleBase *example in invariants_) { + aggregateProgress += [example progress]; + } + return aggregateProgress / ([examples_ count] + [invariants_ count]); } @@ -107,12 +119,17 @@ - (void)runWithDispatcher:(CDRReportDispatcher *)dispatcher { userInfo:nil] raise]; } + [self collectInvariants]; + [dispatcher runWillStartExampleGroup:self]; [startDate_ release]; startDate_ = [[NSDate alloc] init]; [self startObservingExamples]; [examples_ makeObjectsPerformSelector:@selector(runWithDispatcher:) withObject:dispatcher]; + if ([examples_ count] > 0) { + [invariants_ makeObjectsPerformSelector:@selector(runWithDispatcher:) withObject:dispatcher]; + } [self stopObservingExamples]; [endDate_ release]; @@ -121,13 +138,19 @@ - (void)runWithDispatcher:(CDRReportDispatcher *)dispatcher { [beforeBlocks_ release]; beforeBlocks_ = nil; [afterBlocks_ release]; afterBlocks_ = nil; + [invariants_ release]; invariants_ = nil; self.subjectActionBlock = nil; } - (BOOL)hasFocusedExamples { if (self.isFocused) { return YES; + + } else if ([self hasFocusedInvariant]) { + [self forceFocus]; + return YES; } + for (CDRExampleBase *example in examples_) { if ([example hasFocusedExamples]) { return YES; @@ -173,12 +196,47 @@ - (void)startObservingExamples { for (id example in examples_) { [example addObserver:self forKeyPath:@"state" options:0 context:NULL]; } + for (id example in invariants_) { + [example addObserver:self forKeyPath:@"state" options:0 context:NULL]; + } } - (void)stopObservingExamples { for (id example in examples_) { [example removeObserver:self forKeyPath:@"state"]; } + for (id example in invariants_) { + [example removeObserver:self forKeyPath:@"state"]; + } +} + +- (void)collectInvariants { + //Because of recursive call order for runWithDispatcher: all grandparent invariants have been propagated down to parent by the time [self collectInvariants] is called + //So no recursive call is necessary + if ([self.parent isKindOfClass:[CDRExampleGroup class]]) { + for (id inv in ((CDRExampleGroup*)self.parent)->invariants_) { + [self addInvariant: ((CDRExampleBase*)inv)]; + } + } +} + +- (BOOL)hasFocusedInvariant { + for (CDRExampleBase *example in invariants_) { + if ([example isFocused]) { + return YES; + } + } + return NO; +} + +//An invariant is focused, so we need to propagate focus *down* the tree +- (void)forceFocus { + self.focused = YES; + for (id child in examples_) { + if ([child isKindOfClass:[CDRExampleGroup class]]) { + [child forceFocus]; + } + } } @end diff --git a/Source/CDRSpec.m b/Source/CDRSpec.m index 0896a566..cd83e9ac 100644 --- a/Source/CDRSpec.m +++ b/Source/CDRSpec.m @@ -21,6 +21,15 @@ void afterEach(CDRSpecBlock block) { #define with_stack_address(b) \ ((b.stackAddress = CDRCallerStackAddress()), b) +CDRExample * invariant(NSString *text, CDRSpecBlock block) { + NSString * invName = [NSString stringWithFormat:@"should always %@", text]; + CDRExample * example = [CDRExample exampleWithText:invName andBlock:block]; + [CDR_currentSpec.currentGroup addInvariant:example]; + return with_stack_address(example); +} + +CDRExample* (*it_should_always)(NSString *, CDRSpecBlock) = &invariant; + CDRExampleGroup * describe(NSString *text, CDRSpecBlock block) { CDRExampleGroup *group = nil; if (block) { @@ -66,6 +75,14 @@ void subjectAction(CDRSpecBlock block) { return with_stack_address(example); } +CDRExample * xinvariant(NSString *text, CDRSpecBlock block) { + NSString * invName = [NSString stringWithFormat:@"should always %@", text]; + CDRExample *example = [CDRExample exampleWithText:invName andBlock:PENDING]; + return with_stack_address(example); +} + +CDRExample* (*xit_should_always)(NSString *, CDRSpecBlock) = &xinvariant; + #pragma mark - Focused CDRExampleGroup * fdescribe(NSString *text, CDRSpecBlock block) { @@ -82,6 +99,14 @@ void subjectAction(CDRSpecBlock block) { return with_stack_address(example); } +CDRExample * finvariant(NSString *text, CDRSpecBlock block) { + CDRExample *example = invariant(text, block); + example.focused = YES; + return with_stack_address(example); +} + +CDRExample * (*fit_should_always)(NSString *, CDRSpecBlock) = &finvariant; + void fail(NSString *reason) { [[CDRSpecFailure specFailureWithReason:[NSString stringWithFormat:@"Failure: %@", reason]] raise]; } diff --git a/Source/Headers/CDRExampleGroup.h b/Source/Headers/CDRExampleGroup.h index 85a1fa18..1e6f188e 100644 --- a/Source/Headers/CDRExampleGroup.h +++ b/Source/Headers/CDRExampleGroup.h @@ -1,7 +1,7 @@ #import "CDRExampleBase.h" @interface CDRExampleGroup : CDRExampleBase { - NSMutableArray *beforeBlocks_, *examples_, *afterBlocks_; + NSMutableArray *beforeBlocks_, *examples_, *afterBlocks_, *invariants_; BOOL isRoot_; CDRSpecBlock subjectActionBlock_; } @@ -15,5 +15,6 @@ - (void)add:(CDRExampleBase *)example; - (void)addBefore:(CDRSpecBlock)block; - (void)addAfter:(CDRSpecBlock)block; +- (void)addInvariant:(CDRExampleBase *)inv; @end diff --git a/Source/Headers/CDRSpec.h b/Source/Headers/CDRSpec.h index 13206e5d..c3460261 100644 --- a/Source/Headers/CDRSpec.h +++ b/Source/Headers/CDRSpec.h @@ -15,21 +15,29 @@ extern "C" { #endif void beforeEach(CDRSpecBlock); void afterEach(CDRSpecBlock); + +CDRExample * invariant(NSString *, CDRSpecBlock); +extern CDRExample * (*it_should_always)(NSString *, CDRSpecBlock); CDRExampleGroup * describe(NSString *, CDRSpecBlock); extern CDRExampleGroup* (*context)(NSString *, CDRSpecBlock); CDRExample * it(NSString *, CDRSpecBlock); +void subjectAction(CDRSpecBlock); + CDRExampleGroup * xdescribe(NSString *, CDRSpecBlock); extern CDRExampleGroup* (*xcontext)(NSString *, CDRSpecBlock); -void subjectAction(CDRSpecBlock); CDRExample * xit(NSString *, CDRSpecBlock); +CDRExample * xinvariant(NSString *, CDRSpecBlock); +extern CDRExample* (*xit_should_always)(NSString *, CDRSpecBlock); CDRExampleGroup * fdescribe(NSString *, CDRSpecBlock); extern CDRExampleGroup* (*fcontext)(NSString *, CDRSpecBlock); CDRExample * fit(NSString *, CDRSpecBlock); - +CDRExample * finvariant(NSString *, CDRSpecBlock); +extern CDRExample* (*fit_should_always)(NSString *, CDRSpecBlock); + void fail(NSString *); #ifdef __cplusplus } diff --git a/Spec/CDRExampleGroupSpec.mm b/Spec/CDRExampleGroupSpec.mm index 1643c3bf..f570164b 100644 --- a/Spec/CDRExampleGroupSpec.mm +++ b/Spec/CDRExampleGroupSpec.mm @@ -39,6 +39,11 @@ errorExample = [[[CDRExample alloc] initWithText:@"I should raise an error" andBlock:^{ @throw @"wibble"; }] autorelease]; nonFocusedExample = [[[CDRExample alloc] initWithText:@"I should not be focused" andBlock:^{}] autorelease]; }); + + invariant(@"progress between 0 and 1", ^{ + group.progress should be_greater_than_or_equal_to(0); + group.progress should be_less_than_or_equal_to(1); + }); describe(@"runWithDispatcher:", ^{ beforeEach(^{ @@ -123,6 +128,70 @@ expect(hasChildren).to(be_truthy()); }); }); + + describe(@"for a group with an invariant", ^{ + beforeEach(^{ + [group addInvariant:incompleteExample]; + NSUInteger count = group.examples.count; + expect(count).to(equal(0)); + }); + + it(@"should return false", ^{ + BOOL hasChildren = group.hasChildren; + expect(hasChildren).to(be_falsy()); + }); + + describe(@"and a nested group", ^{ + __block CDRExampleGroup *innerGroup; + + beforeEach(^{ + innerGroup = [[[CDRExampleGroup alloc] initWithText:groupText] autorelease]; + [group add:innerGroup]; + }); + + it(@"should return true for the group", ^{ + BOOL hasChildren = group.hasChildren; + expect(hasChildren).to(be_truthy()); + }); + + it(@"should return false for the inner group", ^{ + BOOL hasChildren = innerGroup.hasChildren; + expect(hasChildren).to(be_falsy()); + }); + }); + }); + + describe(@"for a group with a nested group", ^{ + __block CDRExampleGroup *innerGroup; + + beforeEach(^{ + innerGroup = [[[CDRExampleGroup alloc] initWithText:groupText] autorelease]; + [group add:innerGroup]; + NSUInteger count = group.examples.count; + expect(count).to_not(equal(0)); + }); + + it(@"should return true", ^{ + BOOL hasChildren = group.hasChildren; + expect(hasChildren).to(be_truthy()); + }); + + it(@"should return false for the inner group", ^{ + BOOL hasChildren = innerGroup.hasChildren; + expect(hasChildren).to(be_falsy()); + }); + + describe(@"and an invariant", ^{ + beforeEach(^{ + [group addInvariant:incompleteExample]; + }); + + it(@"should return false for the inner group", ^{ + BOOL hasChildren = innerGroup.hasChildren; + expect(hasChildren).to(be_falsy()); + }); + }); + }); }); describe(@"isFocused", ^{ @@ -172,6 +241,40 @@ expect([group hasFocusedExamples]).to(be_truthy()); }); }); + + context(@"and has at least one focused invariant", ^{ + __block CDRExampleGroup *anotherInnerGroup; + __block CDRExampleGroup *innerGroup; + + beforeEach(^{ + passingExample.focused = YES; + [group addInvariant:passingExample]; + [group add:failingExample]; + + innerGroup = [[CDRExampleGroup alloc] initWithText:@"Inner group"]; + [group add:innerGroup]; + + anotherInnerGroup = [[CDRExampleGroup alloc] initWithText:@"Another inner group"]; + [innerGroup add:anotherInnerGroup]; + + [innerGroup release]; + [anotherInnerGroup release]; + }); + + it(@"should return true", ^{ + expect([group hasFocusedExamples]).to(be_truthy()); + }); + + it(@"should have a focused inner group", ^{ + [group hasFocusedExamples]; + expect([innerGroup hasFocusedExamples]).to(be_truthy()); + }); + + it(@"should have a focused inner inner group", ^{ + [group hasFocusedExamples]; + expect([anotherInnerGroup hasFocusedExamples]).to(be_truthy()); + }); + }); context(@"and has at least one focused group", ^{ beforeEach(^{ @@ -210,6 +313,59 @@ blockInvocationCount should equal(3); }); }); + + describe(@"invariant", ^{ + __block NSInteger blockInvocationCount; + + beforeEach(^{ + CDRSpecBlock invariantBlock = ^{ ++blockInvocationCount; }; + [group addInvariant:[CDRExample exampleWithText:@"inv" andBlock:invariantBlock]]; + }); + + describe(@"for a single block", ^{ + beforeEach(^{ + blockInvocationCount = 0; + [group add:errorExample]; + [group add:failingExample]; + [group add:passingExample]; + [group runWithDispatcher:dispatcher]; + }); + + it(@"should be called once, regardless of failures or errors", ^{ + blockInvocationCount should equal(1); + }); + }); + + describe(@"for a pending block", ^{ + beforeEach(^{ + blockInvocationCount = 0; + [group runWithDispatcher:dispatcher]; + }); + + it(@"should not be called", ^{ + blockInvocationCount should equal(0); + }); + }); + + describe(@"for nested blocks", ^{ + beforeEach(^{ + blockInvocationCount = 0; + CDRExampleGroup * innerInnerGroup = [[[CDRExampleGroup alloc] initWithText:groupText] autorelease]; + [innerInnerGroup add:errorExample]; + [innerInnerGroup add:failingExample]; + CDRExampleGroup * innerGroup = [[[CDRExampleGroup alloc] initWithText:groupText] autorelease]; + [innerGroup add:innerInnerGroup]; + [group add:passingExample]; + [group add:innerGroup]; + + [group runWithDispatcher:dispatcher]; + }); + + it(@"should be called three times, once as it pases through each block", ^{ + blockInvocationCount should equal(3); + }); + }); + }); describe(@"state", ^{ describe(@"for a group containing no examples", ^{ @@ -562,6 +718,19 @@ expect(progress).to(be_close_to(2.0 / 3.0)); }); }); + + describe(@"when the group contains an invariant", ^{ + beforeEach(^{ + [group add:passingExample]; + [group addInvariant:incompleteExample]; + [passingExample runWithDispatcher:dispatcher]; + }); + + it(@"should count the invariant like a regular example", ^{ + float progress = group.progress; + expect(progress).to(be_close_to(1.0 / 2.0)); + }); + }); }); describe(@"message", ^{ diff --git a/Spec/SpecSpec.mm b/Spec/SpecSpec.mm index baec78f5..43bfaafd 100644 --- a/Spec/SpecSpec.mm +++ b/Spec/SpecSpec.mm @@ -34,6 +34,10 @@ void expectFailure(CDRSpecBlock block) { afterEach(^{ // NSLog(@"=====================> I should run after all specs."); }); + + invariant(@"an invariant run in multiple places", ^{ + // NSLog(@"=====================> Invariant was run here."); + }); describe(@"a nested spec", ^{ beforeEach(^{ @@ -51,6 +55,10 @@ void expectFailure(CDRSpecBlock block) { it(@"should also also run", ^{ // NSLog(@"=====================> Another nested spec"); }); + + it(@"should run the invariant below here", ^{ + // NSLog(@"vvvvvvvvvvvvvvvvvvvvvv Invariant below"); + }); }); context(@"a nested spec (context)", ^{ @@ -69,16 +77,42 @@ void expectFailure(CDRSpecBlock block) { it(@"should also also run", ^{ // NSLog(@"=====================> Another nested spec"); }); + + context(@"a doubly nested spec", ^{ + it(@"should also run", ^{ + // NSLog(@"=====================> Nested spec"); + }); + + it(@"should run the invariant below here", ^{ + // NSLog(@"vvvvvvvvvvvvvvvvvvvvvv Invariant below"); + }); + }); + + it(@"should run the invariant below here", ^{ + // NSLog(@"vvvvvvvvvvvvvvvvvvvvvv Invariant below"); + }); }); it(@"should run", ^{ // NSLog(@"=====================> Spec"); }); + + it(@"should run the invariant below here", ^{ + // NSLog(@"vvvvvvvvvvvvvvvvvvvvvv Invariant below"); + }); it(@"should be pending", PENDING); it(@"should also be pending", nil); xit(@"should also be pending (xit)", ^{}); + describe(@"invariants", ^{ + it_should_always(@"be pending", PENDING); + it_should_always(@"also be pending", nil); + xit_should_always(@"also be pending (xit_should_always)", ^{}); + + it(@"should force invariants", ^{}); + }); + describe(@"described specs should be pending", PENDING); describe(@"described specs should also be pending", nil); xdescribe(@"xdescribed specs should be pending", ^{}); @@ -214,6 +248,144 @@ void expectFailure(CDRSpecBlock block) { [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Should have thrown an exception" userInfo:nil]; }); +describe(@"an invariant", ^{ + __block NSInteger x; + __block NSInteger y; + __block BOOL ran; + + beforeEach(^{ + x = 0; + y = 0; + ran = NO; + }); + + invariant(@"invariant equates the two variables", ^{ + expect(x).to(equal(y)); + ran = YES; + }); + + context(@"in a context block", ^{ + beforeEach(^{ + x = 5; + y = 5; + }); + + context(@"in a nested context block", ^{ + beforeEach(^{ + x = -2; + y = -2; + }); + + it(@"force the invariant", ^{expect(true).to(be_truthy());}); + + afterEach(^{ + it(@"should run the invariant", ^{ + expect(ran).to(be_truthy()); + }); + }); + }); + + afterEach(^{ + it(@"should run the invariant", ^{ + expect(ran).to(be_truthy()); + }); + }); + }); + + context(@"in a pending context block should be pending", ^{}); + + afterEach(^{ + it(@"should run the invariant", ^{ + expect(ran).to(be_truthy()); + }); + }); +}); + +describe(@"a failing invariant", ^{ + __block BOOL tried; + __block BOOL ran; + + beforeEach(^{ + tried = NO; + ran = NO; + }); + + invariant(@"invariant tries to do the impossible", ^{ + expectFailure(^{ + tried = YES; + expect(true).to(be_falsy()); + ran = YES; + }); + }); + + it(@"force the invariant", ^{expect(true).to(be_truthy());}); + + afterEach(^{ + it(@"should run the invariant", ^{ + expect(tried).to(be_truthy()); + }); + + it(@"should not complete running the invariant", ^{ + expect(ran).to(be_falsy()); + }); + }); +}); + +describe(@"an invariant and a subject action block", ^{ + __block NSInteger x; + __block NSInteger y; + __block BOOL ran; + + beforeEach(^{ + x = 5; + y = 0; + ran = NO; + }); + + invariant(@"invariant equates the two variables", ^{ + expect(x).to(equal(y)); + ran = YES; + }); + + subjectAction(^{ y = 5; }); + + context(@"in a context block", ^{ + beforeEach(^{ + x = 5; + y = 0; + }); + + context(@"in a nested context block", ^{ + beforeEach(^{ + x = 5; + y = 0; + }); + + it(@"force the invariant", ^{expect(true).to(be_truthy());}); + + afterEach(^{ + it(@"should run the invariant after the subject action", ^{ + expect(ran).to(be_truthy()); + }); + }); + }); + + afterEach(^{ + it(@"should run the invariant after the subject action", ^{ + expect(ran).to(be_truthy()); + }); + }); + }); + + context(@"in a pending context block should be pending", ^{}); + + afterEach(^{ + it(@"should run the invariant after the subject action", ^{ + expect(ran).to(be_truthy()); + }); + }); +}); + SPEC_END