From 20cde07d383c469cb6f6cb447c1bd34fe9d71595 Mon Sep 17 00:00:00 2001 From: Daniel Hall Date: Mon, 3 Apr 2023 22:49:48 -0600 Subject: [PATCH] Various improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 } --- .../Parsing/GherkinParser.swift | 12 ++--- .../RequirementsTestRunner.swift | 13 +++++- .../RequirementsKit/StatementHandler.swift | 46 +++++++++++++------ .../XCTestCase+Extensions.swift | 12 ++--- 4 files changed, 55 insertions(+), 28 deletions(-) diff --git a/Sources/RequirementsKit/Parsing/GherkinParser.swift b/Sources/RequirementsKit/Parsing/GherkinParser.swift index 3449268..b4296fa 100644 --- a/Sources/RequirementsKit/Parsing/GherkinParser.swift +++ b/Sources/RequirementsKit/Parsing/GherkinParser.swift @@ -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 { diff --git a/Sources/RequirementsKit/RequirementsTestRunner.swift b/Sources/RequirementsKit/RequirementsTestRunner.swift index 931fcb5..fd07484 100644 --- a/Sources/RequirementsKit/RequirementsTestRunner.swift +++ b/Sources/RequirementsKit/RequirementsTestRunner.swift @@ -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) @@ -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 { @@ -99,6 +107,9 @@ class RequirementsTestRunner: NSObject, XCTestObservation { try matches.first?.action(matches.first?.getMatch(statement)) } catch { XCTFail(error.localizedDescription) + if !continueAfterFailure { + hasFailed = true + } } } } diff --git a/Sources/RequirementsKit/StatementHandler.swift b/Sources/RequirementsKit/StatementHandler.swift index e95c5aa..62a268c 100644 --- a/Sources/RequirementsKit/StatementHandler.swift +++ b/Sources/RequirementsKit/StatementHandler.swift @@ -47,45 +47,61 @@ public struct StatementHandler { self.timeout = timeout } - fileprivate init(statementType: Requirement.Example.StatementType, statement: String, timeout: TimeInterval?, handler: @escaping (Input) throws -> Void) { + fileprivate init(statementType: Requirement.Example.StatementType, statement: [String], timeout: TimeInterval?, handler: @escaping (Input) throws -> Void) { type = statementType getMatch = { exampleStatement in - exampleStatement.description.wholeMatch(of: Regex(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) } + self.action = { try handler($0 as! Input) } self.timeout = timeout } } public extension StatementHandler { - static func `if`(_ statement: Regex, timeout: TimeInterval? = nil, handler: @escaping (Input) -> Void) -> StatementHandler { + static func `if`(_ statement: Regex, timeout: TimeInterval? = nil, handler: @escaping (Input) throws -> Void) -> StatementHandler { .init(statementType: .if, statement: statement, timeout: timeout, handler: handler) } - static func `if`(_ statement: String, timeout: TimeInterval? = nil, handler: @escaping (Input) -> Void) -> StatementHandler { + static func `if`(_ statement: String..., timeout: TimeInterval? = nil, handler: @escaping (Input) throws -> Void) -> StatementHandler { .init(statementType: .if, statement: statement, timeout: timeout, handler: handler) } - static func given(_ statement: Regex, timeout: TimeInterval? = nil, handler: @escaping (Input) -> 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) -> Void) -> StatementHandler { + static func given(_ statement: Regex, timeout: TimeInterval? = nil, handler: @escaping (Input) throws -> Void) -> StatementHandler { .if(statement, timeout: timeout, handler: handler) } - static func when(_ statement: Regex, timeout: TimeInterval? = nil, handler: @escaping (Input) -> Void) -> StatementHandler { + static func given(_ statement: String..., timeout: TimeInterval? = nil, handler: @escaping (Input) 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(_ statement: Regex, timeout: TimeInterval? = nil, handler: @escaping (Input) throws -> Void) -> StatementHandler { .init(statementType: .when, statement: statement, timeout: timeout, handler: handler) } - static func when(_ statement: String, timeout: TimeInterval? = nil, handler: @escaping (Input) -> Void) -> StatementHandler { + static func when(_ statement: String..., timeout: TimeInterval? = nil, handler: @escaping (Input) throws -> Void) -> StatementHandler { .init(statementType: .when, statement: statement, timeout: timeout, handler: handler) } - static func expect(_ statement: Regex, timeout: TimeInterval? = nil, handler: @escaping (Input) -> 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(_ statement: Regex, timeout: TimeInterval? = nil, handler: @escaping (Input) throws -> Void) -> StatementHandler { .init(statementType: .expect, statement: statement, timeout: timeout, handler: handler) } - static func expect(_ statement: String, timeout: TimeInterval? = nil, handler: @escaping (Input) -> Void) -> StatementHandler { + static func expect(_ statement: String..., timeout: TimeInterval? = nil, handler: @escaping (Input) throws -> Void) -> StatementHandler { .init(statementType: .expect, statement: statement, timeout: timeout, handler: handler) } - static func then(_ statement: Regex, timeout: TimeInterval? = nil, handler: @escaping (Input) -> 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) -> Void) -> StatementHandler { + static func then(_ statement: Regex, timeout: TimeInterval? = nil, handler: @escaping (Input) throws -> Void) -> StatementHandler { .expect(statement, timeout: timeout, handler: handler) } + static func then(_ statement: String..., timeout: TimeInterval? = nil, handler: @escaping (Input) 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() }) + } } diff --git a/Sources/RequirementsKit/XCTestCase+Extensions.swift b/Sources/RequirementsKit/XCTestCase+Extensions.swift index 1c58ce3..b326bd7 100644 --- a/Sources/RequirementsKit/XCTestCase+Extensions.swift +++ b/Sources/RequirementsKit/XCTestCase+Extensions.swift @@ -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) @@ -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 { @@ -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] @@ -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) } }