Project import
diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ca257c0 --- /dev/null +++ b/LICENSE
@@ -0,0 +1,20 @@ +Copyright (c) 2012-2014 Specta Team. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +
diff --git a/README.md b/README.md new file mode 100644 index 0000000..431b121 --- /dev/null +++ b/README.md
@@ -0,0 +1,176 @@ +# Specta + +A light-weight TDD / BDD framework for Objective-C. + +### Status +[](https://travis-ci.org/specta/specta) +[](https://coveralls.io/r/specta/specta) + +## FEATURES + +* An Objective-C RSpec-like BDD DSL +* Quick and easy set up +* Built on top of XCTest +* Excellent Xcode integration + +## SCREENSHOT + + + +## SETUP + +Use [CocoaPods](http://github.com/CocoaPods/CocoaPods), [Carthage](https://github.com/carthage/carthage) or [Set up manually](#setting-up-manually) + +### CocoaPods + +1. Add Specta to your project's `Podfile`: + + ```ruby + target :MyApp do + # your app dependencies + end + + target :MyAppTests do + pod 'Specta', '~> 1.0' + # pod 'Expecta', '~> 1.0' # expecta matchers + # pod 'OCMock', '~> 2.2' # OCMock + # pod 'OCHamcrest', '~> 3.0' # hamcrest matchers + # pod 'OCMockito', '~> 1.0' # OCMock + # pod 'LRMocky', '~> 0.9' # LRMocky + end + ``` + +2. Run `pod update` or `pod install` in your project directory. + +### Carthage + +1. Add Specta to your project's `Cartfile.private` + + ``` + github "specta/specta" ~> 1.0 + ``` + +2. Run `carthage update` in your project directory +3. Drag the appropriate `Specta.framework` for your platform (located in Carthage/Build/) into your application’s Xcode project, and add it to your test target(s). +4. If you are building for iOS, a new `Run Script Phase` must be added to copy the framework. The instructions can be found on [Carthage's getting started instructions](https://github.com/carthage/carthage#getting-started) + +### SETTING UP MANUALLY + +1. Clone from Github. +2. Run `rake` in project root to build. +3. Add a "Cocoa/Cocoa Touch Unit Testing Bundle" target if you don't already have one. +4. Copy and add all header files in `Products` folder to the Test target in your Xcode project. +5. For **OS X projects**, copy and add `Specta.framework` in `Products/osx` folder to the test target in your Xcode project. + For **iOS projects**, copy and add `Specta.framework` in `Products/ios` folder to the test target in your Xcode project. + You can alternatively use `libSpecta.a`, if you prefer to add it as a static library for your project. (iOS 7 and below require this) +6. Add `-ObjC` and `-all_load` to the "Other Linker Flags" build setting for the test target in your Xcode project. +7. If you encounter linking issues with `_llvm_*` symbols, ensure your target's "Generate Test Coverage Files" and "Instrument Program Flow" build settings are set to `Yes`. + +## EXAMPLE + +```objective-c +#import <Specta/Specta.h> // #import "Specta.h" if you're using libSpecta.a + +SharedExamplesBegin(MySharedExamples) +// Global shared examples are shared across all spec files. + +sharedExamplesFor(@"foo", ^(NSDictionary *data) { + __block id bar = nil; + beforeEach(^{ + bar = data[@"bar"]; + }); + it(@"should not be nil", ^{ + XCTAssertNotNil(bar); + }); +}); + +SharedExamplesEnd + +SpecBegin(Thing) + +describe(@"Thing", ^{ + sharedExamplesFor(@"another shared behavior", ^(NSDictionary *data) { + // Locally defined shared examples can override global shared examples within its scope. + }); + + beforeAll(^{ + // This is run once and only once before all of the examples + // in this group and before any beforeEach blocks. + }); + + beforeEach(^{ + // This is run before each example. + }); + + it(@"should do stuff", ^{ + // This is an example block. Place your assertions here. + }); + + it(@"should do some stuff asynchronously", ^{ + waitUntil(^(DoneCallback done) { + // Async example blocks need to invoke done() callback. + done(); + }); + }); + + itShouldBehaveLike(@"a shared behavior", @{@"key" : @"obj"}); + + itShouldBehaveLike(@"another shared behavior", ^{ + // Use a block that returns a dictionary if you need the context to be evaluated lazily, + // e.g. to use an object prepared in a beforeEach block. + return @{@"key" : @"obj"}; + }); + + describe(@"Nested examples", ^{ + it(@"should do even more stuff", ^{ + // ... + }); + }); + + pending(@"pending example"); + + pending(@"another pending example", ^{ + // ... + }); + + afterEach(^{ + // This is run after each example. + }); + + afterAll(^{ + // This is run once and only once after all of the examples + // in this group and after any afterEach blocks. + }); +}); + +SpecEnd +``` + +* `beforeEach` and `afterEach` are also aliased as `before` and `after` respectively. +* `describe` is also aliased as `context`. +* `it` is also aliased as `example` and `specify`. +* `itShouldBehaveLike` is also aliased as `itBehavesLike`. +* Use `pending` or prepend `x` to `describe`, `context`, `example`, `it`, and `specify` to mark examples or groups as pending. +* Use `^(DoneCallback done)` as shown in the example above to make examples wait for completion. `done()` callback needs to be invoked to let Specta know that your test is complete. The default timeout is 10.0 seconds but this can be changed by calling the function `setAsyncSpecTimeout(NSTimeInterval timeout)`. +* `(before|after)(Each/All)` also accept `^(DoneCallback done)`s. +* Do `#define SPT_CEDAR_SYNTAX` before importing Specta if you prefer to write `SPEC_BEGIN` and `SPEC_END` instead of `SpecBegin` and `SpecEnd`. +* Prepend `f` to your `describe`, `context`, `example`, `it`, and `specify` to set focus on examples or groups. When specs are focused, all unfocused specs are skipped. +* To use original XCTest reporter, set an environment variable named `SPECTA_REPORTER_CLASS` to `SPTXCTestReporter` in your test scheme. +* Set an environment variable `SPECTA_NO_SHUFFLE` with value `1` to disable test shuffling. +* Set an environment variable `SPECTA_SEED` to specify the random seed for test shuffling. + +Standard XCTest matchers such as `XCTAssertEqualObjects` and `XCTAssertNil` work, but you probably want to add a nicer matcher framework - [Expecta](http://github.com/specta/expecta/) to your setup. Or if you really prefer, [OCHamcrest](https://github.com/jonreid/OCHamcrest) works fine too. Also, add a mocking framework: [OCMock](http://ocmock.org/). + +## RUNNING TESTS IN COMMAND LINE + +* Run `rake test` in the cloned folder. + +## CONTRIBUTION GUIDELINES + +* Please use only spaces and indent 2 spaces at a time. +* Please prefix instance variable names with a single underscore (`_`). +* Please prefix custom classes and functions defined in the global scope with `SPT`. + +## LICENSE + +Copyright (c) 2012-2015 [Specta Team](https://github.com/specta?tab=members). This software is licensed under the [MIT License](http://github.com/specta/specta/raw/master/LICENSE).
diff --git a/Specta/Specta/SPTCallSite.h b/Specta/Specta/SPTCallSite.h new file mode 100644 index 0000000..adcd3b0 --- /dev/null +++ b/Specta/Specta/SPTCallSite.h
@@ -0,0 +1,12 @@ +#import <Foundation/Foundation.h> + +@interface SPTCallSite : NSObject + +@property (nonatomic, copy, readonly) NSString *file; +@property (nonatomic, readonly) NSUInteger line; + ++ (instancetype)callSiteWithFile:(NSString *)file line:(NSUInteger)line; + +- (instancetype)initWithFile:(NSString *)file line:(NSUInteger)line; + +@end
diff --git a/Specta/Specta/SPTCallSite.m b/Specta/Specta/SPTCallSite.m new file mode 100644 index 0000000..585cecd --- /dev/null +++ b/Specta/Specta/SPTCallSite.m
@@ -0,0 +1,18 @@ +#import "SPTCallSite.h" + +@implementation SPTCallSite + ++ (instancetype)callSiteWithFile:(NSString *)file line:(NSUInteger)line { + return [[self alloc] initWithFile:file line:line]; +} + +- (instancetype)initWithFile:(NSString *)file line:(NSUInteger)line { + self = [super init]; + if (self) { + _file = file; + _line = line; + } + return self; +} + +@end
diff --git a/Specta/Specta/SPTCompiledExample.h b/Specta/Specta/SPTCompiledExample.h new file mode 100644 index 0000000..adb3aaa --- /dev/null +++ b/Specta/Specta/SPTCompiledExample.h
@@ -0,0 +1,17 @@ +#import <Foundation/Foundation.h> +#import "SpectaTypes.h" + +@interface SPTCompiledExample : NSObject + +@property (nonatomic, copy) NSString *name; +@property (nonatomic, copy) NSString *testCaseName; +@property (nonatomic, copy) SPTSpecBlock block; + +@property (nonatomic) BOOL pending; +@property (nonatomic, getter=isFocused) BOOL focused; + +@property (nonatomic) SEL testMethodSelector; + +- (id)initWithName:(NSString *)name testCaseName:(NSString *)testCaseName block:(SPTSpecBlock)block pending:(BOOL)pending focused:(BOOL)focused; + +@end
diff --git a/Specta/Specta/SPTCompiledExample.m b/Specta/Specta/SPTCompiledExample.m new file mode 100644 index 0000000..e762165 --- /dev/null +++ b/Specta/Specta/SPTCompiledExample.m
@@ -0,0 +1,17 @@ +#import "SPTCompiledExample.h" + +@implementation SPTCompiledExample + +- (id)initWithName:(NSString *)name testCaseName:(NSString *)testCaseName block:(SPTSpecBlock)block pending:(BOOL)pending focused:(BOOL)focused { + self = [super init]; + if (self) { + self.name = name; + self.testCaseName = testCaseName; + self.block = block; + self.pending = pending; + self.focused = focused; + } + return self; +} + +@end
diff --git a/Specta/Specta/SPTExample.h b/Specta/Specta/SPTExample.h new file mode 100644 index 0000000..849aacb --- /dev/null +++ b/Specta/Specta/SPTExample.h
@@ -0,0 +1,17 @@ +#import <Foundation/Foundation.h> +#import "SpectaTypes.h" + +@class SPTCallSite; + +@interface SPTExample : NSObject + +@property (nonatomic, copy) NSString *name; +@property (nonatomic, retain) SPTCallSite *callSite; +@property (nonatomic, copy) SPTVoidBlock block; + +@property (nonatomic) BOOL pending; +@property (nonatomic, getter=isFocused) BOOL focused; + +- (id)initWithName:(NSString *)name callSite:(SPTCallSite *)callSite focused:(BOOL)focused block:(SPTVoidBlock)block; + +@end
diff --git a/Specta/Specta/SPTExample.m b/Specta/Specta/SPTExample.m new file mode 100644 index 0000000..e5905a9 --- /dev/null +++ b/Specta/Specta/SPTExample.m
@@ -0,0 +1,17 @@ +#import "SPTExample.h" + +@implementation SPTExample + +- (id)initWithName:(NSString *)name callSite:(SPTCallSite *)callSite focused:(BOOL)focused block:(SPTVoidBlock)block { + self = [super init]; + if (self) { + self.name = name; + self.callSite = callSite; + self.block = block; + self.focused = focused; + self.pending = block == nil; + } + return self; +} + +@end
diff --git a/Specta/Specta/SPTExampleGroup.h b/Specta/Specta/SPTExampleGroup.h new file mode 100644 index 0000000..dce3db6 --- /dev/null +++ b/Specta/Specta/SPTExampleGroup.h
@@ -0,0 +1,36 @@ +#import <Foundation/Foundation.h> +#import <XCTest/XCTest.h> +#import "SpectaTypes.h" + +@class SPTExample; +@class SPTCallSite; + +@interface SPTExampleGroup : NSObject + +@property (nonatomic, copy) NSString *name; +@property (nonatomic, weak) SPTExampleGroup *root; +@property (nonatomic, weak) SPTExampleGroup *parent; +@property (nonatomic, strong) NSMutableArray *children; +@property (nonatomic, strong) NSMutableArray *beforeAllArray; +@property (nonatomic, strong) NSMutableArray *afterAllArray; +@property (nonatomic, strong) NSMutableArray *beforeEachArray; +@property (nonatomic, strong) NSMutableArray *afterEachArray; +@property (nonatomic, strong) NSMutableDictionary *sharedExamples; +@property (nonatomic) unsigned int exampleCount; +@property (nonatomic) unsigned int ranExampleCount; +@property (nonatomic) unsigned int pendingExampleCount; +@property (nonatomic, getter=isFocused) BOOL focused; + +- (id)initWithName:(NSString *)name parent:(SPTExampleGroup *)parent root:(SPTExampleGroup *)root; + +- (SPTExampleGroup *)addExampleGroupWithName:(NSString *)name focused:(BOOL)focused; +- (SPTExample *)addExampleWithName:(NSString *)name callSite:(SPTCallSite *)callSite focused:(BOOL)focused block:(SPTVoidBlock)block; + +- (void)addBeforeAllBlock:(SPTVoidBlock)block; +- (void)addAfterAllBlock:(SPTVoidBlock)block; +- (void)addBeforeEachBlock:(SPTVoidBlock)block; +- (void)addAfterEachBlock:(SPTVoidBlock)block; + +- (NSArray *)compileExamplesWithStack:(NSArray *)stack; + +@end
diff --git a/Specta/Specta/SPTExampleGroup.m b/Specta/Specta/SPTExampleGroup.m new file mode 100644 index 0000000..cf51800 --- /dev/null +++ b/Specta/Specta/SPTExampleGroup.m
@@ -0,0 +1,335 @@ +#import "SPTExampleGroup.h" +#import "SPTExample.h" +#import "SPTCompiledExample.h" +#import "SPTSpec.h" +#import "SpectaUtility.h" +#import "XCTest+Private.h" +#import "SPTGlobalBeforeAfterEach.h" +#import <libkern/OSAtomic.h> +#import <objc/runtime.h> + +static NSArray *ClassesWithClassMethod(SEL classMethodSelector) { + NSMutableArray *classesWithClassMethod = [[NSMutableArray alloc] init]; + + int numberOfClasses = objc_getClassList(NULL, 0); + if (numberOfClasses > 0) { + Class *classes = (Class *)malloc(sizeof(Class) *numberOfClasses); + numberOfClasses = objc_getClassList(classes, numberOfClasses); + + for(int classIndex = 0; classIndex < numberOfClasses; classIndex++) { + Class aClass = classes[classIndex]; + + if (class_conformsToProtocol(aClass, @protocol(SPTGlobalBeforeAfterEach))) { + Method globalMethod = class_getClassMethod(aClass, classMethodSelector); + if (globalMethod) { + [classesWithClassMethod addObject:aClass]; + } + } + } + + free(classes); + } + + return classesWithClassMethod; +} + +static void runExampleBlock(void (^block)(), NSString *name) { + if (!SPTIsBlock(block)) { + return; + } + + ((SPTVoidBlock)block)(); +} + +typedef NS_ENUM(NSInteger, SPTExampleGroupOrder) { + SPTExampleGroupOrderOutermostFirst = 1, + SPTExampleGroupOrderInnermostFirst +}; + +@interface SPTExampleGroup () <SPTGlobalBeforeAfterEach> + +- (void)incrementExampleCount; +- (void)incrementPendingExampleCount; +- (void)resetRanExampleCountIfNeeded; +- (void)incrementRanExampleCount; +- (void)runBeforeHooks:(NSString *)compiledName; +- (void)runBeforeAllHooks:(NSString *)compiledName; +- (void)runBeforeEachHooks:(NSString *)compiledName; +- (void)runAfterHooks:(NSString *)compiledName; +- (void)runAfterEachHooks:(NSString *)compiledName; +- (void)runAfterAllHooks:(NSString *)compiledName; + +@end + +@implementation SPTExampleGroup + +- (id)init { + self = [super init]; + if (self) { + self.name = nil; + self.root = nil; + self.parent = nil; + self.children = [NSMutableArray array]; + self.beforeAllArray = [NSMutableArray array]; + self.afterAllArray = [NSMutableArray array]; + self.beforeEachArray = [NSMutableArray array]; + self.afterEachArray = [NSMutableArray array]; + self.sharedExamples = [NSMutableDictionary dictionary]; + self.exampleCount = 0; + self.pendingExampleCount = 0; + self.ranExampleCount = 0; + } + return self; +} + +- (id)initWithName:(NSString *)name parent:(SPTExampleGroup *)parent root:(SPTExampleGroup *)root { + self = [self init]; + if (self) { + self.name = name; + self.parent = parent; + self.root = root; + } + return self; +} + +- (SPTExampleGroup *)addExampleGroupWithName:(NSString *)name focused:(BOOL)focused { + SPTExampleGroup *group = [[SPTExampleGroup alloc] initWithName:name parent:self root:self.root]; + group.focused = focused; + [self.children addObject:group]; + return group; +} + +- (SPTExample *)addExampleWithName:(NSString *)name callSite:(SPTCallSite *)callSite focused:(BOOL)focused block:(SPTVoidBlock)block { + SPTExample *example; + @synchronized(self) { + example = [[SPTExample alloc] initWithName:name callSite:callSite focused:focused block:block]; + [self.children addObject:example]; + + [self incrementExampleCount]; + if (example.pending) { + [self incrementPendingExampleCount]; + } + } + return example; +} + +- (void)incrementExampleCount { + SPTExampleGroup *group = self; + while (group != nil) { + group.exampleCount ++; + group = group.parent; + } +} + +- (void)incrementPendingExampleCount { + SPTExampleGroup *group = self; + while (group != nil) { + group.pendingExampleCount ++; + group = group.parent; + } +} + +- (void)resetRanExampleCountIfNeeded { + SPTExampleGroup *group = self; + while (group != nil) { + if (group.ranExampleCount >= group.exampleCount) { + group.ranExampleCount = 0; + } + group = group.parent; + } +} + +- (void)incrementRanExampleCount { + SPTExampleGroup *group = self; + while (group != nil) { + group.ranExampleCount ++; + group = group.parent; + } +} + +- (void)addBeforeAllBlock:(SPTVoidBlock)block { + if (!block) return; + [self.beforeAllArray addObject:[block copy]]; +} + +- (void)addAfterAllBlock:(SPTVoidBlock)block { + if (!block) return; + [self.afterAllArray addObject:[block copy]]; +} + +- (void)addBeforeEachBlock:(SPTVoidBlock)block { + if (!block) return; + [self.beforeEachArray addObject:[block copy]]; +} + +- (void)addAfterEachBlock:(SPTVoidBlock)block { + if (!block) return; + [self.afterEachArray addObject:[block copy]]; +} + +- (void)runGlobalBeforeEachHooks:(NSString *)compiledName { + static NSArray *globalBeforeEachClasses; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + globalBeforeEachClasses = ClassesWithClassMethod(@selector(beforeEach)); + }); + + for (Class class in globalBeforeEachClasses) { + [class beforeEach]; + } +} + +- (void)runGlobalAfterEachHooks:(NSString *)compiledName { + static NSArray *globalAfterEachClasses; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + globalAfterEachClasses = ClassesWithClassMethod(@selector(afterEach)); + }); + + for (Class class in globalAfterEachClasses) { + [class afterEach]; + } +} + +// Builds an array of example groups that enclose the current group. +// When in innermost-first order, the most deeply nested example groups, +// beginning with self, are placed at the beginning of the array. +// When in outermost-first order, the opposite is true, and the last +// group in the array (self) is the most deeply nested. +- (NSArray *)exampleGroupStackInOrder:(SPTExampleGroupOrder)order { + NSMutableArray *groups = [NSMutableArray array]; + SPTExampleGroup *group = self; + while (group != nil) { + switch (order) { + case SPTExampleGroupOrderOutermostFirst: + [groups insertObject:group atIndex:0]; + break; + case SPTExampleGroupOrderInnermostFirst: + [groups addObject:group]; + break; + } + group = group.parent; + } + + return [groups copy]; +} + +- (void)runBeforeHooks:(NSString *)compiledName { + [self runBeforeAllHooks:compiledName]; + [self runBeforeEachHooks:compiledName]; +} + +- (void)runBeforeAllHooks:(NSString *)compiledName { + for(SPTExampleGroup *group in [self exampleGroupStackInOrder:SPTExampleGroupOrderOutermostFirst]) { + if (group.ranExampleCount == 0) { + for (id beforeAllBlock in group.beforeAllArray) { + runExampleBlock(beforeAllBlock, [NSString stringWithFormat:@"%@ - before all block", compiledName]); + } + } + } +} + +- (void)runBeforeEachHooks:(NSString *)compiledName { + [self runGlobalBeforeEachHooks:compiledName]; + for (SPTExampleGroup *group in [self exampleGroupStackInOrder:SPTExampleGroupOrderOutermostFirst]) { + for (id beforeEachBlock in group.beforeEachArray) { + runExampleBlock(beforeEachBlock, [NSString stringWithFormat:@"%@ - before each block", compiledName]); + } + } +} + +- (void)runAfterHooks:(NSString *)compiledName { + [self runAfterEachHooks:compiledName]; + [self runAfterAllHooks:compiledName]; +} + +- (void)runAfterEachHooks:(NSString *)compiledName { + for (SPTExampleGroup *group in [self exampleGroupStackInOrder:SPTExampleGroupOrderInnermostFirst]) { + for (id afterEachBlock in group.afterEachArray) { + runExampleBlock(afterEachBlock, [NSString stringWithFormat:@"%@ - after each block", compiledName]); + } + } + [self runGlobalAfterEachHooks:compiledName]; +} + +- (void)runAfterAllHooks:(NSString *)compiledName { + for (SPTExampleGroup *group in [self exampleGroupStackInOrder:SPTExampleGroupOrderInnermostFirst]) { + if (group.ranExampleCount == group.exampleCount) { + for (id afterAllBlock in group.afterAllArray) { + runExampleBlock(afterAllBlock, [NSString stringWithFormat:@"%@ - after all block", compiledName]); + } + } + } +} + +- (BOOL)isFocusedOrHasFocusedAncestor { + SPTExampleGroup *ancestor = self; + while (ancestor != nil) { + if (ancestor.focused) { + return YES; + } else { + ancestor = ancestor.parent; + } + } + + return NO; +} + +- (NSArray *)compileExamplesWithStack:(NSArray *)stack { + BOOL groupIsFocusedOrHasFocusedAncestor = [self isFocusedOrHasFocusedAncestor]; + + NSArray *compiled = @[]; + + for(id child in self.children) { + if ([child isKindOfClass:[SPTExampleGroup class]]) { + SPTExampleGroup *group = child; + NSArray *newStack = [stack arrayByAddingObject:group]; + compiled = [compiled arrayByAddingObjectsFromArray:[group compileExamplesWithStack:newStack]]; + + } else if ([child isKindOfClass:[SPTExample class]]) { + SPTExample *example = child; + NSArray *newStack = [stack arrayByAddingObject:example]; + + NSString *compiledName = [spt_map(newStack, ^id(id obj, NSUInteger idx) { + return [obj name]; + }) componentsJoinedByString:@" "]; + + NSString *testCaseName = [spt_map(newStack, ^id(id obj, NSUInteger idx) { + return spt_underscorize([obj name]); + }) componentsJoinedByString:@"__"]; + + // If example is pending, run only before- and after-all hooks. + // Otherwise, run the example and all before and after hooks. + SPTSpecBlock compiledBlock = example.pending ? ^(SPTSpec *spec){ + @synchronized(self.root) { + [self resetRanExampleCountIfNeeded]; + [self runBeforeAllHooks:compiledName]; + [self incrementRanExampleCount]; + [self runAfterAllHooks:compiledName]; + } + } : ^(SPTSpec *spec) { + @synchronized(self.root) { + [self resetRanExampleCountIfNeeded]; + [self runBeforeHooks:compiledName]; + } + @try { + runExampleBlock(example.block, compiledName); + } + @catch(NSException *e) { + [spec spt_handleException:e]; + } + @finally { + @synchronized(self.root) { + [self incrementRanExampleCount]; + [self runAfterHooks:compiledName]; + } + } + }; + SPTCompiledExample *compiledExample = [[SPTCompiledExample alloc] initWithName:compiledName testCaseName:testCaseName block:compiledBlock pending:example.pending focused:(groupIsFocusedOrHasFocusedAncestor || example.focused)]; + compiled = [compiled arrayByAddingObject:compiledExample]; + } + } + return compiled; +} + +@end
diff --git a/Specta/Specta/SPTExcludeGlobalBeforeAfterEach.h b/Specta/Specta/SPTExcludeGlobalBeforeAfterEach.h new file mode 100644 index 0000000..9581f0b --- /dev/null +++ b/Specta/Specta/SPTExcludeGlobalBeforeAfterEach.h
@@ -0,0 +1,10 @@ +/* + * Copyright (c) 2015 Specta Team. All rights reserved. + */ +#import <Foundation/Foundation.h> + +// This protocol was used for blacklisting classes for global beforeEach and afterEach blocks. +// Now, instead, classes are whitelisted by implementing the SPTGlobalBeforeAfterEach protocol. +__deprecated_msg("Please whitelist classes instead with the SPTGlobalBeforeAfterEach protocol") +@protocol SPTExcludeGlobalBeforeAfterEach <NSObject> +@end
diff --git a/Specta/Specta/SPTGlobalBeforeAfterEach.h b/Specta/Specta/SPTGlobalBeforeAfterEach.h new file mode 100644 index 0000000..490359d --- /dev/null +++ b/Specta/Specta/SPTGlobalBeforeAfterEach.h
@@ -0,0 +1,15 @@ +/* + * Copyright (c) 2015 Specta Team. All rights reserved. + */ +#import <Foundation/Foundation.h> + +// This protocol is used for whitelisting classes for global beforeEach and afterEach blocks. +// If you want a class to participate in those just add this protocol to a category and it will be +// included. +@protocol SPTGlobalBeforeAfterEach <NSObject> + +@optional ++ (void)beforeEach; ++ (void)afterEach; + +@end
diff --git a/Specta/Specta/SPTSharedExampleGroups.h b/Specta/Specta/SPTSharedExampleGroups.h new file mode 100644 index 0000000..090acba --- /dev/null +++ b/Specta/Specta/SPTSharedExampleGroups.h
@@ -0,0 +1,17 @@ +#import <XCTest/XCTest.h> +#import <Specta/XCTestCase+Specta.h> +#import <Specta/SpectaTypes.h> + +@class _XCTestCaseImplementation; + +@class SPTExampleGroup; + +@interface SPTSharedExampleGroups : XCTestCase + ++ (void)addSharedExampleGroupWithName:(NSString *)name block:(SPTDictionaryBlock)block exampleGroup:(SPTExampleGroup *)exampleGroup; ++ (SPTDictionaryBlock)sharedExampleGroupWithName:(NSString *)name exampleGroup:(SPTExampleGroup *)exampleGroup; + +- (void)sharedExampleGroups; + +@end +
diff --git a/Specta/Specta/SPTSharedExampleGroups.m b/Specta/Specta/SPTSharedExampleGroups.m new file mode 100644 index 0000000..35b405c --- /dev/null +++ b/Specta/Specta/SPTSharedExampleGroups.m
@@ -0,0 +1,74 @@ +#import "SPTSharedExampleGroups.h" +#import "SPTExampleGroup.h" +#import "SPTSpec.h" +#import "SpectaUtility.h" +#import <objc/runtime.h> + +NSMutableDictionary *globalSharedExampleGroups = nil; +BOOL initialized = NO; + +@implementation SPTSharedExampleGroups + ++ (void)initialize { + Class SPTSharedExampleGroupsClass = [SPTSharedExampleGroups class]; + if ([self class] == SPTSharedExampleGroupsClass) { + if (!initialized) { + initialized = YES; + globalSharedExampleGroups = [[NSMutableDictionary alloc] init]; + + Class *classes = NULL; + int numClasses = objc_getClassList(NULL, 0); + + if (numClasses > 0) { + classes = (Class *)malloc(sizeof(Class) * numClasses); + numClasses = objc_getClassList(classes, numClasses); + + Class klass, superClass; + for(uint i = 0; i < numClasses; i++) { + klass = classes[i]; + superClass = class_getSuperclass(klass); + if (superClass == SPTSharedExampleGroupsClass) { + [[[klass alloc] init] sharedExampleGroups]; + } + } + + free(classes); + } + } + } +} + ++ (void)addSharedExampleGroupWithName:(NSString *)name block:(SPTDictionaryBlock)block exampleGroup:(SPTExampleGroup *)exampleGroup { + (exampleGroup == nil ? globalSharedExampleGroups : exampleGroup.sharedExamples)[name] = [block copy]; +} + ++ (SPTDictionaryBlock)sharedExampleGroupWithName:(NSString *)name exampleGroup:(SPTExampleGroup *)exampleGroup { + SPTDictionaryBlock sharedExampleGroup = nil; + while (exampleGroup != nil) { + if ((sharedExampleGroup = exampleGroup.sharedExamples[name])) { + return sharedExampleGroup; + } + exampleGroup = exampleGroup.parent; + } + return globalSharedExampleGroups[name]; +} + +- (void)sharedExampleGroups {} + +- (void)spt_handleException:(NSException *)exception { + [SPTCurrentSpec spt_handleException:exception]; +} + +- (void)recordFailureWithDescription:(NSString *)description inFile:(NSString *)filename atLine:(NSUInteger)lineNumber expected:(BOOL)expected { + [SPTCurrentSpec recordFailureWithDescription:description inFile:filename atLine:lineNumber expected:expected]; +} + +- (void)_recordUnexpectedFailureWithDescription:(NSString *)description exception:(NSException *)exception { + [SPTCurrentSpec _recordUnexpectedFailureWithDescription:description exception:exception]; +} + +- (_XCTestCaseImplementation *)internalImplementation { + return [SPTCurrentSpec internalImplementation]; +} + +@end
diff --git a/Specta/Specta/SPTSpec.h b/Specta/Specta/SPTSpec.h new file mode 100644 index 0000000..ada4ea2 --- /dev/null +++ b/Specta/Specta/SPTSpec.h
@@ -0,0 +1,28 @@ +#import <XCTest/XCTest.h> +#import <Specta/XCTestCase+Specta.h> + +@class + SPTTestSuite +, SPTCompiledExample +; + +@interface SPTSpec : XCTestCase + +@property (strong) XCTestCaseRun *spt_run; +@property (nonatomic) BOOL spt_pending; +@property (nonatomic) BOOL spt_skipped; + ++ (BOOL)spt_isDisabled; ++ (void)spt_setDisabled:(BOOL)disabled; ++ (BOOL)spt_focusedExamplesExist; ++ (SEL)spt_convertToTestMethod:(SPTCompiledExample *)example; ++ (SPTTestSuite *)spt_testSuite; ++ (void)spt_setCurrentTestSuite; ++ (void)spt_unsetCurrentTestSuite; ++ (void)spt_setCurrentTestSuiteFileName:(NSString *)fileName lineNumber:(NSUInteger)lineNumber; + +- (void)spec; +- (BOOL)spt_shouldRunExample:(SPTCompiledExample *)example; +- (void)spt_runExample:(SPTCompiledExample *)example; + +@end
diff --git a/Specta/Specta/SPTSpec.m b/Specta/Specta/SPTSpec.m new file mode 100644 index 0000000..35ad313 --- /dev/null +++ b/Specta/Specta/SPTSpec.m
@@ -0,0 +1,210 @@ +#import "SPTSpec.h" +#import "SPTTestSuite.h" +#import "SPTCompiledExample.h" +#import "SPTSharedExampleGroups.h" +#import "SpectaUtility.h" +#import <objc/runtime.h> +#import "XCTest+Private.h" + +@implementation SPTSpec + ++ (void)initialize { + [SPTSharedExampleGroups initialize]; + SPTTestSuite *testSuite = [[SPTTestSuite alloc] init]; + SPTSpec *spec = [[[self class] alloc] init]; + NSString *specName = NSStringFromClass([self class]); + objc_setAssociatedObject(self, "spt_testSuite", testSuite, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [self spt_setCurrentTestSuite]; + @try { + [spec spec]; + } + @catch (NSException *exception) { + fprintf(stderr, "%s: An exception has occured outside of tests, aborting.\n\n%s (%s) \n", [specName UTF8String], [[exception name] UTF8String], [[exception reason] UTF8String]); + if ([exception respondsToSelector:@selector(callStackSymbols)]) { + NSArray *callStackSymbols = [exception callStackSymbols]; + if (callStackSymbols) { + NSString *callStack = [NSString stringWithFormat:@"\n Call Stack:\n %@\n", [callStackSymbols componentsJoinedByString:@"\n "]]; + fprintf(stderr, "%s", [callStack UTF8String]); + } + } + exit(1); + } + @finally { + [self spt_unsetCurrentTestSuite]; + } + [testSuite compile]; + [[self class] testInvocations]; + [super initialize]; +} + ++ (SPTTestSuite *)spt_testSuite { + return objc_getAssociatedObject(self, "spt_testSuite"); +} + ++ (BOOL)spt_isDisabled { + return [self spt_testSuite].disabled; +} + ++ (void)spt_setDisabled:(BOOL)disabled { + [self spt_testSuite].disabled = disabled; +} + ++ (NSArray *)spt_allSpecClasses { + static NSArray *allSpecClasses = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + NSMutableArray *specClasses = [[NSMutableArray alloc] init]; + + int numberOfClasses = objc_getClassList(NULL, 0); + if (numberOfClasses > 0) { + Class *classes = (Class *)malloc(sizeof(Class) * numberOfClasses); + numberOfClasses = objc_getClassList(classes, numberOfClasses); + + for (int classIndex = 0; classIndex < numberOfClasses; classIndex++) { + Class aClass = classes[classIndex]; + if (spt_isSpecClass(aClass)) { + [specClasses addObject:aClass]; + } + } + + free(classes); + } + + allSpecClasses = [specClasses copy]; + }); + + return allSpecClasses; +} + ++ (BOOL)spt_focusedExamplesExist { + for (Class specClass in [self spt_allSpecClasses]) { + SPTTestSuite *testSuite = [specClass spt_testSuite]; + if (testSuite.disabled == NO && [testSuite hasFocusedExamples]) { + return YES; + } + } + + return NO; +} + ++ (SEL)spt_convertToTestMethod:(SPTCompiledExample *)example { + @synchronized(example) { + if (!example.testMethodSelector) { + IMP imp = imp_implementationWithBlock(^(SPTSpec *self) { + [self spt_runExample:example]; + }); + + SEL sel; + unsigned int i = 0; + + do { + i++; + if (i == 1) { + sel = NSSelectorFromString([NSString stringWithFormat:@"test_%@", example.testCaseName]); + } else { + sel = NSSelectorFromString([NSString stringWithFormat:@"test_%@_%u", example.testCaseName, i]); + } + } while([self instancesRespondToSelector:sel]); + + class_addMethod(self, sel, imp, "@@:"); + example.testMethodSelector = sel; + } + } + + return example.testMethodSelector; +} + ++ (void)spt_setCurrentTestSuite { + SPTTestSuite *testSuite = [self spt_testSuite]; + [[NSThread currentThread] threadDictionary][spt_kCurrentTestSuiteKey] = testSuite; +} + ++ (void)spt_unsetCurrentTestSuite { + [[[NSThread currentThread] threadDictionary] removeObjectForKey:spt_kCurrentTestSuiteKey]; +} + ++ (void)spt_setCurrentTestSuiteFileName:(NSString *)fileName lineNumber:(NSUInteger)lineNumber { + SPTTestSuite *testSuite = [self spt_testSuite]; + testSuite.fileName = fileName; + testSuite.lineNumber = lineNumber; +} + +- (void)spec {} + +- (BOOL)spt_shouldRunExample:(SPTCompiledExample *)example { + return [[self class] spt_isDisabled] == NO && + (example.focused || [[self class] spt_focusedExamplesExist] == NO); +} + +- (void)spt_runExample:(SPTCompiledExample *)example { + [[NSThread currentThread] threadDictionary][spt_kCurrentSpecKey] = self; + + if ([self spt_shouldRunExample:example]) { + self.spt_pending = example.pending; + example.block(self); + } else if (!example.pending) { + self.spt_skipped = YES; + } + + [[[NSThread currentThread] threadDictionary] removeObjectForKey:spt_kCurrentSpecKey]; +} + +#pragma mark - XCTestCase overrides + ++ (NSArray *)testInvocations { + NSArray *compiledExamples = [self spt_testSuite].compiledExamples; + [NSMutableArray arrayWithCapacity:[compiledExamples count]]; + + NSMutableSet *addedSelectors = [NSMutableSet setWithCapacity:[compiledExamples count]]; + NSMutableArray *selectors = [NSMutableArray arrayWithCapacity:[compiledExamples count]]; + + // dynamically generate test methods with compiled examples + for (SPTCompiledExample *example in compiledExamples) { + SEL sel = [self spt_convertToTestMethod:example]; + NSString *selName = NSStringFromSelector(sel); + [selectors addObject: selName]; + [addedSelectors addObject: selName]; + } + + // look for any other test methods that may be present in class. + unsigned int n; + Method *imethods = class_copyMethodList(self, &n); + + for (NSUInteger i = 0; i < n; i++) { + struct objc_method_description *desc = method_getDescription(imethods[i]); + + char *types = desc->types; + SEL sel = desc->name; + NSString *selName = NSStringFromSelector(sel); + + if (strcmp(types, "@@:") == 0 && [selName hasPrefix:@"test"] && ![addedSelectors containsObject:selName]) { + [selectors addObject:NSStringFromSelector(sel)]; + } + } + + free(imethods); + + // create invocations from test method selectors + NSMutableArray *invocations = [NSMutableArray arrayWithCapacity:[selectors count]]; + for (NSString *selName in selectors) { + SEL sel = NSSelectorFromString(selName); + NSInvocation *inv = [NSInvocation invocationWithMethodSignature:[self instanceMethodSignatureForSelector:sel]]; + [inv setSelector:sel]; + [invocations addObject:inv]; + } + + return spt_shuffle(invocations); +} + +- (void)recordFailureWithDescription:(NSString *)description inFile:(NSString *)filename atLine:(NSUInteger)lineNumber expected:(BOOL)expected { + SPTSpec *currentSpec = SPTCurrentSpec; + [currentSpec.spt_run recordFailureWithDescription:description inFile:filename atLine:lineNumber expected:expected]; +} + +- (void)performTest:(XCTestRun *)run { + self.spt_run = (XCTestCaseRun *)run; + [super performTest:run]; +} + +@end
diff --git a/Specta/Specta/SPTTestSuite.h b/Specta/Specta/SPTTestSuite.h new file mode 100644 index 0000000..9bd9016 --- /dev/null +++ b/Specta/Specta/SPTTestSuite.h
@@ -0,0 +1,21 @@ +#import <Foundation/Foundation.h> + +@class + SPTExample +, SPTExampleGroup +; + +@interface SPTTestSuite : NSObject + +@property (nonatomic, strong) SPTExampleGroup *rootGroup; +@property (nonatomic, strong) NSMutableArray *groupStack; +@property (nonatomic, strong) NSArray *compiledExamples; +@property (nonatomic, copy) NSString *fileName; +@property (nonatomic) NSUInteger lineNumber; +@property (nonatomic, getter = isDisabled) BOOL disabled; +@property (nonatomic) BOOL hasFocusedExamples; + +- (SPTExampleGroup *)currentGroup; +- (void)compile; + +@end
diff --git a/Specta/Specta/SPTTestSuite.m b/Specta/Specta/SPTTestSuite.m new file mode 100644 index 0000000..7053edd --- /dev/null +++ b/Specta/Specta/SPTTestSuite.m
@@ -0,0 +1,31 @@ +#import "SPTTestSuite.h" +#import "SPTExampleGroup.h" +#import "SPTCompiledExample.h" + +@implementation SPTTestSuite + +- (id)init { + self = [super init]; + if (self) { + self.rootGroup = [[SPTExampleGroup alloc] init]; + self.rootGroup.root = self.rootGroup; + self.groupStack = [NSMutableArray arrayWithObject:self.rootGroup]; + } + return self; +} + +- (SPTExampleGroup *)currentGroup { + return [self.groupStack lastObject]; +} + +- (void)compile { + self.compiledExamples = [self.rootGroup compileExamplesWithStack:@[]]; + for (SPTCompiledExample *example in self.compiledExamples) { + if (example.focused) { + self.hasFocusedExamples = YES; + break; + } + } +} + +@end
diff --git a/Specta/Specta/Specta.h b/Specta/Specta/Specta.h new file mode 100644 index 0000000..dda17f9 --- /dev/null +++ b/Specta/Specta/Specta.h
@@ -0,0 +1,14 @@ +#import <Foundation/Foundation.h> +#import <XCTest/XCTest.h> + +//! Project version number for Specta. +FOUNDATION_EXPORT double SpectaVersionNumber; + +//! Project version string for Specta. +FOUNDATION_EXPORT const unsigned char SpectaVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import <Specta/PublicHeader.h> + +#import <Specta/SpectaDSL.h> +#import <Specta/SPTSpec.h> +#import <Specta/SPTSharedExampleGroups.h>
diff --git a/Specta/Specta/SpectaDSL.h b/Specta/Specta/SpectaDSL.h new file mode 100644 index 0000000..284d4f5 --- /dev/null +++ b/Specta/Specta/SpectaDSL.h
@@ -0,0 +1,90 @@ +#import <XCTest/XCTest.h> + +#define SpecBegin(name) _SPTSpecBegin(name, __FILE__, __LINE__) +#define SpecEnd _SPTSpecEnd + +#define SharedExamplesBegin(name) _SPTSharedExampleGroupsBegin(name) +#define SharedExamplesEnd _SPTSharedExampleGroupsEnd +#define SharedExampleGroupsBegin(name) _SPTSharedExampleGroupsBegin(name) +#define SharedExampleGroupsEnd _SPTSharedExampleGroupsEnd + +typedef void (^DoneCallback)(void); + +OBJC_EXTERN void describe(NSString *name, void (^block)()); +OBJC_EXTERN void fdescribe(NSString *name, void (^block)()); + +OBJC_EXTERN void context(NSString *name, void (^block)()); +OBJC_EXTERN void fcontext(NSString *name, void (^block)()); + +OBJC_EXTERN void it(NSString *name, void (^block)()); +OBJC_EXTERN void fit(NSString *name, void (^block)()); + +OBJC_EXTERN void example(NSString *name, void (^block)()); +OBJC_EXTERN void fexample(NSString *name, void (^block)()); + +OBJC_EXTERN void specify(NSString *name, void (^block)()); +OBJC_EXTERN void fspecify(NSString *name, void (^block)()); + +#define pending(...) spt_pending_(__VA_ARGS__, nil) +#define xdescribe(...) spt_pending_(__VA_ARGS__, nil) +#define xcontext(...) spt_pending_(__VA_ARGS__, nil) +#define xexample(...) spt_pending_(__VA_ARGS__, nil) +#define xit(...) spt_pending_(__VA_ARGS__, nil) +#define xspecify(...) spt_pending_(__VA_ARGS__, nil) + +OBJC_EXTERN void beforeAll(void (^block)()); +OBJC_EXTERN void afterAll(void (^block)()); + +OBJC_EXTERN void beforeEach(void (^block)()); +OBJC_EXTERN void afterEach(void (^block)()); + +OBJC_EXTERN void before(void (^block)()); +OBJC_EXTERN void after(void (^block)()); + +OBJC_EXTERN void sharedExamplesFor(NSString *name, void (^block)(NSDictionary *data)); +OBJC_EXTERN void sharedExamples(NSString *name, void (^block)(NSDictionary *data)); + +#define itShouldBehaveLike(...) spt_itShouldBehaveLike_(@(__FILE__), __LINE__, __VA_ARGS__) +#define itBehavesLike(...) spt_itShouldBehaveLike_(@(__FILE__), __LINE__, __VA_ARGS__) + +OBJC_EXTERN void waitUntil(void (^block)(DoneCallback done)); +/** + * Runs the @c block and waits until the @c done block is called or the + * @c timeout has passed. + * + * @param timeout timeout for this @c block only; does not affect the global + * timeout, as @c setAsyncSpecTimeout() does. + * @param ^block runs test code + */ +OBJC_EXTERN void waitUntilTimeout(NSTimeInterval timeout, void (^block)(DoneCallback done)); + +OBJC_EXTERN void setAsyncSpecTimeout(NSTimeInterval timeout); + +// ---------------------------------------------------------------------------- + +#define _SPTSpecBegin(name, file, line) \ +@interface name##Spec : SPTSpec \ +@end \ +@implementation name##Spec \ +- (void)spec { \ + [[self class] spt_setCurrentTestSuiteFileName:(@(file)) lineNumber:(line)]; + +#define _SPTSpecEnd \ +} \ +@end + +#define _SPTSharedExampleGroupsBegin(name) \ +@interface name##SharedExampleGroups : SPTSharedExampleGroups \ +@end \ +@implementation name##SharedExampleGroups \ +- (void)sharedExampleGroups { + +#define _SPTSharedExampleGroupsEnd \ +} \ +@end + +OBJC_EXTERN void spt_it_(NSString *name, NSString *fileName, NSUInteger lineNumber, void (^block)()); +OBJC_EXTERN void spt_fit_(NSString *name, NSString *fileName, NSUInteger lineNumber, void (^block)()); +OBJC_EXTERN void spt_pending_(NSString *name, ...); +OBJC_EXTERN void spt_itShouldBehaveLike_(NSString *fileName, NSUInteger lineNumber, NSString *name, id dictionaryOrBlock); +OBJC_EXTERN void spt_itShouldBehaveLike_block(NSString *fileName, NSUInteger lineNumber, NSString *name, NSDictionary *(^block)());
diff --git a/Specta/Specta/SpectaDSL.m b/Specta/Specta/SpectaDSL.m new file mode 100644 index 0000000..10edcd5 --- /dev/null +++ b/Specta/Specta/SpectaDSL.m
@@ -0,0 +1,189 @@ +#import "SpectaDSL.h" +#import "SpectaTypes.h" +#import "SpectaUtility.h" +#import "SPTTestSuite.h" +#import "SPTExampleGroup.h" +#import "SPTSharedExampleGroups.h" +#import "SPTSpec.h" +#import "SPTCallSite.h" +#import <libkern/OSAtomic.h> + +static NSTimeInterval asyncSpecTimeout = 10.0; + +static void spt_defineItBlock(NSString *name, NSString *fileName, NSUInteger lineNumber, BOOL focused, void (^block)()) { + SPTReturnUnlessBlockOrNil(block); + SPTCallSite *site = nil; + if (lineNumber && fileName) { + site = [SPTCallSite callSiteWithFile:fileName line:lineNumber]; + } + [SPTCurrentGroup addExampleWithName:name callSite:site focused:focused block:block]; +} + +static void spt_defineDescribeBlock(NSString *name, BOOL focused, void (^block)()) { + if (block) { + [SPTGroupStack addObject:[SPTCurrentGroup addExampleGroupWithName:name focused:focused]]; + block(); + [SPTGroupStack removeLastObject]; + } else { + spt_defineItBlock(name, nil, 0, focused, nil); + } +} + +void spt_it_(NSString *name, NSString *fileName, NSUInteger lineNumber, void (^block)()) { + spt_defineItBlock(name, fileName, lineNumber, NO, block); +} + +void spt_fit_(NSString *name, NSString *fileName, NSUInteger lineNumber, void (^block)()) { + spt_defineItBlock(name, fileName, lineNumber, YES, block); +} + +void spt_pending_(NSString *name, ...) { + spt_defineItBlock(name, nil, 0, NO, nil); +} + +void spt_itShouldBehaveLike_(NSString *fileName, NSUInteger lineNumber, NSString *name, id dictionaryOrBlock) { + SPTDictionaryBlock block = [SPTSharedExampleGroups sharedExampleGroupWithName:name exampleGroup:SPTCurrentGroup]; + if (block) { + if (SPTIsBlock(dictionaryOrBlock)) { + id (^dataBlock)(void) = [dictionaryOrBlock copy]; + + describe(name, ^{ + __block NSMutableDictionary *dataDict = [[NSMutableDictionary alloc] init]; + + beforeEach(^{ + NSDictionary *blockData = dataBlock(); + [dataDict removeAllObjects]; + [dataDict addEntriesFromDictionary:blockData]; + }); + + block(dataDict); + + afterAll(^{ + dataDict = nil; + }); + }); + } else { + NSDictionary *data = dictionaryOrBlock; + + describe(name, ^{ + block(data); + }); + } + } else { + SPTSpec *currentSpec = SPTCurrentSpec; + if (currentSpec) { + [currentSpec recordFailureWithDescription:@"itShouldBehaveLike should not be invoked inside an example block!" inFile:fileName atLine:lineNumber expected:NO]; + } else { + it(name, ^{ + [SPTCurrentSpec recordFailureWithDescription:[NSString stringWithFormat:@"Shared example group \"%@\" does not exist.", name] inFile:fileName atLine:lineNumber expected:NO]; + }); + } + } +} + +void spt_itShouldBehaveLike_block(NSString *fileName, NSUInteger lineNumber, NSString *name, NSDictionary *(^block)()) { + spt_itShouldBehaveLike_(fileName, lineNumber, name, (id)block); +} + +void describe(NSString *name, void (^block)()) { + spt_defineDescribeBlock(name, NO, block); +} + +void fdescribe(NSString *name, void (^block)()) { + spt_defineDescribeBlock(name, YES, block); +} + +void context(NSString *name, void (^block)()) { + describe(name, block); +} + +void fcontext(NSString *name, void (^block)()) { + fdescribe(name, block); +} + +void it(NSString *name, void (^block)()) { + spt_defineItBlock(name, nil, 0, NO, block); +} + +void fit(NSString *name, void (^block)()) { + spt_defineItBlock(name, nil, 0, YES, block); +} + +void example(NSString *name, void (^block)()) { + it(name, block); +} + +void fexample(NSString *name, void (^block)()) { + fit(name, block); +} + +void specify(NSString *name, void (^block)()) { + it(name, block); +} + +void fspecify(NSString *name, void (^block)()) { + fit(name, block); +} + +void beforeAll(void (^block)()) { + SPTReturnUnlessBlockOrNil(block); + [SPTCurrentGroup addBeforeAllBlock:block]; +} + +void afterAll(void (^block)()) { + SPTReturnUnlessBlockOrNil(block); + [SPTCurrentGroup addAfterAllBlock:block]; +} + +void beforeEach(void (^block)()) { + SPTReturnUnlessBlockOrNil(block); + [SPTCurrentGroup addBeforeEachBlock:block]; +} + +void afterEach(void (^block)()) { + SPTReturnUnlessBlockOrNil(block); + [SPTCurrentGroup addAfterEachBlock:block]; +} + +void before(void (^block)()) { + beforeEach(block); +} + +void after(void (^block)()) { + afterEach(block); +} + +void sharedExamplesFor(NSString *name, void (^block)(NSDictionary *data)) { + [SPTSharedExampleGroups addSharedExampleGroupWithName:name block:block exampleGroup:SPTCurrentGroup]; +} + +void sharedExamples(NSString *name, void (^block)(NSDictionary *data)) { + sharedExamplesFor(name, block); +} + +void waitUntil(void (^block)(DoneCallback done)) { + waitUntilTimeout(asyncSpecTimeout, block); +} + +void waitUntilTimeout(NSTimeInterval timeout, void (^block)(DoneCallback done)) { + __block uint32_t complete = 0; + dispatch_async(dispatch_get_main_queue(), ^{ + block(^{ + OSAtomicOr32Barrier(1, &complete); + }); + }); + NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:timeout]; + while (!complete && [timeoutDate timeIntervalSinceNow] > 0) { + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]]; + } + if (!complete) { + NSString *message = [NSString stringWithFormat:@"failed to invoke done() callback before timeout (%f seconds)", timeout]; + SPTSpec *currentSpec = SPTCurrentSpec; + SPTTestSuite *testSuite = [[currentSpec class] spt_testSuite]; + [currentSpec recordFailureWithDescription:message inFile:testSuite.fileName atLine:testSuite.lineNumber expected:YES]; + } +} + +void setAsyncSpecTimeout(NSTimeInterval timeout) { + asyncSpecTimeout = timeout; +}
diff --git a/Specta/Specta/SpectaTypes.h b/Specta/Specta/SpectaTypes.h new file mode 100644 index 0000000..f1f0ae3 --- /dev/null +++ b/Specta/Specta/SpectaTypes.h
@@ -0,0 +1,5 @@ +@class SPTSpec; + +typedef void (^SPTVoidBlock)(); +typedef void (^SPTSpecBlock)(SPTSpec *spec); +typedef void (^SPTDictionaryBlock)(NSDictionary *dictionary);
diff --git a/Specta/Specta/SpectaUtility.h b/Specta/Specta/SpectaUtility.h new file mode 100644 index 0000000..a3a8f07 --- /dev/null +++ b/Specta/Specta/SpectaUtility.h
@@ -0,0 +1,18 @@ +#import <Foundation/Foundation.h> + +extern NSString * const spt_kCurrentTestSuiteKey; +extern NSString * const spt_kCurrentSpecKey; + +#define SPTCurrentTestSuite [[NSThread mainThread] threadDictionary][spt_kCurrentTestSuiteKey] +#define SPTCurrentSpec [[NSThread mainThread] threadDictionary][spt_kCurrentSpecKey] +#define SPTCurrentGroup [SPTCurrentTestSuite currentGroup] +#define SPTGroupStack [SPTCurrentTestSuite groupStack] + +#define SPTReturnUnlessBlockOrNil(block) if ((block) && !SPTIsBlock((block))) return; +#define SPTIsBlock(obj) [(obj) isKindOfClass:NSClassFromString(@"NSBlock")] + +BOOL spt_isSpecClass(Class aClass); +NSString *spt_underscorize(NSString *string); +NSArray *spt_map(NSArray *array, id (^block)(id obj, NSUInteger idx)); +NSArray *spt_shuffle(NSArray *array); +unsigned int spt_seed();
diff --git a/Specta/Specta/SpectaUtility.m b/Specta/Specta/SpectaUtility.m new file mode 100644 index 0000000..9b2ee80 --- /dev/null +++ b/Specta/Specta/SpectaUtility.m
@@ -0,0 +1,79 @@ +#import "SpectaUtility.h" +#import "SPTSpec.h" +#import <objc/runtime.h> + +NSString * const spt_kCurrentTestSuiteKey = @"SPTCurrentTestSuite"; +NSString * const spt_kCurrentSpecKey = @"SPTCurrentSpec"; + +static unsigned int seed = 0; + +BOOL spt_isSpecClass(Class aClass) { + Class superclass = class_getSuperclass(aClass); + while (superclass != Nil) { + if (superclass == [SPTSpec class]) { + return YES; + } else { + superclass = class_getSuperclass(superclass); + } + } + return NO; +} + +NSString *spt_underscorize(NSString *string) { + static NSMutableCharacterSet *invalidCharSet; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + invalidCharSet = [[NSMutableCharacterSet alloc] init]; + [invalidCharSet formUnionWithCharacterSet:[NSCharacterSet controlCharacterSet]]; + [invalidCharSet formUnionWithCharacterSet:[NSCharacterSet illegalCharacterSet]]; + [invalidCharSet formUnionWithCharacterSet:[NSCharacterSet newlineCharacterSet]]; + [invalidCharSet formUnionWithCharacterSet:[NSCharacterSet nonBaseCharacterSet]]; + [invalidCharSet formUnionWithCharacterSet:[NSCharacterSet punctuationCharacterSet]]; + [invalidCharSet formUnionWithCharacterSet:[NSCharacterSet symbolCharacterSet]]; + }); + NSString *stripped = [string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + stripped = [[stripped componentsSeparatedByCharactersInSet:invalidCharSet] componentsJoinedByString:@""]; + + NSArray *components = [stripped componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + stripped = [[components objectsAtIndexes:[components indexesOfObjectsPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) { + return ![obj isEqualToString:@""]; + }]] componentsJoinedByString:@"_"]; + return stripped; +} + +NSArray *spt_map(NSArray *array, id (^block)(id obj, NSUInteger idx)) { + NSMutableArray *mapped = [NSMutableArray arrayWithCapacity:[array count]]; + [array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + [mapped addObject:block(obj, idx)]; + }]; + return mapped; +} + +NSArray *spt_shuffle(NSArray *array) { + if (![[[[NSProcessInfo processInfo] environment] objectForKey:@"SPECTA_SHUFFLE"] isEqualToString:@"1"]) { + return array; + } + spt_seed(); + NSMutableArray *shuffled = [array mutableCopy]; + NSUInteger count = [shuffled count]; + for (NSUInteger i = 0; i < count; i++) { + NSUInteger r = random() % count; + [shuffled exchangeObjectAtIndex:i withObjectAtIndex:r]; + } + return shuffled; +} + +unsigned int spt_seed() { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSString *envSeed = [[[NSProcessInfo processInfo] environment] objectForKey:@"SPECTA_SEED"]; + if (envSeed) { + sscanf([envSeed UTF8String], "%u", &seed); + } else { + seed = arc4random(); + } + srandom(seed); + printf("Test Seed: %u\n", seed); + }); + return seed; +}
diff --git a/Specta/Specta/XCTest+Private.h b/Specta/Specta/XCTest+Private.h new file mode 100644 index 0000000..0b83aeb --- /dev/null +++ b/Specta/Specta/XCTest+Private.h
@@ -0,0 +1,40 @@ +#import <XCTest/XCTest.h> + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 90000 || __MAC_OS_X_VERSION_MAX_ALLOWED >= 101100 + +@interface XCTestObservationCenter (SPTTestSuspention) + +- (void)_suspendObservationForBlock:(void (^)(void))block; + +@end + +#else + +@interface XCTestObservationCenter : NSObject + ++ (id)sharedObservationCenter; +- (void)_suspendObservationForBlock:(void (^)(void))block; + +@end + +#endif + +@protocol XCTestObservation <NSObject> +@end + +@interface _XCTestDriverTestObserver : NSObject <XCTestObservation> + +- (void)stopObserving; +- (void)startObserving; + +@end + +@interface _XCTestCaseImplementation : NSObject +@end + +@interface XCTestCase () + +- (_XCTestCaseImplementation *)internalImplementation; +- (void)_recordUnexpectedFailureWithDescription:(NSString *)description exception:(NSException *)exception; + +@end
diff --git a/Specta/Specta/XCTestCase+Specta.h b/Specta/Specta/XCTestCase+Specta.h new file mode 100644 index 0000000..9ca8f8a --- /dev/null +++ b/Specta/Specta/XCTestCase+Specta.h
@@ -0,0 +1,7 @@ +#import <XCTest/XCTest.h> + +@interface XCTestCase (Specta) + +- (void)spt_handleException:(NSException *)exception; + +@end
diff --git a/Specta/Specta/XCTestCase+Specta.m b/Specta/Specta/XCTestCase+Specta.m new file mode 100644 index 0000000..4c503ee --- /dev/null +++ b/Specta/Specta/XCTestCase+Specta.m
@@ -0,0 +1,65 @@ +#import <objc/runtime.h> +#import "XCTestCase+Specta.h" +#import "SPTSpec.h" +#import "SPTExample.h" +#import "SPTSharedExampleGroups.h" +#import "SpectaUtility.h" +#import "XCTest+Private.h" + +@interface XCTestCase (xct_allSubclasses) + +- (NSArray *)allSubclasses; +- (void)_dequeueFailures; + +@end + +@implementation XCTestCase (Specta) + ++ (void)load { + Method allSubclasses = class_getClassMethod(self, @selector(allSubclasses)); + Method allSubclasses_swizzle = class_getClassMethod(self , @selector(spt_allSubclasses_swizzle)); + method_exchangeImplementations(allSubclasses, allSubclasses_swizzle); + + Method dequeueFailures = class_getInstanceMethod(self, @selector(_dequeueFailures)); + Method dequeueFailures_swizzle = class_getInstanceMethod(self, @selector(spt_dequeueFailures)); + method_exchangeImplementations(dequeueFailures, dequeueFailures_swizzle); +} + ++ (NSArray *)spt_allSubclasses_swizzle { + NSArray *subclasses = [self spt_allSubclasses_swizzle]; // call original + NSMutableArray *filtered = [NSMutableArray arrayWithCapacity:[subclasses count]]; + // exclude SPTSpec base class and all subclasses of SPTSharedExampleGroups + for (id subclass in subclasses) { + if (subclass != [SPTSpec class] && ![subclass isKindOfClass:[SPTSharedExampleGroups class]]) { + [filtered addObject:subclass]; + } + } + return spt_shuffle(filtered); +} + +- (void)spt_dequeueFailures { + void(^dequeueFailures)() = ^() { + [self spt_dequeueFailures]; + }; + + if ([NSThread isMainThread]) { + dequeueFailures(); + } else { + dispatch_sync(dispatch_get_main_queue(), dequeueFailures); + } +} + +- (void)spt_handleException:(NSException *)exception { + NSString *description = [exception reason]; + if ([exception userInfo]) { + id line = [exception userInfo][@"line"]; + id file = [exception userInfo][@"file"]; + if ([line isKindOfClass:[NSNumber class]] && [file isKindOfClass:[NSString class]]) { + [self recordFailureWithDescription:description inFile:file atLine:[line unsignedIntegerValue] expected:YES]; + return; + } + } + [self _recordUnexpectedFailureWithDescription:description exception:exception]; +} + +@end