Skip to content

Commit

Permalink
Various improvements
Browse files Browse the repository at this point in the history
- Default the “in directory” parameter to nil for the most common case of just discovering all requirements

- Call the beforeEachExample closure before each Example is run

- StatementHandler can now be created with one of more String statements to match

- Fixed the static factory methods on StatementHandler to take throwing closures

- Added factory method overloads for accepting handler closures that don’t use any input. This is a very common case that should always require explicitly ignoring an input parameter, e.g. { _ in }
  • Loading branch information
daniel-hall committed Apr 4, 2023
1 parent c8c8898 commit 20cde07
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 28 deletions.
12 changes: 6 additions & 6 deletions Sources/RequirementsKit/Parsing/GherkinParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -182,12 +182,12 @@ private let parseStatementKeyword = Parser<(Requirement.Example.StatementType, S
switch trimmed.prefix(upTo: trimmed.firstIndex(of: " ") ?? trimmed.startIndex) {
case "Given":
type = .if
case "When":
type = .when
case "Then":
type = .expect
default:
throw "No Given, When, or Then keyword found"
case "When":
type = .when
case "Then":
type = .expect
default:
throw "No Given, When, or Then keyword found"
}
let description = trimmed.drop { $0 != " " }.dropFirst().trimmingCharacters(in: .whitespaces)
guard !description.isEmpty else {
Expand Down
13 changes: 12 additions & 1 deletion Sources/RequirementsKit/RequirementsTestRunner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,14 @@ class RequirementsTestRunner: NSObject, XCTestObservation {
}
}

func run(timeout: TimeInterval = 180, continueAfterFailure: Bool = false, beforeEachExample: (Requirement.Example) -> Void) {
func run(timeout: TimeInterval = 180, continueAfterFailure: Bool = false, beforeEachExample: ((Requirement.Example) -> Void)? = nil) {
guard Self.current == nil else { fatalError("Can't start running a RequirementsTestRunner when there is already one running") }
Self.current = self

self.timeout = timeout
self.hasFailed = false
self.continueAfterFailure = true
self.beforeEachExample = beforeEachExample

XCTestObservationCenter.shared.addTestObserver(self)

Expand All @@ -80,14 +81,21 @@ class RequirementsTestRunner: NSObject, XCTestObservation {
}
}
XCTContext.runActivity(named: example.activity(syntax: file.syntax)) { _ in
self.beforeEachExample?(example)
for statement in example.statements {
guard !self.hasFailed || continueAfterFailure else { break }
XCTContext.runActivity(named: statement.activity(syntax: file.syntax)) { _ in
let matches = self.statementHandlers.filter { $0.type == statement.type && $0.getMatch(statement) != nil }
if matches.isEmpty {
XCTFail("No StatementHandler provided for the statement '\(statement.activity(syntax: file.syntax))'")
if !continueAfterFailure {
hasFailed = true
}
} else if matches.count > 1 {
XCTFail("Multiple matching StatementHandlers provided for the statement '\(statement.activity(syntax: file.syntax))'")
if !continueAfterFailure {
hasFailed = true
}
} else {
do {
let timeoutWorkItem = DispatchWorkItem {
Expand All @@ -99,6 +107,9 @@ class RequirementsTestRunner: NSObject, XCTestObservation {
try matches.first?.action(matches.first?.getMatch(statement))
} catch {
XCTFail(error.localizedDescription)
if !continueAfterFailure {
hasFailed = true
}
}
}
}
Expand Down
46 changes: 31 additions & 15 deletions Sources/RequirementsKit/StatementHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,45 +47,61 @@ public struct StatementHandler {
self.timeout = timeout
}

fileprivate init(statementType: Requirement.Example.StatementType, statement: String, timeout: TimeInterval?, handler: @escaping (Input<Substring>) throws -> Void) {
fileprivate init(statementType: Requirement.Example.StatementType, statement: [String], timeout: TimeInterval?, handler: @escaping (Input<AnyRegexOutput>) throws -> Void) {
type = statementType
getMatch = { exampleStatement in
exampleStatement.description.wholeMatch(of: Regex<Substring>(verbatim: statement)).map { Input(statement: exampleStatement, match: $0.output) }

exampleStatement.description.wholeMatch(of: try! Regex("(" + statement.joined(separator: "|") + ")")).map { Input(statement: exampleStatement, match: AnyRegexOutput($0)) }
}
self.action = { try handler($0 as! Input<Substring>) }
self.action = { try handler($0 as! Input<AnyRegexOutput>) }
self.timeout = timeout
}
}

public extension StatementHandler {
static func `if`<T>(_ statement: Regex<T>, timeout: TimeInterval? = nil, handler: @escaping (Input<T>) -> Void) -> StatementHandler {
static func `if`<T>(_ statement: Regex<T>, timeout: TimeInterval? = nil, handler: @escaping (Input<T>) throws -> Void) -> StatementHandler {
.init(statementType: .if, statement: statement, timeout: timeout, handler: handler)
}
static func `if`(_ statement: String, timeout: TimeInterval? = nil, handler: @escaping (Input<Substring>) -> Void) -> StatementHandler {
static func `if`(_ statement: String..., timeout: TimeInterval? = nil, handler: @escaping (Input<AnyRegexOutput>) throws -> Void) -> StatementHandler {
.init(statementType: .if, statement: statement, timeout: timeout, handler: handler)
}
static func given<T>(_ statement: Regex<T>, timeout: TimeInterval? = nil, handler: @escaping (Input<T>) -> Void) -> StatementHandler {
.if(statement, timeout: timeout, handler: handler)
static func `if`(_ statement: String..., timeout: TimeInterval? = nil, handler: @escaping () throws -> Void) -> StatementHandler {
.init(statementType: .if, statement: statement, timeout: timeout, handler: { _ in try handler() })
}
static func given(_ statement: String, timeout: TimeInterval? = nil, handler: @escaping (Input<Substring>) -> Void) -> StatementHandler {
static func given<T>(_ statement: Regex<T>, timeout: TimeInterval? = nil, handler: @escaping (Input<T>) throws -> Void) -> StatementHandler {
.if(statement, timeout: timeout, handler: handler)
}
static func when<T>(_ statement: Regex<T>, timeout: TimeInterval? = nil, handler: @escaping (Input<T>) -> Void) -> StatementHandler {
static func given(_ statement: String..., timeout: TimeInterval? = nil, handler: @escaping (Input<AnyRegexOutput>) throws -> Void) -> StatementHandler {
.init(statementType: .if, statement: statement, timeout: timeout, handler: handler)
}
static func given(_ statement: String..., timeout: TimeInterval? = nil, handler: @escaping () throws -> Void) -> StatementHandler {
.init(statementType: .if, statement: statement, timeout: timeout, handler: { _ in try handler() })
}
static func when<T>(_ statement: Regex<T>, timeout: TimeInterval? = nil, handler: @escaping (Input<T>) throws -> Void) -> StatementHandler {
.init(statementType: .when, statement: statement, timeout: timeout, handler: handler)
}
static func when(_ statement: String, timeout: TimeInterval? = nil, handler: @escaping (Input<Substring>) -> Void) -> StatementHandler {
static func when(_ statement: String..., timeout: TimeInterval? = nil, handler: @escaping (Input<AnyRegexOutput>) throws -> Void) -> StatementHandler {
.init(statementType: .when, statement: statement, timeout: timeout, handler: handler)
}
static func expect<T>(_ statement: Regex<T>, timeout: TimeInterval? = nil, handler: @escaping (Input<T>) -> Void) -> StatementHandler {
static func when(_ statement: String..., timeout: TimeInterval? = nil, handler: @escaping () throws -> Void) -> StatementHandler {
.init(statementType: .when, statement: statement, timeout: timeout, handler: { _ in try handler() })
}
static func expect<T>(_ statement: Regex<T>, timeout: TimeInterval? = nil, handler: @escaping (Input<T>) throws -> Void) -> StatementHandler {
.init(statementType: .expect, statement: statement, timeout: timeout, handler: handler)
}
static func expect(_ statement: String, timeout: TimeInterval? = nil, handler: @escaping (Input<Substring>) -> Void) -> StatementHandler {
static func expect(_ statement: String..., timeout: TimeInterval? = nil, handler: @escaping (Input<AnyRegexOutput>) throws -> Void) -> StatementHandler {
.init(statementType: .expect, statement: statement, timeout: timeout, handler: handler)
}
static func then<T>(_ statement: Regex<T>, timeout: TimeInterval? = nil, handler: @escaping (Input<T>) -> Void) -> StatementHandler {
.expect(statement, timeout: timeout, handler: handler)
static func expect(_ statement: String..., timeout: TimeInterval? = nil, handler: @escaping () throws -> Void) -> StatementHandler {
.init(statementType: .expect, statement: statement, timeout: timeout, handler: { _ in try handler() })
}
static func then(_ statement: String, timeout: TimeInterval? = nil, handler: @escaping (Input<Substring>) -> Void) -> StatementHandler {
static func then<T>(_ statement: Regex<T>, timeout: TimeInterval? = nil, handler: @escaping (Input<T>) throws -> Void) -> StatementHandler {
.expect(statement, timeout: timeout, handler: handler)
}
static func then(_ statement: String..., timeout: TimeInterval? = nil, handler: @escaping (Input<AnyRegexOutput>) throws -> Void) -> StatementHandler {
.init(statementType: .expect, statement: statement, timeout: timeout, handler: handler)
}
static func then(_ statement: String..., timeout: TimeInterval? = nil, handler: @escaping () throws -> Void) -> StatementHandler {
.init(statementType: .expect, statement: statement, timeout: timeout, handler: { _ in try handler() })
}
}
12 changes: 6 additions & 6 deletions Sources/RequirementsKit/XCTestCase+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import XCTest

public extension XCTestCase {

func testRequirements(from file: File, statementHandlers: [StatementHandler], matching: LabelExpression? = nil, continueAfterFailure: Bool = false, timeout: TimeInterval = 180, beforeEachExample: @escaping (Requirement.Example) -> Void) {
func testRequirements(from file: File, statementHandlers: [StatementHandler], matching: LabelExpression? = nil, continueAfterFailure: Bool = false, timeout: TimeInterval = 180, beforeEachExample: ((Requirement.Example) -> Void)? = nil) {
let runner = RequirementsTestRunner(file: file, statementHandlers: statementHandlers, matching: matching)
if Thread.isMainThread {
runner.run(timeout: timeout, continueAfterFailure: continueAfterFailure, beforeEachExample: beforeEachExample)
Expand All @@ -39,12 +39,12 @@ public extension XCTestCase {
}
}

func testRequirements(from url: URL, statementHandlers: [StatementHandler], matching: LabelExpression? = nil, continueAfterFailure: Bool = false, timeout: TimeInterval = 180, beforeEachExample: @escaping (Requirement.Example) -> Void) throws {
func testRequirements(from url: URL, statementHandlers: [StatementHandler], matching: LabelExpression? = nil, continueAfterFailure: Bool = false, timeout: TimeInterval = 180, beforeEachExample: ((Requirement.Example) -> Void)? = nil) throws {
let file = try File.parseFrom(url: url)
testRequirements(from: file, statementHandlers: statementHandlers, beforeEachExample: beforeEachExample)
testRequirements(from: file, statementHandlers: statementHandlers, matching: matching, continueAfterFailure: continueAfterFailure, timeout: timeout, beforeEachExample: beforeEachExample)
}

func testRequirements(from urls: [URL], statementHandlers: [StatementHandler], matching: LabelExpression? = nil, continueAfterFailure: Bool = false, timeout: TimeInterval = 180, beforeEachExample: @escaping (Requirement.Example) -> Void) throws {
func testRequirements(from urls: [URL], statementHandlers: [StatementHandler], matching: LabelExpression? = nil, continueAfterFailure: Bool = false, timeout: TimeInterval = 180, beforeEachExample: ((Requirement.Example) -> Void)? = nil) throws {
let files = try urls.map { try File.parseFrom(url: $0) }
let runner = RequirementsTestRunner(files: files, statementHandlers: statementHandlers, matching: matching)
if Thread.isMainThread {
Expand All @@ -56,7 +56,7 @@ public extension XCTestCase {
}
}

func testRequirements(in directory: String?, recursively: Bool = true, statementHandlers: [StatementHandler], matching: LabelExpression? = nil, continueAfterFailure: Bool = false, timeout: TimeInterval = 180, beforeEachExample: @escaping (Requirement.Example) -> Void) throws {
func testRequirements(in directory: String? = nil, recursively: Bool = true, statementHandlers: [StatementHandler], matching: LabelExpression? = nil, continueAfterFailure: Bool = false, timeout: TimeInterval = 180, beforeEachExample: ((Requirement.Example) -> Void)? = nil) throws {
let bundle = Bundle(for: type(of: self))
let subdirectoryPath = directory.map { "/\($0)" } ?? ""
let urls: [URL]
Expand All @@ -78,7 +78,7 @@ public extension XCTestCase {
}
urls = recursiveURLs
}
try testRequirements(from: urls, statementHandlers: statementHandlers, beforeEachExample: beforeEachExample)
try testRequirements(from: urls, statementHandlers: statementHandlers, matching: matching, continueAfterFailure: continueAfterFailure, timeout: timeout, beforeEachExample: beforeEachExample)
}
}

Expand Down

0 comments on commit 20cde07

Please sign in to comment.