Skip to content

Commit

Permalink
Raise exceptions on misuse of test DSL
Browse files Browse the repository at this point in the history
This fixes #393

Shout out to @briancroom for

1. finding this bug
2. not fixing it so that someone else could have the glory

We'll miss you, Brian.
  • Loading branch information
tjarratt committed Jul 28, 2016
1 parent 0d5b81e commit 4530d24
Show file tree
Hide file tree
Showing 20 changed files with 561 additions and 86 deletions.
148 changes: 117 additions & 31 deletions Cedar.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion Source/CDRFunctions.m
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#import "CDROTestNamer.h"
#import "CDRVersion.h"
#import "CDRSpecRun.h"
#import "CDRStateTracker.h"

static NSString * const CDRBuildVersionKey = @"CDRBuildVersionSHA";

Expand Down Expand Up @@ -332,7 +333,9 @@ void __attribute__((weak)) __gcov_flush(void) {

int CDRRunSpecsWithCustomExampleReporters(NSArray *reporters) {
@autoreleasepool {
CDRSpecRun *run = [[CDRSpecRun alloc] initWithExampleReporters:reporters];
CDRStateTracker *stateTracker = [[[CDRStateTracker alloc] init] autorelease];
CDRSpecRun *run = [[CDRSpecRun alloc] initWithStateTracker:stateTracker
exampleReporters:reporters];

int result = [run performSpecRun:^{
[run.rootGroups makeObjectsPerformSelector:@selector(runWithDispatcher:) withObject:run.dispatcher];
Expand Down
11 changes: 11 additions & 0 deletions Source/CDRRunState.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#import "CDRRunState.h"

static CedarRunState CDRCurrentRunState = CedarRunStateNotYetStarted;

CedarRunState CDRCurrentState() {
return CDRCurrentRunState;
}

void CDRSetCurrentRunState(CedarRunState runState) {
CDRCurrentRunState = runState;
}
155 changes: 124 additions & 31 deletions Source/CDRSpec.m
Original file line number Diff line number Diff line change
@@ -1,15 +1,46 @@
#import <objc/runtime.h>

#import "CDRSpec.h"
#import "CDRExample.h"
#import "CDRExampleGroup.h"
#import "CDRSpecFailure.h"
#import "CDRRunState.h"
#import "CDRSpecHelper.h"
#import "CDRSpecFailure.h"
#import "CDRExampleGroup.h"
#import "CDRSymbolicator.h"
#import <objc/runtime.h>

#define with_stack_address(b) \
((b.stackAddress = CDRCallerStackAddress()), b)

CDRSpec *CDR_currentSpec;

static void(^placeholderPendingTestBlock)() = ^{ it(@"is pending", PENDING); };
#pragma mark - Spec Validation

// runs validation when it(), context(), describe() are invoked
// generally this is a useful feature, but you may disable it if you
// care to do metaprogramming with Cedar specs
// (or other usecases we couldn't imagine).
BOOL CDR_validateSpecs = YES;

void CDRDisableSpecValidation() {
CDR_validateSpecs = NO;
}

void CDREnableSpecValidation() {
CDR_validateSpecs = YES;
}

#pragma mark - static vars

static void(^placeholderPendingTestBlock)() = ^{
BOOL originalState = CDR_validateSpecs;
CDR_validateSpecs = NO;

it(@"is pending", PENDING);

CDR_validateSpecs = originalState;
};

#pragma mark - public API
void beforeEach(CDRSpecBlock block) {
[CDR_currentSpec.currentGroup addBefore:block];
}
Expand All @@ -18,43 +49,96 @@ void afterEach(CDRSpecBlock block) {
[CDR_currentSpec.currentGroup addAfter:block];
}

#define with_stack_address(b) \
((b.stackAddress = CDRCallerStackAddress()), b)
void ensureCurrentSpecExists(NSString *functionName) {
if (!CDR_validateSpecs) {
return;
}

CDRExampleGroup * describe(NSString *text, CDRSpecBlock block) {
CDRExampleGroup *group = nil;
if (block) {
CDRExampleGroup *parentGroup = CDR_currentSpec.currentGroup;
group = [CDRExampleGroup groupWithText:text];
[parentGroup add:group];
CDR_currentSpec.currentGroup = group;

@try {
block();
}
@catch (CDRSpecFailure *failure) {
NSString *reason = [NSString stringWithFormat:@"Caught a spec failure before the specs began to run. Did you forget to put your assertion into an `it` block?. The failure was:\n%@", failure];
[[NSException exceptionWithName:NSInternalInconsistencyException reason:reason userInfo:nil] raise];
}
if (CDR_currentSpec == nil) {
NSString * reason = [NSString stringWithFormat:@"%@() was invoked outside of a spec. It may only be called when a spec has been defined with SPEC_BEGIN and SPEC_END macros.", functionName];
[[NSException exceptionWithName:NSInternalInconsistencyException
reason:reason
userInfo:nil] raise];
}
}

if ([group.examples count] == 0) {
block = placeholderPendingTestBlock;
block();
void ensureTestsAreNotYetRunning(NSString *functionName) {
if (!CDR_validateSpecs) {
return;
}

switch (CDRCurrentState()) {
case CedarRunStateNotYetStarted:
// exceedingly unlikely
break;
case CedarRunStatePreparingTests:
// happy path, no-op, we should hit this branch 100% of the time
break;
case CedarRunStateRunningTests: {
// someone done goofed
NSString * reason = [NSString stringWithFormat:@"%@() was invoked during a test run. Make sure your '%@()' is not inside of an it() block.", functionName, functionName];
[[NSException exceptionWithName:NSInternalInconsistencyException
reason:reason
userInfo:nil] raise];
break;
}
CDR_currentSpec.currentGroup = parentGroup;
} else {
group = describe(text, placeholderPendingTestBlock);
case CedarRunStateFinished:
// someone ... REALLY done goofed
break;
}
}

CDRExampleGroup *groupFromSpecBlock(NSString *text, CDRSpecBlock block) {
if (!block) {
return groupFromSpecBlock(text, placeholderPendingTestBlock);
}

CDRExampleGroup *parentGroup = CDR_currentSpec.currentGroup;
CDRExampleGroup *group = [CDRExampleGroup groupWithText:text];
[parentGroup add:group];
CDR_currentSpec.currentGroup = group;

@try {
block();
}
@catch (CDRSpecFailure *failure) {
NSString *reason = [NSString stringWithFormat:@"Caught a spec failure before the specs began to run. Did you forget to put your assertion into an `it` block?. The failure was:\n%@", failure];
[[NSException exceptionWithName:NSInternalInconsistencyException reason:reason userInfo:nil] raise];
}

if ([group.examples count] == 0) {
block = placeholderPendingTestBlock;
block();
}
CDR_currentSpec.currentGroup = parentGroup;

return group;
}

CDRExampleGroup * describe(NSString *text, CDRSpecBlock block) {
ensureCurrentSpecExists(@"describe");
ensureTestsAreNotYetRunning(@"describe");

CDRExampleGroup *group = groupFromSpecBlock(text, block);
return with_stack_address(group);
}

CDRExampleGroup* (*context)(NSString *, CDRSpecBlock) = &describe;
CDRExampleGroup* context(NSString *text, CDRSpecBlock block) {
ensureCurrentSpecExists(@"context");
ensureTestsAreNotYetRunning(@"context");

CDRExampleGroup *group = groupFromSpecBlock(text, block);
return with_stack_address(group);
}

void subjectAction(CDRSpecBlock block) {
CDR_currentSpec.currentGroup.subjectActionBlock = block;
}

CDRExample * it(NSString *text, CDRSpecBlock block) {
ensureCurrentSpecExists(@"it");
ensureTestsAreNotYetRunning(@"it");

CDRExample *example = [CDRExample exampleWithText:text andBlock:block];
[CDR_currentSpec.currentGroup add:example];
return with_stack_address(example);
Expand All @@ -67,7 +151,10 @@ void subjectAction(CDRSpecBlock block) {
return with_stack_address(group);
}

CDRExampleGroup* (*xcontext)(NSString *, CDRSpecBlock) = &xdescribe;
CDRExampleGroup * xcontext(NSString *text, CDRSpecBlock block) {
CDRExampleGroup *group = context(text, placeholderPendingTestBlock);
return with_stack_address(group);
}

CDRExample * xit(NSString *text, CDRSpecBlock block) {
CDRExample *example = it(text, PENDING);
Expand All @@ -82,7 +169,11 @@ void subjectAction(CDRSpecBlock block) {
return with_stack_address(group);
}

CDRExampleGroup* (*fcontext)(NSString *, CDRSpecBlock) = &fdescribe;
CDRExampleGroup * fcontext(NSString *text, CDRSpecBlock block) {
CDRExampleGroup *group = context(text, block);
group.focused = YES;
return with_stack_address(group);
}

CDRExample * fit(NSString *text, CDRSpecBlock block) {
CDRExample *example = it(text, block);
Expand Down Expand Up @@ -118,7 +209,9 @@ - (void)dealloc {
}

- (void)commonInit {
self.rootGroup = [[[CDRExampleGroup alloc] initWithText:[[self class] description] isRoot:YES] autorelease];
NSString *text = self.class.description;
self.rootGroup = [[[CDRExampleGroup alloc] initWithText:text
isRoot:YES] autorelease];
self.rootGroup.parent = [CDRSpecHelper specHelper];
self.currentGroup = self.rootGroup;
self.symbolicator = [[[CDRSymbolicator alloc] init] autorelease];
Expand Down
36 changes: 27 additions & 9 deletions Source/CDRSpecRun.m
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
#import "CDRSpecRun.h"
#import "CDRSpec.h"
#import "CDRSpecRun.h"
#import "CDRPrivateFunctions.h"
#import "CDRReportDispatcher.h"

@interface CDRSpecRun ()
@property (nonatomic, retain) id<CDRStateTracking> stateTracker;
@end

@implementation CDRSpecRun

- (instancetype)initWithExampleReporters:(NSArray *)reporters {
- (instancetype)initWithStateTracker:(id<CDRStateTracking>)stateTracker
exampleReporters:(NSArray *)reporters {
if (self = [super init]) {
_stateTracker = [stateTracker retain];
[_stateTracker didStartPreparingTests];

CDRDefineSharedExampleGroups();
CDRDefineGlobalBeforeAndAfterEachBlocks();

Expand All @@ -24,21 +32,31 @@ - (instancetype)initWithExampleReporters:(NSArray *)reporters {
return self;
}

- (void)dealloc {
[_specs release]; _specs = nil;
[_rootGroups release]; _rootGroups = nil;
[_dispatcher release]; _dispatcher = nil;
[super dealloc];
}

- (int)performSpecRun:(void (^)(void))runBlock {
[self.stateTracker didStartRunningTests];
[self.dispatcher runWillStartWithGroups:self.rootGroups andRandomSeed:self.seed];

runBlock();

[self.dispatcher runDidComplete];

[self.stateTracker didFinishRunningTests];
return [self.dispatcher result];
}

#pragma mark - NSObject

- (instancetype)init {
[self doesNotRecognizeSelector:_cmd];
return nil;
}

- (void)dealloc {
[_specs release]; _specs = nil;
[_rootGroups release]; _rootGroups = nil;
[_dispatcher release]; _dispatcher = nil;
[_stateTracker release]; _stateTracker = nil;
[super dealloc];
}

@end
6 changes: 6 additions & 0 deletions Source/CDRStateTracker.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#import <Foundation/Foundation.h>
#import "CDRStateTracking.h"

@interface CDRStateTracker : NSObject <CDRStateTracking>

@end
18 changes: 18 additions & 0 deletions Source/CDRStateTracker.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#import "CDRStateTracker.h"
#import "CDRRunState.h"

@implementation CDRStateTracker

- (void)didStartPreparingTests {
CDRSetCurrentRunState(CedarRunStatePreparingTests);
}

- (void)didStartRunningTests {
CDRSetCurrentRunState(CedarRunStateRunningTests);
}

- (void)didFinishRunningTests {
CDRSetCurrentRunState(CedarRunStateFinished);
}

@end
9 changes: 9 additions & 0 deletions Source/CDRStateTracking.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#import <Foundation/Foundation.h>

@protocol CDRStateTracking <NSObject>

- (void)didStartPreparingTests;
- (void)didStartRunningTests;
- (void)didFinishRunningTests;

@end
11 changes: 11 additions & 0 deletions Source/Headers/Project/CDRRunState.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#import <Foundation/Foundation.h>

typedef NS_ENUM(NSInteger, CedarRunState) {
CedarRunStateNotYetStarted = 0,
CedarRunStatePreparingTests = 1,
CedarRunStateRunningTests = 2,
CedarRunStateFinished = 3
};

OBJC_EXPORT CedarRunState CDRCurrentState();
OBJC_EXPORT void CDRSetCurrentRunState(CedarRunState);
4 changes: 3 additions & 1 deletion Source/Headers/Project/CDRSpecRun.h
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#import <Foundation/Foundation.h>
#import "CDRStateTracking.h"

@class CDRReportDispatcher;

Expand All @@ -9,7 +10,8 @@
@property (nonatomic, retain, readonly) CDRReportDispatcher *dispatcher;
@property (nonatomic, assign, readonly) unsigned int seed;

- (instancetype)initWithExampleReporters:(NSArray *)reporters;
- (instancetype)initWithStateTracker:(id<CDRStateTracking>)stateTracker
exampleReporters:(NSArray *)reporters;
- (int)performSpecRun:(void (^)(void))runBlock;

@end
4 changes: 2 additions & 2 deletions Source/Headers/Public/CDRFunctions.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ NSArray *CDRReportersToRun();
NSString *CDRGetTestBundleExtension();
void CDRSuppressStandardPipesWhileLoadingClasses();

NS_ASSUME_NONNULL_END

#ifdef __cplusplus
}
#endif

NS_ASSUME_NONNULL_END
Loading

0 comments on commit 4530d24

Please sign in to comment.