diff --git a/Sources/TSCUtility/Version.swift b/Sources/TSCUtility/Version.swift index 45817493..d9b08af1 100644 --- a/Sources/TSCUtility/Version.swift +++ b/Sources/TSCUtility/Version.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors + Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors Licensed under Apache License v2.0 with Runtime Library Exception See http://swift.org/LICENSE.txt for license information @@ -11,7 +11,7 @@ import TSCBasic /// A struct representing a semver version. -public struct Version: Hashable { +public struct Version { /// The major version. public let major: Int @@ -28,7 +28,7 @@ public struct Version: Hashable { /// The build metadata. public let buildMetadataIdentifiers: [String] - /// Create a version object. + /// Creates a version object. public init( _ major: Int, _ minor: Int, @@ -45,12 +45,122 @@ public struct Version: Hashable { } } -extension Version: Comparable { +/// An error that occurs during the creation of a version. +public enum VersionError: Error, CustomStringConvertible { + /// The version string contains non-ASCII characters. + case nonASCIIVersionString(_ versionString: String) + /// The version core contains an invalid number of Identifiers. + case invalidVersionCoreIdentifiersCount(_ identifiers: [String]) + /// Some or all of the version core identifiers contain non-numerical characters or are empty. + case nonNumericalOrEmptyVersionCoreIdentifiers(_ identifiers: [String]) + /// Some or all of the pre-release identifiers contain characters other than alpha-numerics and hyphens. + case nonAlphaNumerHyphenalPrereleaseIdentifiers(_ identifiers: [String]) + /// Some or all of the build metadata identifiers contain characters other than alpha-numerics and hyphens. + case nonAlphaNumerHyphenalBuildMetadataIdentifiers(_ identifiers: [String]) + + public var description: String { + switch self { + case let .nonASCIIVersionString(versionString): + return "non-ASCII characters in version string '\(versionString)'" + case let .invalidVersionCoreIdentifiersCount(identifiers): + return "\(identifiers.count < 3 ? "fewer" : "more") than 3 identifiers in version core '\(identifiers.joined(separator: "."))'" + case let .nonNumericalOrEmptyVersionCoreIdentifiers(identifiers): + if !identifiers.allSatisfy( { !$0.isEmpty } ) { + return "empty identifiers in version core '\(identifiers.joined(separator: "."))'" + } else { + // Not checking for `.isASCII` here because non-ASCII characters should've already been caught before this. + let nonNumericalIdentifiers = identifiers.filter { !$0.allSatisfy(\.isNumber) } + return "non-numerical characters in version core identifier\(nonNumericalIdentifiers.count > 1 ? "s" : "") \(nonNumericalIdentifiers.map { "'\($0)'" } .joined(separator: ", "))" + } + case let .nonAlphaNumerHyphenalPrereleaseIdentifiers(identifiers): + // Not checking for `.isASCII` here because non-ASCII characters should've already been caught before this. + let nonAlphaNumericalIdentifiers = identifiers.filter { !$0.allSatisfy { $0.isLetter || $0.isNumber || $0 == "-" } } + return "characters other than alpha-numerics and hyphens in pre-release identifier\(nonAlphaNumericalIdentifiers.count > 1 ? "s" : "") \(nonAlphaNumericalIdentifiers.map { "'\($0)'" } .joined(separator: ", "))" + case let .nonAlphaNumerHyphenalBuildMetadataIdentifiers(identifiers): + // Not checking for `.isASCII` here because non-ASCII characters should've already been caught before this. + let nonAlphaNumericalIdentifiers = identifiers.filter { !$0.allSatisfy { $0.isLetter || $0.isNumber || $0 == "-" } } + return "characters other than alpha-numerics and hyphens in build metadata identifier\(nonAlphaNumericalIdentifiers.count > 1 ? "s" : "") \(nonAlphaNumericalIdentifiers.map { "'\($0)'" } .joined(separator: ", "))" + } + } +} + +extension Version { + // TODO: Rename this function to `init(string: String) throws`, after `init?(string: String)` is removed. + // TODO: Find a better error-checking order. + // Currently, if a version string is "forty-two", this initializer throws an error that says "forty" is only 1 version core identifier, which is not enough. + // But this is misleading the user to consider "forty" as a valid version core identifier. + // We should find a way to check for (or throw) "wrong characters used" errors first, but without overly-complicating the logic. + /// Creates a version from the given string. + /// - Parameter versionString: The string to create the version from. + /// - Throws: A `VersionError` instance if the `versionString` doesn't follow [SemVer 2.0.0](https://semver.org). + public init(versionString: String) throws { + // SemVer 2.0.0 allows only ASCII alphanumerical characters and "-" in the version string, except for "." and "+" as delimiters. ("-" is used as a delimiter between the version core and pre-release identifiers, but it's allowed within pre-release and metadata identifiers as well.) + // Alphanumerics check will come later, after each identifier is split out (i.e. after the delimiters are removed). + guard versionString.allSatisfy(\.isASCII) else { + throw VersionError.nonASCIIVersionString(versionString) + } + + let metadataDelimiterIndex = versionString.firstIndex(of: "+") + // SemVer 2.0.0 requires that pre-release identifiers come before build metadata identifiers + let prereleaseDelimiterIndex = versionString[..<(metadataDelimiterIndex ?? versionString.endIndex)].firstIndex(of: "-") + + let versionCore = versionString[..<(prereleaseDelimiterIndex ?? metadataDelimiterIndex ?? versionString.endIndex)] + let versionCoreIdentifiers = versionCore.split(separator: ".", omittingEmptySubsequences: false) + + guard versionCoreIdentifiers.count == 3 else { + throw VersionError.invalidVersionCoreIdentifiersCount(versionCoreIdentifiers.map { String($0) }) + } + + guard + // Major, minor, and patch versions must be ASCII numbers, according to the semantic versioning standard. + // Converting each identifier from a substring to an integer doubles as checking if the identifiers have non-numeric characters. + let major = Int(versionCoreIdentifiers[0]), + let minor = Int(versionCoreIdentifiers[1]), + let patch = Int(versionCoreIdentifiers[2]) + else { + throw VersionError.nonNumericalOrEmptyVersionCoreIdentifiers(versionCoreIdentifiers.map { String($0) }) + } + + self.major = major + self.minor = minor + self.patch = patch + + if let prereleaseDelimiterIndex = prereleaseDelimiterIndex { + let prereleaseStartIndex = versionString.index(after: prereleaseDelimiterIndex) + let prereleaseIdentifiers = versionString[prereleaseStartIndex..<(metadataDelimiterIndex ?? versionString.endIndex)].split(separator: ".", omittingEmptySubsequences: false) + guard prereleaseIdentifiers.allSatisfy( { $0.allSatisfy { $0.isLetter || $0.isNumber || $0 == "-" } } ) else { + throw VersionError.nonAlphaNumerHyphenalPrereleaseIdentifiers(prereleaseIdentifiers.map { String($0) }) + } + self.prereleaseIdentifiers = prereleaseIdentifiers.map { String($0) } + } else { + self.prereleaseIdentifiers = [] + } + + if let metadataDelimiterIndex = metadataDelimiterIndex { + let metadataStartIndex = versionString.index(after: metadataDelimiterIndex) + let buildMetadataIdentifiers = versionString[metadataStartIndex...].split(separator: ".", omittingEmptySubsequences: false) + guard buildMetadataIdentifiers.allSatisfy( { $0.allSatisfy { $0.isLetter || $0.isNumber || $0 == "-" } } ) else { + throw VersionError.nonAlphaNumerHyphenalBuildMetadataIdentifiers(buildMetadataIdentifiers.map { String($0) }) + } + self.buildMetadataIdentifiers = buildMetadataIdentifiers.map { String($0) } + } else { + self.buildMetadataIdentifiers = [] + } + } +} + +extension Version: Comparable, Hashable { func isEqualWithoutPrerelease(_ other: Version) -> Bool { return major == other.major && minor == other.minor && patch == other.patch } + // Although `Comparable` inherits from `Equatable`, it does not provide a new default implementation of `==`, but instead uses `Equatable`'s default synthesised implementation. The compiler-synthesised `==`` is composed of [member-wise comparisons](https://github.com/apple/swift-evolution/blob/main/proposals/0185-synthesize-equatable-hashable.md#implementation-details), which leads to a false `false` when 2 semantic versions differ by only their build metadata identifiers, contradicting SemVer 2.0.0's [comparison rules](https://semver.org/#spec-item-10). + @inlinable + public static func == (lhs: Version, rhs: Version) -> Bool { + !(lhs < rhs) && !(lhs > rhs) + } + public static func < (lhs: Version, rhs: Version) -> Bool { let lhsComparators = [lhs.major, lhs.minor, lhs.patch] let rhsComparators = [rhs.major, rhs.minor, rhs.patch] @@ -64,7 +174,7 @@ extension Version: Comparable { } guard rhs.prereleaseIdentifiers.count > 0 else { - return true // Prerelease lhs < non-prerelease rhs + return true // Prerelease lhs < non-prerelease rhs } let zippedIdentifiers = zip(lhs.prereleaseIdentifiers, rhs.prereleaseIdentifiers) @@ -88,6 +198,15 @@ extension Version: Comparable { return lhs.prereleaseIdentifiers.count < rhs.prereleaseIdentifiers.count } + + // Custom `Equatable` conformance leads to custom `Hashable` conformance. + // [SR-11588](https://bugs.swift.org/browse/SR-11588) + public func hash(into hasher: inout Hasher) { + hasher.combine(major) + hasher.combine(minor) + hasher.combine(patch) + hasher.combine(prereleaseIdentifiers) + } } extension Version: CustomStringConvertible { @@ -103,47 +222,28 @@ extension Version: CustomStringConvertible { } } -public extension Version { +extension Version: LosslessStringConvertible { + /// Initializes a version struct with the provided version string. + /// - Parameter version: A version string to use for creating a new version struct. + public init?(_ versionString: String) { + try? self.init(versionString: versionString) + } +} +extension Version { + // This initialiser is no longer necessary, but kept around for source compatibility with SwiftPM. /// Create a version object from string. - /// - /// - Parameters: - /// - string: The string to parse. - init?(string: String) { - let prereleaseStartIndex = string.firstIndex(of: "-") - let metadataStartIndex = string.firstIndex(of: "+") - - let requiredEndIndex = prereleaseStartIndex ?? metadataStartIndex ?? string.endIndex - let requiredCharacters = string.prefix(upTo: requiredEndIndex) - let requiredComponents = requiredCharacters - .split(separator: ".", maxSplits: 2, omittingEmptySubsequences: false) - .map(String.init).compactMap({ Int($0) }).filter({ $0 >= 0 }) - - guard requiredComponents.count == 3 else { return nil } - - self.major = requiredComponents[0] - self.minor = requiredComponents[1] - self.patch = requiredComponents[2] - - func identifiers(start: String.Index?, end: String.Index) -> [String] { - guard let start = start else { return [] } - let identifiers = string[string.index(after: start).. Bool { // Special cases if version contains prerelease identifiers. if !version.prereleaseIdentifiers.isEmpty { - // If the ranage does not contain prerelease identifiers, return false. + // If the range does not contain prerelease identifiers, return false. if lowerBound.prereleaseIdentifiers.isEmpty && upperBound.prereleaseIdentifiers.isEmpty { return false } diff --git a/Tests/TSCUtilityTests/VersionTests.swift b/Tests/TSCUtilityTests/VersionTests.swift index 3dfbdf82..f36b5d29 100644 --- a/Tests/TSCUtilityTests/VersionTests.swift +++ b/Tests/TSCUtilityTests/VersionTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors + Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors Licensed under Apache License v2.0 with Runtime Library Exception See http://swift.org/LICENSE.txt for license information @@ -9,73 +9,682 @@ */ import struct TSCUtility.Version +import enum TSCUtility.VersionError import XCTest class VersionTests: XCTestCase { - func testEquality() { - let versions: [Version] = ["1.2.3", "0.0.0", - "0.0.0-alpha+yol", "0.0.0-alpha.1+pol", - "0.1.2", "10.7.3", - ] - // Test that each version is equal to itself and not equal to others. - for (idx, version) in versions.enumerated() { - for (ridx, rversion) in versions.enumerated() { - if idx == ridx { - XCTAssertEqual(version, rversion) - // Construct the object again with different initializer. - XCTAssertEqual(version, - Version(rversion.major, rversion.minor, rversion.patch, - prereleaseIdentifiers: rversion.prereleaseIdentifiers, - buildMetadataIdentifiers: rversion.buildMetadataIdentifiers)) - } else { - XCTAssertNotEqual(version, rversion) - } + func testVersionNonthrowingInitialization() { + let v0 = Version(0, 0, 0, prereleaseIdentifiers: [], buildMetadataIdentifiers: []) + XCTAssertEqual(v0.minor, 0) + XCTAssertEqual(v0.minor, 0) + XCTAssertEqual(v0.patch, 0) + XCTAssertEqual(v0.prereleaseIdentifiers, []) + XCTAssertEqual(v0.buildMetadataIdentifiers, []) + + let v1 = Version(1, 1, 2, prereleaseIdentifiers: ["3", "5"], buildMetadataIdentifiers: ["8", "13"]) + XCTAssertEqual(v1.minor, 1) + XCTAssertEqual(v1.minor, 1) + XCTAssertEqual(v1.patch, 2) + XCTAssertEqual(v1.prereleaseIdentifiers, ["3", "5"]) + XCTAssertEqual(v1.buildMetadataIdentifiers, ["8", "13"]) + + XCTAssertEqual( + Version(3, 5, 8), + Version(3, 5, 8, prereleaseIdentifiers: [], buildMetadataIdentifiers: []) + ) + + XCTAssertEqual( + Version(13, 21, 34, prereleaseIdentifiers: ["55"]), + Version(13, 21, 34, prereleaseIdentifiers: ["55"], buildMetadataIdentifiers: []) + ) + + XCTAssertEqual( + Version(89, 144, 233, buildMetadataIdentifiers: ["377"]), + Version(89, 144, 233, prereleaseIdentifiers: [], buildMetadataIdentifiers: ["377"]) + ) + } + + func testVersionThrowingInitialization() { + + // MARK: Well-formed version core + + XCTAssertNoThrow(try Version(versionString: "0.0.0")) + XCTAssertEqual(try! Version(versionString: "0.0.0"), Version(0, 0, 0)) + + XCTAssertNoThrow(try Version(versionString: "1.1.2")) + XCTAssertEqual(try! Version(versionString: "1.1.2"), Version(1, 1, 2)) + + // MARK: Malformed version core + + XCTAssertThrowsError(try Version(versionString: "3")) { error in + guard let error = error as? VersionError, case .invalidVersionCoreIdentifiersCount(["3"]) = error else { + XCTFail() + return } + XCTAssertEqual(error.description, "fewer than 3 identifiers in version core '3'") } - } - func testHashable() { - let versions: [Version] = ["1.2.3", "1.2.3", "1.2.3", - "1.0.0-alpha", "1.0.0-alpha", - "1.0.0", "1.0.0" - ] - XCTAssertEqual(Set(versions), Set(["1.0.0-alpha", "1.2.3", "1.0.0"])) + XCTAssertThrowsError(try Version(versionString: "3 5")) { error in + // checking for version core identifier count comes before checking for alpha-numerical characters + guard let error = error as? VersionError, case .invalidVersionCoreIdentifiersCount(["3 5"]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "fewer than 3 identifiers in version core '3 5'") + } - XCTAssertEqual(Set([Version(1,2,3)]), Set([Version(1,2,3)])) - XCTAssertNotEqual(Set([Version(1,2,3)]), Set([Version(1,2,3, prereleaseIdentifiers: ["alpha"])])) - XCTAssertNotEqual(Set([Version(1,2,3)]), Set([Version(1,2,3, buildMetadataIdentifiers: ["1011"])])) - } + XCTAssertThrowsError(try Version(versionString: "5.8")) { error in + guard let error = error as? VersionError, case .invalidVersionCoreIdentifiersCount(["5", "8"]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "fewer than 3 identifiers in version core '5.8'") + } - func testDescription() { - let v: Version = "123.234.345-alpha.beta+sha1.1011" - XCTAssertEqual(v.description, "123.234.345-alpha.beta+sha1.1011") - XCTAssertEqual(v.major, 123) - XCTAssertEqual(v.minor, 234) - XCTAssertEqual(v.patch, 345) - XCTAssertEqual(v.prereleaseIdentifiers, ["alpha", "beta"]) - XCTAssertEqual(v.buildMetadataIdentifiers, ["sha1", "1011"]) - } + XCTAssertThrowsError(try Version(versionString: "-5.8.13")) { error in + // the version core is considered empty because of the leading '-' + // everything after the first '-' is considered as the pre-release information (until the first '+', which doesn't exist in this version string) + // the version core is NOT considered missing, because it has 1 identifier, despite the identifier being empty + guard let error = error as? VersionError, case .invalidVersionCoreIdentifiersCount([""]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "fewer than 3 identifiers in version core ''") + } - func testFromString() { - let badStrings = [ - "", "1", "1.2", "1.2.3.4", "1.2.3.4.5", - "a", "1.a", "a.2", "a.2.3", "1.a.3", "1.2.a", - "-1.2.3", "1.-2.3", "1.2.-3", ".1.2.3", "v.1.2.3", "1.2..3", "v1.2.3", - ] - for str in badStrings { - XCTAssertNil(Version(string: str)) + XCTAssertThrowsError(try Version(versionString: "8.-13.21")) { error in + guard let error = error as? VersionError, case .invalidVersionCoreIdentifiersCount(["8", ""]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "fewer than 3 identifiers in version core '8.'") } - XCTAssertEqual(Version(1,2,3), Version(string: "1.2.3")) - XCTAssertEqual(Version(1,2,3), Version(string: "01.002.0003")) - XCTAssertEqual(Version(0,9,21), Version(string: "0.9.21")) - XCTAssertEqual(Version(0,9,21, prereleaseIdentifiers: ["alpha", "beta"], buildMetadataIdentifiers: ["1011"]), - Version(string: "0.9.21-alpha.beta+1011")) - XCTAssertEqual(Version(0,9,21, prereleaseIdentifiers: [], buildMetadataIdentifiers: ["1011"]), Version(string: "0.9.21+1011")) + XCTAssertThrowsError(try Version(versionString: "13.21.-34")) { error in + guard let error = error as? VersionError, case .nonNumericalOrEmptyVersionCoreIdentifiers(["13", "21", ""]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "empty identifiers in version core '13.21.'") + } + + XCTAssertThrowsError(try Version(versionString: ("-0.0.0" as String))) { error in + guard let error = error as? VersionError, case .invalidVersionCoreIdentifiersCount([""]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "fewer than 3 identifiers in version core ''") + } + + XCTAssertThrowsError(try Version(versionString: "0.-0.0")) { error in + guard let error = error as? VersionError, case .invalidVersionCoreIdentifiersCount(["0", ""]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "fewer than 3 identifiers in version core '0.'") + } + + XCTAssertThrowsError(try Version(versionString: "0.0.O")) { error in + guard let error = error as? VersionError, case .nonNumericalOrEmptyVersionCoreIdentifiers(["0", "0", "O"]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "non-numerical characters in version core identifier 'O'") + } + + XCTAssertThrowsError(try Version(versionString: "1.l1.O")) { error in + guard let error = error as? VersionError, case .nonNumericalOrEmptyVersionCoreIdentifiers(["1", "l1", "O"]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "non-numerical characters in version core identifiers 'l1', 'O'") + } + + XCTAssertThrowsError(try Version(versionString: "21.34.55.89")) { error in + guard let error = error as? VersionError, case .invalidVersionCoreIdentifiersCount(["21", "34", "55", "89"]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "more than 3 identifiers in version core '21.34.55.89'") + } + + XCTAssertThrowsError(try Version(versionString: "6 x 9 = 42")) { error in + guard let error = error as? VersionError, case .invalidVersionCoreIdentifiersCount(["6 x 9 = 42"]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "fewer than 3 identifiers in version core '6 x 9 = 42'") + } + + XCTAssertThrowsError(try Version(versionString: "forty two")) { error in + guard let error = error as? VersionError, case .invalidVersionCoreIdentifiersCount(["forty two"]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "fewer than 3 identifiers in version core 'forty two'") + } + + XCTAssertThrowsError(try Version(versionString: "一点二点三")) { error in + guard let error = error as? VersionError, case .nonASCIIVersionString("一点二点三") = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "non-ASCII characters in version string '一点二点三'") + } + + // MARK: Well-formed version core, well-formed pre-release identifiers + + XCTAssertNoThrow(try Version(versionString: "0.0.0-pre-alpha")) + XCTAssertEqual(try! Version(versionString: "0.0.0-pre-alpha"), Version(0, 0, 0, prereleaseIdentifiers: ["pre-alpha"])) + + XCTAssertNoThrow(try Version(versionString: "55.89.144-beta.1")) + XCTAssertEqual(try! Version(versionString: "55.89.144-beta.1"), Version(55, 89, 144, prereleaseIdentifiers: ["beta", "1"])) + + XCTAssertNoThrow(try Version(versionString: "89.144.233-a.whole..lot.of.pre-release.identifiers")) + XCTAssertEqual(try! Version(versionString: "89.144.233-a.whole..lot.of.pre-release.identifiers"), Version(89, 144, 233, prereleaseIdentifiers: ["a", "whole", "", "lot", "of", "pre-release", "identifiers"])) + + XCTAssertNoThrow(try Version(versionString: "144.233.377-")) + XCTAssertEqual(try! Version(versionString: "144.233.377-"), Version(144, 233, 377, prereleaseIdentifiers: [""])) + + // MARK: Well-formed version core, malformed pre-release identifiers + + XCTAssertThrowsError(try Version(versionString: "233.377.610-hello world")) { error in + guard let error = error as? VersionError, case .nonAlphaNumerHyphenalPrereleaseIdentifiers(["hello world"]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "characters other than alpha-numerics and hyphens in pre-release identifier 'hello world'") + } + + XCTAssertThrowsError(try Version(versionString: "1.2.3-测试版")) { error in + guard let error = error as? VersionError, case .nonASCIIVersionString("1.2.3-测试版") = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "non-ASCII characters in version string '1.2.3-测试版'") + } + + // MARK: Malformed version core, well-formed pre-release identifiers + + XCTAssertThrowsError(try Version(versionString: "987-Hello.world--------")) { error in + guard let error = error as? VersionError, case .invalidVersionCoreIdentifiersCount(["987"]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "fewer than 3 identifiers in version core '987'") + } + + XCTAssertThrowsError(try Version(versionString: "987.1597-half-life.3")) { error in + guard let error = error as? VersionError, case .invalidVersionCoreIdentifiersCount(["987", "1597"]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "fewer than 3 identifiers in version core '987.1597'") + } + + XCTAssertThrowsError(try Version(versionString: "1597.2584.4181.6765-a.whole.lot.of.pre-release.identifiers")) { error in + guard let error = error as? VersionError, case .invalidVersionCoreIdentifiersCount(["1597", "2584", "4181", "6765"]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "more than 3 identifiers in version core '1597.2584.4181.6765'") + } + + XCTAssertThrowsError(try Version(versionString: "6 x 9 = 42-")) { error in + guard let error = error as? VersionError, case .invalidVersionCoreIdentifiersCount(["6 x 9 = 42"]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "fewer than 3 identifiers in version core '6 x 9 = 42'") + } + + XCTAssertThrowsError(try Version(versionString: "forty-two")) { error in + guard let error = error as? VersionError, case .invalidVersionCoreIdentifiersCount(["forty"]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "fewer than 3 identifiers in version core 'forty'") + } + + XCTAssertThrowsError(try Version(versionString: "l.2.3")) { error in + guard let error = error as? VersionError, case .nonNumericalOrEmptyVersionCoreIdentifiers(["l", "2", "3"]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "non-numerical characters in version core identifier 'l'") + } + + XCTAssertThrowsError(try Version(versionString: "l.b.3")) { error in + guard let error = error as? VersionError, case .nonNumericalOrEmptyVersionCoreIdentifiers(["l", "b", "3"]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "non-numerical characters in version core identifiers 'l', 'b'") + } + + XCTAssertThrowsError(try Version(versionString: "l.2.З")) { error in + guard let error = error as? VersionError, case .nonASCIIVersionString("l.2.З") = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "non-ASCII characters in version string 'l.2.З'") + } + + XCTAssertThrowsError(try Version(versionString: "一点二点三-beta")) { error in + guard let error = error as? VersionError, case .nonASCIIVersionString("一点二点三-beta") = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "non-ASCII characters in version string '一点二点三-beta'") + } + + // MARK: Well-formed version core, well-formed build metadata identifiers + + XCTAssertNoThrow(try Version(versionString: "0.0.0+some-metadata")) + XCTAssertEqual(try! Version(versionString: "0.0.0+some-metadata"), Version(0, 0, 0, buildMetadataIdentifiers: ["some-metadata"])) + + XCTAssertNoThrow(try Version(versionString: "4181.6765.10946+more.meta..more.data")) + XCTAssertEqual(try! Version(versionString: "4181.6765.10946+more.meta..more.data"), Version(4181, 6765, 10946, buildMetadataIdentifiers: ["more", "meta", "", "more", "data"])) + + XCTAssertNoThrow(try Version(versionString: "6765.10946.17711+-a-very--long---build-----metadata--------identifier-------------with---------------------many----------------------------------hyphens-------------------------------------------------------")) + XCTAssertEqual(try! Version(versionString: "6765.10946.17711+-a-very--long---build-----metadata--------identifier-------------with---------------------many----------------------------------hyphens-------------------------------------------------------"), Version(6765, 10946, 17711, buildMetadataIdentifiers: ["-a-very--long---build-----metadata--------identifier-------------with---------------------many----------------------------------hyphens-------------------------------------------------------"])) + + XCTAssertNoThrow(try Version(versionString: "10946.17711.28657+")) + XCTAssertEqual(try! Version(versionString: "10946.17711.28657+"), Version(10946, 17711, 28657, buildMetadataIdentifiers: [""])) + + // MARK: Well-formed version core, malformed build metadata identifiers + + XCTAssertThrowsError(try Version(versionString: "17711.28657.46368+hello world.hello-.-world")) { error in + guard let error = error as? VersionError, case .nonAlphaNumerHyphenalBuildMetadataIdentifiers(["hello world", "hello-", "-world"]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "characters other than alpha-numerics and hyphens in build metadata identifier 'hello world'") + } + + XCTAssertThrowsError(try Version(versionString: "28657.46368.75025+hello+world.hello world")) { error in + guard let error = error as? VersionError, case .nonAlphaNumerHyphenalBuildMetadataIdentifiers(["hello+world", "hello world"]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "characters other than alpha-numerics and hyphens in build metadata identifiers 'hello+world', 'hello world'") + } + + // MARK: Malformed version core, well-formed build metadata identifiers + + XCTAssertThrowsError(try Version(versionString: "121393+Hello.world--------")) { error in + guard let error = error as? VersionError, case .invalidVersionCoreIdentifiersCount(["121393"]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "fewer than 3 identifiers in version core '121393'") + } + + XCTAssertThrowsError(try Version(versionString: "121393.196418+half-life.3")) { error in + guard let error = error as? VersionError, case .invalidVersionCoreIdentifiersCount(["121393", "196418"]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "fewer than 3 identifiers in version core '121393.196418'") + } + + XCTAssertThrowsError(try Version(versionString: "196418.317811.514229.832040+a.whole.lot.of.build.metadata.identifiers")) { error in + guard let error = error as? VersionError, case .invalidVersionCoreIdentifiersCount(["196418", "317811", "514229", "832040"]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "more than 3 identifiers in version core '196418.317811.514229.832040'") + } + + XCTAssertThrowsError(try Version(versionString: "196418.317811.514229.83204O+a.whole.lot.of.build.metadata.identifiers")) { error in + guard let error = error as? VersionError, case .invalidVersionCoreIdentifiersCount(["196418", "317811", "514229", "83204O"]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "more than 3 identifiers in version core '196418.317811.514229.83204O'") + } + + XCTAssertThrowsError(try Version(versionString: "196418.317811.83204O+a.whole.lot.of.build.metadata.identifiers")) { error in + guard let error = error as? VersionError, case .nonNumericalOrEmptyVersionCoreIdentifiers(["196418", "317811", "83204O"]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "non-numerical characters in version core identifier '83204O'") + } + + XCTAssertThrowsError(try Version(versionString: "abc.def.ghi+a.whole.lot.of.build.metadata.identifiers")) { error in + guard let error = error as? VersionError, case .nonNumericalOrEmptyVersionCoreIdentifiers(["abc", "def", "ghi"]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "non-numerical characters in version core identifiers 'abc', 'def', 'ghi'") + } + + XCTAssertThrowsError(try Version(versionString: "6 x 9 = 42+")) { error in + guard let error = error as? VersionError, case .invalidVersionCoreIdentifiersCount(["6 x 9 = 42"]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "fewer than 3 identifiers in version core '6 x 9 = 42'") + } + + XCTAssertThrowsError(try Version(versionString: "forty two+a-very-long-build-metadata-identifier-with-many-hyphens")) { error in + guard let error = error as? VersionError, case .invalidVersionCoreIdentifiersCount(["forty two"]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "fewer than 3 identifiers in version core 'forty two'") + } + + XCTAssertThrowsError(try Version(versionString: "一.二.三+build.metadata")) { error in + guard let error = error as? VersionError, case .nonASCIIVersionString("一.二.三+build.metadata") = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "non-ASCII characters in version string '一.二.三+build.metadata'") + } + + // MARK: Well-formed version core, well-formed pre-release identifiers, well-formed build metadata identifiers + + XCTAssertNoThrow(try Version(versionString: "0.0.0-beta.-42+42-42.42")) + XCTAssertEqual(try! Version(versionString: "0.0.0-beta.-42+42-42.42"), Version(0, 0, 0, prereleaseIdentifiers: ["beta", "-42"], buildMetadataIdentifiers: ["42-42", "42"])) + + // MARK: Well-formed version core, well-formed pre-release identifiers, malformed build metadata identifiers + + XCTAssertThrowsError(try Version(versionString: "514229.832040.1346269-beta1+ ")) { error in + guard let error = error as? VersionError, case .nonAlphaNumerHyphenalBuildMetadataIdentifiers([" "]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "characters other than alpha-numerics and hyphens in build metadata identifier ' '") + } + + // MARK: Well-formed version core, malformed pre-release identifiers, well-formed build metadata identifiers + + XCTAssertThrowsError(try Version(versionString: "832040.1346269.2178309-beta 1.-+-")) { error in + guard let error = error as? VersionError, case .nonAlphaNumerHyphenalPrereleaseIdentifiers(["beta 1", "-"]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "characters other than alpha-numerics and hyphens in pre-release identifier 'beta 1'") + } + + // MARK: Well-formed version core, malformed pre-release identifiers, malformed build metadata identifiers + + // pre-release is diagnosed before build metadata is + XCTAssertThrowsError(try Version(versionString: "1346269.2178309.3524578-beta 1++")) { error in + guard let error = error as? VersionError, case .nonAlphaNumerHyphenalPrereleaseIdentifiers(["beta 1"]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "characters other than alpha-numerics and hyphens in pre-release identifier 'beta 1'") + } + + // MARK: malformed version core, well-formed pre-release identifiers, well-formed build metadata identifiers + + XCTAssertThrowsError(try Version(versionString: " 832040.1346269.3524578-beta1+abc")) { error in + guard let error = error as? VersionError, case .nonNumericalOrEmptyVersionCoreIdentifiers([" 832040", "1346269", "3524578"]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "non-numerical characters in version core identifier ' 832040'") + } + + // MARK: malformed version core, well-formed pre-release identifiers, malformed build metadata identifiers + + XCTAssertThrowsError(try Version(versionString: "l346269.3524578.5702887-beta1+😀")) { error in + guard let error = error as? VersionError, case .nonASCIIVersionString("l346269.3524578.5702887-beta1+😀") = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "non-ASCII characters in version string 'l346269.3524578.5702887-beta1+😀'") + } + + // version core is diagnosed before build metadata is + XCTAssertThrowsError(try Version(versionString: "l346269.abc.OOO-beta1+++.+.+")) { error in + guard let error = error as? VersionError, case .nonNumericalOrEmptyVersionCoreIdentifiers(["l346269", "abc", "OOO"]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "non-numerical characters in version core identifiers 'l346269', 'abc', 'OOO'") + } + + // MARK: malformed version core, malformed pre-release identifiers, well-formed build metadata identifiers + + XCTAssertNil(Version("352A578.5702887.9227465-beta!@#$%^&*1+asdfghjkl123456789" as String)) + // version core is diagnosed before pre-release is + XCTAssertThrowsError(try Version(versionString: "352A578.5702887.9227465-beta!@#$%^&*1+asdfghjkl123456789")) { error in + guard let error = error as? VersionError, case .nonNumericalOrEmptyVersionCoreIdentifiers(["352A578", "5702887", "9227465"]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "non-numerical characters in version core identifier '352A578'") + } + + // MARK: malformed version core, malformed pre-release identifiers, malformed build metadata identifiers + + XCTAssertThrowsError(try Version(versionString: "5702887.9227465-bètá1+±")) { error in + guard let error = error as? VersionError, case .nonASCIIVersionString("5702887.9227465-bètá1+±") = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "non-ASCII characters in version string '5702887.9227465-bètá1+±'") + } + + XCTAssertThrowsError(try Version(versionString: "5702887.9227465-bet@.1!+met@.d@t@")) { error in + guard let error = error as? VersionError, case .invalidVersionCoreIdentifiersCount(["5702887", "9227465"]) = error else { + XCTFail() + return + } + XCTAssertEqual(error.description, "fewer than 3 identifiers in version core '5702887.9227465'") + } + + } + + func testVersionComparison() { + + // MARK: version core vs. version core + + XCTAssertGreaterThan(Version(2, 1, 1), Version(1, 2, 3)) + XCTAssertGreaterThan(Version(1, 3, 1), Version(1, 2, 3)) + XCTAssertGreaterThan(Version(1, 2, 4), Version(1, 2, 3)) + + // MARK: version core vs. version core + pre-release + + XCTAssertGreaterThan(Version(1, 2, 3), Version(1, 2, 3, prereleaseIdentifiers: [""])) + XCTAssertGreaterThan(Version(1, 2, 3), Version(1, 2, 3, prereleaseIdentifiers: ["beta"])) + XCTAssertLessThan(Version(1, 2, 2), Version(1, 2, 3, prereleaseIdentifiers: ["beta"])) + + // MARK: version core + pre-release vs. version core + pre-release + + XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: [""]), Version(1, 2, 3, prereleaseIdentifiers: [""])) + + XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: ["beta"]), Version(1, 2, 3, prereleaseIdentifiers: ["beta"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["alpha"]), Version(1, 2, 3, prereleaseIdentifiers: ["beta"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["alpha1"]), Version(1, 2, 3, prereleaseIdentifiers: ["alpha2"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["alpha"]), Version(1, 2, 3, prereleaseIdentifiers: ["alpha-"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["beta", "alpha"]), Version(1, 2, 3, prereleaseIdentifiers: ["beta", "beta"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["alpha", "beta"]), Version(1, 2, 3, prereleaseIdentifiers: ["beta", "alpha"])) + + XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: ["1"]), Version(1, 2, 3, prereleaseIdentifiers: ["1"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["1"]), Version(1, 2, 3, prereleaseIdentifiers: ["2"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["1", "1"]), Version(1, 2, 3, prereleaseIdentifiers: ["1", "2"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["1", "2"]), Version(1, 2, 3, prereleaseIdentifiers: ["2", "1"])) + + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["123"]), Version(1, 2, 3, prereleaseIdentifiers: ["123alpha"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["223"]), Version(1, 2, 3, prereleaseIdentifiers: ["123alpha"])) + + // MARK: version core vs. version core + build metadata + + XCTAssertEqual(Version(1, 2, 3), Version(1, 2, 3, buildMetadataIdentifiers: [""])) + XCTAssertEqual(Version(1, 2, 3), Version(1, 2, 3, buildMetadataIdentifiers: ["beta"])) + XCTAssertLessThan(Version(1, 2, 2), Version(1, 2, 3, buildMetadataIdentifiers: ["beta"])) + + // MARK: version core + pre-release vs. version core + build metadata + + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: [""]), Version(1, 2, 3, buildMetadataIdentifiers: [""])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["beta"]), Version(1, 2, 3, buildMetadataIdentifiers: ["alpha"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["alpha"]), Version(1, 2, 3, buildMetadataIdentifiers: ["beta"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["beta"]), Version(1, 2, 3, buildMetadataIdentifiers: ["beta"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["alpha"]), Version(1, 2, 3, buildMetadataIdentifiers: ["alpha-"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["123"]), Version(1, 2, 3, buildMetadataIdentifiers: ["123alpha"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["223"]), Version(1, 2, 3, buildMetadataIdentifiers: ["123alpha"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["123alpha"]), Version(1, 2, 3, buildMetadataIdentifiers: ["123"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["123alpha"]), Version(1, 2, 3, buildMetadataIdentifiers: ["223"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["123alpha"]), Version(1, 2, 3, buildMetadataIdentifiers: ["223"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["alpha"]), Version(1, 2, 3, buildMetadataIdentifiers: ["beta"])) + XCTAssertGreaterThan(Version(2, 2, 3, prereleaseIdentifiers: [""]), Version(1, 2, 3, buildMetadataIdentifiers: [""])) + XCTAssertGreaterThan(Version(1, 3, 3, prereleaseIdentifiers: ["alpha"]), Version(1, 2, 3, buildMetadataIdentifiers: ["beta"])) + XCTAssertGreaterThan(Version(1, 2, 4, prereleaseIdentifiers: ["223"]), Version(1, 2, 3, buildMetadataIdentifiers: ["123alpha"])) + + // MARK: version core + build metadata vs. version core + build metadata + + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: [""]), Version(1, 2, 3, buildMetadataIdentifiers: [""])) + + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["beta"]), Version(1, 2, 3, buildMetadataIdentifiers: ["beta"])) + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["alpha"]), Version(1, 2, 3, buildMetadataIdentifiers: ["beta"])) + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["alpha1"]), Version(1, 2, 3, buildMetadataIdentifiers: ["alpha2"])) + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["alpha"]), Version(1, 2, 3, buildMetadataIdentifiers: ["alpha-"])) + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["beta", "alpha"]), Version(1, 2, 3, buildMetadataIdentifiers: ["beta", "beta"])) + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["alpha", "beta"]), Version(1, 2, 3, buildMetadataIdentifiers: ["beta", "alpha"])) + + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["1"]), Version(1, 2, 3, buildMetadataIdentifiers: ["1"])) + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["1"]), Version(1, 2, 3, buildMetadataIdentifiers: ["2"])) + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["1", "1"]), Version(1, 2, 3, buildMetadataIdentifiers: ["1", "2"])) + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["1", "2"]), Version(1, 2, 3, buildMetadataIdentifiers: ["2", "1"])) + + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["123"]), Version(1, 2, 3, buildMetadataIdentifiers: ["123alpha"])) + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["223"]), Version(1, 2, 3, buildMetadataIdentifiers: ["123alpha"])) + + // MARK: version core vs. version core + pre-release + build metadata + + XCTAssertGreaterThan(Version(1, 2, 3), Version(1, 2, 3, prereleaseIdentifiers: [""], buildMetadataIdentifiers: [""])) + XCTAssertGreaterThan(Version(1, 2, 3), Version(1, 2, 3, prereleaseIdentifiers: [""], buildMetadataIdentifiers: ["123alpha"])) + XCTAssertGreaterThan(Version(1, 2, 3), Version(1, 2, 3, prereleaseIdentifiers: ["alpha"], buildMetadataIdentifiers: ["alpha"])) + XCTAssertGreaterThan(Version(1, 2, 3), Version(1, 2, 3, prereleaseIdentifiers: ["beta"], buildMetadataIdentifiers: ["123"])) + XCTAssertLessThan(Version(1, 2, 2), Version(1, 2, 3, prereleaseIdentifiers: ["beta"], buildMetadataIdentifiers: ["alpha", "beta"])) + XCTAssertLessThan(Version(1, 2, 2), Version(1, 2, 3, prereleaseIdentifiers: ["beta"], buildMetadataIdentifiers: ["alpha-"])) + + // MARK: version core + pre-release vs. version core + pre-release + build metadata + + XCTAssertEqual( + Version(1, 2, 3, prereleaseIdentifiers: [""]), + Version(1, 2, 3, prereleaseIdentifiers: [""], buildMetadataIdentifiers: [""]) + ) + + XCTAssertEqual( + Version(1, 2, 3, prereleaseIdentifiers: ["beta"]), + Version(1, 2, 3, prereleaseIdentifiers: ["beta"], buildMetadataIdentifiers: [""]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["alpha"]), + Version(1, 2, 3, prereleaseIdentifiers: ["beta"], buildMetadataIdentifiers: ["123alpha"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["alpha1"]), + Version(1, 2, 3, prereleaseIdentifiers: ["alpha2"], buildMetadataIdentifiers: ["alpha"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["alpha"]), + Version(1, 2, 3, prereleaseIdentifiers: ["alpha-"], buildMetadataIdentifiers: ["alpha", "beta"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["beta", "alpha"]), + Version(1, 2, 3, prereleaseIdentifiers: ["beta", "beta"], buildMetadataIdentifiers: ["123"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["alpha", "beta"]), + Version(1, 2, 3, prereleaseIdentifiers: ["beta", "alpha"], buildMetadataIdentifiers: ["alpha-"]) + ) + + XCTAssertEqual( + Version(1, 2, 3, prereleaseIdentifiers: ["1"]), + Version(1, 2, 3, prereleaseIdentifiers: ["1"], buildMetadataIdentifiers: [""]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["1"]), + Version(1, 2, 3, prereleaseIdentifiers: ["2"], buildMetadataIdentifiers: ["123alpha"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["1", "1"]), + Version(1, 2, 3, prereleaseIdentifiers: ["1", "2"], buildMetadataIdentifiers: ["123"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["1", "2"]), + Version(1, 2, 3, prereleaseIdentifiers: ["2", "1"], buildMetadataIdentifiers: ["alpha", "beta"]) + ) + + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["123"]), + Version(1, 2, 3, prereleaseIdentifiers: ["123alpha"], buildMetadataIdentifiers: ["-alpha"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["223"]), + Version(1, 2, 3, prereleaseIdentifiers: ["123alpha"], buildMetadataIdentifiers: ["123"]) + ) + + // MARK: version core + pre-release + build metadata vs. version core + pre-release + build metadata + + XCTAssertEqual( + Version(1, 2, 3, prereleaseIdentifiers: [""], buildMetadataIdentifiers: [""]), + Version(1, 2, 3, prereleaseIdentifiers: [""], buildMetadataIdentifiers: [""]) + ) + + XCTAssertEqual( + Version(1, 2, 3, prereleaseIdentifiers: ["beta"], buildMetadataIdentifiers: ["123"]), + Version(1, 2, 3, prereleaseIdentifiers: ["beta"], buildMetadataIdentifiers: [""]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["alpha"], buildMetadataIdentifiers: ["-alpha"]), + Version(1, 2, 3, prereleaseIdentifiers: ["beta"], buildMetadataIdentifiers: ["123alpha"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["alpha1"], buildMetadataIdentifiers: ["alpha", "beta"]), + Version(1, 2, 3, prereleaseIdentifiers: ["alpha2"], buildMetadataIdentifiers: ["alpha"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["alpha"], buildMetadataIdentifiers: ["123"]), + Version(1, 2, 3, prereleaseIdentifiers: ["alpha-"], buildMetadataIdentifiers: ["alpha", "beta"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["beta", "alpha"], buildMetadataIdentifiers: ["123alpha"]), + Version(1, 2, 3, prereleaseIdentifiers: ["beta", "beta"], buildMetadataIdentifiers: ["123"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["alpha", "beta"], buildMetadataIdentifiers: [""]), + Version(1, 2, 3, prereleaseIdentifiers: ["beta", "alpha"], buildMetadataIdentifiers: ["alpha-"]) + ) + + XCTAssertEqual( + Version(1, 2, 3, prereleaseIdentifiers: ["1"], buildMetadataIdentifiers: ["alpha-"]), + Version(1, 2, 3, prereleaseIdentifiers: ["1"], buildMetadataIdentifiers: [""]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["1"], buildMetadataIdentifiers: ["123"]), + Version(1, 2, 3, prereleaseIdentifiers: ["2"], buildMetadataIdentifiers: ["123alpha"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["1", "1"], buildMetadataIdentifiers: ["alpha", "beta"]), + Version(1, 2, 3, prereleaseIdentifiers: ["1", "2"], buildMetadataIdentifiers: ["123"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["1", "2"], buildMetadataIdentifiers: ["alpha"]), + Version(1, 2, 3, prereleaseIdentifiers: ["2", "1"], buildMetadataIdentifiers: ["alpha", "beta"]) + ) + + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["123"], buildMetadataIdentifiers: ["123alpha"]), + Version(1, 2, 3, prereleaseIdentifiers: ["123alpha"], buildMetadataIdentifiers: ["-alpha"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["223"], buildMetadataIdentifiers: ["123alpha"]), + Version(1, 2, 3, prereleaseIdentifiers: ["123alpha"], buildMetadataIdentifiers: ["123"]) + ) + } - func testComparable() { + func testAdditionalVersionComparison() { do { let v1 = Version(1,2,3) let v2 = Version(2,1,2) @@ -160,9 +769,7 @@ class VersionTests: XCTestCase { v1 = v2 } } - } - func testOrder() { XCTAssertLessThan(Version(0,0,0), Version(0,0,1)) XCTAssertLessThan(Version(0,0,1), Version(0,1,0)) XCTAssertLessThan(Version(0,1,0), Version(0,10,0)) @@ -172,6 +779,256 @@ class VersionTests: XCTestCase { XCTAssert(!(Version(2,0,0) < Version(1,0,0))) } + func testAdditionalVersionEquality() { + let versions: [Version] = ["1.2.3", "0.0.0", + "0.0.0-alpha+yol", "0.0.0-alpha.1+pol", + "0.1.2", "10.7.3", + ] + // Test that each version is equal to itself and not equal to others. + for (idx, version) in versions.enumerated() { + for (ridx, rversion) in versions.enumerated() { + if idx == ridx { + XCTAssertEqual(version, rversion) + // Construct the object again with different initializer. + XCTAssertEqual(version, + Version(rversion.major, rversion.minor, rversion.patch, + prereleaseIdentifiers: rversion.prereleaseIdentifiers, + buildMetadataIdentifiers: rversion.buildMetadataIdentifiers)) + } else { + XCTAssertNotEqual(version, rversion) + } + } + } + } + + func testHashable() { + let versions: [Version] = ["1.2.3", "1.2.3", "1.2.3", + "1.0.0-alpha", "1.0.0-alpha", + "1.0.0", "1.0.0" + ] + XCTAssertEqual(Set(versions), Set(["1.0.0-alpha", "1.2.3", "1.0.0"])) + + XCTAssertEqual(Set([Version(1,2,3)]), Set([Version(1,2,3)])) + XCTAssertNotEqual(Set([Version(1,2,3)]), Set([Version(1,2,3, prereleaseIdentifiers: ["alpha"])])) + XCTAssertEqual(Set([Version(1,2,3)]), Set([Version(1,2,3, buildMetadataIdentifiers: ["1011"])])) + } + + func testCustomConversionFromVersionToString() { + + // MARK: Version.description + + XCTAssertEqual(Version(0, 0, 0).description, "0.0.0" as String) + XCTAssertEqual(Version(1, 2, 3).description, "1.2.3" as String) + XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: [""]).description, "1.2.3-" as String) + XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: ["", ""]).description, "1.2.3-." as String) + XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: ["beta1"]).description, "1.2.3-beta1" as String) + XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: ["beta", "1"]).description, "1.2.3-beta.1" as String) + XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: ["beta", "", "1"]).description, "1.2.3-beta..1" as String) + XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: ["be-ta", "", "1"]).description, "1.2.3-be-ta..1" as String) + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: [""]).description, "1.2.3+" as String) + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["", ""]).description, "1.2.3+." as String) + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["beta1"]).description, "1.2.3+beta1" as String) + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["beta", "1"]).description, "1.2.3+beta.1" as String) + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["beta", "", "1"]).description, "1.2.3+beta..1" as String) + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["be-ta", "", "1"]).description, "1.2.3+be-ta..1" as String) + XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: [""], buildMetadataIdentifiers: [""]).description, "1.2.3-+" as String) + XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: ["", ""], buildMetadataIdentifiers: ["", "-", ""]).description, "1.2.3-.+.-." as String) + XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: ["beta1"], buildMetadataIdentifiers: ["alpha1"]).description, "1.2.3-beta1+alpha1" as String) + XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: ["beta", "1"], buildMetadataIdentifiers: ["alpha", "1"]).description, "1.2.3-beta.1+alpha.1" as String) + XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: ["beta", "", "1"], buildMetadataIdentifiers: ["alpha", "", "1"]).description, "1.2.3-beta..1+alpha..1" as String) + XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: ["be-ta", "", "1"], buildMetadataIdentifiers: ["al-pha", "", "1"]).description, "1.2.3-be-ta..1+al-pha..1" as String) + + // MARK: String interpolation + + XCTAssertEqual("\(Version(0, 0, 0))", "0.0.0" as String) + XCTAssertEqual("\(Version(1, 2, 3))", "1.2.3" as String) + XCTAssertEqual("\(Version(1, 2, 3, prereleaseIdentifiers: [""]))", "1.2.3-" as String) + XCTAssertEqual("\(Version(1, 2, 3, prereleaseIdentifiers: ["", ""]))", "1.2.3-." as String) + XCTAssertEqual("\(Version(1, 2, 3, prereleaseIdentifiers: ["beta1"]))", "1.2.3-beta1" as String) + XCTAssertEqual("\(Version(1, 2, 3, prereleaseIdentifiers: ["beta", "1"]))", "1.2.3-beta.1" as String) + XCTAssertEqual("\(Version(1, 2, 3, prereleaseIdentifiers: ["beta", "", "1"]))", "1.2.3-beta..1" as String) + XCTAssertEqual("\(Version(1, 2, 3, prereleaseIdentifiers: ["be-ta", "", "1"]))", "1.2.3-be-ta..1" as String) + XCTAssertEqual("\(Version(1, 2, 3, buildMetadataIdentifiers: [""]))", "1.2.3+" as String) + XCTAssertEqual("\(Version(1, 2, 3, buildMetadataIdentifiers: ["", ""]))", "1.2.3+." as String) + XCTAssertEqual("\(Version(1, 2, 3, buildMetadataIdentifiers: ["beta1"]))", "1.2.3+beta1" as String) + XCTAssertEqual("\(Version(1, 2, 3, buildMetadataIdentifiers: ["beta", "1"]))", "1.2.3+beta.1" as String) + XCTAssertEqual("\(Version(1, 2, 3, buildMetadataIdentifiers: ["beta", "", "1"]))", "1.2.3+beta..1" as String) + XCTAssertEqual("\(Version(1, 2, 3, buildMetadataIdentifiers: ["be-ta", "", "1"]))", "1.2.3+be-ta..1" as String) + XCTAssertEqual("\(Version(1, 2, 3, prereleaseIdentifiers: [""], buildMetadataIdentifiers: [""]))", "1.2.3-+" as String) + XCTAssertEqual("\(Version(1, 2, 3, prereleaseIdentifiers: ["", ""], buildMetadataIdentifiers: ["", "-", ""]))", "1.2.3-.+.-." as String) + XCTAssertEqual("\(Version(1, 2, 3, prereleaseIdentifiers: ["beta1"], buildMetadataIdentifiers: ["alpha1"]))", "1.2.3-beta1+alpha1" as String) + XCTAssertEqual("\(Version(1, 2, 3, prereleaseIdentifiers: ["beta", "1"], buildMetadataIdentifiers: ["alpha", "1"]))", "1.2.3-beta.1+alpha.1" as String) + XCTAssertEqual("\(Version(1, 2, 3, prereleaseIdentifiers: ["beta", "", "1"], buildMetadataIdentifiers: ["alpha", "", "1"]))", "1.2.3-beta..1+alpha..1" as String) + XCTAssertEqual("\(Version(1, 2, 3, prereleaseIdentifiers: ["be-ta", "", "1"], buildMetadataIdentifiers: ["al-pha", "", "1"]))", "1.2.3-be-ta..1+al-pha..1" as String) + + } + + func testAdditionalCustomConversionFromVersionToString() { + let v: Version = "123.234.345-alpha.beta+sha1.1011" + XCTAssertEqual(v.description, "123.234.345-alpha.beta+sha1.1011") + XCTAssertEqual(v.major, 123) + XCTAssertEqual(v.minor, 234) + XCTAssertEqual(v.patch, 345) + XCTAssertEqual(v.prereleaseIdentifiers, ["alpha", "beta"]) + XCTAssertEqual(v.buildMetadataIdentifiers, ["sha1", "1011"]) + } + + func testLosslessConversionFromStringToVersion() { + + // We use type coercion `as String` in `Version(_:)` because there is a pair of overloaded initializers: `init(_ version: Version)` and `init?(_ versionString: String)`, and we want to test the latter in this function. + + // MARK: Well-formed version core + + XCTAssertNotNil(Version("0.0.0" as String)) + XCTAssertEqual(Version("0.0.0" as String), Version(0, 0, 0)) + + XCTAssertNotNil(Version("1.1.2" as String)) + XCTAssertEqual(Version("1.1.2" as String), Version(1, 1, 2)) + + // MARK: Malformed version core + + XCTAssertNil(Version("3" as String)) + XCTAssertNil(Version("3 5" as String)) + XCTAssertNil(Version("5.8" as String)) + XCTAssertNil(Version("-5.8.13" as String)) + XCTAssertNil(Version("8.-13.21" as String)) + XCTAssertNil(Version("13.21.-34" as String)) + XCTAssertNil(Version("-0.0.0" as String)) + XCTAssertNil(Version("0.-0.0" as String)) + XCTAssertNil(Version("0.0.-0" as String)) + XCTAssertNil(Version("21.34.55.89" as String)) + XCTAssertNil(Version("6 x 9 = 42" as String)) + XCTAssertNil(Version("forty two" as String)) + + // MARK: Well-formed version core, well-formed pre-release identifiers + + XCTAssertNotNil(Version("0.0.0-pre-alpha" as String)) + XCTAssertEqual(Version("0.0.0-pre-alpha" as String), Version(0, 0, 0, prereleaseIdentifiers: ["pre-alpha"])) + + XCTAssertNotNil(Version("55.89.144-beta.1" as String)) + XCTAssertEqual(Version("55.89.144-beta.1" as String), Version(55, 89, 144, prereleaseIdentifiers: ["beta", "1"])) + + XCTAssertNotNil(Version("89.144.233-a.whole..lot.of.pre-release.identifiers" as String)) + XCTAssertEqual(Version("89.144.233-a.whole..lot.of.pre-release.identifiers" as String), Version(89, 144, 233, prereleaseIdentifiers: ["a", "whole", "", "lot", "of", "pre-release", "identifiers"])) + + XCTAssertNotNil(Version("144.233.377-" as String)) + XCTAssertEqual(Version("144.233.377-" as String), Version(144, 233, 377, prereleaseIdentifiers: [""])) + + // MARK: Well-formed version core, malformed pre-release identifiers + + XCTAssertNil(Version("233.377.610-hello world" as String)) + + // MARK: Malformed version core, well-formed pre-release identifiers + + XCTAssertNil(Version("987-Hello.world--------" as String)) + XCTAssertNil(Version("987.1597-half-life.3" as String)) + XCTAssertNil(Version("1597.2584.4181.6765-a.whole.lot.of.pre-release.identifiers" as String)) + XCTAssertNil(Version("6 x 9 = 42-" as String)) + XCTAssertNil(Version("forty-two" as String)) + + // MARK: Well-formed version core, well-formed build metadata identifiers + + XCTAssertNotNil(Version("0.0.0+some-metadata" as String)) + XCTAssertEqual(Version("0.0.0+some-metadata" as String), Version(0, 0, 0, buildMetadataIdentifiers: ["some-metadata"])) + + XCTAssertNotNil(Version("4181.6765.10946+more.meta..more.data" as String)) + XCTAssertEqual(Version("4181.6765.10946+more.meta..more.data" as String), Version(4181, 6765, 10946, buildMetadataIdentifiers: ["more", "meta", "", "more", "data"])) + + XCTAssertNotNil(Version("6765.10946.17711+-a-very--long---build-----metadata--------identifier-------------with---------------------many----------------------------------hyphens-------------------------------------------------------" as String)) + XCTAssertEqual(Version("6765.10946.17711+-a-very--long---build-----metadata--------identifier-------------with---------------------many----------------------------------hyphens-------------------------------------------------------" as String), Version(6765, 10946, 17711, buildMetadataIdentifiers: ["-a-very--long---build-----metadata--------identifier-------------with---------------------many----------------------------------hyphens-------------------------------------------------------"])) + + XCTAssertNotNil(Version("10946.17711.28657+" as String)) + XCTAssertEqual(Version("10946.17711.28657+" as String), Version(10946, 17711, 28657, buildMetadataIdentifiers: [""])) + + // MARK: Well-formed version core, malformed build metadata identifiers + + XCTAssertNil(Version("17711.28657.46368+hello world" as String)) + XCTAssertNil(Version("28657.46368.75025+hello+world" as String)) + + // MARK: Malformed version core, well-formed build metadata identifiers + + XCTAssertNil(Version("121393+Hello.world--------" as String)) + XCTAssertNil(Version("121393.196418+half-life.3" as String)) + XCTAssertNil(Version("196418.317811.514229.832040+a.whole.lot.of.build.metadata.identifiers" as String)) + XCTAssertNil(Version("6 x 9 = 42+" as String)) + XCTAssertNil(Version("forty two+a-very-long-build-metadata-identifier-with-many-hyphens" as String)) + + // MARK: Well-formed version core, well-formed pre-release identifiers, well-formed build metadata identifiers + + XCTAssertNotNil(Version("0.0.0-beta.-42+42-42.42" as String)) + XCTAssertEqual(Version("0.0.0-beta.-42+42-42.42" as String), Version(0, 0, 0, prereleaseIdentifiers: ["beta", "-42"], buildMetadataIdentifiers: ["42-42", "42"])) + + // MARK: Well-formed version core, well-formed pre-release identifiers, malformed build metadata identifiers + + XCTAssertNil(Version("514229.832040.1346269-beta1+ " as String)) + + // MARK: Well-formed version core, malformed pre-release identifiers, well-formed build metadata identifiers + + XCTAssertNil(Version("832040.1346269.2178309-beta 1+-" as String)) + + // MARK: Well-formed version core, malformed pre-release identifiers, malformed build metadata identifiers + + XCTAssertNil(Version("1346269.2178309.3524578-beta 1++" as String)) + + // MARK: malformed version core, well-formed pre-release identifiers, well-formed build metadata identifiers + + XCTAssertNil(Version(" 832040.1346269.3524578-beta1+abc" as String)) + + // MARK: malformed version core, well-formed pre-release identifiers, malformed build metadata identifiers + + XCTAssertNil(Version("l346269.3524578.5702887-beta1+😀" as String)) + + // MARK: malformed version core, malformed pre-release identifiers, well-formed build metadata identifiers + + XCTAssertNil(Version("352A578.5702887.9227465-beta!@#$%^&*1+asdfghjkl123456789" as String)) + + // MARK: malformed version core, malformed pre-release identifiers, malformed build metadata identifiers + + XCTAssertNil(Version("5702887.9227465-bètá1+±" as String)) + + } + + func testExpressingVersionByStringLiteral() { + + // MARK: Well-formed version core + + XCTAssertEqual("0.0.0" as Version, Version(0, 0, 0)) + XCTAssertEqual("1.1.2" as Version, Version(1, 1, 2)) + + // MARK: Well-formed version core, well-formed pre-release identifiers + + XCTAssertEqual("0.0.0-pre-alpha" as Version, Version(0, 0, 0, prereleaseIdentifiers: ["pre-alpha"])) + XCTAssertEqual("55.89.144-beta.1" as Version, Version(55, 89, 144, prereleaseIdentifiers: ["beta", "1"])) + XCTAssertEqual("89.144.233-a.whole..lot.of.pre-release.identifiers" as Version, Version(89, 144, 233, prereleaseIdentifiers: ["a", "whole", "", "lot", "of", "pre-release", "identifiers"])) + XCTAssertEqual("144.233.377-" as Version, Version(144, 233, 377, prereleaseIdentifiers: [""])) + + // MARK: Well-formed version core, well-formed build metadata identifiers + + XCTAssertEqual("0.0.0+some-metadata" as Version, Version(0, 0, 0, buildMetadataIdentifiers: ["some-metadata"])) + XCTAssertEqual("4181.6765.10946+more.meta..more.data" as Version, Version(4181, 6765, 10946, buildMetadataIdentifiers: ["more", "meta", "", "more", "data"])) + XCTAssertEqual("6765.10946.17711+-a-very--long---build-----metadata--------identifier-------------with---------------------many----------------------------------hyphens-------------------------------------------------------" as Version, Version(6765, 10946, 17711, buildMetadataIdentifiers: ["-a-very--long---build-----metadata--------identifier-------------with---------------------many----------------------------------hyphens-------------------------------------------------------"])) + XCTAssertEqual("10946.17711.28657+" as Version, Version(10946, 17711, 28657, buildMetadataIdentifiers: [""])) + + } + + func testAdditionalInitializationFromString() { + let badStrings = [ + "", "1", "1.2", "1.2.3.4", "1.2.3.4.5", + "a", "1.a", "a.2", "a.2.3", "1.a.3", "1.2.a", + "-1.2.3", "1.-2.3", "1.2.-3", ".1.2.3", "v.1.2.3", "1.2..3", "v1.2.3", + ] + for str in badStrings { + XCTAssertNil(Version(string: str)) + } + + XCTAssertEqual(Version(1,2,3), Version(string: "1.2.3")) + XCTAssertEqual(Version(1,2,3), Version(string: "01.002.0003")) + XCTAssertEqual(Version(0,9,21), Version(string: "0.9.21")) + XCTAssertEqual(Version(0,9,21, prereleaseIdentifiers: ["alpha", "beta"], buildMetadataIdentifiers: ["1011"]), + Version(string: "0.9.21-alpha.beta+1011")) + XCTAssertEqual(Version(0,9,21, prereleaseIdentifiers: [], buildMetadataIdentifiers: ["1011"]), Version(string: "0.9.21+1011")) + } + func testRange() { switch Version(1,2,4) { case Version(1,2,3)..