init iOS module

This commit is contained in:
pengfei.zhou
2019-07-25 19:26:33 +08:00
parent 40416ff3fd
commit f86e7623a2
211 changed files with 20246 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
/**
A `Behavior` encapsulates a set of examples that can be re-used in several locations using the `itBehavesLike` function with a context instance of the generic type.
*/
open class Behavior<Context> {
open static var name: String { return String(describing: self) }
/**
override this method in your behavior to define a set of reusable examples.
This behaves just like an example group defines using `describe` or `context`--it may contain any number of `beforeEach`
and `afterEach` closures, as well as any number of examples (defined using `it`).
- parameter aContext: A closure that, when evaluated, returns a `Context` instance that provide the information on the subject.
*/
open class func spec(_ aContext: @escaping () -> Context) {}
}

View File

@@ -0,0 +1,45 @@
import Foundation
// `#if swift(>=3.2) && (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE`
// does not work as expected.
#if swift(>=3.2)
#if (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE
@objcMembers
public class _CallsiteBase: NSObject {}
#else
public class _CallsiteBase: NSObject {}
#endif
#else
public class _CallsiteBase: NSObject {}
#endif
/**
An object encapsulating the file and line number at which
a particular example is defined.
*/
final public class Callsite: _CallsiteBase {
/**
The absolute path of the file in which an example is defined.
*/
public let file: String
/**
The line number on which an example is defined.
*/
public let line: UInt
internal init(file: String, line: UInt) {
self.file = file
self.line = line
}
}
extension Callsite {
/**
Returns a boolean indicating whether two Callsite objects are equal.
If two callsites are in the same file and on the same line, they must be equal.
*/
@nonobjc public static func == (lhs: Callsite, rhs: Callsite) -> Bool {
return lhs.file == rhs.file && lhs.line == rhs.line
}
}

View File

@@ -0,0 +1,161 @@
import Foundation
/**
A closure that temporarily exposes a Configuration object within
the scope of the closure.
*/
public typealias QuickConfigurer = (_ configuration: Configuration) -> Void
/**
A closure that, given metadata about an example, returns a boolean value
indicating whether that example should be run.
*/
public typealias ExampleFilter = (_ example: Example) -> Bool
/**
A configuration encapsulates various options you can use
to configure Quick's behavior.
*/
final public class Configuration: NSObject {
internal let exampleHooks = ExampleHooks()
internal let suiteHooks = SuiteHooks()
internal var exclusionFilters: [ExampleFilter] = [ { example in
if let pending = example.filterFlags[Filter.pending] {
return pending
} else {
return false
}
}]
internal var inclusionFilters: [ExampleFilter] = [ { example in
if let focused = example.filterFlags[Filter.focused] {
return focused
} else {
return false
}
}]
/**
Run all examples if none match the configured filters. True by default.
*/
public var runAllWhenEverythingFiltered = true
/**
Registers an inclusion filter.
All examples are filtered using all inclusion filters.
The remaining examples are run. If no examples remain, all examples are run.
- parameter filter: A filter that, given an example, returns a value indicating
whether that example should be included in the examples
that are run.
*/
public func include(_ filter: @escaping ExampleFilter) {
inclusionFilters.append(filter)
}
/**
Registers an exclusion filter.
All examples that remain after being filtered by the inclusion filters are
then filtered via all exclusion filters.
- parameter filter: A filter that, given an example, returns a value indicating
whether that example should be excluded from the examples
that are run.
*/
public func exclude(_ filter: @escaping ExampleFilter) {
exclusionFilters.append(filter)
}
/**
Identical to Quick.Configuration.beforeEach, except the closure is
provided with metadata on the example that the closure is being run
prior to.
*/
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
@objc(beforeEachWithMetadata:)
public func beforeEach(_ closure: @escaping BeforeExampleWithMetadataClosure) {
exampleHooks.appendBefore(closure)
}
#else
public func beforeEach(_ closure: @escaping BeforeExampleWithMetadataClosure) {
exampleHooks.appendBefore(closure)
}
#endif
/**
Like Quick.DSL.beforeEach, this configures Quick to execute the
given closure before each example that is run. The closure
passed to this method is executed before each example Quick runs,
globally across the test suite. You may call this method multiple
times across mulitple +[QuickConfigure configure:] methods in order
to define several closures to run before each example.
Note that, since Quick makes no guarantee as to the order in which
+[QuickConfiguration configure:] methods are evaluated, there is no
guarantee as to the order in which beforeEach closures are evaluated
either. Mulitple beforeEach defined on a single configuration, however,
will be executed in the order they're defined.
- parameter closure: The closure to be executed before each example
in the test suite.
*/
public func beforeEach(_ closure: @escaping BeforeExampleClosure) {
exampleHooks.appendBefore(closure)
}
/**
Identical to Quick.Configuration.afterEach, except the closure
is provided with metadata on the example that the closure is being
run after.
*/
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
@objc(afterEachWithMetadata:)
public func afterEach(_ closure: @escaping AfterExampleWithMetadataClosure) {
exampleHooks.appendAfter(closure)
}
#else
public func afterEach(_ closure: @escaping AfterExampleWithMetadataClosure) {
exampleHooks.appendAfter(closure)
}
#endif
/**
Like Quick.DSL.afterEach, this configures Quick to execute the
given closure after each example that is run. The closure
passed to this method is executed after each example Quick runs,
globally across the test suite. You may call this method multiple
times across mulitple +[QuickConfigure configure:] methods in order
to define several closures to run after each example.
Note that, since Quick makes no guarantee as to the order in which
+[QuickConfiguration configure:] methods are evaluated, there is no
guarantee as to the order in which afterEach closures are evaluated
either. Mulitple afterEach defined on a single configuration, however,
will be executed in the order they're defined.
- parameter closure: The closure to be executed before each example
in the test suite.
*/
public func afterEach(_ closure: @escaping AfterExampleClosure) {
exampleHooks.appendAfter(closure)
}
/**
Like Quick.DSL.beforeSuite, this configures Quick to execute
the given closure prior to any and all examples that are run.
The two methods are functionally equivalent.
*/
public func beforeSuite(_ closure: @escaping BeforeSuiteClosure) {
suiteHooks.appendBefore(closure)
}
/**
Like Quick.DSL.afterSuite, this configures Quick to execute
the given closure after all examples have been run.
The two methods are functionally equivalent.
*/
public func afterSuite(_ closure: @escaping AfterSuiteClosure) {
suiteHooks.appendAfter(closure)
}
}

View File

@@ -0,0 +1,271 @@
/**
Defines a closure to be run prior to any examples in the test suite.
You may define an unlimited number of these closures, but there is no
guarantee as to the order in which they're run.
If the test suite crashes before the first example is run, this closure
will not be executed.
- parameter closure: The closure to be run prior to any examples in the test suite.
*/
public func beforeSuite(_ closure: @escaping BeforeSuiteClosure) {
World.sharedWorld.beforeSuite(closure)
}
/**
Defines a closure to be run after all of the examples in the test suite.
You may define an unlimited number of these closures, but there is no
guarantee as to the order in which they're run.
If the test suite crashes before all examples are run, this closure
will not be executed.
- parameter closure: The closure to be run after all of the examples in the test suite.
*/
public func afterSuite(_ closure: @escaping AfterSuiteClosure) {
World.sharedWorld.afterSuite(closure)
}
/**
Defines a group of shared examples. These examples can be re-used in several locations
by using the `itBehavesLike` function.
- parameter name: The name of the shared example group. This must be unique across all shared example
groups defined in a test suite.
- parameter closure: A closure containing the examples. This behaves just like an example group defined
using `describe` or `context`--the closure may contain any number of `beforeEach`
and `afterEach` closures, as well as any number of examples (defined using `it`).
*/
public func sharedExamples(_ name: String, closure: @escaping () -> Void) {
World.sharedWorld.sharedExamples(name) { _ in closure() }
}
/**
Defines a group of shared examples. These examples can be re-used in several locations
by using the `itBehavesLike` function.
- parameter name: The name of the shared example group. This must be unique across all shared example
groups defined in a test suite.
- parameter closure: A closure containing the examples. This behaves just like an example group defined
using `describe` or `context`--the closure may contain any number of `beforeEach`
and `afterEach` closures, as well as any number of examples (defined using `it`).
The closure takes a SharedExampleContext as an argument. This context is a function
that can be executed to retrieve parameters passed in via an `itBehavesLike` function.
*/
public func sharedExamples(_ name: String, closure: @escaping SharedExampleClosure) {
World.sharedWorld.sharedExamples(name, closure: closure)
}
/**
Defines an example group. Example groups are logical groupings of examples.
Example groups can share setup and teardown code.
- parameter description: An arbitrary string describing the example group.
- parameter closure: A closure that can contain other examples.
- parameter flags: A mapping of string keys to booleans that can be used to filter examples or example groups.
*/
public func describe(_ description: String, flags: FilterFlags = [:], closure: () -> Void) {
World.sharedWorld.describe(description, flags: flags, closure: closure)
}
/**
Defines an example group. Equivalent to `describe`.
*/
public func context(_ description: String, flags: FilterFlags = [:], closure: () -> Void) {
World.sharedWorld.context(description, flags: flags, closure: closure)
}
/**
Defines a closure to be run prior to each example in the current example
group. This closure is not run for pending or otherwise disabled examples.
An example group may contain an unlimited number of beforeEach. They'll be
run in the order they're defined, but you shouldn't rely on that behavior.
- parameter closure: The closure to be run prior to each example.
*/
public func beforeEach(_ closure: @escaping BeforeExampleClosure) {
World.sharedWorld.beforeEach(closure)
}
/**
Identical to Quick.DSL.beforeEach, except the closure is provided with
metadata on the example that the closure is being run prior to.
*/
public func beforeEach(_ closure: @escaping BeforeExampleWithMetadataClosure) {
World.sharedWorld.beforeEach(closure: closure)
}
/**
Defines a closure to be run after each example in the current example
group. This closure is not run for pending or otherwise disabled examples.
An example group may contain an unlimited number of afterEach. They'll be
run in the order they're defined, but you shouldn't rely on that behavior.
- parameter closure: The closure to be run after each example.
*/
public func afterEach(_ closure: @escaping AfterExampleClosure) {
World.sharedWorld.afterEach(closure)
}
/**
Identical to Quick.DSL.afterEach, except the closure is provided with
metadata on the example that the closure is being run after.
*/
public func afterEach(_ closure: @escaping AfterExampleWithMetadataClosure) {
World.sharedWorld.afterEach(closure: closure)
}
/**
Defines an example. Examples use assertions to demonstrate how code should
behave. These are like "tests" in XCTest.
- parameter description: An arbitrary string describing what the example is meant to specify.
- parameter closure: A closure that can contain assertions.
- parameter flags: A mapping of string keys to booleans that can be used to filter examples or example groups.
Empty by default.
- parameter file: The absolute path to the file containing the example. A sensible default is provided.
- parameter line: The line containing the example. A sensible default is provided.
*/
public func it(_ description: String, flags: FilterFlags = [:], file: String = #file, line: UInt = #line, closure: @escaping () -> Void) {
World.sharedWorld.it(description, flags: flags, file: file, line: line, closure: closure)
}
/**
Inserts the examples defined using a `sharedExamples` function into the current example group.
The shared examples are executed at this location, as if they were written out manually.
- parameter name: The name of the shared examples group to be executed. This must be identical to the
name of a shared examples group defined using `sharedExamples`. If there are no shared
examples that match the name given, an exception is thrown and the test suite will crash.
- parameter flags: A mapping of string keys to booleans that can be used to filter examples or example groups.
Empty by default.
- parameter file: The absolute path to the file containing the current example group. A sensible default is provided.
- parameter line: The line containing the current example group. A sensible default is provided.
*/
public func itBehavesLike(_ name: String, flags: FilterFlags = [:], file: String = #file, line: UInt = #line) {
itBehavesLike(name, flags: flags, file: file, line: line, sharedExampleContext: { return [:] })
}
/**
Inserts the examples defined using a `sharedExamples` function into the current example group.
The shared examples are executed at this location, as if they were written out manually.
This function also passes those shared examples a context that can be evaluated to give the shared
examples extra information on the subject of the example.
- parameter name: The name of the shared examples group to be executed. This must be identical to the
name of a shared examples group defined using `sharedExamples`. If there are no shared
examples that match the name given, an exception is thrown and the test suite will crash.
- parameter sharedExampleContext: A closure that, when evaluated, returns key-value pairs that provide the
shared examples with extra information on the subject of the example.
- parameter flags: A mapping of string keys to booleans that can be used to filter examples or example groups.
Empty by default.
- parameter file: The absolute path to the file containing the current example group. A sensible default is provided.
- parameter line: The line containing the current example group. A sensible default is provided.
*/
public func itBehavesLike(_ name: String, flags: FilterFlags = [:], file: String = #file, line: UInt = #line, sharedExampleContext: @escaping SharedExampleContext) {
World.sharedWorld.itBehavesLike(name, sharedExampleContext: sharedExampleContext, flags: flags, file: file, line: line)
}
/**
Inserts the examples defined using a `Behavior` into the current example group.
The shared examples are executed at this location, as if they were written out manually.
This function also passes a strongly-typed context that can be evaluated to give the shared examples extra information on the subject of the example.
- parameter behavior: The type of `Behavior` class defining the example group to be executed.
- parameter context: A closure that, when evaluated, returns an instance of `Behavior`'s context type to provide its example group with extra information on the subject of the example.
- parameter flags: A mapping of string keys to booleans that can be used to filter examples or example groups.
Empty by default.
- parameter file: The absolute path to the file containing the current example group. A sensible default is provided.
- parameter line: The line containing the current example group. A sensible default is provided.
*/
public func itBehavesLike<C>(_ behavior: Behavior<C>.Type, flags: FilterFlags = [:], file: String = #file, line: UInt = #line, context: @escaping () -> C) {
World.sharedWorld.itBehavesLike(behavior, context: context, flags: flags, file: file, line: line)
}
/**
Defines an example or example group that should not be executed. Use `pending` to temporarily disable
examples or groups that should not be run yet.
- parameter description: An arbitrary string describing the example or example group.
- parameter closure: A closure that will not be evaluated.
*/
public func pending(_ description: String, closure: () -> Void) {
World.sharedWorld.pending(description, closure: closure)
}
/**
Use this to quickly mark a `describe` closure as pending.
This disables all examples within the closure.
*/
public func xdescribe(_ description: String, flags: FilterFlags, closure: () -> Void) {
World.sharedWorld.xdescribe(description, flags: flags, closure: closure)
}
/**
Use this to quickly mark a `context` closure as pending.
This disables all examples within the closure.
*/
public func xcontext(_ description: String, flags: FilterFlags, closure: () -> Void) {
xdescribe(description, flags: flags, closure: closure)
}
/**
Use this to quickly mark an `it` closure as pending.
This disables the example and ensures the code within the closure is never run.
*/
public func xit(_ description: String, flags: FilterFlags = [:], file: String = #file, line: UInt = #line, closure: @escaping () -> Void) {
World.sharedWorld.xit(description, flags: flags, file: file, line: line, closure: closure)
}
/**
Use this to quicklu mark an `itBehavesLike` closure as pending.
This disables the example group defined by this behavior and ensures the code within is never run.
*/
public func xitBehavesLike<C>(_ behavior: Behavior<C>.Type, flags: FilterFlags = [:], file: String = #file, line: UInt = #line, context: @escaping () -> C) {
World.sharedWorld.xitBehavesLike(behavior, context: context, flags: flags, file: file, line: line)
}
/**
Use this to quickly focus a `describe` closure, focusing the examples in the closure.
If any examples in the test suite are focused, only those examples are executed.
This trumps any explicitly focused or unfocused examples within the closure--they are all treated as focused.
*/
public func fdescribe(_ description: String, flags: FilterFlags = [:], closure: () -> Void) {
World.sharedWorld.fdescribe(description, flags: flags, closure: closure)
}
/**
Use this to quickly focus a `context` closure. Equivalent to `fdescribe`.
*/
public func fcontext(_ description: String, flags: FilterFlags = [:], closure: () -> Void) {
fdescribe(description, flags: flags, closure: closure)
}
/**
Use this to quickly focus an `it` closure, focusing the example.
If any examples in the test suite are focused, only those examples are executed.
*/
public func fit(_ description: String, flags: FilterFlags = [:], file: String = #file, line: UInt = #line, closure: @escaping () -> Void) {
World.sharedWorld.fit(description, flags: flags, file: file, line: line, closure: closure)
}
/**
Use this to quickly focus an `itBehavesLike` closure.
*/
public func fitBehavesLike(_ name: String, flags: FilterFlags = [:], file: String = #file, line: UInt = #line) {
fitBehavesLike(name, flags: flags, file: file, line: line, sharedExampleContext: { return [:] })
}
/**
Use this to quickly focus an `itBehavesLike` closure.
*/
public func fitBehavesLike(_ name: String, flags: FilterFlags = [:], file: String = #file, line: UInt = #line, sharedExampleContext: @escaping SharedExampleContext) {
World.sharedWorld.fitBehavesLike(name, sharedExampleContext: sharedExampleContext, flags: flags, file: file, line: line)
}
/**
Use this to quickly focus on `itBehavesLike` closure.
*/
public func fitBehavesLike<C>(_ behavior: Behavior<C>.Type, flags: FilterFlags = [:], file: String = #file, line: UInt = #line, context: @escaping () -> C) {
World.sharedWorld.fitBehavesLike(behavior, context: context, flags: flags, file: file, line: line)
}

View File

@@ -0,0 +1,205 @@
import Foundation
/**
Adds methods to World to support top-level DSL functions (Swift) and
macros (Objective-C). These functions map directly to the DSL that test
writers use in their specs.
*/
extension World {
internal func beforeSuite(_ closure: @escaping BeforeSuiteClosure) {
suiteHooks.appendBefore(closure)
}
internal func afterSuite(_ closure: @escaping AfterSuiteClosure) {
suiteHooks.appendAfter(closure)
}
internal func sharedExamples(_ name: String, closure: @escaping SharedExampleClosure) {
registerSharedExample(name, closure: closure)
}
internal func describe(_ description: String, flags: FilterFlags, closure: () -> Void) {
guard currentExampleMetadata == nil else {
raiseError("'describe' cannot be used inside '\(currentPhase)', 'describe' may only be used inside 'context' or 'describe'. ")
}
guard currentExampleGroup != nil else {
raiseError("Error: example group was not created by its parent QuickSpec spec. Check that describe() or context() was used in QuickSpec.spec() and not a more general context (i.e. an XCTestCase test)")
}
let group = ExampleGroup(description: description, flags: flags)
currentExampleGroup.appendExampleGroup(group)
performWithCurrentExampleGroup(group, closure: closure)
}
internal func context(_ description: String, flags: FilterFlags, closure: () -> Void) {
guard currentExampleMetadata == nil else {
raiseError("'context' cannot be used inside '\(currentPhase)', 'context' may only be used inside 'context' or 'describe'. ")
}
self.describe(description, flags: flags, closure: closure)
}
internal func fdescribe(_ description: String, flags: FilterFlags, closure: () -> Void) {
var focusedFlags = flags
focusedFlags[Filter.focused] = true
self.describe(description, flags: focusedFlags, closure: closure)
}
internal func xdescribe(_ description: String, flags: FilterFlags, closure: () -> Void) {
var pendingFlags = flags
pendingFlags[Filter.pending] = true
self.describe(description, flags: pendingFlags, closure: closure)
}
internal func beforeEach(_ closure: @escaping BeforeExampleClosure) {
guard currentExampleMetadata == nil else {
raiseError("'beforeEach' cannot be used inside '\(currentPhase)', 'beforeEach' may only be used inside 'context' or 'describe'. ")
}
currentExampleGroup.hooks.appendBefore(closure)
}
#if (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE
@objc(beforeEachWithMetadata:)
internal func beforeEach(closure: @escaping BeforeExampleWithMetadataClosure) {
currentExampleGroup.hooks.appendBefore(closure)
}
#else
internal func beforeEach(closure: @escaping BeforeExampleWithMetadataClosure) {
currentExampleGroup.hooks.appendBefore(closure)
}
#endif
internal func afterEach(_ closure: @escaping AfterExampleClosure) {
guard currentExampleMetadata == nil else {
raiseError("'afterEach' cannot be used inside '\(currentPhase)', 'afterEach' may only be used inside 'context' or 'describe'. ")
}
currentExampleGroup.hooks.appendAfter(closure)
}
#if (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE
@objc(afterEachWithMetadata:)
internal func afterEach(closure: @escaping AfterExampleWithMetadataClosure) {
currentExampleGroup.hooks.appendAfter(closure)
}
#else
internal func afterEach(closure: @escaping AfterExampleWithMetadataClosure) {
currentExampleGroup.hooks.appendAfter(closure)
}
#endif
internal func it(_ description: String, flags: FilterFlags, file: String, line: UInt, closure: @escaping () -> Void) {
if beforesCurrentlyExecuting {
raiseError("'it' cannot be used inside 'beforeEach', 'it' may only be used inside 'context' or 'describe'. ")
}
if aftersCurrentlyExecuting {
raiseError("'it' cannot be used inside 'afterEach', 'it' may only be used inside 'context' or 'describe'. ")
}
guard currentExampleMetadata == nil else {
raiseError("'it' cannot be used inside 'it', 'it' may only be used inside 'context' or 'describe'. ")
}
let callsite = Callsite(file: file, line: line)
let example = Example(description: description, callsite: callsite, flags: flags, closure: closure)
currentExampleGroup.appendExample(example)
}
internal func fit(_ description: String, flags: FilterFlags, file: String, line: UInt, closure: @escaping () -> Void) {
var focusedFlags = flags
focusedFlags[Filter.focused] = true
self.it(description, flags: focusedFlags, file: file, line: line, closure: closure)
}
internal func xit(_ description: String, flags: FilterFlags, file: String, line: UInt, closure: @escaping () -> Void) {
var pendingFlags = flags
pendingFlags[Filter.pending] = true
self.it(description, flags: pendingFlags, file: file, line: line, closure: closure)
}
internal func itBehavesLike(_ name: String, sharedExampleContext: @escaping SharedExampleContext, flags: FilterFlags, file: String, line: UInt) {
guard currentExampleMetadata == nil else {
raiseError("'itBehavesLike' cannot be used inside '\(currentPhase)', 'itBehavesLike' may only be used inside 'context' or 'describe'. ")
}
let callsite = Callsite(file: file, line: line)
let closure = World.sharedWorld.sharedExample(name)
let group = ExampleGroup(description: name, flags: flags)
currentExampleGroup.appendExampleGroup(group)
performWithCurrentExampleGroup(group) {
closure(sharedExampleContext)
}
group.walkDownExamples { (example: Example) in
example.isSharedExample = true
example.callsite = callsite
}
}
internal func fitBehavesLike(_ name: String, sharedExampleContext: @escaping SharedExampleContext, flags: FilterFlags, file: String, line: UInt) {
var focusedFlags = flags
focusedFlags[Filter.focused] = true
self.itBehavesLike(name, sharedExampleContext: sharedExampleContext, flags: focusedFlags, file: file, line: line)
}
internal func itBehavesLike<C>(_ behavior: Behavior<C>.Type, context: @escaping () -> C, flags: FilterFlags, file: String, line: UInt) {
guard currentExampleMetadata == nil else {
raiseError("'itBehavesLike' cannot be used inside '\(currentPhase)', 'itBehavesLike' may only be used inside 'context' or 'describe'. ")
}
let callsite = Callsite(file: file, line: line)
let closure = behavior.spec
let group = ExampleGroup(description: behavior.name, flags: flags)
currentExampleGroup.appendExampleGroup(group)
performWithCurrentExampleGroup(group) {
closure(context)
}
group.walkDownExamples { (example: Example) in
example.isSharedExample = true
example.callsite = callsite
}
}
internal func fitBehavesLike<C>(_ behavior: Behavior<C>.Type, context: @escaping () -> C, flags: FilterFlags, file: String, line: UInt) {
var focusedFlags = flags
focusedFlags[Filter.focused] = true
self.itBehavesLike(behavior, context: context, flags: focusedFlags, file: file, line: line)
}
internal func xitBehavesLike<C>(_ behavior: Behavior<C>.Type, context: @escaping () -> C, flags: FilterFlags, file: String, line: UInt) {
var pendingFlags = flags
pendingFlags[Filter.pending] = true
self.itBehavesLike(behavior, context: context, flags: pendingFlags, file: file, line: line)
}
#if (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE
@objc(itWithDescription:flags:file:line:closure:)
private func objc_it(_ description: String, flags: FilterFlags, file: String, line: UInt, closure: @escaping () -> Void) {
it(description, flags: flags, file: file, line: line, closure: closure)
}
@objc(fitWithDescription:flags:file:line:closure:)
private func objc_fit(_ description: String, flags: FilterFlags, file: String, line: UInt, closure: @escaping () -> Void) {
fit(description, flags: flags, file: file, line: line, closure: closure)
}
@objc(xitWithDescription:flags:file:line:closure:)
private func objc_xit(_ description: String, flags: FilterFlags, file: String, line: UInt, closure: @escaping () -> Void) {
xit(description, flags: flags, file: file, line: line, closure: closure)
}
@objc(itBehavesLikeSharedExampleNamed:sharedExampleContext:flags:file:line:)
private func objc_itBehavesLike(_ name: String, sharedExampleContext: @escaping SharedExampleContext, flags: FilterFlags, file: String, line: UInt) {
itBehavesLike(name, sharedExampleContext: sharedExampleContext, flags: flags, file: file, line: line)
}
#endif
internal func pending(_ description: String, closure: () -> Void) {
print("Pending: \(description)")
}
private var currentPhase: String {
if beforesCurrentlyExecuting {
return "beforeEach"
} else if aftersCurrentlyExecuting {
return "afterEach"
}
return "it"
}
}

View File

@@ -0,0 +1,10 @@
import Foundation
internal func raiseError(_ message: String) -> Never {
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
NSException(name: .internalInconsistencyException, reason: message, userInfo: nil).raise()
#endif
// This won't be reached when ObjC is available and the exception above is raisd
fatalError(message)
}

View File

@@ -0,0 +1,131 @@
import Foundation
private var numberOfExamplesRun = 0
private var numberOfIncludedExamples = 0
// `#if swift(>=3.2) && (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE`
// does not work as expected.
#if swift(>=3.2)
#if (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE
@objcMembers
public class _ExampleBase: NSObject {}
#else
public class _ExampleBase: NSObject {}
#endif
#else
public class _ExampleBase: NSObject {}
#endif
/**
Examples, defined with the `it` function, use assertions to
demonstrate how code should behave. These are like "tests" in XCTest.
*/
final public class Example: _ExampleBase {
/**
A boolean indicating whether the example is a shared example;
i.e.: whether it is an example defined with `itBehavesLike`.
*/
public var isSharedExample = false
/**
The site at which the example is defined.
This must be set correctly in order for Xcode to highlight
the correct line in red when reporting a failure.
*/
public var callsite: Callsite
weak internal var group: ExampleGroup?
private let internalDescription: String
private let closure: () -> Void
private let flags: FilterFlags
internal init(description: String, callsite: Callsite, flags: FilterFlags, closure: @escaping () -> Void) {
self.internalDescription = description
self.closure = closure
self.callsite = callsite
self.flags = flags
}
public override var description: String {
return internalDescription
}
/**
The example name. A name is a concatenation of the name of
the example group the example belongs to, followed by the
description of the example itself.
The example name is used to generate a test method selector
to be displayed in Xcode's test navigator.
*/
public var name: String {
guard let groupName = group?.name else { return description }
return "\(groupName), \(description)"
}
/**
Executes the example closure, as well as all before and after
closures defined in the its surrounding example groups.
*/
public func run() {
let world = World.sharedWorld
if numberOfIncludedExamples == 0 {
numberOfIncludedExamples = world.includedExampleCount
}
if numberOfExamplesRun == 0 {
world.suiteHooks.executeBefores()
}
let exampleMetadata = ExampleMetadata(example: self, exampleIndex: numberOfExamplesRun)
world.currentExampleMetadata = exampleMetadata
world.exampleHooks.executeBefores(exampleMetadata)
group!.phase = .beforesExecuting
for before in group!.befores {
before(exampleMetadata)
}
group!.phase = .beforesFinished
closure()
group!.phase = .aftersExecuting
for after in group!.afters {
after(exampleMetadata)
}
group!.phase = .aftersFinished
world.exampleHooks.executeAfters(exampleMetadata)
numberOfExamplesRun += 1
if !world.isRunningAdditionalSuites && numberOfExamplesRun >= numberOfIncludedExamples {
world.suiteHooks.executeAfters()
}
}
/**
Evaluates the filter flags set on this example and on the example groups
this example belongs to. Flags set on the example are trumped by flags on
the example group it belongs to. Flags on inner example groups are trumped
by flags on outer example groups.
*/
internal var filterFlags: FilterFlags {
var aggregateFlags = flags
for (key, value) in group!.filterFlags {
aggregateFlags[key] = value
}
return aggregateFlags
}
}
extension Example {
/**
Returns a boolean indicating whether two Example objects are equal.
If two examples are defined at the exact same callsite, they must be equal.
*/
@nonobjc public static func == (lhs: Example, rhs: Example) -> Bool {
return lhs.callsite == rhs.callsite
}
}

View File

@@ -0,0 +1,99 @@
import Foundation
/**
Example groups are logical groupings of examples, defined with
the `describe` and `context` functions. Example groups can share
setup and teardown code.
*/
final public class ExampleGroup: NSObject {
weak internal var parent: ExampleGroup?
internal let hooks = ExampleHooks()
internal var phase: HooksPhase = .nothingExecuted
private let internalDescription: String
private let flags: FilterFlags
private let isInternalRootExampleGroup: Bool
private var childGroups = [ExampleGroup]()
private var childExamples = [Example]()
internal init(description: String, flags: FilterFlags, isInternalRootExampleGroup: Bool = false) {
self.internalDescription = description
self.flags = flags
self.isInternalRootExampleGroup = isInternalRootExampleGroup
}
public override var description: String {
return internalDescription
}
/**
Returns a list of examples that belong to this example group,
or to any of its descendant example groups.
*/
public var examples: [Example] {
return childExamples + childGroups.flatMap { $0.examples }
}
internal var name: String? {
guard let parent = parent else {
return isInternalRootExampleGroup ? nil : description
}
guard let name = parent.name else { return description }
return "\(name), \(description)"
}
internal var filterFlags: FilterFlags {
var aggregateFlags = flags
walkUp { group in
for (key, value) in group.flags {
aggregateFlags[key] = value
}
}
return aggregateFlags
}
internal var befores: [BeforeExampleWithMetadataClosure] {
var closures = Array(hooks.befores.reversed())
walkUp { group in
closures.append(contentsOf: Array(group.hooks.befores.reversed()))
}
return Array(closures.reversed())
}
internal var afters: [AfterExampleWithMetadataClosure] {
var closures = hooks.afters
walkUp { group in
closures.append(contentsOf: group.hooks.afters)
}
return closures
}
internal func walkDownExamples(_ callback: (_ example: Example) -> Void) {
for example in childExamples {
callback(example)
}
for group in childGroups {
group.walkDownExamples(callback)
}
}
internal func appendExampleGroup(_ group: ExampleGroup) {
group.parent = self
childGroups.append(group)
}
internal func appendExample(_ example: Example) {
example.group = self
childExamples.append(example)
}
private func walkUp(_ callback: (_ group: ExampleGroup) -> Void) {
var group = self
while let parent = group.parent {
callback(parent)
group = parent
}
}
}

View File

@@ -0,0 +1,37 @@
import Foundation
// `#if swift(>=3.2) && (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE`
// does not work as expected.
#if swift(>=3.2)
#if (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE
@objcMembers
public class _ExampleMetadataBase: NSObject {}
#else
public class _ExampleMetadataBase: NSObject {}
#endif
#else
public class _ExampleMetadataBase: NSObject {}
#endif
/**
A class that encapsulates information about an example,
including the index at which the example was executed, as
well as the example itself.
*/
final public class ExampleMetadata: _ExampleMetadataBase {
/**
The example for which this metadata was collected.
*/
public let example: Example
/**
The index at which this example was executed in the
test suite.
*/
public let exampleIndex: Int
internal init(example: Example, exampleIndex: Int) {
self.example = example
self.exampleIndex = exampleIndex
}
}

View File

@@ -0,0 +1,44 @@
import Foundation
// `#if swift(>=3.2) && (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE`
// does not work as expected.
#if swift(>=3.2)
#if (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE
@objcMembers
public class _FilterBase: NSObject {}
#else
public class _FilterBase: NSObject {}
#endif
#else
public class _FilterBase: NSObject {}
#endif
/**
A mapping of string keys to booleans that can be used to
filter examples or example groups. For example, a "focused"
example would have the flags [Focused: true].
*/
public typealias FilterFlags = [String: Bool]
/**
A namespace for filter flag keys, defined primarily to make the
keys available in Objective-C.
*/
final public class Filter: _FilterBase {
/**
Example and example groups with [Focused: true] are included in test runs,
excluding all other examples without this flag. Use this to only run one or
two tests that you're currently focusing on.
*/
public class var focused: String {
return "focused"
}
/**
Example and example groups with [Pending: true] are excluded from test runs.
Use this to temporarily suspend examples that you know do not pass yet.
*/
public class var pending: String {
return "pending"
}
}

View File

@@ -0,0 +1,35 @@
// MARK: Example Hooks
/**
A closure executed before an example is run.
*/
public typealias BeforeExampleClosure = () -> Void
/**
A closure executed before an example is run. The closure is given example metadata,
which contains information about the example that is about to be run.
*/
public typealias BeforeExampleWithMetadataClosure = (_ exampleMetadata: ExampleMetadata) -> Void
/**
A closure executed after an example is run.
*/
public typealias AfterExampleClosure = BeforeExampleClosure
/**
A closure executed after an example is run. The closure is given example metadata,
which contains information about the example that has just finished running.
*/
public typealias AfterExampleWithMetadataClosure = BeforeExampleWithMetadataClosure
// MARK: Suite Hooks
/**
A closure executed before any examples are run.
*/
public typealias BeforeSuiteClosure = () -> Void
/**
A closure executed after all examples have finished running.
*/
public typealias AfterSuiteClosure = BeforeSuiteClosure

View File

@@ -0,0 +1,42 @@
/**
A container for closures to be executed before and after each example.
*/
final internal class ExampleHooks {
internal var befores: [BeforeExampleWithMetadataClosure] = []
internal var afters: [AfterExampleWithMetadataClosure] = []
internal var phase: HooksPhase = .nothingExecuted
internal func appendBefore(_ closure: @escaping BeforeExampleWithMetadataClosure) {
befores.append(closure)
}
internal func appendBefore(_ closure: @escaping BeforeExampleClosure) {
befores.append { (_: ExampleMetadata) in closure() }
}
internal func appendAfter(_ closure: @escaping AfterExampleWithMetadataClosure) {
afters.append(closure)
}
internal func appendAfter(_ closure: @escaping AfterExampleClosure) {
afters.append { (_: ExampleMetadata) in closure() }
}
internal func executeBefores(_ exampleMetadata: ExampleMetadata) {
phase = .beforesExecuting
for before in befores {
before(exampleMetadata)
}
phase = .beforesFinished
}
internal func executeAfters(_ exampleMetadata: ExampleMetadata) {
phase = .aftersExecuting
for after in afters {
after(exampleMetadata)
}
phase = .aftersFinished
}
}

View File

@@ -0,0 +1,11 @@
/**
A description of the execution cycle of the current example with
respect to the hooks of that example.
*/
internal enum HooksPhase {
case nothingExecuted
case beforesExecuting
case beforesFinished
case aftersExecuting
case aftersFinished
}

View File

@@ -0,0 +1,32 @@
/**
A container for closures to be executed before and after all examples.
*/
final internal class SuiteHooks {
internal var befores: [BeforeSuiteClosure] = []
internal var afters: [AfterSuiteClosure] = []
internal var phase: HooksPhase = .nothingExecuted
internal func appendBefore(_ closure: @escaping BeforeSuiteClosure) {
befores.append(closure)
}
internal func appendAfter(_ closure: @escaping AfterSuiteClosure) {
afters.append(closure)
}
internal func executeBefores() {
phase = .beforesExecuting
for before in befores {
before()
}
phase = .beforesFinished
}
internal func executeAfters() {
phase = .aftersExecuting
for after in afters {
after()
}
phase = .aftersFinished
}
}

View File

@@ -0,0 +1,25 @@
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
import Foundation
extension Bundle {
/**
Locates the first bundle with a '.xctest' file extension.
*/
internal static var currentTestBundle: Bundle? {
return allBundles.first { $0.bundlePath.hasSuffix(".xctest") }
}
/**
Return the module name of the bundle.
Uses the bundle filename and transform it to match Xcode's transformation.
Module name has to be a valid "C99 extended identifier".
*/
internal var moduleName: String {
let fileName = bundleURL.fileName as NSString
return fileName.c99ExtendedIdentifier
}
}
#endif

View File

@@ -0,0 +1,33 @@
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
import Foundation
public extension NSString {
private static var invalidCharacters: CharacterSet = {
var invalidCharacters = CharacterSet()
let invalidCharacterSets: [CharacterSet] = [
.whitespacesAndNewlines,
.illegalCharacters,
.controlCharacters,
.punctuationCharacters,
.nonBaseCharacters,
.symbols
]
for invalidSet in invalidCharacterSets {
invalidCharacters.formUnion(invalidSet)
}
return invalidCharacters
}()
@objc(qck_c99ExtendedIdentifier)
var c99ExtendedIdentifier: String {
let validComponents = components(separatedBy: NSString.invalidCharacters)
let result = validComponents.joined(separator: "_")
return result.isEmpty ? "_" : result
}
}
#endif

View File

@@ -0,0 +1,74 @@
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
import Foundation
/**
Responsible for building a "Selected tests" suite. This corresponds to a single
spec, and all its examples.
*/
internal class QuickSelectedTestSuiteBuilder: QuickTestSuiteBuilder {
/**
The test spec class to run.
*/
let testCaseClass: AnyClass!
/**
For Objective-C classes, returns the class name. For Swift classes without,
an explicit Objective-C name, returns a module-namespaced class name
(e.g., "FooTests.FooSpec").
*/
var testSuiteClassName: String {
return NSStringFromClass(testCaseClass)
}
/**
Given a test case name:
FooSpec/testFoo
Optionally constructs a test suite builder for the named test case class
in the running test bundle.
If no test bundle can be found, or the test case class can't be found,
initialization fails and returns `nil`.
*/
init?(forTestCaseWithName name: String) {
guard let testCaseClass = testCaseClassForTestCaseWithName(name) else {
self.testCaseClass = nil
return nil
}
self.testCaseClass = testCaseClass
}
/**
Returns a `QuickTestSuite` that runs the associated test case class.
*/
func buildTestSuite() -> QuickTestSuite {
return QuickTestSuite(forTestCaseClass: testCaseClass)
}
}
/**
Searches `Bundle.allBundles()` for an xctest bundle, then looks up the named
test case class in that bundle.
Returns `nil` if a bundle or test case class cannot be found.
*/
private func testCaseClassForTestCaseWithName(_ name: String) -> AnyClass? {
func extractClassName(_ name: String) -> String? {
return name.components(separatedBy: "/").first
}
guard let className = extractClassName(name) else { return nil }
guard let bundle = Bundle.currentTestBundle else { return nil }
if let testCaseClass = bundle.classNamed(className) { return testCaseClass }
let moduleName = bundle.moduleName
return NSClassFromString("\(moduleName).\(className)")
}
#endif

View File

@@ -0,0 +1,52 @@
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
import XCTest
/**
This protocol defines the role of an object that builds test suites.
*/
internal protocol QuickTestSuiteBuilder {
/**
Construct a `QuickTestSuite` instance with the appropriate test cases added as tests.
Subsequent calls to this method should return equivalent test suites.
*/
func buildTestSuite() -> QuickTestSuite
}
/**
A base class for a class cluster of Quick test suites, that should correctly
build dynamic test suites for XCTest to execute.
*/
public class QuickTestSuite: XCTestSuite {
private static var builtTestSuites: Set<String> = Set()
/**
Construct a test suite for a specific, selected subset of test cases (rather
than the default, which as all test cases).
If this method is called multiple times for the same test case class, e.g..
FooSpec/testFoo
FooSpec/testBar
It is expected that the first call should return a valid test suite, and
all subsequent calls should return `nil`.
*/
@objc
public static func selectedTestSuite(forTestCaseWithName name: String) -> QuickTestSuite? {
guard let builder = QuickSelectedTestSuiteBuilder(forTestCaseWithName: name) else { return nil }
let (inserted, _) = builtTestSuites.insert(builder.testSuiteClassName)
if inserted {
return builder.buildTestSuite()
} else {
return nil
}
}
}
#endif

View File

@@ -0,0 +1,12 @@
import Foundation
extension URL {
/**
Returns the path file name without file extension.
*/
var fileName: String {
return self.deletingPathExtension().lastPathComponent
}
}

View File

@@ -0,0 +1,247 @@
import Foundation
/**
A closure that, when evaluated, returns a dictionary of key-value
pairs that can be accessed from within a group of shared examples.
*/
public typealias SharedExampleContext = () -> [String: Any]
/**
A closure that is used to define a group of shared examples. This
closure may contain any number of example and example groups.
*/
public typealias SharedExampleClosure = (@escaping SharedExampleContext) -> Void
// `#if swift(>=3.2) && (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE`
// does not work as expected.
#if swift(>=3.2)
#if (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE
@objcMembers
internal class _WorldBase: NSObject {}
#else
internal class _WorldBase: NSObject {}
#endif
#else
internal class _WorldBase: NSObject {}
#endif
/**
A collection of state Quick builds up in order to work its magic.
World is primarily responsible for maintaining a mapping of QuickSpec
classes to root example groups for those classes.
It also maintains a mapping of shared example names to shared
example closures.
You may configure how Quick behaves by calling the -[World configure:]
method from within an overridden +[QuickConfiguration configure:] method.
*/
final internal class World: _WorldBase {
/**
The example group that is currently being run.
The DSL requires that this group is correctly set in order to build a
correct hierarchy of example groups and their examples.
*/
internal var currentExampleGroup: ExampleGroup!
/**
The example metadata of the test that is currently being run.
This is useful for using the Quick test metadata (like its name) at
runtime.
*/
internal var currentExampleMetadata: ExampleMetadata?
/**
A flag that indicates whether additional test suites are being run
within this test suite. This is only true within the context of Quick
functional tests.
*/
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
// Convention of generating Objective-C selector has been changed on Swift 3
@objc(isRunningAdditionalSuites)
internal var isRunningAdditionalSuites = false
#else
internal var isRunningAdditionalSuites = false
#endif
private var specs: [String: ExampleGroup] = [:]
private var sharedExamples: [String: SharedExampleClosure] = [:]
private let configuration = Configuration()
internal private(set) var isConfigurationFinalized = false
internal var exampleHooks: ExampleHooks {return configuration.exampleHooks }
internal var suiteHooks: SuiteHooks { return configuration.suiteHooks }
// MARK: Singleton Constructor
private override init() {}
static let sharedWorld = World()
// MARK: Public Interface
/**
Exposes the World's Configuration object within the scope of the closure
so that it may be configured. This method must not be called outside of
an overridden +[QuickConfiguration configure:] method.
- parameter closure: A closure that takes a Configuration object that can
be mutated to change Quick's behavior.
*/
internal func configure(_ closure: QuickConfigurer) {
assert(!isConfigurationFinalized,
"Quick cannot be configured outside of a +[QuickConfiguration configure:] method. You should not call -[World configure:] directly. Instead, subclass QuickConfiguration and override the +[QuickConfiguration configure:] method.")
closure(configuration)
}
/**
Finalizes the World's configuration.
Any subsequent calls to World.configure() will raise.
*/
internal func finalizeConfiguration() {
isConfigurationFinalized = true
}
/**
Returns an internally constructed root example group for the given
QuickSpec class.
A root example group with the description "root example group" is lazily
initialized for each QuickSpec class. This root example group wraps the
top level of a -[QuickSpec spec] method--it's thanks to this group that
users can define beforeEach and it closures at the top level, like so:
override func spec() {
// These belong to the root example group
beforeEach {}
it("is at the top level") {}
}
- parameter cls: The QuickSpec class for which to retrieve the root example group.
- returns: The root example group for the class.
*/
internal func rootExampleGroupForSpecClass(_ cls: AnyClass) -> ExampleGroup {
let name = String(describing: cls)
if let group = specs[name] {
return group
} else {
let group = ExampleGroup(
description: "root example group",
flags: [:],
isInternalRootExampleGroup: true
)
specs[name] = group
return group
}
}
/**
Returns all examples that should be run for a given spec class.
There are two filtering passes that occur when determining which examples should be run.
That is, these examples are the ones that are included by inclusion filters, and are
not excluded by exclusion filters.
- parameter specClass: The QuickSpec subclass for which examples are to be returned.
- returns: A list of examples to be run as test invocations.
*/
internal func examples(_ specClass: AnyClass) -> [Example] {
// 1. Grab all included examples.
let included = includedExamples
// 2. Grab the intersection of (a) examples for this spec, and (b) included examples.
let spec = rootExampleGroupForSpecClass(specClass).examples.filter { included.contains($0) }
// 3. Remove all excluded examples.
return spec.filter { example in
!self.configuration.exclusionFilters.reduce(false) { $0 || $1(example) }
}
}
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
@objc(examplesForSpecClass:)
private func objc_examples(_ specClass: AnyClass) -> [Example] {
return examples(specClass)
}
#endif
// MARK: Internal
internal func registerSharedExample(_ name: String, closure: @escaping SharedExampleClosure) {
raiseIfSharedExampleAlreadyRegistered(name)
sharedExamples[name] = closure
}
internal func sharedExample(_ name: String) -> SharedExampleClosure {
raiseIfSharedExampleNotRegistered(name)
return sharedExamples[name]!
}
internal var includedExampleCount: Int {
return includedExamples.count
}
internal var beforesCurrentlyExecuting: Bool {
let suiteBeforesExecuting = suiteHooks.phase == .beforesExecuting
let exampleBeforesExecuting = exampleHooks.phase == .beforesExecuting
var groupBeforesExecuting = false
if let runningExampleGroup = currentExampleMetadata?.example.group {
groupBeforesExecuting = runningExampleGroup.phase == .beforesExecuting
}
return suiteBeforesExecuting || exampleBeforesExecuting || groupBeforesExecuting
}
internal var aftersCurrentlyExecuting: Bool {
let suiteAftersExecuting = suiteHooks.phase == .aftersExecuting
let exampleAftersExecuting = exampleHooks.phase == .aftersExecuting
var groupAftersExecuting = false
if let runningExampleGroup = currentExampleMetadata?.example.group {
groupAftersExecuting = runningExampleGroup.phase == .aftersExecuting
}
return suiteAftersExecuting || exampleAftersExecuting || groupAftersExecuting
}
internal func performWithCurrentExampleGroup(_ group: ExampleGroup, closure: () -> Void) {
let previousExampleGroup = currentExampleGroup
currentExampleGroup = group
closure()
currentExampleGroup = previousExampleGroup
}
private var allExamples: [Example] {
var all: [Example] = []
for (_, group) in specs {
group.walkDownExamples { all.append($0) }
}
return all
}
private var includedExamples: [Example] {
let all = allExamples
let included = all.filter { example in
return self.configuration.inclusionFilters.reduce(false) { $0 || $1(example) }
}
if included.isEmpty && configuration.runAllWhenEverythingFiltered {
return all
} else {
return included
}
}
private func raiseIfSharedExampleAlreadyRegistered(_ name: String) {
if sharedExamples[name] != nil {
raiseError("A shared example named '\(name)' has already been registered.")
}
}
private func raiseIfSharedExampleNotRegistered(_ name: String) {
if sharedExamples[name] == nil {
raiseError("No shared example named '\(name)' has been registered. Registered shared examples: '\(Array(sharedExamples.keys))'")
}
}
}

View File

@@ -0,0 +1,30 @@
#import <Foundation/Foundation.h>
@class Configuration;
/**
Subclass QuickConfiguration and override the +[QuickConfiguration configure:]
method in order to configure how Quick behaves when running specs, or to define
shared examples that are used across spec files.
*/
@interface QuickConfiguration : NSObject
/**
This method is executed on each subclass of this class before Quick runs
any examples. You may override this method on as many subclasses as you like, but
there is no guarantee as to the order in which these methods are executed.
You can override this method in order to:
1. Configure how Quick behaves, by modifying properties on the Configuration object.
Setting the same properties in several methods has undefined behavior.
2. Define shared examples using `sharedExamples`.
@param configuration A mutable object that is used to configure how Quick behaves on
a framework level. For details on all the options, see the
documentation in Configuration.swift.
*/
+ (void)configure:(Configuration *)configuration;
@end

View File

@@ -0,0 +1,83 @@
#import "QuickConfiguration.h"
#import "World.h"
#import <objc/runtime.h>
typedef void (^QCKClassEnumerationBlock)(Class klass);
/**
Finds all direct subclasses of the given class and passes them to the block provided.
The classes are iterated over in the order that objc_getClassList returns them.
@param klass The base class to find subclasses of.
@param block A block that takes a Class. This block will be executed once for each subclass of klass.
*/
void qck_enumerateSubclasses(Class klass, QCKClassEnumerationBlock block) {
Class *classes = NULL;
int classesCount = objc_getClassList(NULL, 0);
if (classesCount > 0) {
classes = (Class *)calloc(sizeof(Class), classesCount);
classesCount = objc_getClassList(classes, classesCount);
Class subclass, superclass;
for(int i = 0; i < classesCount; i++) {
subclass = classes[i];
superclass = class_getSuperclass(subclass);
if (superclass == klass && block) {
block(subclass);
}
}
free(classes);
}
}
@implementation QuickConfiguration
#pragma mark - Object Lifecycle
/**
QuickConfiguration is not meant to be instantiated; it merely provides a hook
for users to configure how Quick behaves. Raise an exception if an instance of
QuickConfiguration is created.
*/
- (instancetype)init {
NSString *className = NSStringFromClass([self class]);
NSString *selectorName = NSStringFromSelector(@selector(configure:));
[NSException raise:NSInternalInconsistencyException
format:@"%@ is not meant to be instantiated; "
@"subclass %@ and override %@ to configure Quick.",
className, className, selectorName];
return nil;
}
#pragma mark - NSObject Overrides
/**
Hook into when QuickConfiguration is initialized in the runtime in order to
call +[QuickConfiguration configure:] on each of its subclasses.
*/
+ (void)initialize {
// Only enumerate over the subclasses of QuickConfiguration, not any of its subclasses.
if ([self class] == [QuickConfiguration class]) {
// Only enumerate over subclasses once, even if +[QuickConfiguration initialize]
// were to be called several times. This is necessary because +[QuickSpec initialize]
// manually calls +[QuickConfiguration initialize].
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
qck_enumerateSubclasses([QuickConfiguration class], ^(__unsafe_unretained Class klass) {
[[World sharedWorld] configure:^(Configuration *configuration) {
[klass configure:configuration];
}];
});
[[World sharedWorld] finalizeConfiguration];
});
}
}
#pragma mark - Public Interface
+ (void)configure:(Configuration *)configuration { }
@end

View File

@@ -0,0 +1,234 @@
#import <Foundation/Foundation.h>
@class ExampleMetadata;
/**
Provides a hook for Quick to be configured before any examples are run.
Within this scope, override the +[QuickConfiguration configure:] method
to set properties on a configuration object to customize Quick behavior.
For details, see the documentation for Configuraiton.swift.
@param name The name of the configuration class. Like any Objective-C
class name, this must be unique to the current runtime
environment.
*/
#define QuickConfigurationBegin(name) \
@interface name : QuickConfiguration; @end \
@implementation name \
/**
Marks the end of a Quick configuration.
Make sure you put this after `QuickConfigurationBegin`.
*/
#define QuickConfigurationEnd \
@end \
/**
Defines a new QuickSpec. Define examples and example groups within the space
between this and `QuickSpecEnd`.
@param name The name of the spec class. Like any Objective-C class name, this
must be unique to the current runtime environment.
*/
#define QuickSpecBegin(name) \
@interface name : QuickSpec; @end \
@implementation name \
- (void)spec { \
/**
Marks the end of a QuickSpec. Make sure you put this after `QuickSpecBegin`.
*/
#define QuickSpecEnd \
} \
@end \
typedef NSDictionary *(^QCKDSLSharedExampleContext)(void);
typedef void (^QCKDSLSharedExampleBlock)(QCKDSLSharedExampleContext);
typedef void (^QCKDSLEmptyBlock)(void);
typedef void (^QCKDSLExampleMetadataBlock)(ExampleMetadata *exampleMetadata);
#define QUICK_EXPORT FOUNDATION_EXPORT
QUICK_EXPORT void qck_beforeSuite(QCKDSLEmptyBlock closure);
QUICK_EXPORT void qck_afterSuite(QCKDSLEmptyBlock closure);
QUICK_EXPORT void qck_sharedExamples(NSString *name, QCKDSLSharedExampleBlock closure);
QUICK_EXPORT void qck_describe(NSString *description, QCKDSLEmptyBlock closure);
QUICK_EXPORT void qck_context(NSString *description, QCKDSLEmptyBlock closure);
QUICK_EXPORT void qck_beforeEach(QCKDSLEmptyBlock closure);
QUICK_EXPORT void qck_beforeEachWithMetadata(QCKDSLExampleMetadataBlock closure);
QUICK_EXPORT void qck_afterEach(QCKDSLEmptyBlock closure);
QUICK_EXPORT void qck_afterEachWithMetadata(QCKDSLExampleMetadataBlock closure);
QUICK_EXPORT void qck_pending(NSString *description, QCKDSLEmptyBlock closure);
QUICK_EXPORT void qck_xdescribe(NSString *description, QCKDSLEmptyBlock closure);
QUICK_EXPORT void qck_xcontext(NSString *description, QCKDSLEmptyBlock closure);
QUICK_EXPORT void qck_fdescribe(NSString *description, QCKDSLEmptyBlock closure);
QUICK_EXPORT void qck_fcontext(NSString *description, QCKDSLEmptyBlock closure);
#ifndef QUICK_DISABLE_SHORT_SYNTAX
/**
Defines a closure to be run prior to any examples in the test suite.
You may define an unlimited number of these closures, but there is no
guarantee as to the order in which they're run.
If the test suite crashes before the first example is run, this closure
will not be executed.
@param closure The closure to be run prior to any examples in the test suite.
*/
static inline void beforeSuite(QCKDSLEmptyBlock closure) {
qck_beforeSuite(closure);
}
/**
Defines a closure to be run after all of the examples in the test suite.
You may define an unlimited number of these closures, but there is no
guarantee as to the order in which they're run.
If the test suite crashes before all examples are run, this closure
will not be executed.
@param closure The closure to be run after all of the examples in the test suite.
*/
static inline void afterSuite(QCKDSLEmptyBlock closure) {
qck_afterSuite(closure);
}
/**
Defines a group of shared examples. These examples can be re-used in several locations
by using the `itBehavesLike` function.
@param name The name of the shared example group. This must be unique across all shared example
groups defined in a test suite.
@param closure A closure containing the examples. This behaves just like an example group defined
using `describe` or `context`--the closure may contain any number of `beforeEach`
and `afterEach` closures, as well as any number of examples (defined using `it`).
*/
static inline void sharedExamples(NSString *name, QCKDSLSharedExampleBlock closure) {
qck_sharedExamples(name, closure);
}
/**
Defines an example group. Example groups are logical groupings of examples.
Example groups can share setup and teardown code.
@param description An arbitrary string describing the example group.
@param closure A closure that can contain other examples.
*/
static inline void describe(NSString *description, QCKDSLEmptyBlock closure) {
qck_describe(description, closure);
}
/**
Defines an example group. Equivalent to `describe`.
*/
static inline void context(NSString *description, QCKDSLEmptyBlock closure) {
qck_context(description, closure);
}
/**
Defines a closure to be run prior to each example in the current example
group. This closure is not run for pending or otherwise disabled examples.
An example group may contain an unlimited number of beforeEach. They'll be
run in the order they're defined, but you shouldn't rely on that behavior.
@param closure The closure to be run prior to each example.
*/
static inline void beforeEach(QCKDSLEmptyBlock closure) {
qck_beforeEach(closure);
}
/**
Identical to QCKDSL.beforeEach, except the closure is provided with
metadata on the example that the closure is being run prior to.
*/
static inline void beforeEachWithMetadata(QCKDSLExampleMetadataBlock closure) {
qck_beforeEachWithMetadata(closure);
}
/**
Defines a closure to be run after each example in the current example
group. This closure is not run for pending or otherwise disabled examples.
An example group may contain an unlimited number of afterEach. They'll be
run in the order they're defined, but you shouldn't rely on that behavior.
@param closure The closure to be run after each example.
*/
static inline void afterEach(QCKDSLEmptyBlock closure) {
qck_afterEach(closure);
}
/**
Identical to QCKDSL.afterEach, except the closure is provided with
metadata on the example that the closure is being run after.
*/
static inline void afterEachWithMetadata(QCKDSLExampleMetadataBlock closure) {
qck_afterEachWithMetadata(closure);
}
/**
Defines an example or example group that should not be executed. Use `pending` to temporarily disable
examples or groups that should not be run yet.
@param description An arbitrary string describing the example or example group.
@param closure A closure that will not be evaluated.
*/
static inline void pending(NSString *description, QCKDSLEmptyBlock closure) {
qck_pending(description, closure);
}
/**
Use this to quickly mark a `describe` block as pending.
This disables all examples within the block.
*/
static inline void xdescribe(NSString *description, QCKDSLEmptyBlock closure) {
qck_xdescribe(description, closure);
}
/**
Use this to quickly mark a `context` block as pending.
This disables all examples within the block.
*/
static inline void xcontext(NSString *description, QCKDSLEmptyBlock closure) {
qck_xcontext(description, closure);
}
/**
Use this to quickly focus a `describe` block, focusing the examples in the block.
If any examples in the test suite are focused, only those examples are executed.
This trumps any explicitly focused or unfocused examples within the block--they are all treated as focused.
*/
static inline void fdescribe(NSString *description, QCKDSLEmptyBlock closure) {
qck_fdescribe(description, closure);
}
/**
Use this to quickly focus a `context` block. Equivalent to `fdescribe`.
*/
static inline void fcontext(NSString *description, QCKDSLEmptyBlock closure) {
qck_fcontext(description, closure);
}
#define it qck_it
#define xit qck_xit
#define fit qck_fit
#define itBehavesLike qck_itBehavesLike
#define xitBehavesLike qck_xitBehavesLike
#define fitBehavesLike qck_fitBehavesLike
#endif
#define qck_it qck_it_builder(@{}, @(__FILE__), __LINE__)
#define qck_xit qck_it_builder(@{Filter.pending: @YES}, @(__FILE__), __LINE__)
#define qck_fit qck_it_builder(@{Filter.focused: @YES}, @(__FILE__), __LINE__)
#define qck_itBehavesLike qck_itBehavesLike_builder(@{}, @(__FILE__), __LINE__)
#define qck_xitBehavesLike qck_itBehavesLike_builder(@{Filter.pending: @YES}, @(__FILE__), __LINE__)
#define qck_fitBehavesLike qck_itBehavesLike_builder(@{Filter.focused: @YES}, @(__FILE__), __LINE__)
typedef void (^QCKItBlock)(NSString *description, QCKDSLEmptyBlock closure);
typedef void (^QCKItBehavesLikeBlock)(NSString *description, QCKDSLSharedExampleContext context);
QUICK_EXPORT QCKItBlock qck_it_builder(NSDictionary *flags, NSString *file, NSUInteger line);
QUICK_EXPORT QCKItBehavesLikeBlock qck_itBehavesLike_builder(NSDictionary *flags, NSString *file, NSUInteger line);

View File

@@ -0,0 +1,79 @@
#import "QCKDSL.h"
#import "World.h"
#import "World+DSL.h"
void qck_beforeSuite(QCKDSLEmptyBlock closure) {
[[World sharedWorld] beforeSuite:closure];
}
void qck_afterSuite(QCKDSLEmptyBlock closure) {
[[World sharedWorld] afterSuite:closure];
}
void qck_sharedExamples(NSString *name, QCKDSLSharedExampleBlock closure) {
[[World sharedWorld] sharedExamples:name closure:closure];
}
void qck_describe(NSString *description, QCKDSLEmptyBlock closure) {
[[World sharedWorld] describe:description flags:@{} closure:closure];
}
void qck_context(NSString *description, QCKDSLEmptyBlock closure) {
qck_describe(description, closure);
}
void qck_beforeEach(QCKDSLEmptyBlock closure) {
[[World sharedWorld] beforeEach:closure];
}
void qck_beforeEachWithMetadata(QCKDSLExampleMetadataBlock closure) {
[[World sharedWorld] beforeEachWithMetadata:closure];
}
void qck_afterEach(QCKDSLEmptyBlock closure) {
[[World sharedWorld] afterEach:closure];
}
void qck_afterEachWithMetadata(QCKDSLExampleMetadataBlock closure) {
[[World sharedWorld] afterEachWithMetadata:closure];
}
QCKItBlock qck_it_builder(NSDictionary *flags, NSString *file, NSUInteger line) {
return ^(NSString *description, QCKDSLEmptyBlock closure) {
[[World sharedWorld] itWithDescription:description
flags:flags
file:file
line:line
closure:closure];
};
}
QCKItBehavesLikeBlock qck_itBehavesLike_builder(NSDictionary *flags, NSString *file, NSUInteger line) {
return ^(NSString *name, QCKDSLSharedExampleContext context) {
[[World sharedWorld] itBehavesLikeSharedExampleNamed:name
sharedExampleContext:context
flags:flags
file:file
line:line];
};
}
void qck_pending(NSString *description, QCKDSLEmptyBlock closure) {
[[World sharedWorld] pending:description closure:closure];
}
void qck_xdescribe(NSString *description, QCKDSLEmptyBlock closure) {
[[World sharedWorld] xdescribe:description flags:@{} closure:closure];
}
void qck_xcontext(NSString *description, QCKDSLEmptyBlock closure) {
qck_xdescribe(description, closure);
}
void qck_fdescribe(NSString *description, QCKDSLEmptyBlock closure) {
[[World sharedWorld] fdescribe:description flags:@{} closure:closure];
}
void qck_fcontext(NSString *description, QCKDSLEmptyBlock closure) {
qck_fdescribe(description, closure);
}

View File

@@ -0,0 +1,20 @@
#import <Quick/Quick-Swift.h>
@interface World (SWIFT_EXTENSION(Quick))
- (void)beforeSuite:(void (^ __nonnull)(void))closure;
- (void)afterSuite:(void (^ __nonnull)(void))closure;
- (void)sharedExamples:(NSString * __nonnull)name closure:(void (^ __nonnull)(NSDictionary * __nonnull (^ __nonnull)(void)))closure;
- (void)describe:(NSString * __nonnull)description flags:(NSDictionary * __nonnull)flags closure:(void (^ __nonnull)(void))closure;
- (void)context:(NSString * __nonnull)description flags:(NSDictionary * __nonnull)flags closure:(void (^ __nonnull)(void))closure;
- (void)fdescribe:(NSString * __nonnull)description flags:(NSDictionary * __nonnull)flags closure:(void (^ __nonnull)(void))closure;
- (void)xdescribe:(NSString * __nonnull)description flags:(NSDictionary * __nonnull)flags closure:(void (^ __nonnull)(void))closure;
- (void)beforeEach:(void (^ __nonnull)(void))closure;
- (void)beforeEachWithMetadata:(void (^ __nonnull)(ExampleMetadata * __nonnull))closure;
- (void)afterEach:(void (^ __nonnull)(void))closure;
- (void)afterEachWithMetadata:(void (^ __nonnull)(ExampleMetadata * __nonnull))closure;
- (void)itWithDescription:(NSString * __nonnull)description flags:(NSDictionary * __nonnull)flags file:(NSString * __nonnull)file line:(NSUInteger)line closure:(void (^ __nonnull)(void))closure;
- (void)fitWithDescription:(NSString * __nonnull)description flags:(NSDictionary * __nonnull)flags file:(NSString * __nonnull)file line:(NSUInteger)line closure:(void (^ __nonnull)(void))closure;
- (void)xitWithDescription:(NSString * __nonnull)description flags:(NSDictionary * __nonnull)flags file:(NSString * __nonnull)file line:(NSUInteger)line closure:(void (^ __nonnull)(void))closure;
- (void)itBehavesLikeSharedExampleNamed:(NSString * __nonnull)name sharedExampleContext:(NSDictionary * __nonnull (^ __nonnull)(void))sharedExampleContext flags:(NSDictionary * __nonnull)flags file:(NSString * __nonnull)file line:(NSUInteger)line;
- (void)pending:(NSString * __nonnull)description closure:(void (^ __nonnull)(void))closure;
@end

View File

@@ -0,0 +1,11 @@
#import <Foundation/Foundation.h>
//! Project version number for Quick.
FOUNDATION_EXPORT double QuickVersionNumber;
//! Project version string for Quick.
FOUNDATION_EXPORT const unsigned char QuickVersionString[];
#import "QuickSpec.h"
#import "QCKDSL.h"
#import "QuickConfiguration.h"

View File

@@ -0,0 +1,50 @@
#import <XCTest/XCTest.h>
/**
QuickSpec is a base class all specs written in Quick inherit from.
They need to inherit from QuickSpec, a subclass of XCTestCase, in
order to be discovered by the XCTest framework.
XCTest automatically compiles a list of XCTestCase subclasses included
in the test target. It iterates over each class in that list, and creates
a new instance of that class for each test method. It then creates an
"invocation" to execute that test method. The invocation is an instance of
NSInvocation, which represents a single message send in Objective-C.
The invocation is set on the XCTestCase instance, and the test is run.
Most of the code in QuickSpec is dedicated to hooking into XCTest events.
First, when the spec is first loaded and before it is sent any messages,
the +[NSObject initialize] method is called. QuickSpec overrides this method
to call +[QuickSpec spec]. This builds the example group stacks and
registers them with Quick.World, a global register of examples.
Then, XCTest queries QuickSpec for a list of test methods. Normally, XCTest
automatically finds all methods whose selectors begin with the string "test".
However, QuickSpec overrides this default behavior by implementing the
+[XCTestCase testInvocations] method. This method iterates over each example
registered in Quick.World, defines a new method for that example, and
returns an invocation to call that method to XCTest. Those invocations are
the tests that are run by XCTest. Their selector names are displayed in
the Xcode test navigation bar.
*/
@interface QuickSpec : XCTestCase
/**
Override this method in your spec to define a set of example groups
and examples.
@code
override func spec() {
describe("winter") {
it("is coming") {
// ...
}
}
}
@endcode
See DSL.swift for more information on what syntax is available.
*/
- (void)spec;
@end

View File

@@ -0,0 +1,141 @@
#import "QuickSpec.h"
#import "QuickConfiguration.h"
#import "World.h"
#import <Quick/Quick-Swift.h>
static QuickSpec *currentSpec = nil;
@interface QuickSpec ()
@property (nonatomic, strong) Example *example;
@end
@implementation QuickSpec
#pragma mark - XCTestCase Overrides
/**
The runtime sends initialize to each class in a program just before the class, or any class
that inherits from it, is sent its first message from within the program. QuickSpec hooks into
this event to compile the example groups for this spec subclass.
If an exception occurs when compiling the examples, report it to the user. Chances are they
included an expectation outside of a "it", "describe", or "context" block.
*/
+ (void)initialize {
[QuickConfiguration initialize];
World *world = [World sharedWorld];
[world performWithCurrentExampleGroup:[world rootExampleGroupForSpecClass:self] closure:^{
QuickSpec *spec = [self new];
@try {
[spec spec];
}
@catch (NSException *exception) {
[NSException raise:NSInternalInconsistencyException
format:@"An exception occurred when building Quick's example groups.\n"
@"Some possible reasons this might happen include:\n\n"
@"- An 'expect(...).to' expectation was evaluated outside of "
@"an 'it', 'context', or 'describe' block\n"
@"- 'sharedExamples' was called twice with the same name\n"
@"- 'itBehavesLike' was called with a name that is not registered as a shared example\n\n"
@"Here's the original exception: '%@', reason: '%@', userInfo: '%@'",
exception.name, exception.reason, exception.userInfo];
}
[self testInvocations];
}];
}
/**
Invocations for each test method in the test case. QuickSpec overrides this method to define a
new method for each example defined in +[QuickSpec spec].
@return An array of invocations that execute the newly defined example methods.
*/
+ (NSArray *)testInvocations {
NSArray *examples = [[World sharedWorld] examplesForSpecClass:[self class]];
NSMutableArray *invocations = [NSMutableArray arrayWithCapacity:[examples count]];
NSMutableSet<NSString*> *selectorNames = [NSMutableSet set];
for (Example *example in examples) {
SEL selector = [self addInstanceMethodForExample:example classSelectorNames:selectorNames];
NSMethodSignature *signature = [self instanceMethodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.selector = selector;
[invocations addObject:invocation];
}
return invocations;
}
#pragma mark - Public Interface
- (void)spec { }
#pragma mark - Internal Methods
/**
QuickSpec uses this method to dynamically define a new instance method for the
given example. The instance method runs the example, catching any exceptions.
The exceptions are then reported as test failures.
In order to report the correct file and line number, examples must raise exceptions
containing following keys in their userInfo:
- "SenTestFilenameKey": A String representing the file name
- "SenTestLineNumberKey": An Int representing the line number
These keys used to be used by SenTestingKit, and are still used by some testing tools
in the wild. See: https://github.com/Quick/Quick/pull/41
@return The selector of the newly defined instance method.
*/
+ (SEL)addInstanceMethodForExample:(Example *)example classSelectorNames:(NSMutableSet<NSString*> *)selectorNames {
IMP implementation = imp_implementationWithBlock(^(QuickSpec *self){
self.example = example;
currentSpec = self;
[example run];
});
const char *types = [[NSString stringWithFormat:@"%s%s%s", @encode(void), @encode(id), @encode(SEL)] UTF8String];
NSString *originalName = example.name.qck_c99ExtendedIdentifier;
NSString *selectorName = originalName;
NSUInteger i = 2;
while ([selectorNames containsObject:selectorName]) {
selectorName = [NSString stringWithFormat:@"%@_%tu", originalName, i++];
}
[selectorNames addObject:selectorName];
SEL selector = NSSelectorFromString(selectorName);
class_addMethod(self, selector, implementation, types);
return selector;
}
/**
This method is used to record failures, whether they represent example
expectations that were not met, or exceptions raised during test setup
and teardown. By default, the failure will be reported as an
XCTest failure, and the example will be highlighted in Xcode.
*/
- (void)recordFailureWithDescription:(NSString *)description
inFile:(NSString *)filePath
atLine:(NSUInteger)lineNumber
expected:(BOOL)expected {
if (self.example.isSharedExample) {
filePath = self.example.callsite.file;
lineNumber = self.example.callsite.line;
}
[currentSpec.testRun recordFailureWithDescription:description
inFile:filePath
atLine:lineNumber
expected:expected];
}
@end

View File

@@ -0,0 +1,18 @@
#import <Quick/Quick-Swift.h>
@class ExampleGroup;
@class ExampleMetadata;
SWIFT_CLASS("_TtC5Quick5World")
@interface World
@property (nonatomic) ExampleGroup * __nullable currentExampleGroup;
@property (nonatomic) ExampleMetadata * __nullable currentExampleMetadata;
@property (nonatomic) BOOL isRunningAdditionalSuites;
+ (World * __nonnull)sharedWorld;
- (void)configure:(void (^ __nonnull)(Configuration * __nonnull))closure;
- (void)finalizeConfiguration;
- (ExampleGroup * __nonnull)rootExampleGroupForSpecClass:(Class __nonnull)cls;
- (NSArray * __nonnull)examplesForSpecClass:(Class __nonnull)specClass;
- (void)performWithCurrentExampleGroup:(ExampleGroup * __nonnull)group closure:(void (^ __nonnull)(void))closure;
@end

View File

@@ -0,0 +1,40 @@
#import <XCTest/XCTest.h>
#import <objc/runtime.h>
#import <Quick/Quick-Swift.h>
@interface XCTestSuite (QuickTestSuiteBuilder)
@end
@implementation XCTestSuite (QuickTestSuiteBuilder)
/**
In order to ensure we can correctly build dynamic test suites, we need to
replace some of the default test suite constructors.
*/
+ (void)load {
Method testCaseWithName = class_getClassMethod(self, @selector(testSuiteForTestCaseWithName:));
Method hooked_testCaseWithName = class_getClassMethod(self, @selector(qck_hooked_testSuiteForTestCaseWithName:));
method_exchangeImplementations(testCaseWithName, hooked_testCaseWithName);
}
/**
The `+testSuiteForTestCaseWithName:` method is called when a specific test case
class is run from the Xcode test navigator. If the built test suite is `nil`,
Xcode will not run any tests for that test case.
Given if the following test case class is run from the Xcode test navigator:
FooSpec
testFoo
testBar
XCTest will invoke this once per test case, with test case names following this format:
FooSpec/testFoo
FooSpec/testBar
*/
+ (nullable instancetype)qck_hooked_testSuiteForTestCaseWithName:(nonnull NSString *)name {
return [QuickTestSuite selectedTestSuiteForTestCaseWithName:name];
}
@end

View File

@@ -0,0 +1,55 @@
#import "QuickSpecBase.h"
#pragma mark - _QuickSelectorWrapper
@interface _QuickSelectorWrapper ()
@property(nonatomic, assign) SEL selector;
@end
@implementation _QuickSelectorWrapper
- (instancetype)initWithSelector:(SEL)selector {
self = [super init];
_selector = selector;
return self;
}
@end
#pragma mark - _QuickSpecBase
@implementation _QuickSpecBase
- (instancetype)init {
self = [super initWithInvocation: nil];
return self;
}
/**
Invocations for each test method in the test case. QuickSpec overrides this method to define a
new method for each example defined in +[QuickSpec spec].
@return An array of invocations that execute the newly defined example methods.
*/
+ (NSArray<NSInvocation *> *)testInvocations {
NSArray<_QuickSelectorWrapper *> *wrappers = [self _qck_testMethodSelectors];
NSMutableArray<NSInvocation *> *invocations = [NSMutableArray arrayWithCapacity:wrappers.count];
for (_QuickSelectorWrapper *wrapper in wrappers) {
SEL selector = wrapper.selector;
NSMethodSignature *signature = [self instanceMethodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.selector = selector;
[invocations addObject:invocation];
}
return invocations;
}
+ (NSArray<_QuickSelectorWrapper *> *)_qck_testMethodSelectors {
return @[];
}
@end

View File

@@ -0,0 +1,11 @@
#import <Foundation/Foundation.h>
#import <XCTest/XCTest.h>
@interface _QuickSelectorWrapper : NSObject
- (instancetype)initWithSelector:(SEL)selector;
@end
@interface _QuickSpecBase : XCTestCase
+ (NSArray<_QuickSelectorWrapper *> *)_qck_testMethodSelectors;
- (instancetype)init NS_DESIGNATED_INITIALIZER;
@end