From 941ff70dc28d702e45eee58efb99eb4da83e8473 Mon Sep 17 00:00:00 2001 From: Yauheni Khnykin Date: Mon, 6 Feb 2023 12:03:27 +0100 Subject: [PATCH 1/3] Adds function ExecuteFirstStep to run step which matches given definition. - Adds lambda matchesExpression to Step class to match step definition with chosen matcher (CucumberExpression, string regex, regex object) - Refactors attachClosureToSteps functions to extract common logic into single function --- .../CucumberSwift/Gherkin/Parser/Step.swift | 11 +- Sources/CucumberSwift/Runner/Cucumber.swift | 124 +++++----- .../CucumberSwift/Runner/CucumberTest.swift | 2 +- Sources/CucumberSwift/Runner/Globals.swift | 4 + .../CucumberSwiftTests.swift | 216 +++++++++++++++++- 5 files changed, 292 insertions(+), 65 deletions(-) diff --git a/Sources/CucumberSwift/Gherkin/Parser/Step.swift b/Sources/CucumberSwift/Gherkin/Parser/Step.swift index d2df2fb7..ee274a95 100644 --- a/Sources/CucumberSwift/Gherkin/Parser/Step.swift +++ b/Sources/CucumberSwift/Gherkin/Parser/Step.swift @@ -35,12 +35,15 @@ public class Step: CustomStringConvertible { public private(set) var location: Lexer.Position public internal(set) var testCase: XCTestCase? + typealias MatchesExpression = ((_ str: String) -> Bool) + typealias Execute = ((_ match: String, _ steps: Step) throws -> Void) + var result: Reporter.Result = .pending - var execute: (() throws -> Void)? + var execute: Execute? var executeSelector: Selector? var executeClass: AnyClass? var executeInstance: NSObject? - var regex: String = "" + var matchesExpression: MatchesExpression? var errorMessage: String = "" var startTime: Date? var endTime: Date? @@ -99,8 +102,8 @@ public class Step: CustomStringConvertible { init(with execute: @escaping (([String], Step) -> Void), match: String?, position: Lexer.Position) { location = position self.match ?= match - self.execute = { - execute(self.match.matches(for: self.regex), self) + self.execute = { match, step in + execute(self.match.matches(for: ""), step) } } diff --git a/Sources/CucumberSwift/Runner/Cucumber.swift b/Sources/CucumberSwift/Runner/Cucumber.swift index 4e1f6bd1..7b7c8e13 100644 --- a/Sources/CucumberSwift/Runner/Cucumber.swift +++ b/Sources/CucumberSwift/Runner/Cucumber.swift @@ -177,51 +177,79 @@ import CucumberSwift_ObjC .map { Feature(with: $0, uri: uri) }) } - func attachClosureToSteps(keyword: Step.Keyword? = nil, regex: String, callback: @escaping (([String], Step) throws -> Void), line: Int, file: StaticString) { - features + func executeFirstStep(keyword: Step.Keyword? = nil, matching: String) { + let firstMatchingStep = features .flatMap { $0.scenarios.flatMap { $0.steps } } - .filter { step -> Bool in + .first {step -> Bool in if let k = keyword, step.keyword.contains(k) { - return !step.match.matches(for: regex).isEmpty + return step.matchesExpression?(matching) == true } else if keyword == nil { - return !step.match.matches(for: regex).isEmpty + return step.matchesExpression?(matching) == true } return false } - .forEach { step in - step.result = .undefined - step.execute = { try callback(step.match.matches(for: step.regex), step) } - step.regex = regex - step.sourceLine = line - step.sourceFile = file - } + + if let firstMatchingStep = firstMatchingStep { + XCTAssertNoThrow(try firstMatchingStep.execute?(matching, firstMatchingStep)) + } else { + XCTFail("No CucumberSwift expression found that matches step '\(matching)'") + } } - func attachClosureToSteps(keyword: Step.Keyword? = nil, - expression: CucumberExpression, - callback: @escaping ((CucumberSwiftExpressions.Match, Step) throws -> Void), - line: Int, - file: StaticString) { + private func attachClosureToSteps(keyword: Step.Keyword?, + execute: Step.Execute? = nil, + matchesExpression: @escaping Step.MatchesExpression, + line: Int, + file: StaticString, + executeSelector: Selector? = nil, + executeClass: AnyClass? = nil) { features .flatMap { $0.scenarios.flatMap { $0.steps } } .filter { step -> Bool in if let k = keyword, step.keyword.contains(k) { - return expression.match(in: step.match) != nil + return matchesExpression(step.match) } else if keyword == nil { - return expression.match(in: step.match) != nil + return matchesExpression(step.match) } return false } .forEach { step in step.result = .undefined - step.execute = { try callback(try XCTUnwrap(expression.match(in: step.match)), step) } + step.execute = execute + step.matchesExpression = matchesExpression step.sourceLine = line step.sourceFile = file + step.executeSelector = executeSelector + step.executeClass = executeClass } } + func attachClosureToSteps(keyword: Step.Keyword? = nil, + regex: String, + callback: @escaping (([String], Step) throws -> Void), + line: Int, + file: StaticString) { + attachClosureToSteps(keyword: keyword, + execute: { match, step in try callback(match.matches(for: regex), step) }, + matchesExpression: { str in !str.matches(for: regex).isEmpty }, + line: line, + file: file) + } + + func attachClosureToSteps(keyword: Step.Keyword? = nil, + expression: CucumberExpression, + callback: @escaping ((CucumberSwiftExpressions.Match, Step) throws -> Void), + line: Int, + file: StaticString) { + attachClosureToSteps(keyword: keyword, + execute: { match, step in try callback(try XCTUnwrap(expression.match(in: match)), step) }, + matchesExpression: { str in expression.match(in: str) != nil }, + line: line, + file: file) + } + #if compiler(>=5.7) && canImport(_StringProcessing) @available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) func attachClosureToSteps(keyword: Step.Keyword? = nil, @@ -229,48 +257,26 @@ import CucumberSwift_ObjC callback: @escaping ((Regex.Match, Step) throws -> Void), line: Int, file: StaticString) { - features - .flatMap { $0.scenarios.flatMap { $0.steps } } - .filter { step -> Bool in - if let k = keyword, - step.keyword.contains(k) { - let match = try? regex.wholeMatch(in: step.match) - return match != nil - } else if keyword == nil { - let match = try? regex.wholeMatch(in: step.match) - return match != nil - } - return false - } - .forEach { step in - step.result = .undefined - step.execute = { try callback(try XCTUnwrap(regex.wholeMatch(in: step.match)), step) } - step.sourceLine = line - step.sourceFile = file - } + attachClosureToSteps(keyword: keyword, + execute: { match, step in try callback(try XCTUnwrap(regex.wholeMatch(in: match)), step) }, + matchesExpression: { str in (try? regex.wholeMatch(in: str)) != nil }, + line: line, + file: file) } #endif - func attachClosureToSteps(keyword: Step.Keyword? = nil, regex: String, class: AnyClass, selector: Selector, line: Int, file: StaticString) { - features - .flatMap { $0.scenarios.flatMap { $0.steps } } - .filter { step -> Bool in - if let k = keyword, - step.keyword.contains(k) { - return !step.match.matches(for: regex).isEmpty - } else if keyword == nil { - return !step.match.matches(for: regex).isEmpty - } - return false - } - .forEach { step in - step.result = .undefined - step.executeSelector = selector - step.executeClass = `class` - step.regex = regex - step.sourceLine = line - step.sourceFile = file - } + func attachClosureToSteps(keyword: Step.Keyword? = nil, + regex: String, + class: AnyClass, + selector: Selector, + line: Int, + file: StaticString) { + attachClosureToSteps(keyword: keyword, + matchesExpression: { str in !str.matches(for: regex).isEmpty }, + line: line, + file: file, + executeSelector: selector, + executeClass: `class`) } } diff --git a/Sources/CucumberSwift/Runner/CucumberTest.swift b/Sources/CucumberSwift/Runner/CucumberTest.swift index afb06cbd..0ff0f278 100644 --- a/Sources/CucumberSwift/Runner/CucumberTest.swift +++ b/Sources/CucumberSwift/Runner/CucumberTest.swift @@ -162,7 +162,7 @@ extension Step { instance.perform(selector) } } else { - try execute?() + try execute?(self.match, self) } if execute != nil && result != .failed { result = .passed diff --git a/Sources/CucumberSwift/Runner/Globals.swift b/Sources/CucumberSwift/Runner/Globals.swift index d472cdca..48d19516 100644 --- a/Sources/CucumberSwift/Runner/Globals.swift +++ b/Sources/CucumberSwift/Runner/Globals.swift @@ -27,3 +27,7 @@ public func BeforeStep(priority: UInt? = nil, closure: @escaping ((Step) -> Void public func AfterStep(priority: UInt? = nil, closure: @escaping ((Step) -> Void)) { Cucumber.shared.afterStepHooks.append(.init(priority: priority, hook: closure)) } +// Execute a step matching the given step definition +public func ExecuteFirstStep(keyword: Step.Keyword? = nil, matching: String) { + Cucumber.shared.executeFirstStep(keyword: keyword, matching: matching) +} diff --git a/Tests/CucumberSwiftTests/CucumberSwiftTests.swift b/Tests/CucumberSwiftTests/CucumberSwiftTests.swift index b035d99e..0a6969f8 100644 --- a/Tests/CucumberSwiftTests/CucumberSwiftTests.swift +++ b/Tests/CucumberSwiftTests/CucumberSwiftTests.swift @@ -5,7 +5,7 @@ // Created by Tyler Thompson on 4/7/18. // Copyright © 2018 Tyler Thompson. All rights reserved. // -// swiftlint:disable function_body_length +// swiftlint:disable function_body_length type_body_length file_length import XCTest import CucumberSwiftExpressions @@ -142,6 +142,220 @@ class CucumberSwiftTests: XCTestCase { } #endif + func testExecuteFirstStep_WithoutParameter() { + let featureFile: String = + """ + Feature: Some text + + Scenario: Some determinable business situation + Given some precondition + And some other precondition + """ + + Cucumber.shared.parseIntoFeatures(featureFile) + var givenCalledCount = 0 + Given("some precondition") { _, _ in + givenCalledCount += 1 + } + + Given("some other precondition") { _, _ in + ExecuteFirstStep(matching: "some precondition") + } + Cucumber.shared.executeFeatures() + XCTAssertEqual(givenCalledCount, 2) + } + + func testExecuteFirstStep_WithKeyword() { + let featureFile: String = + """ + Feature: Some text + + Scenario: Some determinable business situation + Given some text + When some text + Then some then text + + """ + + Cucumber.shared.parseIntoFeatures(featureFile) + var givenCalledCount = 0 + Given("some text") { _, _ in + givenCalledCount += 1 + } + + var whenCalledCount = 0 + When("some text") { _, _ in + whenCalledCount += 1 + } + + Then("some then text") { _, _ in + ExecuteFirstStep(keyword: .when, matching: "some text") + ExecuteFirstStep(keyword: .given, matching: "some text") + ExecuteFirstStep(keyword: .given, matching: "some text") + } + Cucumber.shared.executeFeatures() + XCTAssertEqual(givenCalledCount, 3) + XCTAssertEqual(whenCalledCount, 2) + } + + func testExecuteFirstStep_WithParameter() { + let featureFile: String = + """ + Feature: Some text + + Scenario: Some determinable business situation + Given some precondition with parameter1 + And some other precondition + """ + + Cucumber.shared.parseIntoFeatures(featureFile) + var givenParameters = [String]() + Given("some precondition with (.*)") { match, _ in + givenParameters.append(match[1]) + } + + Given("some other precondition") { _, _ in + ExecuteFirstStep(matching: "some precondition with parameter2") + } + Cucumber.shared.executeFeatures() + XCTAssertEqual(givenParameters.count, 2) + XCTAssertEqual(givenParameters[0], "parameter1") + XCTAssertEqual(givenParameters[1], "parameter2") + } + + func testExecuteFirstStep_WithoutParameterWithCucumberExpression() { + let featureFile: String = + """ + Feature: Some text + + Scenario: Some determinable business situation + Given some precondition + And some other precondition + """ + + Cucumber.shared.parseIntoFeatures(featureFile) + var givenCalledCount = 0 + Given("some precondition" as CucumberExpression) { _, _ in + givenCalledCount += 1 + } + + Given("some other precondition" as CucumberExpression) { _, _ in + ExecuteFirstStep(matching: "some precondition") + } + Cucumber.shared.executeFeatures() + XCTAssertEqual(givenCalledCount, 2) + } + + func testExecuteFirstStep_WithParameterWithCucumberExpression() { + let featureFile: String = + """ + Feature: Some text + + Scenario: Some determinable business situation + Given some precondition with parameter1 + And some other precondition + """ + + Cucumber.shared.parseIntoFeatures(featureFile) + var givenParameters = [String]() + Given("some precondition with {}" as CucumberExpression) { match, _ in + givenParameters.append(try match.first(\.anonymous)) + } + + Given("some other precondition" as CucumberExpression) { _, _ in + ExecuteFirstStep(matching: "some precondition with parameter2") + } + Cucumber.shared.executeFeatures() + XCTAssertEqual(givenParameters.count, 2) + XCTAssertEqual(givenParameters[0], "parameter1") + XCTAssertEqual(givenParameters[1], "parameter2") + } + + func testExecuteFirstStep_WithKeywordWithCucumberExpression() { + let featureFile: String = + """ + Feature: Some text + + Scenario: Some determinable business situation + Given some text + When some text + Then some then text + + """ + + Cucumber.shared.parseIntoFeatures(featureFile) + var givenCalledCount = 0 + Given("some text" as CucumberExpression) { _, _ in + givenCalledCount += 1 + } + + var whenCalledCount = 0 + When("some text" as CucumberExpression) { _, _ in + whenCalledCount += 1 + } + + Then("some then text" as CucumberExpression) { _, _ in + ExecuteFirstStep(keyword: .when, matching: "some text") + ExecuteFirstStep(keyword: .given, matching: "some text") + ExecuteFirstStep(keyword: .given, matching: "some text") + } + Cucumber.shared.executeFeatures() + XCTAssertEqual(givenCalledCount, 3) + XCTAssertEqual(whenCalledCount, 2) + } + +#if swift(>=5.7) + @available(iOS 16.0, *) + func testExecuteFirstStep_WithoutParameterWithRegexLiteral() { + let featureFile: String = + """ + Feature: Some text + + Scenario: Some determinable business situation + Given some precondition + And some other precondition + """ + + Cucumber.shared.parseIntoFeatures(featureFile) + var givenCalledCount = 0 + Given(/^some precondition$/) { _, _ in + givenCalledCount += 1 + } + + Given(/^some other precondition$/) { _, _ in + ExecuteFirstStep(matching: "some precondition") + } + Cucumber.shared.executeFeatures() + XCTAssertEqual(givenCalledCount, 2) + } + + @available(iOS 16.0, *) + func testExecuteFirstStep_WithParameterWithRegexLiteral() { + let featureFile: String = + """ + Feature: Some text + + Scenario: Some determinable business situation + Given some precondition with parameter1 + And some other precondition + """ + + Cucumber.shared.parseIntoFeatures(featureFile) + var givenParameters = [String]() + Given(/some precondition with (.*)/) { match, _ in + givenParameters.append("\(match.1)") + } + + Given(/some other precondition/) { _, _ in + ExecuteFirstStep(matching: "some precondition with parameter2") + } + Cucumber.shared.executeFeatures() + XCTAssertEqual(givenParameters.count, 2) + XCTAssertEqual(givenParameters[0], "parameter1") + XCTAssertEqual(givenParameters[1], "parameter2") + } +#endif + func testStepsGetCallbacksAttachedCorrectly_WithCucumberExpressions() { let featureFile: String = """ From 8d8459923f259835abb13dbdb4eea1c7cbea865a Mon Sep 17 00:00:00 2001 From: Yauheni Khnykin Date: Mon, 6 Feb 2023 13:22:47 +0100 Subject: [PATCH 2/3] Fixes static analysis issue --- Sources/CucumberSwift/Gherkin/Parser/Step.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CucumberSwift/Gherkin/Parser/Step.swift b/Sources/CucumberSwift/Gherkin/Parser/Step.swift index ee274a95..9eaf972f 100644 --- a/Sources/CucumberSwift/Gherkin/Parser/Step.swift +++ b/Sources/CucumberSwift/Gherkin/Parser/Step.swift @@ -102,7 +102,7 @@ public class Step: CustomStringConvertible { init(with execute: @escaping (([String], Step) -> Void), match: String?, position: Lexer.Position) { location = position self.match ?= match - self.execute = { match, step in + self.execute = { _, step in execute(self.match.matches(for: ""), step) } } From 5f54fd635eefccebe00a99485b6182faf3430fa2 Mon Sep 17 00:00:00 2001 From: Yauheni Khnykin Date: Mon, 6 Feb 2023 15:06:58 +0100 Subject: [PATCH 3/3] Updates dependency on CucumberSwiftExpressions --- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CucumberSwift.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CucumberSwift.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 43ed1237..44c77ffc 100644 --- a/CucumberSwift.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CucumberSwift.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Tyler-Keith-Thompson/CucumberSwiftExpressions.git", "state" : { - "revision" : "d6bcc190304d5096ae64b95bd6adc7a0342f1245", - "version" : "0.0.7" + "revision" : "5200dc2c5d7cd31f07a13f030ab0027224d2dbea", + "version" : "0.0.8" } }, {