diff --git a/Sources/SymbolKit/SymbolGraph/Misc/SemanticVersion.swift b/Sources/SymbolKit/SymbolGraph/Misc/SemanticVersion.swift index d84c1a5..672e1b0 100644 --- a/Sources/SymbolKit/SymbolGraph/Misc/SemanticVersion.swift +++ b/Sources/SymbolKit/SymbolGraph/Misc/SemanticVersion.swift @@ -10,60 +10,279 @@ extension SymbolGraph { /// A [semantic version](https://semver.org). - public struct SemanticVersion: Codable, Equatable, CustomStringConvertible { - /** - * The major version number. - * - * For example, the `1` in `1.2.3` - */ - public var major: Int - /** - * The minor version number. - * - * For example, the `2` in `1.2.3` - */ - public var minor: Int - /** - * The patch version number. - * - * For example, the `3` in `1.2.3` - */ - public var patch: Int - - /// The optional prerelease version component, which may contain non-numeric characters. - /// - /// For example, the `4` in `1.2.3-4`. - public var prerelease: String? - - /// Optional build metadata. - public var buildMetadata: String? - - public init(major: Int, minor: Int, patch: Int, prerelease: String? = nil, buildMetadata: String? = nil) { + public struct SemanticVersion { + /// The major version. + public let major: Int + /// The minor version. + public let minor: Int + /// The patch version. + public let patch: Int + /// Dot-separated pre-release identifiers. + public var prereleaseIdentifiers: [String] { prerelease.identifiers.map(\.description) } + /// Dot-separated build metadata identifiers. + public let buildMetadataIdentifiers: [String] + + /// The internal storage of pre-release identifiers. + internal let prerelease: Prerelease + + /// Creates a semantic version with the provided components of a semantic version. + /// - Parameters: + /// - major: The major version number. + /// - minor: The minor version number. + /// - patch: The patch version number. + /// - prereleaseIdentifiers: The pre-release identifiers. + /// - buildMetaDataIdentifiers: The build metadata identifiers. + public init( + // FIXME: Should `major`, `minor`, and `patch` be `UInt`? + _ major: Int, + _ minor: Int, + _ patch: Int, + prereleaseIdentifiers: [String] = [], + buildMetadataIdentifiers: [String] = [] + ) throws { + guard major >= 0 else { throw SemanticVersionError.invalidNumericIdentifier(major.description, position: .major, errorKind: .negativeValue)} + guard minor >= 0 else { throw SemanticVersionError.invalidNumericIdentifier(minor.description, position: .minor, errorKind: .negativeValue)} + guard patch >= 0 else { throw SemanticVersionError.invalidNumericIdentifier(patch.description, position: .patch, errorKind: .negativeValue)} self.major = major self.minor = minor self.patch = patch - self.prerelease = prerelease - self.buildMetadata = buildMetadata + + self.prerelease = try Prerelease(prereleaseIdentifiers) + + guard buildMetadataIdentifiers.allSatisfy( { !$0.isEmpty } ) else { + throw SemanticVersionError.emptyIdentifier(position: .buildMetadata) + } + try buildMetadataIdentifiers.forEach { + guard $0.allSatisfy( { $0.isASCII && ( $0.isLetter || $0.isNumber || $0 == "-" ) } ) else { + throw SemanticVersionError.invalidCharacterInIdentifier($0, position: .buildMetadata) + } + } + self.buildMetadataIdentifiers = buildMetadataIdentifiers } - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.major = try container.decode(Int.self, forKey: .major) - self.minor = try container.decodeIfPresent(Int.self, forKey: .minor) ?? 0 - self.patch = try container.decodeIfPresent(Int.self, forKey: .patch) ?? 0 - self.prerelease = try container.decodeIfPresent(String.self, forKey: .prerelease) - self.buildMetadata = try container.decodeIfPresent(String.self, forKey: .buildMetadata) + /// Creates a semantic version with the provided components of a semantic version. + /// - Parameters: + /// - major: The major version number. + /// - minor: The minor version number. + /// - patch: The patch version number. + /// - prerelease: The dot-separated pre-release identifiers; `nil` if the version is not a pre-release. + /// - buildMetadata: The dot-separated build metadata identifiers; `nil` if build metadata is absent. + @available(*, deprecated, renamed: "init(_:_:_:prereleaseIdentifiers:buildMetadataIdentifiers:)") + public init(major: Int, minor: Int, patch: Int, prerelease: String? = nil, buildMetadata: String? = nil) { + try! self.init( + major, minor, patch, + prereleaseIdentifiers: prerelease? + .split(separator: ".", omittingEmptySubsequences: false) + .map { String($0) } ?? [], + buildMetadataIdentifiers: buildMetadata? + .split(separator: ".", omittingEmptySubsequences: false) + .map { String($0) } ?? [] + ) } + } +} + +// MARK: - Inspecting a Semantic Version + +extension SymbolGraph.SemanticVersion { + /// A Boolean value indicating whether the version is a pre-release version. + public var isPrerelease: Bool { !prerelease.identifiers.isEmpty } +} + +// MARK: - - public var description: String { - var result = "\(major).\(minor).\(patch)" - if let prerelease = prerelease { - result += "-\(prerelease)" +extension SymbolGraph.SemanticVersion: Codable { + /// Keys for encoding and decoding `SemanticVersion` properties. + internal enum CodingKeys: String, CodingKey { + /// The major version number. + case major + /// The minor version number. + case minor + /// The patch version number. + case patch + /// The dot-separated pre-release identifiers. + case prerelease + /// The dot-separated build metadata identifiers. + case buildMetadata + } + + /// Creates a semantic version by decoding from the given decoder. + /// - Parameter decoder: The decoder to read data from. + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.major = try container.decode(Int.self, forKey: .major) + guard major >= 0 else { throw SymbolGraph.SemanticVersionError.invalidNumericIdentifier(major.description, position: .major, errorKind: .negativeValue)} + self.minor = try container.decodeIfPresent(Int.self, forKey: .minor) ?? 0 + guard minor >= 0 else { throw SymbolGraph.SemanticVersionError.invalidNumericIdentifier(minor.description, position: .minor, errorKind: .negativeValue)} + self.patch = try container.decodeIfPresent(Int.self, forKey: .patch) ?? 0 + guard patch >= 0 else { throw SymbolGraph.SemanticVersionError.invalidNumericIdentifier(patch.description, position: .patch, errorKind: .negativeValue)} + + self.prerelease = try Prerelease(try container.decodeIfPresent(String.self, forKey: .prerelease)) + + self.buildMetadataIdentifiers = try container.decodeIfPresent(String.self, forKey: .buildMetadata)? + .split(separator: ".", omittingEmptySubsequences: false) + .map { String($0) } ?? [] + guard !buildMetadataIdentifiers.allSatisfy(\.isEmpty) else { + throw SymbolGraph.SemanticVersionError.emptyIdentifier(position: .buildMetadata) + } + try buildMetadataIdentifiers.forEach { identifier in + guard identifier.isSemanticVersionBuildMetadataIdentifier else { + throw SymbolGraph.SemanticVersionError.invalidCharacterInIdentifier(String(identifier), position: .buildMetadata) } - if let buildMetadata = buildMetadata { - result += "+\(buildMetadata)" + } + } + + /// Encodes the semantic version into the given encoder. + /// - Parameter encoder: The encoder to write data to. + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(major, forKey: .major) + try container.encode(minor, forKey: .minor) + try container.encode(patch, forKey: .patch) + if isPrerelease { + try container.encode(prerelease.description, forKey: .prerelease) + } + if !buildMetadataIdentifiers.isEmpty { + try container.encode(buildMetadataIdentifiers.joined(separator: "."), forKey: .buildMetadata) + } + } + +} + +extension SymbolGraph.SemanticVersion: Comparable { + // 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). + /// <#Description#> + /// - Parameters: + /// - lhs: <#lhs description#> + /// - rhs: <#rhs description#> + /// - Returns: <#description#> + @inlinable + public static func == (lhs: Self, rhs: Self) -> Bool { + !(lhs < rhs) && !(lhs > rhs) + } + + /// <#Description#> + /// - Parameters: + /// - lhs: <#lhs description#> + /// - rhs: <#rhs description#> + /// - Returns: <#description#> + public static func < (lhs: Self, rhs: Self) -> Bool { + let lhsVersionCore = [lhs.major, lhs.minor, lhs.patch] + let rhsVersionCore = [rhs.major, rhs.minor, rhs.patch] + + guard lhsVersionCore == rhsVersionCore else { + return lhsVersionCore.lexicographicallyPrecedes(rhsVersionCore) + } + + return lhs.prerelease < rhs.prerelease // not lexicographically compared + } +} + +extension SymbolGraph.SemanticVersion: CustomStringConvertible { + /// A textual description of the `Semantic Version` instance. + public var description: String { + var versionString = "\(major).\(minor).\(patch)" + if !prerelease.identifiers.isEmpty { + versionString += "-\(prerelease)" + } + if !buildMetadataIdentifiers.isEmpty { + versionString += "+" + buildMetadataIdentifiers.joined(separator: ".") + } + return versionString + } +} + +extension SymbolGraph.SemanticVersion: 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) { + 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, + let major = validNumericIdentifier(versionCoreIdentifiers[0]), + let minor = validNumericIdentifier(versionCoreIdentifiers[1]), + let patch = validNumericIdentifier(versionCoreIdentifiers[2]) + else { return nil } + + 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 let prerelease = try? Prerelease(prereleaseIdentifiers) else { + return nil + } + self.prerelease = prerelease + } else { + self.prerelease = Prerelease(identifiers: []) // This is the member-wise initializer taking `[Identifier]` not `[S: StringProtocol]`. + } + + if let metadataDelimiterIndex = metadataDelimiterIndex { + let metadataStartIndex = versionString.index(after: metadataDelimiterIndex) + let buildMetadataIdentifiers = versionString[metadataStartIndex...].split(separator: ".", omittingEmptySubsequences: false) + guard buildMetadataIdentifiers.allSatisfy(\.isSemanticVersionBuildMetadataIdentifier) else { + return nil } - return result + self.buildMetadataIdentifiers = buildMetadataIdentifiers.map { String($0) } + } else { + self.buildMetadataIdentifiers = [] } + + /// Creates an integer-represented numeric identifier from the given identifier. + /// + /// Semantic Versioning 2.0.0 requires valid numeric identifiers to be "0" or ASCII digit sequence without leading "0"s. + /// + /// - Parameter identifier: The given identifier. + /// - Returns: The integer representation of the identifier, if the identifier is a valid Semantic Versioning 2.0.0 numeric identifier, and if it is representable by `Int`; `nil` otherwise. + func validNumericIdentifier(_ identifier: Substring) -> Int? { + // Converting each identifier from a substring to a signed integer doubles as asserting that the identifier is non-empty and that it has no non-ASCII-numeric characters other than an optional leading "+" or "-". + // `Int` is used here instead of `UInt`, because `Int` is a currency type, and because even with `UInt`, the literal '-0' and its leading-zeros variants can still slip through. + guard let numericIdentifier = Int(identifier) else { + return nil + } + // Although `Int.init(_:)` accepts a leading "+" in the argument, we don't need to be check for it here. "+" is the delimiter between pre-release and build metadata, and build metadata does not care for the validity of numeric identifiers. + guard identifier == "0" || (identifier.first != "-" && identifier.first != "0") else { + return nil + } + return numericIdentifier + } + } +} + +extension Character { + /// <#Description#> + internal var isSemanticVersionIdentifierCharacter: Bool { + isASCII && ( isLetter || isNumber || self == "-" ) + } + + /// <#Description#> + internal var isSemanticVersionNumericIdentifierCharacter: Bool { + isASCII && isNumber + } +} + +extension StringProtocol { + /// <#Description#> + internal var isSemanticVersionNumericIdentifier: Bool { + self == "0" || (first != "0" && allSatisfy(\.isSemanticVersionNumericIdentifierCharacter) && !isEmpty) + } + + /// <#Description#> + internal var isSemanticVersionAlphanumericIdentifier: Bool { + allSatisfy(\.isSemanticVersionIdentifierCharacter) && !allSatisfy(\.isSemanticVersionNumericIdentifierCharacter) && !isEmpty + } + + /// <#Description#> + internal var isSemanticVersionBuildMetadataIdentifier: Bool { + allSatisfy(\.isSemanticVersionIdentifierCharacter) && !isEmpty } } diff --git a/Sources/SymbolKit/SymbolGraph/Misc/SemanticVersionError.swift b/Sources/SymbolKit/SymbolGraph/Misc/SemanticVersionError.swift new file mode 100644 index 0000000..ac4898c --- /dev/null +++ b/Sources/SymbolKit/SymbolGraph/Misc/SemanticVersionError.swift @@ -0,0 +1,106 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for Swift project authors + */ + +extension SymbolGraph { + /// An error that occurs during the creation of a semantic version. + public enum SemanticVersionError: Error, CustomStringConvertible { + /// The identifier at the given position is empty. + /// - Parameter position: The empty identifier's position in the semantic version. + case emptyIdentifier(position: IdentifierPosition) + /// The identifier at the given position contains invalid character(s). + /// - Parameters: + /// - identifier: The identifier that contains invalid character(s). + /// - position: The given identifier's position in the semantic version. + case invalidCharacterInIdentifier(_ identifier: String, position: IdentifierPosition) + /// The numeric identifier at the given position is invalid for the given reason. + /// - Parameters: + /// - identifier: The invalid numeric identifier. + /// - position: The given numeric identifier's position in the semantic version. + /// - errorKind: The reason why the given numeric identifier is invalid. + case invalidNumericIdentifier(_ identifier: String, position: NumericIdentifierPosition, errorKind: NumericIdentifierErrorKind) + /// The version core contains an invalid number of Identifiers. + /// - Parameter identifiers: The version core identifiers in the version string. + case invalidVersionCoreIdentifierCount(identifiers: [String]) + + /// A position of an identifier in a semantic version. + public enum IdentifierPosition: String, CustomStringConvertible { + /// The major version number position in a semantic version. + case major + /// The minor version number position in a semantic version. + case minor + /// The patch version number position in a semantic version. + case patch + /// The pre-release position in a semantic version. + case prerelease + /// The build-metadata position in a semantic version. + case buildMetadata + + /// <#Description#> + public var description: String { + self.rawValue + } + } + + /// A position of a numeric identifier in a semantic version. + public enum NumericIdentifierPosition: String, CustomStringConvertible { + /// The major version number position. + case major + /// The minor version number position. + case minor + /// The patch version number position. + case patch + /// The pre-release position. + case prerelease + + /// <#Description#> + public var description: String { + self.rawValue + } + } + + /// A reason why a numeric identifier is invalid. + public enum NumericIdentifierErrorKind { + /// The numeric identifier contains leading "0" characters. + case leadingZeros + /// The numeric identifier represents a negative value. + case negativeValue + /// The numeric identifier contains non-numeric characters. + case nonNumericCharacter + } + + // this description follows [the "grammar and phrasing" section of Swift's diagnostics guidelines](https://github.com/apple/swift/blob/d1bb98b11ede375a1cee739f964b7d23b6657aaf/docs/Diagnostics.md#grammar-and-phrasing) + /// <#Description#> + public var description: String { + switch self { + case let .emptyIdentifier(position): + return "semantic version \(position) identifier cannot be empty" + case let .invalidCharacterInIdentifier(identifier, position): + return "semantic version \(position) identifier '\(identifier)' cannot conatin characters other than ASCII alphanumerics and hyphen-minus ([0-9A-Za-z-])" + case let .invalidNumericIdentifier(identifier, position, errorKind): + let fault: String + switch errorKind { + case .leadingZeros: + fault = "contain leading '0'" + case .negativeValue: + fault = "represent negative value" + case .nonNumericCharacter: + fault = "conatin non-numeric characters" + } + return "semantic version numeric \(position) identifier '\(identifier)' cannot \(fault)" + case let .invalidVersionCoreIdentifierCount(identifiers): + return """ + semantic version must conatins exactly 3 version core identifiers; \ + \(identifiers.count) given\(identifiers.isEmpty ? "" : " : ")\ + \(identifiers.map { "'\($0)'" } .joined(separator: ", ")) + """ + } + } + } +} diff --git a/Sources/SymbolKit/SymbolGraph/Misc/SemanticVersionPrerelease.swift b/Sources/SymbolKit/SymbolGraph/Misc/SemanticVersionPrerelease.swift new file mode 100644 index 0000000..a8b8098 --- /dev/null +++ b/Sources/SymbolKit/SymbolGraph/Misc/SemanticVersionPrerelease.swift @@ -0,0 +1,135 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for Swift project authors + */ + +extension SymbolGraph.SemanticVersion { + /// A storage for pre-release identifiers. + internal struct Prerelease { + /// The identifiers. + internal let identifiers: [Identifier] + /// A pre-release identifier. + internal enum Identifier { + /// A numeric pre-release identifier. + /// - Parameter identifier: The identifier. + case numeric(_ identifier: Int) + /// An alphanumeric pre-release identifier. + /// - Parameter identifier: The identifier. + case alphanumeric(_ identifier: String) + } + } +} + +// MARK: - Initializers + +extension SymbolGraph.SemanticVersion.Prerelease { + /// <#Description#> + /// + /// - Note: Empty string translates to an empty pre-release identifier, which is invalid. + /// - Parameter dotSeparatedIdentifiers: <#dotSeparatedIdentifiers description#> + internal init(_ dotSeparatedIdentifiers: String?) throws { + guard let dotSeparatedIdentifiers = dotSeparatedIdentifiers else { + // FIXME: initialize 'identifiers' directly here after [SR-15670](https://bugs.swift.org/projects/SR/issues/SR-15670?filter=allopenissues) is resolved + // currently 'identifiers' cannot be initialized directly because initializer delegation is flow-insensitive + // self.identifiers = [] + self.init(identifiers: []) + return + } + let identifiers = dotSeparatedIdentifiers.split( + separator: ".", + omittingEmptySubsequences: false // must preserve empty identifiers + ) + try self.init(identifiers) + } + + /// <#Description#> + internal init(_ identifiers: C) throws where C.Element == S { + self.identifiers = try identifiers.map { + try Identifier($0) + } + } +} + +extension SymbolGraph.SemanticVersion.Prerelease.Identifier { + /// <#Description#> + internal init(_ identifier: S) throws { + guard !identifier.isEmpty else { + throw SymbolGraph.SemanticVersionError.emptyIdentifier(position: .prerelease) + } + guard identifier.allSatisfy(\.isSemanticVersionIdentifierCharacter) else { + throw SymbolGraph.SemanticVersionError.invalidCharacterInIdentifier(String(identifier), position: .prerelease) + } + if identifier.allSatisfy(\.isNumber) { + // diagnose the identifier as a numeric identifier, if all characters are ASCII digits + guard identifier.first != "0" || identifier == "0" else { + throw SymbolGraph.SemanticVersionError.invalidNumericIdentifier(String(identifier), position: .prerelease, errorKind: .leadingZeros) + } + self = .numeric(Int(identifier)!) + } else { + self = .alphanumeric(String(identifier)) + } + } +} + +// MARK: - Comparable Conformances + +// Compiler synthesised `Equatable`-conformance is correct here. +extension SymbolGraph.SemanticVersion.Prerelease: Comparable { + /// <#Description#> + /// - Parameters: + /// - lhs: <#lhs description#> + /// - rhs: <#rhs description#> + /// - Returns: <#description#> + internal static func <(lhs: Self, rhs: Self) -> Bool { + guard !lhs.identifiers.isEmpty else { return false } // non-pre-release lhs >= potentially pre-release rhs + guard !rhs.identifiers.isEmpty else { return true } // pre-release lhs < non-pre-release rhs + return lhs.identifiers.lexicographicallyPrecedes(rhs.identifiers) + } +} + +// Compiler synthesised `Equatable`-conformance is correct here. +extension SymbolGraph.SemanticVersion.Prerelease.Identifier: Comparable { + /// <#Description#> + /// - Parameters: + /// - lhs: <#lhs description#> + /// - rhs: <#rhs description#> + /// - Returns: <#description#> + internal static func <(lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case let (.numeric(lhs), .numeric(rhs)): + return lhs < rhs + case let(.alphanumeric(lhs), .alphanumeric(rhs)): + return lhs < rhs + case (.numeric, .alphanumeric): + return true + case (.alphanumeric, .numeric): + return false + } + } +} + +// MARK: CustomStringConvertible Conformances + +extension SymbolGraph.SemanticVersion.Prerelease: CustomStringConvertible { + /// <#Description#> + internal var description: String { + identifiers.map(\.description).joined(separator: ".") + } +} + +extension SymbolGraph.SemanticVersion.Prerelease.Identifier: CustomStringConvertible { + /// <#Description#> + internal var description: String { + switch self { + case .numeric(let identifier): + return identifier.description + case .alphanumeric(let identifier): + return identifier.description + } + } +} diff --git a/Tests/SymbolKitTests/LineList/SemanticVersionTests.swift b/Tests/SymbolKitTests/LineList/SemanticVersionTests.swift deleted file mode 100644 index a0484ff..0000000 --- a/Tests/SymbolKitTests/LineList/SemanticVersionTests.swift +++ /dev/null @@ -1,26 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2021 Apple Inc. and the Swift project authors - Licensed under Apache License v2.0 with Runtime Library Exception - - See https://swift.org/LICENSE.txt for license information - See https://swift.org/CONTRIBUTORS.txt for Swift project authors -*/ - -import XCTest -@testable import SymbolKit - -class SemanticVersionTests: XCTestCase { - typealias SemanticVersion = SymbolGraph.SemanticVersion - - func testVersionInit() { - let version = SemanticVersion(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: "enableX") - - XCTAssertEqual(version.major, 1) - XCTAssertEqual(version.minor, 2) - XCTAssertEqual(version.patch, 3) - XCTAssertEqual(version.prerelease, "beta") - XCTAssertEqual(version.buildMetadata, "enableX") - } -} diff --git a/Tests/SymbolKitTests/SymbolGraph/SemanticVersionTests.swift b/Tests/SymbolKitTests/SymbolGraph/SemanticVersionTests.swift new file mode 100644 index 0000000..0e8c2fd --- /dev/null +++ b/Tests/SymbolKitTests/SymbolGraph/SemanticVersionTests.swift @@ -0,0 +1,1065 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for Swift project authors + */ + +import XCTest +@testable import SymbolKit + +final class SemanticVersionTests: XCTestCase { + + typealias Version = SymbolGraph.SemanticVersion + + func testVersionInitialization() { + 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 testDecodingFromJSONToVersion() { + let jsonDecoder = JSONDecoder() + + let versionObjectString1 = """ + { + "major": 1 + } + """ + let versionObjectData1 = Data(versionObjectString1.utf8) + let version1 = Version(1, 0, 0) + XCTAssertNoThrow( + try { + let decodedVersion1 = try jsonDecoder.decode(Version.self, from: versionObjectData1) + XCTAssertEqual(version1, decodedVersion1) + }() + ) + + let versionObjectString2 = """ + { + "major": 1, + "minor": 42 + } + """ + let versionObjectData2 = Data(versionObjectString2.utf8) + let version2 = Version(1, 42, 0) + XCTAssertNoThrow( + try { + let decodedVersion2 = try jsonDecoder.decode(Version.self, from: versionObjectData2) + XCTAssertEqual(version2, decodedVersion2) + }() + ) + + let versionObjectString3 = """ + { + "major": 4, + "patch": 2 + } + """ + let versionObjectData3 = Data(versionObjectString3.utf8) + let version3 = Version(4, 0, 2) + XCTAssertNoThrow( + try { + let decodedVersion3 = try jsonDecoder.decode(Version.self, from: versionObjectData3) + XCTAssertEqual(version3, decodedVersion3) + }() + ) + + let versionObjectString4 = """ + { + "major": 100, + "prerelease": "bvf7yuv" + } + """ + let versionObjectData4 = Data(versionObjectString4.utf8) + let version4 = Version( + 100, 0, 0, + prereleaseIdentifiers: ["bvf7yuv"] + ) + XCTAssertNoThrow( + try { + let decodedVersion4 = try jsonDecoder.decode(Version.self, from: versionObjectData4) + XCTAssertEqual(version4, decodedVersion4) + }() + ) + + let versionObjectString5 = """ + { + "major": 99999, + "buildMetadata": "UZryk-09btxguch" + } + """ + let versionObjectData5 = Data(versionObjectString5.utf8) + let version5 = Version( + 99999, 0, 0, + buildMetadataIdentifiers: ["UZryk-09btxguch"] + ) + XCTAssertNoThrow( + try { + let decodedVersion5 = try jsonDecoder.decode(Version.self, from: versionObjectData5) + XCTAssertEqual(version5, decodedVersion5) + }() + ) + + let versionObjectString6 = """ + { + "major": 1, + "minor": 2, + "patch": 3, + "prerelease": "abc.def-ghi", + "buildMetadata": "xyz.abc-def.gf765c7v.7867ft.ghi--uvw" + } + """ + let versionObjectData6 = Data(versionObjectString6.utf8) + let version6 = Version( + 1, 2, 3, + prereleaseIdentifiers: ["abc", "def-ghi"], + buildMetadataIdentifiers: ["xyz", "abc-def", "gf765c7v", "7867ft", "ghi--uvw"] + ) + XCTAssertNoThrow( + try { + let decodedVersion6 = try jsonDecoder.decode(Version.self, from: versionObjectData6) + XCTAssertEqual(version6, decodedVersion6) + }() + ) + + let versionObjectString7 = """ + { + "major": 1, + "minor": 2, + "patch": 3, + "prerelease": "" + } + """ + let versionObjectData7 = Data(versionObjectString7.utf8) + let version7 = Version( + 1, 2, 3, + prereleaseIdentifiers: [""], + buildMetadataIdentifiers: [] + ) + XCTAssertNoThrow( + try { + let decodedVersion7 = try jsonDecoder.decode(Version.self, from: versionObjectData7) + XCTAssertEqual(version7, decodedVersion7) + }() + ) + + let versionObjectString8 = """ + { + "major": 1, + "minor": 2, + "patch": 3, + "buildMetadata": "" + } + """ + let versionObjectData8 = Data(versionObjectString8.utf8) + let version8 = Version( + 1, 2, 3, + prereleaseIdentifiers: [], + buildMetadataIdentifiers: [""] + ) + XCTAssertNoThrow( + try { + let decodedVersion8 = try jsonDecoder.decode(Version.self, from: versionObjectData8) + XCTAssertEqual(version8, decodedVersion8) + }() + ) + + let versionObjectString9 = """ + { + "major": 1, + "minor": 2, + "patch": 3, + "prerelease": "", + "buildMetadata": "" + } + """ + let versionObjectData9 = Data(versionObjectString9.utf8) + let version9 = Version( + 1, 2, 3, + prereleaseIdentifiers: [""], + buildMetadataIdentifiers: [""] + ) + XCTAssertNoThrow( + try { + let decodedVersion9 = try jsonDecoder.decode(Version.self, from: versionObjectData9) + XCTAssertEqual(version9, decodedVersion9) + }() + ) + } + + func testEncodingFromVersionToJSON() { + let jsonEncoder1 = JSONEncoder() + jsonEncoder1.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let versionObjectString1 = """ + { + "major": 1, + "minor": 0, + "patch": 0 + } + """ + let version1 = Version(1, 0, 0) + XCTAssertNoThrow( + try { + let encodedVersion1 = try jsonEncoder1.encode(version1) + XCTAssertEqual( + String(data: encodedVersion1, encoding: .utf8), + // Because Semantic Versioning 2.0.0 does not allow whitespace in identifiers, we can remove whitespace from the string with no worry. + versionObjectString1.filter { !$0.isWhitespace } + ) + }() + ) + + let jsonEncoder2 = JSONEncoder() + jsonEncoder2.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let versionObjectString2 = """ + { + "buildMetadata": "", + "major": 1, + "minor": 0, + "patch": 0, + "prerelease": "" + } + """ + let version2 = Version(1, 0, 0, prereleaseIdentifiers: [""], buildMetadataIdentifiers: [""]) + XCTAssertNoThrow( + try { + let encodedVersion2 = try jsonEncoder2.encode(version2) + XCTAssertEqual( + String(data: encodedVersion2, encoding: .utf8), + // Because Semantic Versioning 2.0.0 does not allow whitespace in identifiers, we can remove whitespace from the string with no worry. + versionObjectString2.filter { !$0.isWhitespace } + ) + }() + ) + + let jsonEncoder3 = JSONEncoder() + jsonEncoder3.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let versionObjectString3 = """ + { + "major": 1, + "minor": 42, + "patch": 0 + } + """ + let version3 = Version(1, 42, 0) + XCTAssertNoThrow( + try { + let encodedVersion3 = try jsonEncoder3.encode(version3) + XCTAssertEqual( + String(data: encodedVersion3, encoding: .utf8), + versionObjectString3.filter { !$0.isWhitespace } + ) + }() + ) + + let jsonEncoder4 = JSONEncoder() + jsonEncoder4.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let versionObjectString4 = """ + { + "buildMetadata": "", + "major": 1, + "minor": 42, + "patch": 0, + "prerelease": "" + } + """ + let version4 = Version(1, 42, 0, prereleaseIdentifiers: [""], buildMetadataIdentifiers: [""]) + XCTAssertNoThrow( + try { + let encodedVersion4 = try jsonEncoder4.encode(version4) + XCTAssertEqual( + String(data: encodedVersion4, encoding: .utf8), + versionObjectString4.filter { !$0.isWhitespace } + ) + }() + ) + + let jsonEncoder5 = JSONEncoder() + jsonEncoder5.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let versionObjectString5 = """ + { + "major": 4, + "minor": 0, + "patch": 2 + } + """ + let version5 = Version(4, 0, 2) + XCTAssertNoThrow( + try { + let encodedVersion5 = try jsonEncoder5.encode(version5) + XCTAssertEqual( + String(data: encodedVersion5, encoding: .utf8), + versionObjectString5.filter { !$0.isWhitespace } + ) + }() + ) + + let jsonEncoder6 = JSONEncoder() + jsonEncoder6.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let versionObjectString6 = """ + { + "buildMetadata": "", + "major": 4, + "minor": 0, + "patch": 2, + "prerelease": "" + } + """ + let version6 = Version(4, 0, 2, prereleaseIdentifiers: [""], buildMetadataIdentifiers: [""]) + XCTAssertNoThrow( + try { + let encodedVersion6 = try jsonEncoder6.encode(version6) + XCTAssertEqual( + String(data: encodedVersion6, encoding: .utf8), + versionObjectString6.filter { !$0.isWhitespace } + ) + }() + ) + + let jsonEncoder7 = JSONEncoder() + jsonEncoder7.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let versionObjectString7 = """ + { + "major": 100, + "minor": 0, + "patch": 0, + "prerelease": "-j9uh08y97" + } + """ + let version7 = Version( + 100, 0, 0, + prereleaseIdentifiers: ["-j9uh08y97"] + ) + XCTAssertNoThrow( + try { + let encodedVersion7 = try jsonEncoder7.encode(version7) + XCTAssertEqual( + String(data: encodedVersion7, encoding: .utf8), + versionObjectString7.filter { !$0.isWhitespace } + ) + }() + ) + + let jsonEncoder8 = JSONEncoder() + jsonEncoder8.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let versionObjectString8 = """ + { + "buildMetadata": "", + "major": 100, + "minor": 0, + "patch": 0, + "prerelease": "-j9uh08y97" + } + """ + let version8 = Version( + 100, 0, 0, + prereleaseIdentifiers: ["-j9uh08y97"], + buildMetadataIdentifiers: [""] + ) + XCTAssertNoThrow( + try { + let encodedVersion8 = try jsonEncoder8.encode(version8) + XCTAssertEqual( + String(data: encodedVersion8, encoding: .utf8), + versionObjectString8.filter { !$0.isWhitespace } + ) + }() + ) + + let jsonEncoder9 = JSONEncoder() + jsonEncoder9.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let versionObjectString9 = """ + { + "buildMetadata": "vvbcxqbo-bvy.HIu", + "major": 99999, + "minor": 0, + "patch": 0 + } + """ + let version9 = Version( + 99999, 0, 0, + buildMetadataIdentifiers: ["vvbcxqbo-bvy", "HIu"] + ) + XCTAssertNoThrow( + try { + let encodedVersion9 = try jsonEncoder9.encode(version9) + XCTAssertEqual( + String(data: encodedVersion9, encoding: .utf8), + versionObjectString9.filter { !$0.isWhitespace } + ) + }() + ) + + let jsonEncoder10 = JSONEncoder() + jsonEncoder10.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let versionObjectString10 = """ + { + "buildMetadata": "vvbcxqbo-bvy.HIu", + "major": 99999, + "minor": 0, + "patch": 0, + "prerelease": "" + } + """ + let version10 = Version( + 99999, 0, 0, + prereleaseIdentifiers: [""], + buildMetadataIdentifiers: ["vvbcxqbo-bvy", "HIu"] + ) + XCTAssertNoThrow( + try { + let encodedVersion10 = try jsonEncoder10.encode(version10) + XCTAssertEqual( + String(data: encodedVersion10, encoding: .utf8), + versionObjectString10.filter { !$0.isWhitespace } + ) + }() + ) + + let jsonEncoder11 = JSONEncoder() + jsonEncoder11.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let versionObjectString11 = """ + { + "buildMetadata": "xyz.abc-def.----..ghi-uvw", + "major": 1, + "minor": 2, + "patch": 3, + "prerelease": "abc.def-ghi" + } + """ + let version11 = Version( + 1, 2, 3, + prereleaseIdentifiers: ["abc", "def-ghi"], + buildMetadataIdentifiers: ["xyz", "abc-def", "----", "", "ghi-uvw"] + ) + XCTAssertNoThrow( + try { + let encodedVersion11 = try jsonEncoder11.encode(version11) + XCTAssertEqual( + String(data: encodedVersion11, encoding: .utf8), + versionObjectString11.filter { !$0.isWhitespace } + ) + }() + ) + } + + func testJSONRoundTrip() { + let jsonDecoder = JSONDecoder() + + let jsonEncoder1 = JSONEncoder() + let version1 = Version(1, 0, 0) + XCTAssertNoThrow( + try { + let roundTripVersion1 = try jsonDecoder.decode(Version.self, from: jsonEncoder1.encode(version1)) + print(version1.prereleaseIdentifiers) + print(roundTripVersion1.prereleaseIdentifiers) + XCTAssertEqual(version1, roundTripVersion1) + }() + ) + + let jsonEncoder2 = JSONEncoder() + let version2 = Version(1, 42, 0) + XCTAssertNoThrow( + try { + let roundTripVersion2 = try jsonDecoder.decode(Version.self, from: jsonEncoder2.encode(version2)) + XCTAssertEqual(version2, roundTripVersion2) + }() + ) + + let jsonEncoder3 = JSONEncoder() + let version3 = Version(4, 0, 2) + XCTAssertNoThrow( + try { + let roundTripVersion3 = try jsonDecoder.decode(Version.self, from: jsonEncoder3.encode(version3)) + XCTAssertEqual(version3, roundTripVersion3) + }() + ) + + let jsonEncoder4 = JSONEncoder() + let version4 = Version( + 100, 0, 0, + prereleaseIdentifiers: ["bvf7yuv"] + ) + XCTAssertNoThrow( + try { + let roundTripVersion4 = try jsonDecoder.decode(Version.self, from: jsonEncoder4.encode(version4)) + XCTAssertEqual(version4, roundTripVersion4) + }() + ) + + let jsonEncoder5 = JSONEncoder() + let version5 = Version( + 99999, 0, 0, + buildMetadataIdentifiers: ["UZryk-09btxguch"] + ) + XCTAssertNoThrow( + try { + let roundTripVersion5 = try jsonDecoder.decode(Version.self, from: jsonEncoder5.encode(version5)) + XCTAssertEqual(version5, roundTripVersion5) + }() + ) + + let jsonEncoder6 = JSONEncoder() + let version6 = Version( + 1, 2, 3, + prereleaseIdentifiers: ["abc", "def-ghi"], + buildMetadataIdentifiers: ["xyz", "abc-def", "gf765c7v", "7867ft", "ghi--uvw"] + ) + XCTAssertNoThrow( + try { + let roundTripVersion6 = try jsonDecoder.decode(Version.self, from: jsonEncoder6.encode(version6)) + XCTAssertEqual(version6, roundTripVersion6) + }() + ) + + let jsonEncoder7 = JSONEncoder() + let version7 = Version( + 1, 2, 3, + prereleaseIdentifiers: [""], + buildMetadataIdentifiers: [] + ) + XCTAssertNoThrow( + try { + let roundTripVersion7 = try jsonDecoder.decode(Version.self, from: jsonEncoder7.encode(version7)) + XCTAssertEqual(version7, roundTripVersion7) + }() + ) + + let jsonEncoder8 = JSONEncoder() + let version8 = Version( + 1, 2, 3, + prereleaseIdentifiers: [], + buildMetadataIdentifiers: [""] + ) + XCTAssertNoThrow( + try { + let roundTripVersion8 = try jsonDecoder.decode(Version.self, from: jsonEncoder8.encode(version8)) + XCTAssertEqual(version8, roundTripVersion8) + }() + ) + + let jsonEncoder9 = JSONEncoder() + let version9 = Version( + 1, 2, 3, + prereleaseIdentifiers: [""], + buildMetadataIdentifiers: [""] + ) + XCTAssertNoThrow( + try { + let roundTripVersion9 = try jsonDecoder.decode(Version.self, from: jsonEncoder9.encode(version9)) + XCTAssertEqual(version9, roundTripVersion9) + }() + ) + } + + 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)) + + XCTAssertFalse(Version(2, 1, 1) < Version(1, 2, 3)) + XCTAssertFalse(Version(1, 3, 1) < Version(1, 2, 3)) + XCTAssertFalse(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"])) + XCTAssertFalse(Version(1, 2, 3) < Version(1, 2, 3, prereleaseIdentifiers: [""])) + XCTAssertFalse(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"])) + XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: ["1"]), Version(1, 2, 3, prereleaseIdentifiers: ["001"])) + 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: ["001", "1"]), Version(1, 2, 3, prereleaseIdentifiers: ["1", "2"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["1", "1"]), Version(1, 2, 3, prereleaseIdentifiers: ["001", "2"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["1", "1"]), Version(1, 2, 3, prereleaseIdentifiers: ["1", "002"])) + 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"])) + + XCTAssertGreaterThan(Version(1, 2, 3, prereleaseIdentifiers: ["abc"]), Version(1, 2, 3, prereleaseIdentifiers: ["123"])) + XCTAssertGreaterThan(Version(1, 2, 3, prereleaseIdentifiers: ["123abc"]), Version(1, 2, 3, prereleaseIdentifiers: ["223"])) + + XCTAssertFalse(Version(1, 2, 3, prereleaseIdentifiers: ["abc"]) < Version(1, 2, 3, prereleaseIdentifiers: ["123"])) + XCTAssertFalse(Version(1, 2, 3, prereleaseIdentifiers: ["123abc"]) < Version(1, 2, 3, prereleaseIdentifiers: ["223"])) + + XCTAssertGreaterThan(Version(1, 2, 3, prereleaseIdentifiers: ["baa"]), Version(1, 2, 3, prereleaseIdentifiers: ["azzz"])) + XCTAssertGreaterThan(Version(1, 2, 3, prereleaseIdentifiers: ["b", "z"]), Version(1, 2, 3, prereleaseIdentifiers: ["abc", "a", "zzz"])) + + XCTAssertFalse(Version(1, 2, 3, prereleaseIdentifiers: ["baa"]) < Version(1, 2, 3, prereleaseIdentifiers: ["azzz"])) + XCTAssertFalse(Version(1, 2, 3, prereleaseIdentifiers: ["b", "z"]) < Version(1, 2, 3, prereleaseIdentifiers: ["abc", "a", "zzz"])) + + // 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"])) + + XCTAssertFalse(Version(2, 2, 3, prereleaseIdentifiers: [""]) < Version(1, 2, 3, buildMetadataIdentifiers: [""])) + XCTAssertFalse(Version(1, 3, 3, prereleaseIdentifiers: ["alpha"]) < Version(1, 2, 3, buildMetadataIdentifiers: ["beta"])) + XCTAssertFalse(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"])) + XCTAssertFalse(Version(1, 2, 3) < Version(1, 2, 3, prereleaseIdentifiers: [""], buildMetadataIdentifiers: [""])) + XCTAssertFalse(Version(1, 2, 3) < Version(1, 2, 3, prereleaseIdentifiers: [""], buildMetadataIdentifiers: ["123alpha"])) + XCTAssertFalse(Version(1, 2, 3) < Version(1, 2, 3, prereleaseIdentifiers: ["alpha"], buildMetadataIdentifiers: ["alpha"])) + XCTAssertFalse(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: [""]) + ) + XCTAssertEqual( + Version(1, 2, 3, prereleaseIdentifiers: ["1"]), + Version(1, 2, 3, prereleaseIdentifiers: ["001"], buildMetadataIdentifiers: [""]) + ) + XCTAssertEqual( + Version(1, 2, 3, prereleaseIdentifiers: ["0001"]), + Version(1, 2, 3, prereleaseIdentifiers: ["1"], buildMetadataIdentifiers: [""]) + ) + XCTAssertEqual( + Version(1, 2, 3, prereleaseIdentifiers: ["00001"]), + Version(1, 2, 3, prereleaseIdentifiers: ["000001"], 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", "1"]), + Version(1, 2, 3, prereleaseIdentifiers: ["00000001", "2"], buildMetadataIdentifiers: ["123"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["000000001", "1"]), + Version(1, 2, 3, prereleaseIdentifiers: ["1", "2"], buildMetadataIdentifiers: ["123"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["01", "1"]), + Version(1, 2, 3, prereleaseIdentifiers: ["0000000001", "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"]) + ) + + XCTAssertGreaterThan( + Version(1, 2, 3, prereleaseIdentifiers: ["xyz"]), + Version(1, 2, 3, prereleaseIdentifiers: ["123"], buildMetadataIdentifiers: ["hgjkalmfvdfua"]) + ) + XCTAssertGreaterThan( + Version(1, 2, 3, prereleaseIdentifiers: ["111uvw"]), + Version(1, 2, 3, prereleaseIdentifiers: ["999999"], buildMetadataIdentifiers: ["iouiytrdfghj", "3rfey89rr"]) + ) + + XCTAssertFalse( + Version(1, 2, 3, prereleaseIdentifiers: ["xyz"]) < + Version(1, 2, 3, prereleaseIdentifiers: ["123"], buildMetadataIdentifiers: ["hgjkalmfvdfua"]) + ) + XCTAssertFalse( + Version(1, 2, 3, prereleaseIdentifiers: ["111uvw"]) < + Version(1, 2, 3, prereleaseIdentifiers: ["999999"], buildMetadataIdentifiers: ["dfghjkiohgf", "3rfey89rr"]) + ) + + // 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: [""]) + ) + XCTAssertEqual( + Version(1, 2, 3, prereleaseIdentifiers: ["01"], buildMetadataIdentifiers: ["alpha-"]), + Version(1, 2, 3, prereleaseIdentifiers: ["1"], buildMetadataIdentifiers: [""]) + ) + XCTAssertEqual( + Version(1, 2, 3, prereleaseIdentifiers: ["1"], buildMetadataIdentifiers: ["alpha-"]), + Version(1, 2, 3, prereleaseIdentifiers: ["01"], buildMetadataIdentifiers: [""]) + ) + XCTAssertEqual( + Version(1, 2, 3, prereleaseIdentifiers: ["001"], buildMetadataIdentifiers: ["alpha-"]), + Version(1, 2, 3, prereleaseIdentifiers: ["0001"], 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: ["00001", "1"], buildMetadataIdentifiers: ["alpha", "beta"]), + Version(1, 2, 3, prereleaseIdentifiers: ["1", "2"], buildMetadataIdentifiers: ["123"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["1", "1"], buildMetadataIdentifiers: ["alpha", "beta"]), + Version(1, 2, 3, prereleaseIdentifiers: ["000001", "2"], buildMetadataIdentifiers: ["123"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["0000001", "1"], buildMetadataIdentifiers: ["alpha", "beta"]), + Version(1, 2, 3, prereleaseIdentifiers: ["001", "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"]) + ) + + XCTAssertGreaterThan( + Version(1, 2, 3, prereleaseIdentifiers: ["xyz"], buildMetadataIdentifiers: ["-09tyfvgubh"]), + Version(1, 2, 3, prereleaseIdentifiers: ["123"], buildMetadataIdentifiers: ["765resdfu89"]) + ) + XCTAssertGreaterThan( + Version(1, 2, 3, prereleaseIdentifiers: ["111uvw"], buildMetadataIdentifiers: ["-----MNgyftcyvu---vcxzAQwsd-------"]), + Version(1, 2, 3, prereleaseIdentifiers: ["999999"], buildMetadataIdentifiers: ["bvgh--9-ygtfyvg", "hgvh-0vb-"]) + ) + + XCTAssertFalse( + Version(1, 2, 3, prereleaseIdentifiers: ["xyz"], buildMetadataIdentifiers: ["fhieaw98y76ftrcwrjk"]) < + Version(1, 2, 3, prereleaseIdentifiers: ["123"], buildMetadataIdentifiers: ["-jhgbivuy-gh"]) + ) + XCTAssertFalse( + Version(1, 2, 3, prereleaseIdentifiers: ["111uvw"], buildMetadataIdentifiers: ["bvcx67t"]) < + Version(1, 2, 3, prereleaseIdentifiers: ["999999"], buildMetadataIdentifiers: ["nuybvfcrd6ty", "3rfey89rr"]) + ) + + } + + 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 testLosslessConversionFromStringToVersion() { + + // 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("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("1346269.3524578.5702887-beta1+😀" as String)) + + // MARK: malformed version core, malformed pre-release identifiers, well-formed build metadata identifiers + + XCTAssertNil(Version("3524578.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)) + + } +}