diff --git a/Sources/RequirementsKit/RequirementsTestConfiguration.swift b/Sources/RequirementsKit/RequirementsTestConfiguration.swift index 7ab8004..ee16320 100644 --- a/Sources/RequirementsKit/RequirementsTestConfiguration.swift +++ b/Sources/RequirementsKit/RequirementsTestConfiguration.swift @@ -78,6 +78,10 @@ fileprivate extension XCTestCase { var issue = issue let context = XCTSourceCodeContext(callStack: issue.sourceCodeContext.callStack, location: .init(fileURL: runner.file.url, lineNumber: line)) issue.sourceCodeContext = context + if runner.setupActive { + issue.compactDescription = "Failure in setup closure: " + issue.compactDescription + issue.detailedDescription = issue.detailedDescription.map { "Failure in setup closure: " + $0 } + } self.modifiedRecord(issue) } self.modifiedRecord(issue) diff --git a/Sources/RequirementsKit/RequirementsTestRunner.swift b/Sources/RequirementsKit/RequirementsTestRunner.swift index 7648d3c..230e663 100644 --- a/Sources/RequirementsKit/RequirementsTestRunner.swift +++ b/Sources/RequirementsKit/RequirementsTestRunner.swift @@ -31,6 +31,7 @@ class RequirementTestRunner: NSObject, XCTestObservation { var requirement: Requirement var hasFailed = false var continueAfterFailure = false + var setupActive = false var currentStatement: Requirement.Example.Statement? var shouldDisplayFailuresOnRequirement: Bool = true let matchLabels: LabelExpression? @@ -68,6 +69,30 @@ class RequirementTestRunner: NSObject, XCTestObservation { } XCTContext.runActivity(named: "💡 " + example.activity(syntax: file.syntax)) { _ in self.beforeEachExample?(example) + let setups = statementHandlers.filter { $0.setup != nil } + setups.forEach { handler in + if let match = example.statements.first(where: { + $0.type == handler.type && handler.getMatch($0) != nil + }) { + currentStatement = match + setupActive = true + defer { setupActive = false } + do { + let timeoutWorkItem = DispatchWorkItem { + XCTFail("Statement setup timed out after \(handler.timeout ?? timeout) seconds") + } + self.timeoutDispatchWorkItem?.cancel() + self.timeoutDispatchWorkItem = timeoutWorkItem + DispatchQueue.global().asyncAfter(deadline: .now() + (handler.timeout ?? timeout), execute: timeoutWorkItem) + try handler.setup?(handler.getMatch(match)) + } catch { + XCTFail(error.localizedDescription) + if !continueAfterFailure { + hasFailed = true + } + } + } + } for statement in example.statements { guard !self.hasFailed || continueAfterFailure else { break } currentStatement = statement diff --git a/Sources/RequirementsKit/StatementHandler.swift b/Sources/RequirementsKit/StatementHandler.swift index 62a268c..9323318 100644 --- a/Sources/RequirementsKit/StatementHandler.swift +++ b/Sources/RequirementsKit/StatementHandler.swift @@ -37,17 +37,19 @@ public struct StatementHandler { let timeout: TimeInterval? let getMatch: (Requirement.Example.Statement) -> Any? let action: (Any?) throws -> Void + let setup: ((Any?) throws -> Void)? - fileprivate init(statementType: Requirement.Example.StatementType, statement: Regex, timeout: TimeInterval?, handler: @escaping (Input) throws -> Void) { + fileprivate init(statementType: Requirement.Example.StatementType, statement: Regex, timeout: TimeInterval?, handler: @escaping (Input) throws -> Void, setup: ((Input) throws -> Void)?) { type = statementType getMatch = { exampleStatement in exampleStatement.description.wholeMatch(of: statement).map { Input(statement: exampleStatement, match: $0.output) } } self.action = { try handler($0 as! Input) } self.timeout = timeout + self.setup = { try setup?($0 as! Input) } } - 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, setup: ((Input) throws -> Void)?) { type = statementType getMatch = { exampleStatement in @@ -55,53 +57,100 @@ public struct StatementHandler { } self.action = { try handler($0 as! Input) } self.timeout = timeout + self.setup = { try setup?($0 as! Input) } } } public extension StatementHandler { static func `if`(_ statement: Regex, timeout: TimeInterval? = nil, handler: @escaping (Input) throws -> Void) -> StatementHandler { - .init(statementType: .if, statement: statement, timeout: timeout, handler: handler) + .init(statementType: .if, statement: statement, timeout: timeout, handler: handler, setup: nil) } static func `if`(_ statement: String..., timeout: TimeInterval? = nil, handler: @escaping (Input) throws -> Void) -> StatementHandler { - .init(statementType: .if, statement: statement, timeout: timeout, handler: handler) + .init(statementType: .if, statement: statement, timeout: timeout, handler: handler, setup: nil) } static func `if`(_ statement: String..., timeout: TimeInterval? = nil, handler: @escaping () throws -> Void) -> StatementHandler { - .init(statementType: .if, statement: statement, timeout: timeout, handler: { _ in try handler() }) + .init(statementType: .if, statement: statement, timeout: timeout, handler: { _ in try handler() }, setup: nil) } static func given(_ statement: Regex, timeout: TimeInterval? = nil, handler: @escaping (Input) throws -> Void) -> StatementHandler { .if(statement, timeout: timeout, handler: handler) } static func given(_ statement: String..., timeout: TimeInterval? = nil, handler: @escaping (Input) throws -> Void) -> StatementHandler { - .init(statementType: .if, statement: statement, timeout: timeout, handler: handler) + .init(statementType: .if, statement: statement, timeout: timeout, handler: handler, setup: nil) } static func given(_ statement: String..., timeout: TimeInterval? = nil, handler: @escaping () throws -> Void) -> StatementHandler { - .init(statementType: .if, statement: statement, timeout: timeout, handler: { _ in try handler() }) + .init(statementType: .if, statement: statement, timeout: timeout, handler: { _ in try handler() }, setup: nil) } static func when(_ statement: Regex, timeout: TimeInterval? = nil, handler: @escaping (Input) throws -> Void) -> StatementHandler { - .init(statementType: .when, statement: statement, timeout: timeout, handler: handler) + .init(statementType: .when, statement: statement, timeout: timeout, handler: handler, setup: nil) } static func when(_ statement: String..., timeout: TimeInterval? = nil, handler: @escaping (Input) throws -> Void) -> StatementHandler { - .init(statementType: .when, statement: statement, timeout: timeout, handler: handler) + .init(statementType: .when, statement: statement, timeout: timeout, handler: handler, setup: nil) } static func when(_ statement: String..., timeout: TimeInterval? = nil, handler: @escaping () throws -> Void) -> StatementHandler { - .init(statementType: .when, statement: statement, timeout: timeout, handler: { _ in try handler() }) + .init(statementType: .when, statement: statement, timeout: timeout, handler: { _ in try handler() }, setup: nil) } static func expect(_ statement: Regex, timeout: TimeInterval? = nil, handler: @escaping (Input) throws -> Void) -> StatementHandler { - .init(statementType: .expect, statement: statement, timeout: timeout, handler: handler) + .init(statementType: .expect, statement: statement, timeout: timeout, handler: handler, setup: nil) } - 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 expect(_ statement: Regex, timeout: TimeInterval? = nil, handler: @escaping (Input) throws -> Void, setup: @escaping (Input) throws -> Void) -> StatementHandler { + .init(statementType: .expect, statement: statement, timeout: timeout, handler: handler, setup: setup) } - 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 expect(_ statement: Regex, timeout: TimeInterval? = nil, handler: @escaping (Input) throws -> Void, setup: @escaping () throws -> Void) -> StatementHandler { + .init(statementType: .expect, statement: statement, timeout: timeout, handler: handler, setup: { _ in try setup() }) + } + static func expect(_ statement: Regex, timeout: TimeInterval? = nil, handler: @escaping () throws -> Void, setup: @escaping (Input) throws -> Void) -> StatementHandler { + .init(statementType: .expect, statement: statement, timeout: timeout, handler: { _ in try handler() }, setup: setup) + } + static func expect(_ statement: Regex, timeout: TimeInterval? = nil, handler: @escaping () throws -> Void, setup: @escaping () throws -> Void) -> StatementHandler { + .init(statementType: .expect, statement: statement, timeout: timeout, handler: { _ in try handler() }, setup: { _ in try setup() }) + } + static func expect(_ statement: Regex, timeout: TimeInterval? = nil, handler: @escaping () throws -> Void) -> StatementHandler { + .init(statementType: .expect, statement: statement, timeout: timeout, handler: { _ in try handler() }, setup: nil) + } + static func expect(_ statement: String..., timeout: TimeInterval? = nil, handler: @escaping (Input) throws -> Void, setup: ((Input) throws -> Void)? = nil) -> StatementHandler { + .init(statementType: .expect, statement: statement, timeout: timeout, handler: handler, setup: setup) + } + static func expect(_ statement: String..., timeout: TimeInterval? = nil, handler: @escaping () throws -> Void, setup: ((Input) throws -> Void)? = nil) -> StatementHandler { + .init(statementType: .expect, statement: statement, timeout: timeout, handler: {_ in try handler() }, setup: setup) + } + static func expect(_ statement: String..., timeout: TimeInterval? = nil, handler: @escaping (Input) throws -> Void, setup: (() throws -> Void)? = nil) -> StatementHandler { + .init(statementType: .expect, statement: statement, timeout: timeout, handler: handler, setup: { _ in try setup?() }) + } + static func expect(_ statement: String..., timeout: TimeInterval? = nil, handler: @escaping () throws -> Void, setup: (() throws -> Void)? = nil) -> StatementHandler { + .init(statementType: .expect, statement: statement, timeout: timeout, handler: { _ in try handler() }, setup: { _ in try setup?() }) + } + static func then(_ statement: Regex, timeout: TimeInterval? = nil, handler: @escaping (Input) throws -> Void, setup: @escaping (Input) throws -> Void) -> StatementHandler { + .expect(statement, timeout: timeout, handler: handler, setup: setup) + } + static func then(_ statement: Regex, timeout: TimeInterval? = nil, handler: @escaping () throws -> Void, setup: @escaping (Input) throws -> Void) -> StatementHandler { + .expect(statement, timeout: timeout, handler: { _ in try handler() }, setup: setup) + } + static func then(_ statement: Regex, timeout: TimeInterval? = nil, handler: @escaping () throws -> Void, setup: @escaping () throws -> Void) -> StatementHandler { + .expect(statement, timeout: timeout, handler: { _ in try handler() }, setup: { _ in try setup() }) + } + static func then(_ statement: Regex, timeout: TimeInterval? = nil, handler: @escaping (Input) throws -> Void, setup: @escaping () throws -> Void) -> StatementHandler { + .expect(statement, timeout: timeout, handler: handler, setup: { _ in try setup() }) } 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, setup: @escaping (Input) throws -> Void) -> StatementHandler { + .init(statementType: .expect, statement: statement, timeout: timeout, handler: handler, setup: setup) + } + static func then(_ statement: String..., timeout: TimeInterval? = nil, handler: @escaping () throws -> Void, setup: @escaping (Input) throws -> Void) -> StatementHandler { + .init(statementType: .expect, statement: statement, timeout: timeout, handler: { _ in try handler() }, setup: setup) + } + static func then(_ statement: String..., timeout: TimeInterval? = nil, handler: @escaping (Input) throws -> Void, setup: @escaping () throws -> Void) -> StatementHandler { + .init(statementType: .expect, statement: statement, timeout: timeout, handler: handler, setup: { _ in try setup() }) + } static func then(_ statement: String..., timeout: TimeInterval? = nil, handler: @escaping (Input) throws -> Void) -> StatementHandler { - .init(statementType: .expect, statement: statement, timeout: timeout, handler: handler) + .init(statementType: .expect, statement: statement, timeout: timeout, handler: handler, setup: nil) } + static func then(_ statement: String..., timeout: TimeInterval? = nil, handler: @escaping () throws -> Void, setup: @escaping () throws -> Void) -> StatementHandler { + .init(statementType: .expect, statement: statement, timeout: timeout, handler: { _ in try handler() }, setup: { _ in try setup() }) + } + static func then(_ statement: String..., timeout: TimeInterval? = nil, handler: @escaping () throws -> Void) -> StatementHandler { - .init(statementType: .expect, statement: statement, timeout: timeout, handler: { _ in try handler() }) + .init(statementType: .expect, statement: statement, timeout: timeout, handler: { _ in try handler() }, setup: nil) } }