diff --git a/CucumberSwift.podspec b/CucumberSwift.podspec index 4fac83f0..291a92e9 100644 --- a/CucumberSwift.podspec +++ b/CucumberSwift.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'CucumberSwift' - s.version = '4.1.1' + s.version = '4.2.0' s.summary = 'A lightweight swift only cucumber implementation.' s.description = <<-DESC diff --git a/CucumberSwift.xcodeproj/project.pbxproj b/CucumberSwift.xcodeproj/project.pbxproj index 55568f82..43cfecb4 100644 --- a/CucumberSwift.xcodeproj/project.pbxproj +++ b/CucumberSwift.xcodeproj/project.pbxproj @@ -107,6 +107,8 @@ CAAD49C1266368D6007B6E6E /* .swiftlint.yml in Resources */ = {isa = PBXBuildFile; fileRef = CAAD49C0266368D6007B6E6E /* .swiftlint.yml */; }; CAB20B3C24CCF5C10024C703 /* RuleDSL.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAB20B3B24CCF5C10024C703 /* RuleDSL.swift */; }; CAB20B3E24CCF6B20024C703 /* DSLRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAB20B3D24CCF6B20024C703 /* DSLRuleTests.swift */; }; + CAB40A5729BEE22900A01980 /* StubGeneratorToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAB40A5629BEE22900A01980 /* StubGeneratorToken.swift */; }; + CAB40A5929BEE28700A01980 /* StubGeneratorLexer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAB40A5829BEE28700A01980 /* StubGeneratorLexer.swift */; }; CADD26DD265B1D1F00EE8707 /* JSONReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CADD26DC265B1D1F00EE8707 /* JSONReporter.swift */; }; CAEE41EE28B324BF000D4703 /* CucumberSwiftExpressions in Frameworks */ = {isa = PBXBuildFile; productRef = CAEE41ED28B324BF000D4703 /* CucumberSwiftExpressions */; }; CAF87A55281453EF00F7946A /* CucumberSwift.docc in Sources */ = {isa = PBXBuildFile; fileRef = CAF87A54281453EF00F7946A /* CucumberSwift.docc */; }; @@ -240,6 +242,8 @@ CAAD49C0266368D6007B6E6E /* .swiftlint.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = ""; }; CAB20B3B24CCF5C10024C703 /* RuleDSL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleDSL.swift; sourceTree = ""; }; CAB20B3D24CCF6B20024C703 /* DSLRuleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DSLRuleTests.swift; sourceTree = ""; }; + CAB40A5629BEE22900A01980 /* StubGeneratorToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubGeneratorToken.swift; sourceTree = ""; }; + CAB40A5829BEE28700A01980 /* StubGeneratorLexer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubGeneratorLexer.swift; sourceTree = ""; }; CABED02C256A17F2001E92C1 /* CucumberSwift.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = CucumberSwift.xctestplan; sourceTree = ""; }; CADD26DC265B1D1F00EE8707 /* JSONReporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONReporter.swift; sourceTree = ""; }; CAF51EA42557B1C70095A5C4 /* Package.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = SOURCE_ROOT; }; @@ -322,6 +326,8 @@ children = ( AD01723021166C3D00FBC9AF /* StubGenerator.swift */, AD017233211671D200FBC9AF /* Method.swift */, + CAB40A5629BEE22900A01980 /* StubGeneratorToken.swift */, + CAB40A5829BEE28700A01980 /* StubGeneratorLexer.swift */, ); path = StubGeneration; sourceTree = ""; @@ -868,6 +874,7 @@ 1DBF9329239C97AC0035722A /* RuleParser.swift in Sources */, 1DE862E02391F7F70027326A /* Position.swift in Sources */, CA09729D264F517200573DA5 /* CucumberTestObservable.swift in Sources */, + CAB40A5929BEE28700A01980 /* StubGeneratorLexer.swift in Sources */, AD7F1C0E2104DB240004852A /* Language.swift in Sources */, AD99DB59213269D500E57823 /* StepImplementation.swift in Sources */, CA4E2D0324CCDDDC00A44983 /* BackgroundDSL.swift in Sources */, @@ -883,6 +890,7 @@ ADAA738E20A9356000D2344C /* CollectionExtensions.swift in Sources */, 1DA761A2239A7A74001CBA5B /* ASTToken.swift in Sources */, AD9EB47B2079CFA1002494C0 /* Cucumber.swift in Sources */, + CAB40A5729BEE22900A01980 /* StubGeneratorToken.swift in Sources */, AD9EB4832079D606002494C0 /* Scenario.swift in Sources */, CA1C47E72634C671008D8E12 /* Hook.swift in Sources */, AD01723121166C3D00FBC9AF /* StubGenerator.swift in Sources */, diff --git a/Sources/CucumberSwift/Custom Operators/MightEqualOperator.swift b/Sources/CucumberSwift/Custom Operators/MightEqualOperator.swift index 88fbba6e..4cd6f85c 100644 --- a/Sources/CucumberSwift/Custom Operators/MightEqualOperator.swift +++ b/Sources/CucumberSwift/Custom Operators/MightEqualOperator.swift @@ -6,7 +6,7 @@ precedencegroup MightBePrecedence { } infix operator ?= : MightBePrecedence -internal func ?= ( lhs: inout T, rhs: T?) { +internal func ?= ( lhs: inout T, rhs: T?) { if let r = rhs { lhs = r } diff --git a/Sources/CucumberSwift/Extensions/CharacterExtensions.swift b/Sources/CucumberSwift/Extensions/CharacterExtensions.swift index 3b07d650..ec45f83b 100644 --- a/Sources/CucumberSwift/Extensions/CharacterExtensions.swift +++ b/Sources/CucumberSwift/Extensions/CharacterExtensions.swift @@ -56,7 +56,6 @@ extension Character { isNewline || isTagMarker || isQuote || - isNumeric || isTableCellDelimiter || isHeaderToken || isEscapeCharacter diff --git a/Sources/CucumberSwift/Gherkin/Lexer/Lexer.swift b/Sources/CucumberSwift/Gherkin/Lexer/Lexer.swift index 2a86c0fc..35f95f3c 100644 --- a/Sources/CucumberSwift/Gherkin/Lexer/Lexer.swift +++ b/Sources/CucumberSwift/Gherkin/Lexer/Lexer.swift @@ -151,7 +151,6 @@ public class Lexer: StringReader { } else { return advance(.match(position, "\(char)" + readLineUntil { $0.isSymbol })) } - case _ where char.isNumeric: return .integer(position, readLineUntil { !$0.isNumeric }) case _ where lastKeyword != nil: return .match(position, readLineUntil { $0.isSymbol }) default: return advance(advanceToNextToken()) } @@ -189,8 +188,7 @@ public class Lexer: StringReader { return advance(.docString(position, DocString(literal: docStringValues.dropFirst().joined(separator: "\n"), contentType: docStringValues.first?.trimmingCharacters(in: .whitespacesAndNewlines)))) } else if char == .quote { - let str = advance(readLineUntil { $0.isQuote }) - return advance(.string(position, str)) + return advance(.match(position, "\(Character.quote)")) } return nil } diff --git a/Sources/CucumberSwift/Gherkin/Lexer/Token.swift b/Sources/CucumberSwift/Gherkin/Lexer/Token.swift index da9caefe..575645bd 100644 --- a/Sources/CucumberSwift/Gherkin/Lexer/Token.swift +++ b/Sources/CucumberSwift/Gherkin/Lexer/Token.swift @@ -30,8 +30,6 @@ extension Sequence where Element == Lexer.Token { extension Lexer { indirect enum Token: Equatable, Hashable { case newLine(Lexer.Position) - case integer(Lexer.Position, String) - case string(Lexer.Position, String) case docString(Lexer.Position, DocString) case match(Lexer.Position, String) case title(Lexer.Position, String) @@ -45,8 +43,6 @@ extension Lexer { var position: Lexer.Position { switch self { case .newLine(let pos): return pos - case .integer(let pos, _): return pos - case .string(let pos, _): return pos case .docString(let pos, _): return pos case .match(let pos, _): return pos case .title(let pos, _): return pos @@ -71,10 +67,6 @@ extension Lexer { return description1 == description2 case let (.tag(_, tag1), .tag(_, tag2)): return tag1 == tag2 - case let (.integer(_, num1), .integer(_, num2)): - return num1 == num2 - case let (.string(_, string1), .string(_, string2)): - return string1 == string2 case let (.docString(_, string1), .docString(_, string2)): return string1.literal == string2.literal case let (.tableHeader(_, tableHeader1), .tableHeader(_, tableHeader2)): @@ -89,8 +81,6 @@ extension Lexer { var valueDescription: String { switch self { case .newLine: return "\n" - case .integer(_, let val): return "\(val)" - case .string(_, let val): return "\(val)" case .docString(_, let val): return "\(val)" case .match(_, let val): return "\(val)" case .title(_, let val): return "\(val)" @@ -122,18 +112,6 @@ extension Lexer { } return false } - func isString() -> Bool { - if case .string = self { - return true - } - return false - } - func isInteger() -> Bool { - if case .integer = self { - return true - } - return false - } func isDescription() -> Bool { if case .description = self { return true diff --git a/Sources/CucumberSwift/Gherkin/Parser/Step.swift b/Sources/CucumberSwift/Gherkin/Parser/Step.swift index 9eaf972f..b7f31ac5 100644 --- a/Sources/CucumberSwift/Gherkin/Parser/Step.swift +++ b/Sources/CucumberSwift/Gherkin/Parser/Step.swift @@ -68,10 +68,6 @@ public class Step: CustomStringConvertible { keyword = kw } else if case Lexer.Token.match(_, let m) = token { match += m - } else if case Lexer.Token.string(_, let s) = token { - match += "\"\(s)\"" - } else if case Lexer.Token.integer(_, let n) = token { - match += n } else if case Lexer.Token.tableHeader(_, let h) = token { match += h } else if case Lexer.Token.docString(_, let s) = token { diff --git a/Sources/CucumberSwift/Runner/Cucumber.swift b/Sources/CucumberSwift/Runner/Cucumber.swift index 7dce7cf0..75455809 100644 --- a/Sources/CucumberSwift/Runner/Cucumber.swift +++ b/Sources/CucumberSwift/Runner/Cucumber.swift @@ -10,7 +10,7 @@ import Foundation import XCTest import CucumberSwiftExpressions -@objc public class Cucumber: NSObject { +@objc public class Cucumber: NSObject { // swiftlint:disable:this type_body_length static var shared = Cucumber() var features = [Feature]() diff --git a/Sources/CucumberSwift/Runner/CucumberTest.swift b/Sources/CucumberSwift/Runner/CucumberTest.swift index 35c13258..58c5ab7c 100644 --- a/Sources/CucumberSwift/Runner/CucumberTest.swift +++ b/Sources/CucumberSwift/Runner/CucumberTest.swift @@ -69,7 +69,7 @@ open class CucumberTest: XCTestCase { scenario .steps .lazy - .compactMap { step -> (step: Step, XCTestCase.Type, Selector)? in + .compactMap { step -> (step: Step, XCTestCase.Type, Selector)? in // swiftlint:disable:this large_tuple if let (testCase, methodSelector) = TestCaseGenerator.initWith(className: className.appending(scenario.title.toClassString()), method: step.method) { return (step, testCase, methodSelector) diff --git a/Sources/CucumberSwift/StubGeneration/StubGenerator.swift b/Sources/CucumberSwift/StubGeneration/StubGenerator.swift index 9589e5ff..fc51a98c 100644 --- a/Sources/CucumberSwift/StubGeneration/StubGenerator.swift +++ b/Sources/CucumberSwift/StubGeneration/StubGenerator.swift @@ -8,15 +8,15 @@ import Foundation enum StubGenerator { - private static func regexForTokens(_ tokens: [Lexer.Token]) -> String { + private static func regexForTokens(_ tokens: [Token]) -> String { var regex = "" for token in tokens { - if case Lexer.Token.match(_, let m) = token { + if case .match(let m) = token { regex += NSRegularExpression .escapedPattern(for: m) - } else if case Lexer.Token.string = token { + } else if case .string = token { regex += "\\\"(.*?)\\\"" - } else if case Lexer.Token.integer = token { + } else if case .int = token { regex += "(\\d+)" } } @@ -37,9 +37,10 @@ enum StubGenerator { let methods = executableSteps .filter { !$0.canExecute } .reduce(into: [(step: Step, method: Method)]()) { - let regex = regexForTokens($1.tokens) - let stringCount = $1.tokens.filter { $0.isString() }.count - let integerCount = $1.tokens.filter { $0.isInteger() }.count + let tokens = StubGenerator.Lexer($1.match).lex() + let regex = regexForTokens(tokens) + let stringCount = tokens.filter { $0.isString() }.count + let integerCount = tokens.filter { $0.isInteger() }.count let matchesParameter = (stringCount > 0 || integerCount > 0) ? "matches" : "_" let variables = [ (type: "string", count: stringCount), diff --git a/Sources/CucumberSwift/StubGeneration/StubGeneratorLexer.swift b/Sources/CucumberSwift/StubGeneration/StubGeneratorLexer.swift new file mode 100644 index 00000000..f28a55eb --- /dev/null +++ b/Sources/CucumberSwift/StubGeneration/StubGeneratorLexer.swift @@ -0,0 +1,43 @@ +// +// StubGeneratorLexer.swift +// CucumberSwift +// +// Created by Tyler Thompson on 3/12/23. +// Copyright © 2023 Tyler Thompson. All rights reserved. +// + +import Foundation +extension StubGenerator { + public class Lexer: StringReader { + override internal init(_ str: String) { + super.init(str) + } + + @discardableResult private func advance(_ t: @autoclosure () -> T) -> T { + advanceIndex() + return t() + } + + internal func advanceToNextToken() -> Token? { + guard let char = currentChar else { return nil } + + switch char { + case .quote: + let str = advance(readUntil { $0.isQuote }) + return advance(.string(value: str)) + case _ where char.isNumeric: + let allIntegerValues = readUntil { !$0.isNumeric } + return .int(value: allIntegerValues) + default: return .match(value: readUntil { $0.isQuote || $0.isNumeric }) + } + } + + internal func lex() -> [Token] { + var toks = [Token]() + while let tok = advanceToNextToken() { + toks.append(tok) + } + return toks + } + } +} diff --git a/Sources/CucumberSwift/StubGeneration/StubGeneratorToken.swift b/Sources/CucumberSwift/StubGeneration/StubGeneratorToken.swift new file mode 100644 index 00000000..50509ab0 --- /dev/null +++ b/Sources/CucumberSwift/StubGeneration/StubGeneratorToken.swift @@ -0,0 +1,31 @@ +// +// StubGeneratorToken.swift +// CucumberSwift +// +// Created by Tyler Thompson on 3/12/23. +// Copyright © 2023 Tyler Thompson. All rights reserved. +// + +import Foundation + +extension StubGenerator { + enum Token { + case match(value: String) + case string(value: String) + case int(value: String) + + func isString() -> Bool { + if case .string = self { + return true + } + return false + } + + func isInteger() -> Bool { + if case .int = self { + return true + } + return false + } + } +} diff --git a/Tests/CucumberSwiftTests/CucumberTests/CucumberTests.swift b/Tests/CucumberSwiftTests/CucumberTests/CucumberTests.swift index 182dd578..5d834985 100644 --- a/Tests/CucumberSwiftTests/CucumberTests/CucumberTests.swift +++ b/Tests/CucumberSwiftTests/CucumberTests/CucumberTests.swift @@ -15,10 +15,6 @@ extension Collection where Element == Lexer.Token { public var text: String { return compactMap { (token) -> String? in switch token { - case .integer(_, let t): - return t - case .string(_, let t): - return "\"\(t)\"" case .match(_, let t): return t case .tableHeader(_, let t): diff --git a/Tests/CucumberSwiftTests/Gherkin/TableTests.swift b/Tests/CucumberSwiftTests/Gherkin/TableTests.swift index c28bbf0e..4ed67fc2 100644 --- a/Tests/CucumberSwiftTests/Gherkin/TableTests.swift +++ b/Tests/CucumberSwiftTests/Gherkin/TableTests.swift @@ -167,6 +167,21 @@ class TableTests: XCTestCase { XCTAssertEqual(firstScenario?.title, "the u|o (example 1)") } + func testTableCellInQuotes() { + let cucumber = Cucumber(withString: """ + Feature: Some terse yet descriptive text of what is desired + Scenario Outline: the "" + Given the "" + + Examples: + | one | two | + | u\\|o | dos | + """) + let firstScenario = cucumber.features.first?.scenarios.first + XCTAssertEqual(firstScenario?.title, "the \"u|o\" (example 1)") + XCTAssertEqual(firstScenario?.steps.first?.match, "the \"dos\"") + } + func testTableGetAttachedToSteps() { Cucumber.shared.features.removeAll() Cucumber.shared.parseIntoFeatures(""" diff --git a/Tests/CucumberSwiftTests/Reporter/ReporterTests.swift b/Tests/CucumberSwiftTests/Reporter/ReporterTests.swift index 3305eee7..f5f50997 100644 --- a/Tests/CucumberSwiftTests/Reporter/ReporterTests.swift +++ b/Tests/CucumberSwiftTests/Reporter/ReporterTests.swift @@ -59,7 +59,7 @@ class ReporterTests: XCTestCase { XCTAssertEqual(scenarios?.first?["name"] as? String, "S1") XCTAssertEqual(scenarios?.first?["description"] as? String, "") } - + func testStepsAreWrittenToFile() throws { let reporter = try XCTUnwrap(Cucumber.shared.reporters.compactMap { $0 as? CucumberJSONReporter }.first) Feature("F1") { @@ -70,7 +70,7 @@ class ReporterTests: XCTestCase { } reporter.testSuiteStarted(at: Date()) Cucumber.shared.executeFeatures() - + let actual = try XCTUnwrap(try JSONSerialization.jsonObject(with: JSONEncoder().encode(reporter.features)) as? [[AnyHashable: Any]]) XCTAssertEqual(actual.count, 1) let scenarios = actual.first?["elements"] as? [[AnyHashable: Any]] @@ -82,7 +82,7 @@ class ReporterTests: XCTestCase { let result = steps?.first?["result"] as? [AnyHashable: Any] XCTAssertEqual(result?["status"] as? String, "passed") } - + func testFailingStepsAreWrittenToFile() throws { enum Err: Error { case e1 } let reporter = try XCTUnwrap(Cucumber.shared.reporters.compactMap { $0 as? CucumberJSONReporter }.first) @@ -94,7 +94,7 @@ class ReporterTests: XCTestCase { reporter.didStart(scenario: scenario, at: Date()) reporter.didStart(step: step, at: Date()) reporter.didFinish(step: step, result: .failed(Err.e1.localizedDescription), duration: .init(value: 1, unit: .seconds)) - + let actual = try XCTUnwrap(try JSONSerialization.jsonObject(with: JSONEncoder().encode(reporter.features)) as? [[AnyHashable: Any]]) XCTAssertEqual(actual.count, 1) let scenarios = actual.first?["elements"] as? [[AnyHashable: Any]] @@ -109,19 +109,19 @@ class ReporterTests: XCTestCase { let actualDuration = try XCTUnwrap(result?["duration"] as? Double) XCTAssertEqual(actualDuration, 1_000_000_000, accuracy: 0.9) } - + func testPendingStepsAreWrittenToFile() throws { let reporter = try XCTUnwrap(Cucumber.shared.reporters.compactMap { $0 as? CucumberJSONReporter }.first) - + let step = Given(I: print("")) let scenario = Scenario("S1") { step } let feature = Feature("F1") { scenario } - + reporter.testSuiteStarted(at: Date()) reporter.didStart(feature: feature, at: Date()) reporter.didStart(scenario: scenario, at: Date()) reporter.didStart(step: step, at: Date()) - + let actual = try XCTUnwrap(try JSONSerialization.jsonObject(with: JSONEncoder().encode(reporter.features)) as? [[AnyHashable: Any]]) XCTAssertEqual(actual.count, 1) let scenarios = actual.first?["elements"] as? [[AnyHashable: Any]] @@ -133,7 +133,7 @@ class ReporterTests: XCTestCase { let result = steps?.first?["result"] as? [AnyHashable: Any] XCTAssertEqual(result?["status"] as? String, "pending") } - + func testReporterJsonConformsToCucumberJsonSchema() throws { let path = URL(fileURLWithPath: #file) .deletingLastPathComponent()