-
-
Notifications
You must be signed in to change notification settings - Fork 602
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce the require-dsl. expect, but it returns the result of the e…
…xpression As implied, this is basically a copy-paste of the functionality of expect. It's not complete, and it's not well-tested. But it gets the idea across. Future work is cleaning this up, backfilling tests for features (such as the unwrap function), and adding support for polling requirements (i.e. toEventually). Other than functionality, require also files the errorThrown issue type with XCTest, whereas expect files the assertionFailed issue type. This has minor differences that are mostly semantics. Also, in addition to the require dsl, this also adds unwrap, which is a shorthand for `require(...).toNot(beNil())`.
- Loading branch information
Showing
8 changed files
with
282 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
/// Make a ``Requirement`` on a given actual value. The value given is lazily evaluated. | ||
public func require<T>(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure @escaping () throws -> T?) -> SyncRequirement<T> { | ||
return SyncRequirement( | ||
expression: Expression( | ||
expression: expression, | ||
location: SourceLocation(file: file, line: line), | ||
isClosure: true)) | ||
} | ||
|
||
/// Make a ``Requirement`` on a given actual value. The closure is lazily invoked. | ||
public func require<T>(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure () -> (() throws -> T)) -> SyncRequirement<T> { | ||
return SyncRequirement( | ||
expression: Expression( | ||
expression: expression(), | ||
location: SourceLocation(file: file, line: line), | ||
isClosure: true)) | ||
} | ||
|
||
/// Make a ``Requirement`` on a given actual value. The closure is lazily invoked. | ||
public func require<T>(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure () -> (() throws -> T?)) -> SyncRequirement<T> { | ||
return SyncRequirement( | ||
expression: Expression( | ||
expression: expression(), | ||
location: SourceLocation(file: file, line: line), | ||
isClosure: true)) | ||
} | ||
|
||
/// Make a ``Requirement`` on a given actual value. The closure is lazily invoked. | ||
public func require(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure () -> (() throws -> Void)) -> SyncRequirement<Void> { | ||
return SyncRequirement( | ||
expression: Expression( | ||
expression: expression(), | ||
location: SourceLocation(file: file, line: line), | ||
isClosure: true)) | ||
} | ||
|
||
// MARK: - Unwrap | ||
|
||
/// Makes sure that the expression evaluates to a non-nil value, otherwise throw an error. | ||
/// As you can tell, this is a much less verbose equivalent to `require(expression).toNot(beNil())` | ||
public func unwrap<T>(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure @escaping () throws -> T?) throws -> T { | ||
try require(file: file, line: line, expression()).toNot(beNil()) | ||
} | ||
|
||
/// Makes sure that the expression evaluates to a non-nil value, otherwise throw an error. | ||
/// As you can tell, this is a much less verbose equivalent to `require(expression).toNot(beNil())` | ||
@discardableResult | ||
public func unwrap<T>(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure () -> (() throws -> T?)) throws -> T { | ||
try require(file: file, line: line, expression()).toNot(beNil()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
public struct RequirementError: Error, CustomNSError { | ||
let message: String | ||
let location: SourceLocation | ||
|
||
var localizedDescription: String { message } | ||
public var errorUserInfo: [String: Any] { | ||
// Required to prevent Xcode from reporting that we threw an error. | ||
// The default assertionHandlers will report this to XCode for us. | ||
["XCTestErrorUserInfoKeyShouldIgnore": true] | ||
} | ||
|
||
static func unknown(_ location: SourceLocation) -> RequirementError { | ||
RequirementError(message: "Nimble error - file a bug if you see this!", location: location) | ||
} | ||
} | ||
|
||
public enum RequireError: Error { | ||
case requirementFailed | ||
case exceptionRaised(name: String, reason: String?, userInfo: [AnyHashable: Any]?) | ||
} | ||
|
||
internal func executeRequire<T>(_ expression: Expression<T>, _ style: ExpectationStyle, _ matcher: Matcher<T>, to: String, description: String?, captureExceptions: Bool = true) -> (Bool, FailureMessage, T?) { | ||
func run() -> (Bool, FailureMessage, T?) { | ||
let msg = FailureMessage() | ||
msg.userDescription = description | ||
msg.to = to | ||
do { | ||
let result = try matcher.satisfies(expression) | ||
let value = try expression.evaluate() | ||
result.message.update(failureMessage: msg) | ||
if msg.actualValue == "" { | ||
msg.actualValue = "<\(stringify(value))>" | ||
} | ||
return (result.toBoolean(expectation: style), msg, value) | ||
} catch let error { | ||
msg.stringValue = "unexpected error thrown: <\(error)>" | ||
return (false, msg, nil) | ||
} | ||
} | ||
|
||
var result: (Bool, FailureMessage, T?) = (false, FailureMessage(), nil) | ||
if captureExceptions { | ||
let capture = NMBExceptionCapture(handler: ({ exception -> Void in | ||
let msg = FailureMessage() | ||
msg.stringValue = "unexpected exception raised: \(exception)" | ||
result = (false, msg, nil) | ||
}), finally: nil) | ||
capture.tryBlock { | ||
result = run() | ||
} | ||
} else { | ||
result = run() | ||
} | ||
|
||
return result | ||
} | ||
|
||
internal func executeRequire<T>(_ expression: AsyncExpression<T>, _ style: ExpectationStyle, _ matcher: AsyncMatcher<T>, to: String, description: String?) async -> (Bool, FailureMessage, T?) { | ||
let msg = FailureMessage() | ||
msg.userDescription = description | ||
msg.to = to | ||
do { | ||
let result = try await matcher.satisfies(expression) | ||
let value = try await expression.evaluate() | ||
result.message.update(failureMessage: msg) | ||
if msg.actualValue == "" { | ||
msg.actualValue = "<\(stringify(value))>" | ||
} | ||
return (result.toBoolean(expectation: style), msg, value) | ||
} catch let error { | ||
msg.stringValue = "unexpected error thrown: <\(error)>" | ||
return (false, msg, nil) | ||
} | ||
} | ||
|
||
import XCTest | ||
|
||
public struct SyncRequirement<Value> { | ||
public let expression: Expression<Value> | ||
public let status: ExpectationStatus | ||
|
||
public var location: SourceLocation { expression.location } | ||
|
||
private init(expression: Expression<Value>, status: ExpectationStatus) { | ||
self.expression = expression | ||
self.status = status | ||
} | ||
|
||
public init(expression: Expression<Value>) { | ||
self.init(expression: expression, status: .pending) | ||
} | ||
|
||
@discardableResult | ||
public func verify(_ pass: Bool, _ message: FailureMessage, _ value: Value?) throws -> Value { | ||
let handler = NimbleEnvironment.activeInstance.assertionHandler | ||
handler.require(pass, message: message, location: expression.location) | ||
guard pass, let value else { | ||
throw RequirementError(message: message.stringValue, location: self.location) | ||
} | ||
return value | ||
|
||
// return try value.get() | ||
} | ||
|
||
/// Tests the actual value using a matcher to match. | ||
@discardableResult | ||
public func to(_ matcher: Matcher<Value>, description: String? = nil) throws -> Value { | ||
let (pass, msg, result) = executeRequire(expression, .toMatch, matcher, to: "to", description: description) | ||
return try verify(pass, msg, result) | ||
} | ||
|
||
/// Tests the actual value using a matcher to not match. | ||
@discardableResult | ||
public func toNot(_ matcher: Matcher<Value>, description: String? = nil) throws -> Value { | ||
let (pass, msg, result) = executeRequire(expression, .toNotMatch, matcher, to: "to not", description: description) | ||
return try verify(pass, msg, result) | ||
} | ||
|
||
/// Tests the actual value using a matcher to not match. | ||
/// | ||
/// Alias to toNot(). | ||
@discardableResult | ||
public func notTo(_ matcher: Matcher<Value>, description: String? = nil) throws -> Value { | ||
try toNot(matcher, description: description) | ||
} | ||
|
||
// MARK: - AsyncMatchers | ||
/// Tests the actual value using a matcher to match. | ||
@discardableResult | ||
public func to(_ matcher: AsyncMatcher<Value>, description: String? = nil) async throws -> Value { | ||
let (pass, msg, result) = await executeRequire(expression.toAsyncExpression(), .toMatch, matcher, to: "to", description: description) | ||
return try verify(pass, msg, result) | ||
} | ||
|
||
/// Tests the actual value using a matcher to not match. | ||
@discardableResult | ||
public func toNot(_ matcher: AsyncMatcher<Value>, description: String? = nil) async throws -> Value { | ||
let (pass, msg, result) = await executeRequire(expression.toAsyncExpression(), .toNotMatch, matcher, to: "to not", description: description) | ||
return try verify(pass, msg, result) | ||
} | ||
|
||
/// Tests the actual value using a matcher to not match. | ||
/// | ||
/// Alias to toNot(). | ||
@discardableResult | ||
public func notTo(_ matcher: AsyncMatcher<Value>, description: String? = nil) async throws -> Value { | ||
try await toNot(matcher, description: description) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters