diff --git a/.travis.yml b/.travis.yml index 05a71d8..01df2ca 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,29 @@ -language: objective-c -osx_image: xcode9 -install: - - gem install xcpretty -script: - - xcodebuild -scheme 'Docopt' clean build test | xcpretty -c +matrix: + include: + - os: linux + language: generic + dist: trusty + sudo: required + env: + - SWIFT_BRANCH=swift-4.1-branch + - SWIFT_VERSION=swift-4.1-DEVELOPMENT-SNAPSHOT-2018-03-09-a + install: + - sudo apt-get install clang libicu-dev + - mkdir swift + - curl https://swift.org/builds/$SWIFT_BRANCH/ubuntu1404/$SWIFT_VERSION/$SWIFT_VERSION-ubuntu14.04.tar.gz -s | tar xz -C swift &> /dev/null + - export PATH=$(pwd)/swift/$SWIFT_VERSION-ubuntu14.04/usr/bin:$PATH + - swift package update + script: + - swift test + + - os: osx + language: objective-c + osx_image: xcode9 + install: + - gem install xcpretty + script: + - xcodebuild -scheme 'Docopt' clean build test | xcpretty -c + + notifications: email: false diff --git a/Package.swift b/Package.swift index 4c38955..9e465f7 100644 --- a/Package.swift +++ b/Package.swift @@ -10,14 +10,9 @@ let package = Package( targets: [ .target( name: "Docopt", - path: "Sources" - ) - // Commented out until SPM supports resources - //, - // .testTarget( - // name: "DocoptTests", - // dependencies: ["Docopt"], - // path: "DocoptTests" - // ) + dependencies: []), + .testTarget( + name: "DocoptTests", + dependencies: ["Docopt"]) ] ) diff --git a/Sources/Argument.swift b/Sources/Docopt/Argument.swift similarity index 100% rename from Sources/Argument.swift rename to Sources/Docopt/Argument.swift diff --git a/Sources/BranchPattern.swift b/Sources/Docopt/BranchPattern.swift similarity index 100% rename from Sources/BranchPattern.swift rename to Sources/Docopt/BranchPattern.swift diff --git a/Sources/Command.swift b/Sources/Docopt/Command.swift similarity index 88% rename from Sources/Command.swift rename to Sources/Docopt/Command.swift index 8e9bed4..b354138 100644 --- a/Sources/Command.swift +++ b/Sources/Docopt/Command.swift @@ -18,7 +18,7 @@ internal class Command: Argument { let pattern = left[i] if pattern is Argument { if pattern.value as? String == self.name { - return (i, Command(self.name, value: true as AnyObject)) + return (i, Command(self.name, value: true)) } } } diff --git a/Sources/Docopt.h b/Sources/Docopt/Docopt.h similarity index 100% rename from Sources/Docopt.h rename to Sources/Docopt/Docopt.h diff --git a/Sources/Docopt.swift b/Sources/Docopt/Docopt.swift similarity index 90% rename from Sources/Docopt.swift rename to Sources/Docopt/Docopt.swift index 392770a..5d5a611 100644 --- a/Sources/Docopt.swift +++ b/Sources/Docopt/Docopt.swift @@ -7,27 +7,25 @@ // import Foundation -import Darwin -@objc -open class Docopt : NSObject { - fileprivate(set) open var result: [String: AnyObject]! +open class Docopt { + fileprivate(set) open var result: [String: Any]! fileprivate let doc: String fileprivate let version: String? fileprivate let help: Bool fileprivate let optionsFirst: Bool fileprivate let arguments: [String] - - @objc open static func parse(_ doc: String, argv: [String], help: Bool = false, version: String? = nil, optionsFirst: Bool = false) -> [String: AnyObject] { + + open static func parse(_ doc: String, argv: [String], help: Bool = false, version: String? = nil, optionsFirst: Bool = false) -> [String: Any] { return Docopt(doc, argv: argv, help: help, version: version, optionsFirst: optionsFirst).result } - + internal init(_ doc: String, argv: [String]? = nil, help: Bool = false, version: String? = nil, optionsFirst: Bool = false) { self.doc = doc self.version = version self.help = help self.optionsFirst = optionsFirst - + var args: [String] if argv == nil { if CommandLine.argc > 1 { @@ -39,13 +37,12 @@ open class Docopt : NSObject { } else { args = argv! } - + arguments = args.filter { $0 != "" } - super.init() result = parse(optionsFirst) } - - fileprivate func parse(_ optionsFirst: Bool) -> [String: AnyObject] { + + fileprivate func parse(_ optionsFirst: Bool) -> [String: Any] { let usageSections = Docopt.parseSection("usage:", source: doc) if usageSections.count == 0 { @@ -53,31 +50,31 @@ open class Docopt : NSObject { } else if usageSections.count > 1 { DocoptLanguageError("More than one \"usage:\" (case-insensitive).").raise() } - + DocoptExit.usage = usageSections[0] - + var options = Docopt.parseDefaults(doc) let pattern = Docopt.parsePattern(Docopt.formalUsage(DocoptExit.usage), options: &options) let argv = Docopt.parseArgv(Tokens(arguments), options: &options, optionsFirst: optionsFirst) let patternOptions = Set(pattern.flat(Option.self)) - + for optionsShortcut in pattern.flat(OptionsShortcut.self) { let docOptions = Set(Docopt.parseDefaults(doc)) optionsShortcut.children = Array(docOptions.subtracting(patternOptions)) } Docopt.extras(help, version: version, options: argv, doc: doc) - + let (matched, left, collected) = pattern.fix().match(argv) - - var result = [String: AnyObject]() - + + var result = [String: Any]() + if matched && left.isEmpty { let collectedLeafs = collected as! [LeafPattern] let flatPattern = pattern.flat().filter { pattern in (collectedLeafs.filter {$0.name == pattern.name}).isEmpty } + collectedLeafs - + for leafChild: LeafPattern in flatPattern { result[leafChild.name!] = leafChild.value ?? NSNull() } @@ -87,7 +84,7 @@ open class Docopt : NSObject { DocoptExit().raise() return result } - + static fileprivate func extras(_ help: Bool, version: String?, options: [LeafPattern], doc: String) { let helpOption = options.filter { $0.name == "--help" || $0.name == "-h" } if help && !(helpOption.isEmpty) { @@ -100,11 +97,11 @@ open class Docopt : NSObject { exit(0) } } - + static internal func parseSection(_ name: String, source: String) -> [String] { return source.findAll("^([^\n]*\(name)[^\n]*\n?(?:[ \t].*?(?:\n|$))*)", flags: [.caseInsensitive, .anchorsMatchLines] ) } - + static internal func parseDefaults(_ doc: String) -> [Option] { var defaults = [Option]() let optionsSection = parseSection("options:", source: doc) @@ -122,14 +119,14 @@ open class Docopt : NSObject { } return defaults } - + static internal func parseLong(_ tokens: Tokens, options: inout [Option]) -> [Option] { let (long, eq, val) = tokens.move()!.partition("=") assert(long.hasPrefix("--")) - + var value: String? = eq != "" || val != "" ? val : nil var similar = options.filter {$0.long == long} - + if tokens.error is DocoptExit && similar.isEmpty { // if no exact match similar = options.filter {$0.long?.hasPrefix(long) ?? false} } @@ -144,7 +141,7 @@ open class Docopt : NSObject { o = Option(nil, long: long, argCount: argCount) options.append(o) if tokens.error is DocoptExit { - o = Option(nil, long: long, argCount: argCount, value: (argCount > 0) ? value as AnyObject : true as AnyObject) + o = Option(nil, long: long, argCount: argCount, value: (argCount > 0) ? value : true) } } else { o = Option(similar[0]) @@ -162,12 +159,12 @@ open class Docopt : NSObject { } } if tokens.error is DocoptExit { - o.value = value as AnyObject? ?? true as AnyObject + o.value = value ?? true } } return [o] } - + static internal func parseShorts(_ tokens: Tokens, options: inout [Option]) -> [Option] { let token = tokens.move()! assert(token.hasPrefix("-") && !token.hasPrefix("--")) @@ -178,7 +175,7 @@ open class Docopt : NSObject { let similar = options.filter {$0.short == short} var o: Option left = left[1.. 1 { tokens.error.raise("\(short) is specified ambiguously \(similar.count) times") return [] @@ -186,7 +183,7 @@ open class Docopt : NSObject { o = Option(short) options.append(o) if tokens.error is DocoptExit { - o = Option(short, value: true as AnyObject) + o = Option(short, value: true) } } else { var value: String? = nil @@ -202,19 +199,19 @@ open class Docopt : NSObject { left = "" } if tokens.error is DocoptExit { - o.value = true as AnyObject + o.value = true if let val = value { - o.value = val as AnyObject + o.value = val } } } - + parsed.append(o) } return parsed } - + static internal func parseAtom(_ tokens: Tokens, options: inout [Option]) -> [Pattern] { let token = tokens.current()! if ["(", "["].contains(token) { @@ -223,14 +220,14 @@ open class Docopt : NSObject { let (matching, result): (String, [BranchPattern]) = (token == "(") ? (")", [Required(u)]) : ("]", [Optional(u)]) - + if tokens.move() != matching { tokens.error.raise("unmatched '\(token)'") } - + return result } - + if token == "options" { _ = tokens.move() return [OptionsShortcut()] @@ -244,7 +241,7 @@ open class Docopt : NSObject { if (token.hasPrefix("<") && token.hasSuffix(">")) || token.isupper() { return [Argument(tokens.move()!)] } - + return [Command(tokens.move()!)] } @@ -261,20 +258,20 @@ open class Docopt : NSObject { return result } - + static internal func parseExpr(_ tokens: Tokens, options: inout [Option]) -> [Pattern] { var seq = parseSeq(tokens, options: &options) if tokens.current() != "|" { return seq } - + var result = seq.count > 1 ? [Required(seq)] : seq while tokens.current() == "|" { _ = tokens.move() seq = parseSeq(tokens, options: &options) result += seq.count > 1 ? [Required(seq)] : seq } - + return result.count > 1 ? [Either(result)] : result } @@ -291,7 +288,7 @@ open class Docopt : NSObject { while let current = tokens.current() { if tokens.current() == "--" { while let token = tokens.move() { - parsed.append(Argument(nil, value: token as AnyObject)) + parsed.append(Argument(nil, value: token)) } return parsed } else if current.hasPrefix("--") { @@ -304,27 +301,27 @@ open class Docopt : NSObject { } } else if optionsFirst { while let token = tokens.move() { - parsed.append(Argument(nil, value: token as AnyObject)) + parsed.append(Argument(nil, value: token)) } return parsed } else { - parsed.append(Command(nil, value: tokens.move() as AnyObject)) + parsed.append(Command(nil, value: tokens.move())) } } return parsed } - + static internal func parsePattern(_ source: String, options: inout [Option]) -> Pattern { let tokens: Tokens = Tokens.fromPattern(source) let result: [Pattern] = parseExpr(tokens, options: &options) - + if tokens.current() != nil { tokens.error.raise("unexpected ending: \(tokens)") } - + return Required(result) } - + static internal func formalUsage(_ section: String) -> String { let (_, _, s) = section.partition(":") // drop "usage:" let pu = s.split() diff --git a/Sources/DocoptError.swift b/Sources/Docopt/DocoptError.swift similarity index 98% rename from Sources/DocoptError.swift rename to Sources/Docopt/DocoptError.swift index 68962e2..2cc2f0f 100644 --- a/Sources/DocoptError.swift +++ b/Sources/Docopt/DocoptError.swift @@ -7,7 +7,6 @@ // import Foundation -import Darwin internal class DocoptError { var message: String diff --git a/Sources/Either.swift b/Sources/Docopt/Either.swift similarity index 100% rename from Sources/Either.swift rename to Sources/Docopt/Either.swift diff --git a/Sources/Info.plist b/Sources/Docopt/Info.plist similarity index 100% rename from Sources/Info.plist rename to Sources/Docopt/Info.plist diff --git a/Sources/LeafPattern.swift b/Sources/Docopt/LeafPattern.swift similarity index 89% rename from Sources/LeafPattern.swift rename to Sources/Docopt/LeafPattern.swift index d9465a1..46cd57b 100644 --- a/Sources/LeafPattern.swift +++ b/Sources/Docopt/LeafPattern.swift @@ -18,7 +18,7 @@ internal class LeafPattern : Pattern { var name: String? var valueDescription: String = "Never been set!" - var value: AnyObject? { + var value: Any? { willSet { valueDescription = newValue.debugDescription switch newValue { @@ -46,50 +46,50 @@ internal class LeafPattern : Pattern { case .nil: fallthrough default: return "LeafPattern(\(String(describing: name)), \(String(describing: value)))" } - + } } - + init(_ name: String?, value: Any? = nil) { self.name = name if let val = value { - self.value = val as AnyObject + self.value = val } } - + override func flat(_: T.Type) -> [T] { if let cast = self as? T { return [cast] } return [] } - + override func match(_ left: [T], collected clld: [T]? = nil) -> MatchResult { let collected: [Pattern] = clld ?? [] let (pos, mtch) = singleMatch(left) - + if mtch == nil { return (false, left, collected) } let match = mtch as! LeafPattern - + var left_ = left left_.remove(at: pos) - + var sameName = collected.filter({ item in if let cast = item as? LeafPattern { return self.name == cast.name } return false }) as! [LeafPattern] - + if (valueType == .int) || (valueType == .list) { - var increment: AnyObject? = 1 as NSNumber + var increment: Any? = 1 if valueType != .int { increment = match.value if let val = match.value as? String { - increment = [val] as NSArray + increment = [val] } } if sameName.isEmpty { @@ -98,14 +98,14 @@ internal class LeafPattern : Pattern { return (true, left_, collected + [match]) } if let inc = increment as? Int { - sameName[0].value = (sameName[0].value as! Int + inc) as NSNumber + sameName[0].value = (sameName[0].value as! Int + inc) sameName[0].valueType = .int } else if let inc = increment as? [String] { - sameName[0].value = (((sameName[0].value as? [String]) ?? [String]()) + inc) as NSArray + sameName[0].value = (((sameName[0].value as? [String]) ?? [String]()) + inc) } return (true, left_, collected) } - + return (true, left_, collected + [match]) } } @@ -118,10 +118,10 @@ func ==(lhs: LeafPattern, rhs: LeafPattern) -> Bool { valEqual = lval == rval } else if let lval = lhs.value as? [String], let rval = rhs.value as? [String] { valEqual = lval == rval - } else if let lval = lhs.value as? NSNumber, let rval = rhs.value as? NSNumber { + } else if let lval = lhs.value as? Int, let rval = rhs.value as? Int { valEqual = lval == rval } else { - valEqual = lhs.value === rhs.value + valEqual = lhs.value as? AnyObject === rhs.value as? AnyObject } return lhs.name == rhs.name && valEqual } diff --git a/Sources/OneOrMore.swift b/Sources/Docopt/OneOrMore.swift similarity index 100% rename from Sources/OneOrMore.swift rename to Sources/Docopt/OneOrMore.swift diff --git a/Sources/Option.swift b/Sources/Docopt/Option.swift similarity index 91% rename from Sources/Option.swift rename to Sources/Docopt/Option.swift index d0dddbc..8f1497d 100644 --- a/Sources/Option.swift +++ b/Sources/Docopt/Option.swift @@ -21,7 +21,7 @@ internal class Option: LeafPattern { } override var description: String { get { - var valueDescription : String = value?.description ?? "nil" + var valueDescription : String = value == nil ? "nil" : "\(value!)" if value is Bool, let val = value as? Bool { valueDescription = val ? "true" : "false" @@ -29,12 +29,12 @@ internal class Option: LeafPattern { return "Option(\(String(describing: short)), \(String(describing: long)), \(argCount), \(valueDescription))" } } - + convenience init(_ option: Option) { self.init(option.short, long: option.long, argCount: option.argCount, value: option.value) } - - init(_ short: String? = nil, long: String? = nil, argCount: UInt = 0, value: AnyObject? = false as NSNumber) { + + init(_ short: String? = nil, long: String? = nil, argCount: UInt = 0, value: Any? = false) { assert(argCount <= 1) self.short = short self.long = long @@ -47,17 +47,17 @@ internal class Option: LeafPattern { self.value = value } } - + static func parse(_ optionDescription: String) -> Option { var short: String? = nil var long: String? = nil var argCount: UInt = 0 - var value: AnyObject? = kCFBooleanFalse - + var value: Any? = false + var (options, _, description) = optionDescription.strip().partition(" ") options = options.replacingOccurrences(of: ",", with: " ", options: [], range: nil) options = options.replacingOccurrences(of: "=", with: " ", options: [], range: nil) - + for s in options.components(separatedBy: " ").filter({!$0.isEmpty}) { if s.hasPrefix("--") { long = s @@ -67,22 +67,22 @@ internal class Option: LeafPattern { argCount = 1 } } - + if argCount == 1 { let matched = description.findAll("\\[default: (.*)\\]", flags: .caseInsensitive) if matched.count > 0 { - value = matched[0] as AnyObject + value = matched[0] } else { value = nil } } - + return Option(short, long: long, argCount: argCount, value: value) } - + override func singleMatch(_ left: [T]) -> SingleMatchResult { for i in 0.. Pattern { let either = Pattern.transform(self).children.map { ($0 as! Required).children } - + for c in either { for ch in c { let filteredChildren = c.filter {$0 == ch} @@ -39,23 +39,23 @@ internal class Pattern: Equatable, Hashable, CustomStringConvertible { let e = child as! LeafPattern if ((e is Argument) && !(e is Command)) || ((e is Option) && (e as! Option).argCount != 0) { if e.value == nil { - e.value = [String]() as AnyObject + e.value = [String]() } else if !(e.value is [String]) { - e.value = String(describing:e.value!).split() as AnyObject + e.value = String(describing:e.value!).split() } } if (e is Command) || ((e is Option) && (e as! Option).argCount == 0) { - e.value = 0 as AnyObject + e.value = 0 e.valueType = .int } } } } } - + return self } - + static func isInParents(_ child: Pattern) -> Bool { return (child as? Required != nil) || (child as? Optional != nil) @@ -63,18 +63,18 @@ internal class Pattern: Equatable, Hashable, CustomStringConvertible { || (child as? Either != nil) || (child as? OneOrMore != nil) } - + static func transform(_ pattern: Pattern) -> Either { var result = [[Pattern]]() var groups = [[pattern]] while !groups.isEmpty { var children = groups.remove(at: 0) let child: BranchPattern? = children.filter({ self.isInParents($0) }).first as? BranchPattern - + if let child = child { let index = children.index(of: child)! children.remove(at: index) - + if child is Either { for pattern in child.children { groups.append([pattern] + children) @@ -88,7 +88,7 @@ internal class Pattern: Equatable, Hashable, CustomStringConvertible { result.append(children) } } - + return Either(result.map {Required($0)}) } @@ -99,11 +99,11 @@ internal class Pattern: Equatable, Hashable, CustomStringConvertible { func flat(_: T.Type) -> [T] { // abstract return [] } - + func match(_ left: T, collected clld: [T]? = nil) -> MatchResult { return match([left], collected: clld) } - + func match(_ left: [T], collected clld: [T]? = nil) -> MatchResult { // abstract return (false, [], []) } diff --git a/Sources/Required.swift b/Sources/Docopt/Required.swift similarity index 100% rename from Sources/Required.swift rename to Sources/Docopt/Required.swift diff --git a/Sources/String.swift b/Sources/Docopt/String.swift similarity index 100% rename from Sources/String.swift rename to Sources/Docopt/String.swift diff --git a/Sources/Tokens.swift b/Sources/Docopt/Tokens.swift similarity index 100% rename from Sources/Tokens.swift rename to Sources/Docopt/Tokens.swift diff --git a/DocoptTests/DocoptTestCase.swift b/Tests/DocoptTests/DocoptTestCase.swift similarity index 86% rename from DocoptTests/DocoptTestCase.swift rename to Tests/DocoptTests/DocoptTestCase.swift index 10266ba..0269a49 100644 --- a/DocoptTests/DocoptTestCase.swift +++ b/Tests/DocoptTests/DocoptTestCase.swift @@ -11,14 +11,14 @@ import Foundation public class DocoptTestCase { public var name: String = "" public var usage: String = "" - + public let programName: String public let arguments: [String]? - public let expectedOutput: AnyObject - - public init(_ programName: String, arguments: [String]?, expectedOutput: AnyObject) { + public let expectedOutput: Any + + public init(_ programName: String, arguments: [String]?, expectedOutput: Any) { self.programName = programName self.arguments = arguments self.expectedOutput = expectedOutput } -} \ No newline at end of file +} diff --git a/DocoptTests/DocoptTestCaseParser.swift b/Tests/DocoptTests/DocoptTestCaseParser.swift similarity index 92% rename from DocoptTests/DocoptTestCaseParser.swift rename to Tests/DocoptTests/DocoptTestCaseParser.swift index 4273ceb..3443597 100644 --- a/DocoptTests/DocoptTestCaseParser.swift +++ b/Tests/DocoptTests/DocoptTestCaseParser.swift @@ -11,30 +11,30 @@ import Foundation public struct DocoptTestCaseParser { public var testCases: [DocoptTestCase]! - + public init(_ stringOfTestCases: String) { testCases = parse(stringOfTestCases: stringOfTestCases) } - + private func parse(stringOfTestCases: String) -> [DocoptTestCase] { let fixturesWithCommentsStripped: String = removeComments(string: stringOfTestCases) let fixtures: [String] = parseFixtures(fixturesString: fixturesWithCommentsStripped) let testCases: [DocoptTestCase] = parseFixturesArray(fixtureStrings: fixtures) - + return testCases } - + private func removeComments(string: String) -> String { let removeCommentsRegEx = try! NSRegularExpression(pattern: "(?m)#.*$", options: []) let fullRange: NSRange = NSMakeRange(0, string.count) return removeCommentsRegEx.stringByReplacingMatches(in: string, options: [], range: fullRange, withTemplate: "") } - + private func parseFixtures(fixturesString: String) -> [String] { let fixtures: [String] = fixturesString.components(separatedBy:"r\"\"\"") return fixtures.filter { !$0.strip().isEmpty } } - + private func parseFixturesArray(fixtureStrings: [String]) -> [DocoptTestCase] { var allTestCases = [DocoptTestCase]() let testBaseName: String = "Test" @@ -45,20 +45,20 @@ public struct DocoptTestCaseParser { testCase.name = testBaseName + String(testIndex) testIndex += 1 } - + allTestCases += newTestCases } - + return allTestCases } - + private func testCasesFromFixtureString(fixtureString: String) -> [DocoptTestCase] { var testCases = [DocoptTestCase]() let fixtureComponents: [String] = fixtureString.components(separatedBy:"\"\"\"") assert(fixtureComponents.count == 2, "Could not split fixture: \(fixtureString) into components") let usageDoc: String = fixtureComponents[0] let testInvocationString: String = fixtureComponents[1] - + let testInvocations: [String] = parseTestInvocations(stringOfTestInvocations: testInvocationString) for testInvocation in testInvocations { let testCase: DocoptTestCase? = parseTestCase(invocationString: testInvocation) @@ -67,42 +67,44 @@ public struct DocoptTestCaseParser { testCases.append(testCase) } } - + return testCases } - + private func parseTestCase(invocationString: String) -> DocoptTestCase? { let trimmedTestInvocation: String = invocationString.strip() var testInvocationComponents: [String] = trimmedTestInvocation.components(separatedBy:"\n") assert(testInvocationComponents.count >= 2, "Could not split test case: \(trimmedTestInvocation) into components") - + let input: String = testInvocationComponents.remove(at: 0) // first line let expectedOutput: String = testInvocationComponents.joined(separator: "\n") // all remaining lines - + var inputComponents: [String] = input.components(separatedBy:" ") let programName: String = inputComponents.remove(at: 0) // first part - + var error : NSError? - let jsonData: NSData? = expectedOutput.data(using: String.Encoding.utf8, allowLossyConversion: false) as NSData? + let jsonData = expectedOutput.data(using: String.Encoding.utf8, allowLossyConversion: false) if jsonData == nil { NSLog("Error parsing \(expectedOutput) to JSON: \(String(describing: error))") return nil } - let expectedOutputJSON: AnyObject? + let expectedOutputJSON: Any? do { - expectedOutputJSON = try JSONSerialization.jsonObject(with: jsonData! as Data, options: .allowFragments) as AnyObject + expectedOutputJSON = try JSONSerialization.jsonObject(with: jsonData! as Data, options: .allowFragments) } catch let error1 as NSError { error = error1 expectedOutputJSON = nil + } catch { + expectedOutputJSON = nil } if (expectedOutputJSON == nil) { NSLog("Error parsing \(expectedOutput) to JSON: \(String(describing: error))") return nil } - + return DocoptTestCase(programName, arguments: inputComponents, expectedOutput: expectedOutputJSON!) } - + private func parseTestInvocations(stringOfTestInvocations: String) -> [String] { let testInvocations: [String] = stringOfTestInvocations.components(separatedBy:"$ ") return testInvocations.filter { !$0.strip().isEmpty } diff --git a/DocoptTests/DocoptTestCasesTests.swift b/Tests/DocoptTests/DocoptTestCasesTests.swift similarity index 55% rename from DocoptTests/DocoptTestCasesTests.swift rename to Tests/DocoptTests/DocoptTestCasesTests.swift index ddaf89f..0457f59 100644 --- a/DocoptTests/DocoptTestCasesTests.swift +++ b/Tests/DocoptTests/DocoptTestCasesTests.swift @@ -23,47 +23,55 @@ class DocoptTestCasesTests: XCTestCase { XCTAssertTrue(exists, "Fixtures file testcases.docopt does not exist in testing bundle") } } - + func testFixturesFileCanBeOpened() { XCTAssertNotEqual(fixturesFileContents(), "", "Could not read fixtures file") } - + func testTestCases() { let rawTestCases = fixturesFileContents() let parser = DocoptTestCaseParser(rawTestCases) - + for testCase in parser.testCases { - let expectedOutput: AnyObject = testCase.expectedOutput - var result: AnyObject = "user-error" as AnyObject + let expectedOutput: Any = testCase.expectedOutput + var result: Any = "user-error" let opt = Docopt(testCase.usage, argv: testCase.arguments) if DocoptError.errorMessage == nil { - result = opt.result as AnyObject + result = opt.result } else { DocoptError.errorMessage = nil } - if let expectedDictionary = expectedOutput as? NSDictionary, - let resultDictionary = result as? NSDictionary { - if resultDictionary != expectedDictionary - { - XCTAssert(false, - "Test \(testCase.name) failed. Expected:\n\(expectedDictionary)\n\n, got: \(resultDictionary)\n\n") - } - } else if let expectedString = expectedOutput as? String, - let resultString = result as? String { - XCTAssertTrue(resultString == expectedString, - "Test \(testCase.name) failed. Expected:\n\(expectedString)\n\n, got: \(resultString)\n\n") - } else { + if !valuesMatch(expectedOutput, result) { XCTFail("Test \(testCase.name) failed. Expected:\n\(expectedOutput)\n\n, got: \(result)\n\n\(testCase.usage)\n\(String(describing: testCase.arguments))\n\n") } } } - + + private func fallbackFilePath(from exeURL : URL) -> String? { + // SwiftPM currently doesn't support building bundles, and Linux doesn't support + // them at all, so if the tests are run with SwiftPM or on Linux, + // we'll fail to find the bundle path here. + // As a temporary workaround, we can fall back on a relative path from the executable. + // This is fragile as it relies on the assumption that we know where SwiftPM will + // put the build products, and where the testcases file lives relative to them, + // but it's better than just disabling all the tests... + let path = exeURL.appendingPathComponent("../../../../Tests/DocoptTests/testcases.docopt").standardized.path + return path + } + private func fixturesFilePath() -> String? { + #if os(Linux) + return fallbackFilePath(from: URL(fileURLWithPath: CommandLine.arguments[0])) + #else let testBundle: Bundle = Bundle(for: type(of: self)) - return testBundle.path(forResource: "testcases", ofType: "docopt") + guard let path = testBundle.path(forResource: "testcases", ofType: "docopt") else { + return fallbackFilePath(from: testBundle.bundleURL) + } + return path + #endif } - + private func fixturesFileContents() -> String { if let filePath = self.fixturesFilePath() { let fileContents = try! String(contentsOfFile: filePath, encoding: String.Encoding.utf8) @@ -71,4 +79,10 @@ class DocoptTestCasesTests: XCTestCase { } return "" } + + static var allTests = [ + ("testTestCasesFileExists", testTestCasesFileExists), + ("testFixturesFileCanBeOpened", testFixturesFileCanBeOpened), + ("testTestCases", testTestCases), + ] } diff --git a/DocoptTests/DocoptTests.h b/Tests/DocoptTests/DocoptTests.h similarity index 100% rename from DocoptTests/DocoptTests.h rename to Tests/DocoptTests/DocoptTests.h diff --git a/DocoptTests/DocoptTests.swift b/Tests/DocoptTests/DocoptTests.swift similarity index 76% rename from DocoptTests/DocoptTests.swift rename to Tests/DocoptTests/DocoptTests.swift index 0b349b0..89d1f57 100644 --- a/DocoptTests/DocoptTests.swift +++ b/Tests/DocoptTests/DocoptTests.swift @@ -13,7 +13,7 @@ class DocoptTests: XCTestCase { override func setUp() { DocoptError.test = true } - + func testPatternFlat() { XCTAssertEqual(Required([OneOrMore(Argument("N")), Option("-a"), Argument("M")]).flat(), [Argument("N"), Option("-a"), Argument("M")]) XCTAssertEqual(Required([Optional(OptionsShortcut()), Optional(Option("-a"))]).flat(OptionsShortcut.self), [OptionsShortcut()]) @@ -25,7 +25,7 @@ class DocoptTests: XCTestCase { let fixture = [Option("-a"), Option("-r"), Option("-m", argCount: 1)] XCTAssertEqual(parsedDefaults, fixture) } - + func testParseSection() { let usage = "usage: this\nusage:hai\nusage: this that\nusage: foo\n bar\nPROGRAM USAGE:\n foo\n bar\nusage:\n\ttoo\n\ttar\nUsage: eggs spam\nBAZZ\nusage: pit stop" XCTAssertEqual(Docopt.parseSection("usage:", source: "foo bar fizz buzz"), []) @@ -42,12 +42,12 @@ class DocoptTests: XCTestCase { "usage: pit stop", ]) } - + func testFormalUsage() { let doc = "\nUsage: prog [-hv] ARG\n prog N M\n\n prog is a program." let usage = Docopt.parseSection("usage:", source: doc)[0] let formalUsage = Docopt.formalUsage(usage) - + XCTAssertEqual(usage, "Usage: prog [-hv] ARG\n prog N M") XCTAssertEqual(formalUsage, "( [-hv] ARG ) | ( N M )") } @@ -55,29 +55,29 @@ class DocoptTests: XCTestCase { func testParseArgv() { var o = [Option("-h"), Option("-v", long: "--verbose"), Option("-f", long:"--file", argCount: 1)] let TS = {(s: String) in return Tokens(s, error: DocoptExit()) } - + XCTAssertEqual(Docopt.parseArgv(TS(""), options: &o), []) - XCTAssertEqual(Docopt.parseArgv(TS("-h"), options: &o), [Option("-h", value: true as AnyObject)]) + XCTAssertEqual(Docopt.parseArgv(TS("-h"), options: &o), [Option("-h", value: true)]) XCTAssertEqual(Docopt.parseArgv(TS("-h --verbose"), options: &o), - [Option("-h", value: true as AnyObject), Option("-v", long: "--verbose", value: true as AnyObject)]) + [Option("-h", value: true), Option("-v", long: "--verbose", value: true)]) XCTAssertEqual(Docopt.parseArgv(TS("-h --file f.txt"), options: &o), - [Option("-h", value: true as AnyObject), Option("-f", long: "--file", argCount: 1, value: "f.txt" as AnyObject)]) + [Option("-h", value: true), Option("-f", long: "--file", argCount: 1, value: "f.txt")]) XCTAssertEqual(Docopt.parseArgv(TS("-h --file f.txt arg"), options: &o), - [Option("-h", value: true as AnyObject), - Option("-f", long: "--file", argCount: 1, value: "f.txt" as AnyObject), - Argument(nil, value: "arg" as AnyObject)]) + [Option("-h", value: true), + Option("-f", long: "--file", argCount: 1, value: "f.txt"), + Argument(nil, value: "arg")]) XCTAssertEqual(Docopt.parseArgv(TS("-h --file f.txt arg arg2"), options: &o), - [Option("-h", value: true as AnyObject), - Option("-f", long: "--file", argCount: 1, value: "f.txt" as AnyObject), - Argument(nil, value: "arg" as AnyObject), - Argument(nil, value: "arg2" as AnyObject)]) + [Option("-h", value: true), + Option("-f", long: "--file", argCount: 1, value: "f.txt"), + Argument(nil, value: "arg"), + Argument(nil, value: "arg2")]) XCTAssertEqual(Docopt.parseArgv(TS("-h arg -- -v"), options: &o), - [Option("-h", value: true as AnyObject), - Argument(nil, value: "arg" as AnyObject), - Argument(nil, value: "--" as AnyObject), - Argument(nil, value: "-v" as AnyObject)]) + [Option("-h", value: true), + Argument(nil, value: "arg"), + Argument(nil, value: "--"), + Argument(nil, value: "-v")]) } - + func testOptionParse() { XCTAssertEqual(Option.parse("-h"), Option("-h")) XCTAssertEqual(Option.parse("--help"), Option(long: "--help")) @@ -97,23 +97,23 @@ class DocoptTests: XCTestCase { XCTAssertEqual(Option.parse(" -h"), Option("-h")) XCTAssertEqual(Option.parse("-h TOPIC Descripton... [default: 2]"), - Option("-h", argCount: 1, value: "2" as AnyObject)) + Option("-h", argCount: 1, value: "2")) XCTAssertEqual(Option.parse("-h TOPIC Descripton... [default: topic-1]"), - Option("-h", argCount: 1, value: "topic-1" as AnyObject)) + Option("-h", argCount: 1, value: "topic-1")) XCTAssertEqual(Option.parse("--help=TOPIC ... [default: 3.14]"), - Option(long: "--help", argCount: 1, value: "3.14" as AnyObject)) + Option(long: "--help", argCount: 1, value: "3.14")) XCTAssertEqual(Option.parse("-h, --help=DIR ... [default: ./]"), - Option("-h", long: "--help", argCount: 1, value: "./" as AnyObject)) + Option("-h", long: "--help", argCount: 1, value: "./")) XCTAssertEqual(Option.parse("-h TOPIC Descripton... [dEfAuLt: 2]"), - Option("-h", argCount: 1, value: "2" as AnyObject)) + Option("-h", argCount: 1, value: "2")) } - + func testOptionName() { XCTAssertEqual(Option("-h").name!, "-h") XCTAssertEqual(Option("-h", long: "--help").name!, "--help") XCTAssertEqual(Option(long: "--help").name!, "--help") } - + func testParsePattern() { var o = [Option("-h"), Option("-v", long: "--verbose"), Option("-f", long:"--file", argCount: 1)] XCTAssertEqual(Docopt.parsePattern("[ -h ]", options: &o), Required(Optional(Option("-h")))) @@ -152,42 +152,42 @@ class DocoptTests: XCTestCase { XCTAssertEqual(Docopt.parsePattern("", options: &o), Required(Argument(""))) XCTAssertEqual(Docopt.parsePattern("add", options: &o), Required(Command("add"))) } - + func testOptionMatch() { - XCTAssertTrue(Option("-a").match([Option("-a", value: true as AnyObject)]) == - (true, [], [Option("-a", value: true as AnyObject)])) + XCTAssertTrue(Option("-a").match([Option("-a", value: true)]) == + (true, [], [Option("-a", value: true)])) XCTAssertTrue(Option("-a").match([Option("-x")]) == (false, [Option("-x")], [])) XCTAssertTrue(Option("-a").match([Argument("N")]) == (false, [Argument("N")], [])) XCTAssertTrue(Option("-a").match([Option("-x"), Option("-a"), Argument("N")]) == (true, [Option("-x"), Argument("N")], [Option("-a")])) - XCTAssertTrue(Option("-a").match([Option("-a", value: true as AnyObject), Option("-a")]) == - (true, [Option("-a")], [Option("-a", value: true as AnyObject)])) + XCTAssertTrue(Option("-a").match([Option("-a", value: true), Option("-a")]) == + (true, [Option("-a")], [Option("-a", value: true)])) } - + func testArgumentMatch() { XCTAssertTrue(Argument("N").match(Argument(nil, value: 9)) == (true, [], [Argument("N", value: 9)])) XCTAssertTrue(Argument("N").match(Option("-x")) == (false, [Option("-x")], [])) - XCTAssertTrue(Argument("N").match([Option("-x"), Option("-a"), Argument(nil, value: 5 as AnyObject)]) == - (true, [Option("-x"), Option("-a")], [Argument("N", value: 5 as AnyObject)])) - XCTAssertTrue(Argument("N").match([Argument(nil, value: 9 as AnyObject), Argument(nil, value: 0 as AnyObject)]) == - (true, [Argument(nil, value: 0 as AnyObject)], [Argument("N", value: 9 as AnyObject)])) + XCTAssertTrue(Argument("N").match([Option("-x"), Option("-a"), Argument(nil, value: 5)]) == + (true, [Option("-x"), Option("-a")], [Argument("N", value: 5)])) + XCTAssertTrue(Argument("N").match([Argument(nil, value: 9), Argument(nil, value: 0)]) == + (true, [Argument(nil, value: 0)], [Argument("N", value: 9)])) } - + func testCommandMatch() { - XCTAssertTrue(Command("c").match(Argument(nil, value: "c" as AnyObject)) == - (true, [], [Command("c", value: true as AnyObject)])) + XCTAssertTrue(Command("c").match(Argument(nil, value: "c")) == + (true, [], [Command("c", value: true)])) XCTAssertTrue(Command("c").match(Option("-x")) == (false, [Option("-x")], [])) - XCTAssertTrue(Command("c").match([Option("-x"), Option("-a"), Argument(nil, value: "c" as AnyObject)]) == - (true, [Option("-x"), Option("-a")], [Command("c", value: true as AnyObject)])) - XCTAssertTrue(Either([Command("add"), Command("rm")]).match(Argument(nil, value: "rm" as AnyObject)) == - (true, [], [Command("rm", value: true as AnyObject)])) + XCTAssertTrue(Command("c").match([Option("-x"), Option("-a"), Argument(nil, value: "c")]) == + (true, [Option("-x"), Option("-a")], [Command("c", value: true)])) + XCTAssertTrue(Either([Command("add"), Command("rm")]).match(Argument(nil, value: "rm")) == + (true, [], [Command("rm", value: true)])) } - + func testOptionalMatch() { XCTAssertTrue(Optional(Option("-a")).match([Option("-a")]) == (true, [], [Option("-a")])) @@ -200,13 +200,13 @@ class DocoptTests: XCTestCase { (true, [], [Option("-b")])) XCTAssertTrue(Optional([Option("-a"), Option("-b")]).match([Option("-x")]) == (true, [Option("-x")], [])) - XCTAssertTrue(Optional(Argument("N")).match([Argument(nil, value: 9 as AnyObject)]) == - (true, [], [Argument("N", value: 9 as AnyObject)])) + XCTAssertTrue(Optional(Argument("N")).match([Argument(nil, value: 9)]) == + (true, [], [Argument("N", value: 9)])) XCTAssertTrue(Optional([Option("-a"), Option("-b")]).match( [Option("-b"), Option("-x"), Option("-a")]) == (true, [Option("-x")], [Option("-a"), Option("-b")])) } - + func testRequiredMatch() { XCTAssertTrue(Required(Option("-a")).match([Option("-a")]) == (true, [], [Option("-a")])) @@ -216,7 +216,7 @@ class DocoptTests: XCTestCase { XCTAssertTrue(Required([Option("-a"), Option("-b")]).match([Option("-a")]) == (false, [Option("-a")], [])) } - + func testEitherMatch() { // i'm too lazy to mock up a fixture of some kind. deal with it. var expected: MatchResult @@ -238,38 +238,38 @@ class DocoptTests: XCTestCase { actual = Either([Option("-a"), Option("-b"), Option("-c")]).match([Option("-x"), Option("-b")]) XCTAssertTrue(actual == expected, "\nExpected: \(expected)\nActual: \(actual)\n\n") - expected = (true, [], [Argument("N", value: 1 as AnyObject), Argument("M", value: 2 as AnyObject)]) - actual = Either([Argument("M"), Required([Argument("N"), Argument("M")])]).match([Argument(nil, value: 1 as AnyObject), Argument(nil, value: 2 as AnyObject)]) + expected = (true, [], [Argument("N", value: 1), Argument("M", value: 2)]) + actual = Either([Argument("M"), Required([Argument("N"), Argument("M")])]).match([Argument(nil, value: 1), Argument(nil, value: 2)]) XCTAssertTrue(actual == expected, "\nExpected: \(expected)\nActual: \(actual)\n\n") } func testOneOrMoreMatch() { - XCTAssertTrue(OneOrMore(Argument("N")).match([Argument(nil, value: 9 as AnyObject)]) == - (true, [], [Argument("N", value: 9 as AnyObject)])) + XCTAssertTrue(OneOrMore(Argument("N")).match([Argument(nil, value: 9)]) == + (true, [], [Argument("N", value: 9)])) XCTAssertTrue(OneOrMore(Argument("N")).match([]) == (false, [], [])) XCTAssertTrue(OneOrMore(Argument("N")).match([Option("-x")]) == (false, [Option("-x")], [])) XCTAssertTrue(OneOrMore(Argument("N")).match( - [Argument(nil, value: 9 as AnyObject), Argument(nil, value: 8 as AnyObject)]) == ( - true, [], [Argument("N", value: 9 as AnyObject), Argument("N", value: 8 as AnyObject)])) + [Argument(nil, value: 9), Argument(nil, value: 8)]) == ( + true, [], [Argument("N", value: 9), Argument("N", value: 8)])) XCTAssertTrue(OneOrMore(Argument("N")).match( - [Argument(nil, value: 9 as AnyObject), Option("-x"), Argument(nil, value: 8 as AnyObject)]) == ( - true, [Option("-x")], [Argument("N", value: 9 as AnyObject), Argument("N", value: 8 as AnyObject)])) + [Argument(nil, value: 9), Option("-x"), Argument(nil, value: 8)]) == ( + true, [Option("-x")], [Argument("N", value: 9), Argument("N", value: 8)])) XCTAssertTrue(OneOrMore(Option("-a")).match( - [Option("-a"), Argument(nil, value: 8 as AnyObject), Option("-a")]) == - (true, [Argument(nil, value: 8 as AnyObject)], [Option("-a"), Option("-a")])) - XCTAssertTrue(OneOrMore(Option("-a")).match([Argument(nil, value: 8 as AnyObject), + [Option("-a"), Argument(nil, value: 8), Option("-a")]) == + (true, [Argument(nil, value: 8)], [Option("-a"), Option("-a")])) + XCTAssertTrue(OneOrMore(Option("-a")).match([Argument(nil, value: 8), Option("-x")]) == - (false, [Argument(nil, value: 8 as AnyObject), Option("-x")], [])) + (false, [Argument(nil, value: 8), Option("-x")], [])) XCTAssertTrue(OneOrMore(Required([Option("-a"), Argument("N")])).match( - [Option("-a"), Argument(nil, value: 1 as AnyObject), Option("-x"), - Option("-a"), Argument(nil, value: 2 as AnyObject)]) == + [Option("-a"), Argument(nil, value: 1), Option("-x"), + Option("-a"), Argument(nil, value: 2)]) == (true, [Option("-x")], - [Option("-a"), Argument("N", value: 1 as AnyObject), Option("-a"), Argument("N", value: 2 as AnyObject)])) - XCTAssertTrue(OneOrMore(Optional(Argument("N"))).match([Argument(nil, value: 9 as AnyObject)]) == - (true, [], [Argument("N", value: 9 as AnyObject)])) + [Option("-a"), Argument("N", value: 1), Option("-a"), Argument("N", value: 2)])) + XCTAssertTrue(OneOrMore(Optional(Argument("N"))).match([Argument(nil, value: 9)]) == + (true, [], [Argument("N", value: 9)])) } - + func testPatternEither() { XCTAssertEqual(Pattern.transform(Option("-a")), Either(Required(Option("-a")))) XCTAssertEqual(Pattern.transform(Argument("A")), Either(Required(Argument("A")))) @@ -290,7 +290,7 @@ class DocoptTests: XCTestCase { Either(Required([Argument("N"), Argument("M"), Argument("N"), Argument("M")]))) } - + func testFixRepeatingArguments() { XCTAssertEqual(Option("-a").fixRepeatingArguments(), Option("-a")) XCTAssertEqual(Argument("N").fixRepeatingArguments(), Argument("N")) @@ -301,52 +301,72 @@ class DocoptTests: XCTestCase { OneOrMore(Argument("N"))]).fix(), Either([Argument("N", value: []), OneOrMore(Argument("N", value: []))])) } - + func testListArgumentMatch() { XCTAssertTrue(Required([Argument("N"), Argument("N")]).fix().match( - [Argument(nil, value: "1" as AnyObject), Argument(nil, value: "2" as AnyObject)]) == + [Argument(nil, value: "1"), Argument(nil, value: "2")]) == (true, [], [Argument("N", value: ["1", "2"])])) XCTAssertTrue(OneOrMore(Argument("N")).fix().match( - [Argument(nil, value: "1" as AnyObject), Argument(nil, value: "2" as AnyObject), Argument(nil, value: "3" as AnyObject)]) == + [Argument(nil, value: "1"), Argument(nil, value: "2"), Argument(nil, value: "3")]) == (true, [], [Argument("N", value: ["1", "2", "3"])])) XCTAssertTrue(Required([Argument("N"), OneOrMore(Argument("N"))]).fix().match( - [Argument(nil, value: "1" as AnyObject), Argument(nil, value: "2" as AnyObject), Argument(nil, value: "3" as AnyObject)]) == + [Argument(nil, value: "1"), Argument(nil, value: "2"), Argument(nil, value: "3")]) == (true, [], [Argument("N", value: ["1", "2", "3"])])) XCTAssertTrue(Required([Argument("N"), Required(Argument("N"))]).fix().match( - [Argument(nil, value: "1" as AnyObject), Argument(nil, value: "2" as AnyObject)]) == + [Argument(nil, value: "1"), Argument(nil, value: "2")]) == (true, [], [Argument("N", value: ["1", "2"])])) } - + func testBasicPatternMatch() { // ( -a N [ -x Z ] ) let pattern = Required([Option("-a"), Argument("N"), Optional([Option("-x"), Argument("Z")])]) - + // -a N - XCTAssertTrue(pattern.match([Option("-a"), Argument(nil, value: 9 as AnyObject)]) == - (true, [], [Option("-a"), Argument("N", value: 9 as AnyObject)])) + XCTAssertTrue(pattern.match([Option("-a"), Argument(nil, value: 9)]) == + (true, [], [Option("-a"), Argument("N", value: 9)])) // -a -x N Z - XCTAssertTrue(pattern.match([Option("-a"), Option("-x"), Argument(nil, value: 9 as AnyObject), Argument(nil, value: 5 as AnyObject)]) == - (true, [], [Option("-a"), Argument("N", value: 9 as AnyObject), Option("-x"), Argument("Z", value: 5 as AnyObject)])) + XCTAssertTrue(pattern.match([Option("-a"), Option("-x"), Argument(nil, value: 9), Argument(nil, value: 5)]) == + (true, [], [Option("-a"), Argument("N", value: 9), Option("-x"), Argument("Z", value: 5)])) // -x N Z # BZZ! - XCTAssertTrue(pattern.match([Option("-x"), Argument(nil, value: 9 as AnyObject), Argument(nil, value: 5 as AnyObject)]) == - (false, [Option("-x"), Argument(nil, value: 9 as AnyObject), Argument(nil, value: 5 as AnyObject)], [])) + XCTAssertTrue(pattern.match([Option("-x"), Argument(nil, value: 9), Argument(nil, value: 5)]) == + (false, [Option("-x"), Argument(nil, value: 9), Argument(nil, value: 5)], [])) } - + func testSet() { XCTAssertEqual(Argument("N"), Argument("N")) XCTAssertEqual(Set([Argument("N"), Argument("N")]), Set([Argument("N")])) } - + func testDocopt() { let doc = "Usage: prog [-v] A\n\n Options: -v Be verbose." let result = Docopt(doc, argv: ["arg"]).result - let fixture = ["-v": false as AnyObject, "A": "arg" as AnyObject] - for (key, value) in result! - { - XCTAssertEqual(value as! NSObject, fixture[key]! as! NSObject) - } - XCTAssertEqual(result!.count, fixture.count) + let fixture : [String:Any] = ["-v": false, "A": "arg"] + XCTAssertTrue(valuesMatch(result, fixture)) } + + static var allTests = [ + ("testPatternFlat", testPatternFlat), + ("testParseDefaults", testParseDefaults), + ("testParseSection", testParseSection), + ("testFormalUsage", testFormalUsage), + ("testParseArgv", testParseArgv), + ("testOptionParse", testOptionParse), + ("testOptionName", testOptionName), + ("testParsePattern", testParsePattern), + ("testOptionMatch", testOptionMatch), + ("testArgumentMatch", testArgumentMatch), + ("testCommandMatch", testCommandMatch), + ("testOptionalMatch", testOptionalMatch), + ("testRequiredMatch", testRequiredMatch), + ("testEitherMatch", testEitherMatch), + ("testOneOrMoreMatch", testOneOrMoreMatch), + ("testPatternEither", testPatternEither), + ("testFixRepeatingArguments", testFixRepeatingArguments), + ("testListArgumentMatch", testListArgumentMatch), + ("testBasicPatternMatch", testBasicPatternMatch), + ("testSet", testSet), + ("testDocopt", testDocopt), + ] } internal func ==(lhs: MatchResult, rhs: MatchResult) -> Bool { @@ -354,4 +374,3 @@ internal func ==(lhs: MatchResult, rhs: MatchResult) -> Bool { && lhs.left == rhs.left && lhs.collected == rhs.collected } - diff --git a/DocoptTests/Info.plist b/Tests/DocoptTests/Info.plist similarity index 100% rename from DocoptTests/Info.plist rename to Tests/DocoptTests/Info.plist diff --git a/Tests/DocoptTests/ValueMatching.swift b/Tests/DocoptTests/ValueMatching.swift new file mode 100644 index 0000000..fb415b5 --- /dev/null +++ b/Tests/DocoptTests/ValueMatching.swift @@ -0,0 +1,106 @@ +// +// ValueMatching.swift +// Docopt +// +// Created by Sam Deane on 08/03/2018. +// + +import Foundation + +/** + Crude generic matching. + + Implements a slightly fuzzy generic test for equality between + two values of unknown type. + + Allows us to check dictionaries, arrays for equality, in + situations where values might be equivalent but not strictly equal. + + For example matching the integer 1 against the string "1" for a + given key in a dictionary. + */ + +/** + If the values are both the same type, and it's equatable, then + it's easy. + */ + +internal func valuesMatch(_ v1 : T, _ v2 : T) -> Bool { + return v1 == v2 +} + +/** + If the values could be any type, we work through a series of + alternatives attempting to case them to basic types, then + compare those. + + Worst-case scenario we fall back on describing both values + as strings and comparing that. + */ + +internal func valuesMatch(_ v1 : Any, _ v2 : Any) -> Bool { + if let d1 = v1 as? [String:Any], let d2 = v2 as? [String:Any] { + return d1.matches(d2) + } + if let a1 = v1 as? [Any], let a2 = v2 as? [Any] { + return a1.matches(a2) + } + if let i1 = v1 as? Int, let i2 = v2 as? Int { + return i1 == i2 + } + if let b1 = v1 as? Bool, let b2 = v2 as? Bool { + return b1 == b2 + } + if let s1 = v1 as? String, let s2 = v2 as? String { + return s1 == s2 + } + if let n1 = v1 as? NSNull, let n2 = v2 as? NSNull { + return n1 == n2 + } + + return "\(v1)" == "\(v2)" +} + +/** + Arrays just match each element in turn. + */ + +extension Array { + func matches(_ other : [Any]) -> Bool { + if count != other.count { + return false + } + + var index = 0 + for value in self { + let otherValue = other[index] + if !valuesMatch(value, otherValue) { + return false + } + index += 1 + } + return true + } +} + +/** + Dictionaries match by filtering out all matching + key/value pairs. If there's nothing left + */ + +extension Dictionary { + func matches(_ other : [String:Any]) -> Bool { + if self.count != other.count { + return false + } + + let remaining = self.filter { (key, value) -> Bool in + if let otherValue = other[key as! String] { + return !valuesMatch(value, otherValue) + } + return true + } + + return remaining.count == 0 + } +} diff --git a/Tests/DocoptTests/ValueMatchingTests.swift b/Tests/DocoptTests/ValueMatchingTests.swift new file mode 100644 index 0000000..ec44916 --- /dev/null +++ b/Tests/DocoptTests/ValueMatchingTests.swift @@ -0,0 +1,47 @@ +// +// ValueMatchingSwiftTests.swift +// Docopt +// +// Created by Sam Deane on 08/03/2018. +// + +import XCTest + +class ValueMatchingTests: XCTestCase { + func testNumberNumber() { + XCTAssertTrue(valuesMatch(1, 1)) + XCTAssertFalse(valuesMatch(1, 4)) + } + func testNumberString() { + XCTAssertTrue(valuesMatch(1, "1")) + XCTAssertFalse(valuesMatch(1, "4")) + } + func testStringString() { + XCTAssertTrue(valuesMatch("test", "test")) + XCTAssertFalse(valuesMatch("test", "blah")) + } + func testBoolBool() { + XCTAssertTrue(valuesMatch(false, false)) + XCTAssertFalse(valuesMatch(false, true)) + } + func testArrays() { + XCTAssertTrue(valuesMatch([10,20], ["10", "20"])) + XCTAssertFalse(valuesMatch([10,20], [20,10])) + XCTAssertFalse(valuesMatch([10,20], [10])) + XCTAssertFalse(valuesMatch([10,20], 10)) + } + func testDictionaries() { + XCTAssertTrue(valuesMatch(["name" : "test", "x" : 10, "y" : "20", "check" : false], ["x" : "10", "y" : 20, "name" : "test", "check" : false])) + } + + static var allTests = [ + ("testNumberNumber", testNumberNumber), + ("testNumberString", testNumberString), + ("testStringString", testStringString), + ("testBoolBool", testBoolBool), + ("testArrays", testArrays), + ("testDictionaries", testDictionaries), + ] + +} + diff --git a/DocoptTests/testcases.docopt b/Tests/DocoptTests/testcases.docopt similarity index 100% rename from DocoptTests/testcases.docopt rename to Tests/DocoptTests/testcases.docopt diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift new file mode 100644 index 0000000..a37d80c --- /dev/null +++ b/Tests/LinuxMain.swift @@ -0,0 +1,8 @@ +import XCTest +@testable import DocoptTests + +XCTMain([ + testCase(DocoptTestCasesTests.allTests), + testCase(DocoptTests.allTests), + testCase(ValueMatchingTests.allTests) +])