diff --git a/Sources/SymbolKit/SymbolGraph/SemanticVersion.swift b/Sources/SymbolKit/SymbolGraph/SemanticVersion.swift deleted file mode 100644 index 537e6de..0000000 --- a/Sources/SymbolKit/SymbolGraph/SemanticVersion.swift +++ /dev/null @@ -1,69 +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 -*/ - -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) { - self.major = major - self.minor = minor - self.patch = patch - self.prerelease = prerelease - self.buildMetadata = buildMetadata - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - major = try container.decode(Int.self, forKey: .major) - minor = try container.decodeIfPresent(Int.self, forKey: .minor) ?? 0 - patch = try container.decodeIfPresent(Int.self, forKey: .patch) ?? 0 - prerelease = try container.decodeIfPresent(String.self, forKey: .prerelease) - buildMetadata = try container.decodeIfPresent(String.self, forKey: .buildMetadata) - } - - public var description: String { - var result = "\(major).\(minor).\(patch)" - if let prerelease = prerelease { - result += "-\(prerelease)" - } - if let buildMetadata = buildMetadata { - result += "+\(buildMetadata)" - } - return result - } - } -} diff --git a/Sources/SymbolKit/SymbolGraph/SemanticVersion/Identifier Validation.swift b/Sources/SymbolKit/SymbolGraph/SemanticVersion/Identifier Validation.swift new file mode 100644 index 0000000..b286e66 --- /dev/null +++ b/Sources/SymbolKit/SymbolGraph/SemanticVersion/Identifier Validation.swift @@ -0,0 +1,21 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2022 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 Character { + /// A Boolean value indicating whether this character is allowed in a semantic version's identifier. + internal var isAllowedInSemanticVersionIdentifier: Bool { + isASCII && (isLetter || isNumber || self == "-") + } + + /// A Boolean value indicating whether this character is allowed in a semantic version's numeric identifier. + internal var isAllowedInSemanticVersionNumericIdentifier: Bool { + isASCII && isNumber + } +} diff --git a/Sources/SymbolKit/SymbolGraph/SemanticVersion/Prerelease.swift b/Sources/SymbolKit/SymbolGraph/SemanticVersion/Prerelease.swift new file mode 100644 index 0000000..863ab5a --- /dev/null +++ b/Sources/SymbolKit/SymbolGraph/SemanticVersion/Prerelease.swift @@ -0,0 +1,144 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2022 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: UInt) + /// An alphanumeric pre-release identifier. + /// - Parameter identifier: The identifier. + case alphanumeric(_ identifier: String) + } + } +} + +// MARK: - Initializers + +extension SymbolGraph.SemanticVersion.Prerelease { + /// Creates a semantic version pre-release from the given pre-release string. + /// - Note: Empty string translates to an empty pre-release identifier, which is invalid. + /// - Parameter dotSeparatedIdentifiers: The given pre-release string to create a semantic version pre-release from. + /// - Throws: A `SymbolGraph.SemanticVersionError` instance if `dotSeparatedIdentifiers` is not a valid pre-release string. + 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 // Preserve empty sequences to be able to raise validation errors about empty prerelease identifiers. + ) + try self.init(identifiers) + } + + /// Creates a semantic version pre-release from the given pre-release identifier strings. + /// - Parameter identifiers: The given pre-release identifier strings to create a semantic version pre-release from. + /// - Throws: A `SymbolGraph.SemanticVersionError` instance if any element of `identifiers` is not a valid pre-release identifier string. + internal init(_ identifiers: C) throws where C.Element == S, S.SubSequence == Substring { + self.identifiers = try identifiers.map { + try Identifier($0) + } + } +} + +extension SymbolGraph.SemanticVersion.Prerelease.Identifier { + /// Creates a semantic version pre-release identifier from the given pre-release identifier string. + /// - Parameter identifierString: The given pre-release identifier string to create a semantic version pre-release identifier from. + /// - Throws: A `SymbolGraph.SemanticVersionError` instance if `identifierString` is not a valid pre-release identifier string. + internal init(_ identifierString: S) throws where S.SubSequence == Substring { + guard !identifierString.isEmpty else { + throw SymbolGraph.SemanticVersionError.emptyIdentifier(position: .prerelease) + } + guard identifierString.allSatisfy(\.isAllowedInSemanticVersionIdentifier) else { + throw SymbolGraph.SemanticVersionError.invalidCharacterInIdentifier(String(identifierString), position: .prerelease) + } + if identifierString.allSatisfy(\.isNumber) { + // diagnose the identifier as a numeric identifier, if all characters are ASCII digits + guard identifierString.first != "0" || identifierString == "0" else { + throw SymbolGraph.SemanticVersionError.invalidNumericIdentifier( + String(identifierString), + position: .prerelease, + errorKind: .leadingZeros + ) + } + guard let numericIdentifier = UInt(identifierString) else { + if identifierString.isEmpty { + throw SymbolGraph.SemanticVersionError.emptyIdentifier(position: .prerelease) + } else { + throw SymbolGraph.SemanticVersionError.invalidNumericIdentifier( + String(identifierString), + position: .prerelease, + errorKind: .oversizedValue + ) + } + } + self = .numeric(numericIdentifier) + } else { + self = .alphanumeric(String(identifierString)) + } + } +} + +// MARK: - Comparable Conformances + +// Compiler synthesised `Equatable`-conformance is correct here. +extension SymbolGraph.SemanticVersion.Prerelease: Comparable { + 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 { + 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 { + /// A textual description of the pre-release. + internal var description: String { + identifiers.map(\.description).joined(separator: ".") + } +} + +extension SymbolGraph.SemanticVersion.Prerelease.Identifier: CustomStringConvertible { + /// A textual description of the identifier. + internal var description: String { + switch self { + case .numeric(let identifier): + return identifier.description + case .alphanumeric(let identifier): + return identifier.description + } + } +} diff --git a/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersion+Comparable.swift b/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersion+Comparable.swift new file mode 100644 index 0000000..3febe55 --- /dev/null +++ b/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersion+Comparable.swift @@ -0,0 +1,39 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2022 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: 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). + // [SR-14665](https://github.com/apple/swift/issues/57016) + /// Returns a Boolean value indicating whether two semantic versions are equal. + /// - Parameters: + /// - lhs: A semantic version to compare. + /// - rhs: Another semantic version to compare. + /// - Returns: `true` if `lhs` and `rhs` are equal; `false` otherwise. + @inlinable + public static func == (lhs: Self, rhs: Self) -> Bool { + !(lhs < rhs) && !(lhs > rhs) + } + + /// Returns a Boolean value indicating whether the first semantic version precedes the second semantic version. + /// - Parameters: + /// - lhs: A semantic version to compare. + /// - rhs: Another semantic version to compare. + /// - Returns: `true` if `lhs` precedes `rhs`; `false` otherwise. + 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 + } +} diff --git a/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersion+CustomStringConvertible.swift b/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersion+CustomStringConvertible.swift new file mode 100644 index 0000000..636d989 --- /dev/null +++ b/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersion+CustomStringConvertible.swift @@ -0,0 +1,23 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2022 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: CustomStringConvertible { + /// A textual description of the semantic version. + public var description: String { + var versionString = "\(major).\(minor).\(patch)" + if !prerelease.identifiers.isEmpty { + versionString += "-\(prerelease)" + } + if !buildMetadataIdentifiers.isEmpty { + versionString += "+" + buildMetadataIdentifiers.joined(separator: ".") + } + return versionString + } +} diff --git a/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersion.swift b/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersion.swift new file mode 100644 index 0000000..339d8e0 --- /dev/null +++ b/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersion.swift @@ -0,0 +1,136 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 - 2022 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 { + /// A [semantic version](https://semver.org). + public struct SemanticVersion { + /// The major version. + public let major: UInt + /// The minor version. + public let minor: UInt + /// The patch version. + public let patch: UInt + /// 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. + /// - prerelease: The pre-release information. + /// - buildMetadata: The build metadata. + public init( + major: UInt, + minor: UInt, + patch: UInt, + prerelease: String? = nil, + buildMetadata: String? = nil + ) throws { + self.major = major + self.minor = minor + self.patch = patch + + let prereleaseIdentifiers = prerelease?.split(separator: ".", omittingEmptySubsequences: false) ?? [] + self.prerelease = try Prerelease(prereleaseIdentifiers) + + let buildMetadataIdentifiers = buildMetadata?.split(separator: ".", omittingEmptySubsequences: false).map { String($0) } ?? [] + guard buildMetadataIdentifiers.allSatisfy( { !$0.isEmpty } ) else { + throw SymbolGraph.SemanticVersionError.emptyIdentifier(position: .buildMetadata) + } + try buildMetadataIdentifiers.forEach { + guard $0.allSatisfy(\.isAllowedInSemanticVersionIdentifier) else { + throw SymbolGraph.SemanticVersionError.invalidCharacterInIdentifier($0, position: .buildMetadata) + } + } + self.buildMetadataIdentifiers = buildMetadataIdentifiers + } + + /// 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 pre-release information. + /// - buildMetadata: The build metadata. + @available(*, deprecated, renamed: "init(_:_:_:prerelease:buildMetadata:)") + @_disfavoredOverload + public init( + major: Int, + minor: Int, + patch: Int, + prerelease: String? = nil, + buildMetadata: String? = nil + ) { + try! self.init( + major: UInt(major), + minor: UInt(minor), + patch: UInt(patch), + prerelease: prerelease, + buildMetadata: 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. + public init(_ major: UInt, _ minor: UInt, _ patch: UInt) { + try! self.init(major: major, minor: minor, patch: patch) + } + } +} + +// MARK: - Inspecting the Semantics + +extension SymbolGraph.SemanticVersion { + /// A Boolean value indicating whether the version is for a pre-release. + public var denotesPrerelease: Bool { !prerelease.identifiers.isEmpty } + + /// A Boolean value indicating whether the version is for a stable release. + public var denotesStableRelease: Bool { major > 0 && !denotesPrerelease } + + /// Returns a Boolean value indicating whether a release with this version can introduce source-breaking changes from that with the given other version. + /// - Parameter other: The older version a release with which to check if a release with the current version is allowed to source-break from. + /// - Returns: A Boolean value indicating whether a release with this version can introduce source-breaking changes from that with `other`. + public func denotesSourceBreakableRelease(fromThatWith other: Self) -> Bool { + self > other && ( + self.major != other.major || + self.major == 0 || // When self.major == 0, other.major must also == 0 here. + (self.denotesPrerelease || other.denotesPrerelease) + ) + } +} + +// MARK: - Creating a Version Semantically + +extension SymbolGraph.SemanticVersion { + /// The version that denotes the initial stable release. + public static var initialStableReleaseVersion: Self { + try! Self(major: 1, minor: 0, patch: 0) + } + + /// Returns the version denoting the next major release that comes after the release denoted with the given version. + /// - Parameter version: The version after which the version that denotes the next major release may come. + /// - Returns: The version denoting the next major release that comes after the release denoted with `version`. + public func nextMajorReleaseVersion(from version: Self) -> Self { + if version.denotesPrerelease && version.major > 0 && version.minor == 0 && version.patch == 0 { + return try! Self(major: version.major, minor: 0, patch: 0) + } else { + return try! Self(major: version.major + 1, minor: 0, patch: 0) + } + } +} diff --git a/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersionError.swift b/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersionError.swift new file mode 100644 index 0000000..17ea67e --- /dev/null +++ b/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersionError.swift @@ -0,0 +1,116 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2022 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: AlphanumericIdentifierPosition) + /// 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 = "major version number" + /// The minor version number position in a semantic version. + case minor = "minor version number" + /// The patch version number position in a semantic version. + case patch = "patch version number" + /// The pre-release position in a semantic version. + case prerelease = "pre-release" + /// The build-metadata position in a semantic version. + case buildMetadata = "build metadata" + + /// A textual description of the identifier's position. + public var description: String { + self.rawValue + } + } + + /// A position of an alpha-numeric identifier in a semantic version. + public enum AlphanumericIdentifierPosition: String, CustomStringConvertible { + /// The pre-release position in a semantic version. + case prerelease = "pre-release" // This case is backed by "pre-release" instead of "pre-release alpha-numeric", because it makes mores sense to state it as a general rule for pre-release identifiers instead of just pre-release alpha-numeric identifiers. + /// The build-metadata position in a semantic version. + case buildMetadata = "build metadata" + + /// A textual description of the alpha-numeric identifier's position. + 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 in a semantic version. + case major = "major version number" + /// The minor version number position in a semantic version. + case minor = "minor version number" + /// The patch version number position in a semantic version. + case patch = "patch version number" + /// The pre-release position in a semantic version. + case prerelease = "pre-release numeric" + + /// A textual description of the numeric identifier's position. + 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 contains non-numeric characters. + case nonNumericCharacter + /// The numeric identifier is too large to be representable by `UInt`. + case oversizedValue + } + + // 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) + /// A textual description of the `SymbolGraph.SemanticVersionError` instance. + 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 contain 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 .nonNumericCharacter: fault = "contain non-numeric characters" + case .oversizedValue: fault = "be larger than 'UInt.max'" + } + return "semantic version \(position) identifier '\(identifier)' cannot \(fault)" + case let .invalidVersionCoreIdentifierCount(identifiers): + return """ + semantic version must contain exactly 3 version core identifiers; \ + \(identifiers.count) given\(identifiers.isEmpty ? "" : " : ")\ + \(identifiers.map { "'\($0)'" } .joined(separator: ", ")) + """ + } + } + } +} diff --git a/Sources/SymbolKit/SymbolGraph/SemanticVersion/SematicVerion+Codable.swift b/Sources/SymbolKit/SymbolGraph/SemanticVersion/SematicVerion+Codable.swift new file mode 100644 index 0000000..c23d420 --- /dev/null +++ b/Sources/SymbolKit/SymbolGraph/SemanticVersion/SematicVerion+Codable.swift @@ -0,0 +1,53 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2022 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: Codable { + internal enum CodingKeys: String, CodingKey { + case major + case minor + case patch + case prerelease + 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) + let major = try container.decode(UInt.self, forKey: .major) + let minor = try container.decodeIfPresent(UInt.self, forKey: .minor) ?? 0 + let patch = try container.decodeIfPresent(UInt.self, forKey: .patch) ?? 0 + let prerelease = try container.decodeIfPresent(String.self, forKey: .prerelease) + let buildMetadata = try container.decodeIfPresent(String.self, forKey: .buildMetadata) + try self.init( + major: major, + minor: minor, + patch: patch, + prerelease: prerelease, + buildMetadata: buildMetadata + ) + } + + 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 denotesPrerelease { + try container.encode(prerelease.description, forKey: .prerelease) + } + + if !buildMetadataIdentifiers.isEmpty { + try container.encode(buildMetadataIdentifiers.joined(separator: "."), forKey: .buildMetadata) + } + } +} diff --git a/Tests/SymbolKitTests/SymbolGraph/LineList/SemanticVersionTests.swift b/Tests/SymbolKitTests/SymbolGraph/LineList/SemanticVersionTests.swift deleted file mode 100644 index a0484ff..0000000 --- a/Tests/SymbolKitTests/SymbolGraph/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/SemanticVersion/CodingTests.swift b/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/CodingTests.swift new file mode 100644 index 0000000..fe94d8c --- /dev/null +++ b/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/CodingTests.swift @@ -0,0 +1,94 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2022 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 CodingTests:XCTestCase { + func testEncodingToJSON() throws { + func assertEncoding(_ version: SymbolGraph.SemanticVersion, to expectedJSONObject: String) throws { + let jsonEncoder = JSONEncoder() + jsonEncoder.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let encodedVersion = try jsonEncoder.encode(version) + XCTAssertEqual( + String(data: encodedVersion, encoding: .utf8), + // Because Semantic Versioning 2.0.0 does not allow whitespace in identifiers, we can remove whitespace from the expected JSON object string. + expectedJSONObject.filter { !$0.isWhitespace } + ) + } + + let testCases: [(version: SymbolGraph.SemanticVersion, expectedJSONObject: String)] = [ + (try .init(major: 0, minor: 0, patch: 0), #"{"major":0,"minor":0,"patch":0}"#), + (try .init(major: 6, minor: 9, patch: 42), #"{"major":6,"minor":9,"patch":42}"#), + (try .init(major: 1, minor: 2, patch: 3, prerelease: "beta-004.5"), #"{"major":1,"minor":2,"patch":3,"prerelease":"beta-004.5"}"#), + (try .init(major: 6, minor: 7, patch: 8, buildMetadata: "2020-02-02.sha256-C951120B9D2E83CCCF8F51477FF53943447D56899185E0E0DC7C3EDFBA19CD60"), #"{"buildMetadata":"2020-02-02.sha256-C951120B9D2E83CCCF8F51477FF53943447D56899185E0E0DC7C3EDFBA19CD60","major":6,"minor":7,"patch":8}"#), + (try .init(major: 9, minor: 10, patch: 11, prerelease: "alpha42.release-candidate-1", buildMetadata: "md5-8981EDE66EBF3F83819F709A73B22BBA"), #"{"buildMetadata":"md5-8981EDE66EBF3F83819F709A73B22BBA","major":9,"minor":10,"patch":11,"prerelease":"alpha42.release-candidate-1"}"#) + ] + + try testCases.forEach { try assertEncoding($0.version, to: $0.expectedJSONObject) } + } + + func testDecodingFromJSON() throws { + func assertDecoding(_ jsonObject: String, to expectedVersion: SymbolGraph.SemanticVersion) throws { + let jsonDecoder = JSONDecoder() + let jsonObjectData = Data(jsonObject.utf8) + let decodedVersion = try jsonDecoder.decode(SymbolGraph.SemanticVersion.self, from: jsonObjectData) + XCTAssertEqual(decodedVersion, expectedVersion) + } + + let validCases: [(jsonObject: String, expectedVersion: SymbolGraph.SemanticVersion)] = [ + (#"{"major":0,"minor":0,"patch":0}"#, try .init(major: 0, minor: 0, patch: 0)), + (#"{"major":6,"minor":7,"patch":42}"#, try .init(major: 6, minor: 7, patch: 42)), + (#"{"major":1,"minor":2,"patch":3,"prerelease":"-41ph4-1337.7.0"}"#, try .init(major: 1, minor: 2, patch: 3, prerelease: "-41ph4-1337.7.0")), + (#"{"major":8,"minor":9,"patch":10,"buildMetadata":"LTS.sha256-076F19B8ECCD0B911C407C4881DE9D0C1D8128B631CF52DBB7BB96C11EA5D5EA"}"#, try .init(major: 8, minor: 9, patch: 10, buildMetadata: "LTS.sha256-076F19B8ECCD0B911C407C4881DE9D0C1D8128B631CF52DBB7BB96C11EA5D5EA")), + (#"{"major":11,"minor":12,"patch":13,"prerelease":"beta.golden-master.42","buildMetadata":"md5-5FDD8FAC22BF07D9010F7F482745F6D5"}"#, try .init(major: 11, minor: 12, patch: 13, prerelease: "beta.golden-master.42", buildMetadata: "md5-5FDD8FAC22BF07D9010F7F482745F6D5")) + ] + + func assertSemanticVersionErrorThrownFromDecoding(_ jsonObject: String) { + let jsonDecoder = JSONDecoder() + let jsonObjectData = Data(jsonObject.utf8) + XCTAssertThrowsError(try jsonDecoder.decode(SymbolGraph.SemanticVersion.self, from: jsonObjectData)) { error in + XCTAssertTrue(error is SymbolGraph.SemanticVersionError) + } + } + + try validCases.forEach { try assertDecoding($0.jsonObject, to: $0.expectedVersion) } + + let invalidCases = [ + #"{"major":4,"minor":5,"patch":6,"prerelease":""}"#, + #"{"major":7,"minor":8,"patch":9,"prerelease":" "}"#, + #"{"major":0,"minor":1,"patch":2,"prerelease":"."}"#, + #"{"major":3,"minor":4,"patch":5,"prerelease":".."}"#, + #"{"major":6,"minor":7,"patch":8,"prerelease":".a"}"#, + #"{"major":9,"minor":0,"patch":1,"prerelease":"b."}"#, + #"{"major":2,"minor":3,"patch":4,"prerelease":"00"}"#, + #"{"major":5,"minor":6,"patch":7,"prerelease":"01"}"#, + #"{"major":8,"minor":9,"patch":0,"prerelease":" 0"}"#, + #"{"major":9,"minor":8,"patch":7,"prerelease":"@#"}"#, + #"{"major":6,"minor":5,"patch":4,"prerelease":"œ∑"}"#, + #"{"major":3,"minor":2,"patch":1,"prerelease":"+"}"#, + + #"{"major":0,"minor":9,"patch":8,"buildMetadata":""}"#, + #"{"major":7,"minor":6,"patch":5,"buildMetadata":" "}"#, + #"{"major":4,"minor":3,"patch":2,"buildMetadata":"+"}"#, + #"{"major":1,"minor":0,"patch":9,"buildMetadata":"."}"#, + #"{"major":8,"minor":7,"patch":6,"buildMetadata":".."}"#, + #"{"major":5,"minor":4,"patch":3,"buildMetadata":".a"}"#, + #"{"major":2,"minor":1,"patch":0,"buildMetadata":"b."}"#, + #"{"major":0,"minor":1,"patch":1,"buildMetadata":" 0"}"#, + #"{"major":2,"minor":3,"patch":5,"buildMetadata":"@#"}"#, + #"{"major":8,"minor":1,"patch":3,"buildMetadata":"¥ø"}"#, + + #"{"major":2,"minor":1,"patch":3,"prerelease":"","buildMetadata":""}"#, + ] + + invalidCases.forEach { assertSemanticVersionErrorThrownFromDecoding($0) } + } +} diff --git a/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/DirectInitializationTests.swift b/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/DirectInitializationTests.swift new file mode 100644 index 0000000..f5e3fd6 --- /dev/null +++ b/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/DirectInitializationTests.swift @@ -0,0 +1,133 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2022 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 DirectInitializationTests: XCTestCase { + func testInitializationFromAllComponents() throws { + + // MARK: primary public properties + + let version1 = try SymbolGraph.SemanticVersion( + major: 0, minor: 0, patch: 0, + prerelease: nil, buildMetadata: nil + ) + XCTAssertEqual(version1.major, 0) + XCTAssertEqual(version1.minor, 0) + XCTAssertEqual(version1.patch, 0) + XCTAssertEqual(version1.prereleaseIdentifiers, []) + XCTAssertEqual(version1.buildMetadataIdentifiers, []) + + let version2 = try SymbolGraph.SemanticVersion( + major: 1, minor: 2, patch: 3, + prerelease: nil, buildMetadata: nil + ) + XCTAssertEqual(version2.major, 1) + XCTAssertEqual(version2.minor, 2) + XCTAssertEqual(version2.patch, 3) + XCTAssertEqual(version2.prereleaseIdentifiers, []) + XCTAssertEqual(version2.buildMetadataIdentifiers, []) + + let version3 = try SymbolGraph.SemanticVersion( + major: 42, minor: 41, patch: 40, + prerelease: "beta.1337.0", buildMetadata: nil + ) + XCTAssertEqual(version3.major, 42) + XCTAssertEqual(version3.minor, 41) + XCTAssertEqual(version3.patch, 40) + XCTAssertEqual(version3.prereleaseIdentifiers, ["beta", "1337", "0"]) + XCTAssertEqual(version3.buildMetadataIdentifiers, []) + + let version4 = try SymbolGraph.SemanticVersion( + major: 2, minor: 3, patch: 5, + prerelease: nil, buildMetadata: "2022-05-23" + ) + XCTAssertEqual(version4.major, 2) + XCTAssertEqual(version4.minor, 3) + XCTAssertEqual(version4.patch, 5) + XCTAssertEqual(version4.prereleaseIdentifiers, []) + XCTAssertEqual(version4.buildMetadataIdentifiers, ["2022-05-23"]) + + let version5 = try SymbolGraph.SemanticVersion( + major: 7, minor: 11, patch: 13, + prerelease: "alpha-1.-42", buildMetadata: "010203.md5-d41d8cd98f00b204e9800998ecf8427e" + ) + XCTAssertEqual(version5.major, 7) + XCTAssertEqual(version5.minor, 11) + XCTAssertEqual(version5.patch, 13) + XCTAssertEqual(version5.prereleaseIdentifiers, ["alpha-1", "-42"]) + XCTAssertEqual(version5.buildMetadataIdentifiers, ["010203", "md5-d41d8cd98f00b204e9800998ecf8427e"]) + + // MARK: default parameters + + let version6 = try SymbolGraph.SemanticVersion(major: 0, minor: 0, patch: 0) + XCTAssertEqual(version6.major, version1.major) + XCTAssertEqual(version6.minor, version1.minor) + XCTAssertEqual(version6.patch, version1.patch) + XCTAssertEqual(version6.prereleaseIdentifiers, version1.prereleaseIdentifiers) + XCTAssertEqual(version6.buildMetadataIdentifiers, version1.buildMetadataIdentifiers) + + let version7 = try SymbolGraph.SemanticVersion(major: 1, minor: 2, patch: 3) + XCTAssertEqual(version7.major, version2.major) + XCTAssertEqual(version7.minor, version2.minor) + XCTAssertEqual(version7.patch, version2.patch) + XCTAssertEqual(version7.prereleaseIdentifiers, version2.prereleaseIdentifiers) + XCTAssertEqual(version7.buildMetadataIdentifiers, version2.buildMetadataIdentifiers) + + let version8 = try SymbolGraph.SemanticVersion(major: 42, minor: 41, patch: 40, prerelease: "beta.1337.0") + XCTAssertEqual(version8.major, version3.major) + XCTAssertEqual(version8.minor, version3.minor) + XCTAssertEqual(version8.patch, version3.patch) + XCTAssertEqual(version8.prereleaseIdentifiers, version3.prereleaseIdentifiers) + XCTAssertEqual(version8.buildMetadataIdentifiers, version3.buildMetadataIdentifiers) + + let version9 = try SymbolGraph.SemanticVersion(major: 2, minor: 3, patch: 5, buildMetadata: "2022-05-23") + XCTAssertEqual(version9.major, version4.major) + XCTAssertEqual(version9.minor, version4.minor) + XCTAssertEqual(version9.patch, version4.patch) + XCTAssertEqual(version9.prereleaseIdentifiers, version4.prereleaseIdentifiers) + XCTAssertEqual(version9.buildMetadataIdentifiers, version4.buildMetadataIdentifiers) + + // MARK: invalid components + + XCTAssertThrowsError(try SymbolGraph.SemanticVersion(major: 9, minor: 8, patch: 7, prerelease: "")) { XCTAssertTrue($0 is SymbolGraph.SemanticVersionError) } + XCTAssertThrowsError(try SymbolGraph.SemanticVersion(major: 6, minor: 5, patch: 4, prerelease: " ")) { XCTAssertTrue($0 is SymbolGraph.SemanticVersionError) } + XCTAssertThrowsError(try SymbolGraph.SemanticVersion(major: 3, minor: 2, patch: 1, prerelease: "+")) { XCTAssertTrue($0 is SymbolGraph.SemanticVersionError) } + XCTAssertThrowsError(try SymbolGraph.SemanticVersion(major: 0, minor: 9, patch: 8, prerelease: "...")) { XCTAssertTrue($0 is SymbolGraph.SemanticVersionError) } + XCTAssertThrowsError(try SymbolGraph.SemanticVersion(major: 4, minor: 3, patch: 2, prerelease: ".c.")) { XCTAssertTrue($0 is SymbolGraph.SemanticVersionError) } + XCTAssertThrowsError(try SymbolGraph.SemanticVersion(major: 1, minor: 0, patch: 9, prerelease: "测试版")) { XCTAssertTrue($0 is SymbolGraph.SemanticVersionError) } + XCTAssertThrowsError(try SymbolGraph.SemanticVersion(major: 8, minor: 7, patch: 6, prerelease: "00")) { XCTAssertTrue($0 is SymbolGraph.SemanticVersionError) } + XCTAssertThrowsError(try SymbolGraph.SemanticVersion(major: 5, minor: 4, patch: 3, prerelease: "0123")) { XCTAssertTrue($0 is SymbolGraph.SemanticVersionError) } + + XCTAssertThrowsError(try SymbolGraph.SemanticVersion(major: 0, minor: 1, patch: 2, buildMetadata: "")) { XCTAssertTrue($0 is SymbolGraph.SemanticVersionError) } + XCTAssertThrowsError(try SymbolGraph.SemanticVersion(major: 3, minor: 4, patch: 5, buildMetadata: " ")) { XCTAssertTrue($0 is SymbolGraph.SemanticVersionError) } + XCTAssertThrowsError(try SymbolGraph.SemanticVersion(major: 6, minor: 7, patch: 8, buildMetadata: "+")) { XCTAssertTrue($0 is SymbolGraph.SemanticVersionError) } + XCTAssertThrowsError(try SymbolGraph.SemanticVersion(major: 9, minor: 0, patch: 1, buildMetadata: "...")) { XCTAssertTrue($0 is SymbolGraph.SemanticVersionError) } + XCTAssertThrowsError(try SymbolGraph.SemanticVersion(major: 5, minor: 6, patch: 7, buildMetadata: ".c.")) { XCTAssertTrue($0 is SymbolGraph.SemanticVersionError) } + XCTAssertThrowsError(try SymbolGraph.SemanticVersion(major: 8, minor: 9, patch: 0, buildMetadata: "🙃")) { XCTAssertTrue($0 is SymbolGraph.SemanticVersionError) } + } + + func testInitializationFromOnlyVersionCoreComponents() { + let version1 = SymbolGraph.SemanticVersion(0, 0, 0) + XCTAssertEqual(version1.major, 0) + XCTAssertEqual(version1.minor, 0) + XCTAssertEqual(version1.patch, 0) + XCTAssertEqual(version1.prereleaseIdentifiers, []) + XCTAssertEqual(version1.buildMetadataIdentifiers, []) + + let version2 = SymbolGraph.SemanticVersion(3, 2, 1) + XCTAssertEqual(version2.major, 3) + XCTAssertEqual(version2.minor, 2) + XCTAssertEqual(version2.patch, 1) + XCTAssertEqual(version2.prereleaseIdentifiers, []) + XCTAssertEqual(version2.buildMetadataIdentifiers, []) + } +} diff --git a/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/ErrorTests.swift b/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/ErrorTests.swift new file mode 100644 index 0000000..6660554 --- /dev/null +++ b/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/ErrorTests.swift @@ -0,0 +1,366 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2022 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 ErrorTests: XCTestCase { + func testEmptyIdentifiers() { + // MARK: pre-release + + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try .init(major: 0, minor: 1, patch: 2, prerelease: "")) + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try .init(major: 1, minor: 2, patch: 3, prerelease: ".")) + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try .init(major: 2, minor: 3, patch: 4, prerelease: "alpha.")) + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try .init(major: 3, minor: 4, patch: 5, prerelease: ".beta")) + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try .init(major: 4, minor: 5, patch: 6, prerelease: "123.")) + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try .init(major: 5, minor: 6, patch: 7, prerelease: ".456")) + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try .init(major: 6, minor: 7, patch: 8, prerelease: "y2k.")) + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try .init(major: 7, minor: 8, patch: 9, prerelease: ".mp3")) + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":9,"minor":7,"patch":5,"prerelease":""}"#.utf8))) + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":8,"minor":6,"patch":4,"prerelease":"."}"#.utf8))) + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":7,"minor":5,"patch":3,"prerelease":"gold."}"#.utf8))) + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":6,"minor":4,"patch":2,"prerelease":".master"}"#.utf8))) + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":5,"minor":3,"patch":1,"prerelease":"0."}"#.utf8))) + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":4,"minor":2,"patch":0,"prerelease":".1"}"#.utf8))) + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":3,"minor":1,"patch":9,"prerelease":"2001odyssey."}"#.utf8))) + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":2,"minor":0,"patch":8,"prerelease":".av1"}"#.utf8))) + + // MARK: build metadata + + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try .init(major: 0, minor: 1, patch: 2, buildMetadata: "")) + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try .init(major: 1, minor: 2, patch: 3, buildMetadata: ".")) + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try .init(major: 2, minor: 3, patch: 4, buildMetadata: "alpha.")) + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try .init(major: 3, minor: 4, patch: 5, buildMetadata: ".beta")) + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try .init(major: 4, minor: 5, patch: 6, buildMetadata: "123.")) + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try .init(major: 5, minor: 6, patch: 7, buildMetadata: ".456")) + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try .init(major: 6, minor: 7, patch: 8, buildMetadata: "y2k.")) + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try .init(major: 7, minor: 8, patch: 9, buildMetadata: ".mp3")) + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":9,"minor":7,"patch":5,"buildMetadata":""}"#.utf8))) + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":8,"minor":6,"patch":4,"buildMetadata":"."}"#.utf8))) + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":7,"minor":5,"patch":3,"buildMetadata":"gold."}"#.utf8))) + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":6,"minor":4,"patch":2,"buildMetadata":".master"}"#.utf8))) + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":5,"minor":3,"patch":1,"buildMetadata":"0."}"#.utf8))) + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":4,"minor":2,"patch":0,"buildMetadata":".1"}"#.utf8))) + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":3,"minor":1,"patch":9,"buildMetadata":"2001odyssey."}"#.utf8))) + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":2,"minor":0,"patch":8,"buildMetadata":".av1"}"#.utf8))) + } + + func testEmptyIdentifierDiagnosticPrecedence() { + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try .init(major: 1, minor: 2, patch: 3, prerelease: "", buildMetadata: "")) + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":1,"minor":2,"patch":3,"prerelease":"","buildMetadata":""}"#.utf8))) + + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try .init(major: 1, minor: 2, patch: 3, prerelease: "4", buildMetadata: "")) + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":1,"minor":2,"patch":3,"prerelease":"4","buildMetadata":""}"#.utf8))) + } + + func testNonAlphanumericCharacters() { + // MARK: pre-release + + assertThrowingNonAlphanumericCharacterError(atPosition: .prerelease, inIdentifier: "😛", whenEvaluating: try .init(major: 9, minor: 8, patch: 7, prerelease: "😛")) + assertThrowingNonAlphanumericCharacterError(atPosition: .prerelease, inIdentifier: "😝alpha", whenEvaluating: try .init(major: 6, minor: 5, patch: 4, prerelease: "😝alpha.beta")) + assertThrowingNonAlphanumericCharacterError(atPosition: .prerelease, inIdentifier: "beta😜", whenEvaluating: try .init(major: 3, minor: 2, patch: 1, prerelease: "alpha.beta😜")) + assertThrowingNonAlphanumericCharacterError(atPosition: .prerelease, inIdentifier: "pre🤪release", whenEvaluating: try .init(major: 0, minor: 9, patch: 8, prerelease: "pre🤪release.un🤨stable")) + assertThrowingNonAlphanumericCharacterError(atPosition: .prerelease, inIdentifier: "🥳", whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":5,"minor":4,"patch":3,"prerelease":"🥳"}"#.utf8))) + assertThrowingNonAlphanumericCharacterError(atPosition: .prerelease, inIdentifier: "😏123", whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":2,"minor":1,"patch":0,"prerelease":"😏123.456"}"#.utf8))) + assertThrowingNonAlphanumericCharacterError(atPosition: .prerelease, inIdentifier: "456😒", whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":1,"minor":2,"patch":3,"prerelease":"123.456😒"}"#.utf8))) + assertThrowingNonAlphanumericCharacterError(atPosition: .prerelease, inIdentifier: "13😞37", whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":4,"minor":5,"patch":6,"prerelease":"13😞37.le😔et"}"#.utf8))) + + // MARK: build metadata + + assertThrowingNonAlphanumericCharacterError(atPosition: .buildMetadata, inIdentifier: "😟", whenEvaluating: try .init(major: 7, minor: 8, patch: 9, buildMetadata: "😟")) + assertThrowingNonAlphanumericCharacterError(atPosition: .buildMetadata, inIdentifier: "😕abcd", whenEvaluating: try .init(major: 0, minor: 1, patch: 2, buildMetadata: "😕abcd.efgh")) + assertThrowingNonAlphanumericCharacterError(atPosition: .buildMetadata, inIdentifier: "mnop🙁", whenEvaluating: try .init(major: 3, minor: 4, patch: 5, buildMetadata: "ijkl.mnop🙁")) + assertThrowingNonAlphanumericCharacterError(atPosition: .buildMetadata, inIdentifier: "qr☹️st", whenEvaluating: try .init(major: 6, minor: 7, patch: 8, buildMetadata: "qr☹️st.uv😣wx")) + assertThrowingNonAlphanumericCharacterError(atPosition: .buildMetadata, inIdentifier: "😭", whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":9,"minor":8,"patch":7,"buildMetadata":"😭"}"#.utf8))) + assertThrowingNonAlphanumericCharacterError(atPosition: .buildMetadata, inIdentifier: "😤1a2", whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":6,"minor":5,"patch":4,"buildMetadata":"😤1a2.b3c"}"#.utf8))) + assertThrowingNonAlphanumericCharacterError(atPosition: .buildMetadata, inIdentifier: "e6f😠", whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":3,"minor":2,"patch":1,"buildMetadata":"4d5.e6f😠"}"#.utf8))) + assertThrowingNonAlphanumericCharacterError(atPosition: .buildMetadata, inIdentifier: "7g8😡h9i", whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":0,"minor":9,"patch":8,"buildMetadata":"7g8😡h9i.10j🤬11k"}"#.utf8))) + } + + func testNonNumericCharacters() { + // MARK: pre-release + + XCTAssertNoThrow(try SymbolGraph.SemanticVersion(major: 9, minor: 8, patch: 7, prerelease: "abc123")) + XCTAssertNoThrow(try SymbolGraph.SemanticVersion(major: 6, minor: 5, patch: 4, prerelease: "456def")) + XCTAssertNoThrow(try SymbolGraph.SemanticVersion(major: 3, minor: 2, patch: 1, prerelease: "7g8h9i")) + XCTAssertNoThrow(try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":1,"minor":0,"patch":9,"prerelease":"stu901"}"#.utf8))) + XCTAssertNoThrow(try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":8,"minor":7,"patch":6,"prerelease":"234vwx"}"#.utf8))) + XCTAssertNoThrow(try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":5,"minor":4,"patch":3,"prerelease":"5y6z7a"}"#.utf8))) + + // MARK: build metadata + + XCTAssertNoThrow(try SymbolGraph.SemanticVersion(major: 2, minor: 1, patch: 0, buildMetadata: "bcd890")) + XCTAssertNoThrow(try SymbolGraph.SemanticVersion(major: 9, minor: 8, patch: 7, buildMetadata: "123efg")) + XCTAssertNoThrow(try SymbolGraph.SemanticVersion(major: 6, minor: 5, patch: 4, buildMetadata: "h4i5j6")) + XCTAssertNoThrow(try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":4,"minor":3,"patch":2,"buildMetadata":"tuv678"}"#.utf8))) + XCTAssertNoThrow(try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":1,"minor":0,"patch":9,"buildMetadata":"901wxy"}"#.utf8))) + XCTAssertNoThrow(try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":8,"minor":7,"patch":6,"buildMetadata":"z2a3b4"}"#.utf8))) + } + + func testLeadingZeros() { + // MARK: pre-release + + assertThrowingLeadingZerosError(atPosition: .prerelease, inIdentifier: "0246", whenEvaluating: try .init(major: 1, minor: 3, patch: 5, prerelease: "0246")) + assertThrowingLeadingZerosError(atPosition: .prerelease, inIdentifier: "0068", whenEvaluating: try .init(major: 3, minor: 5, patch: 7, prerelease: "0068")) + assertThrowingLeadingZerosError(atPosition: .prerelease, inIdentifier: "0000", whenEvaluating: try .init(major: 5, minor: 7, patch: 9, prerelease: "0000")) + assertThrowingLeadingZerosError(atPosition: .prerelease, inIdentifier: "0654", whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":9,"minor":8,"patch":7,"prerelease":"0654"}"#.utf8))) + assertThrowingLeadingZerosError(atPosition: .prerelease, inIdentifier: "0065", whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":9,"minor":8,"patch":7,"prerelease":"0065"}"#.utf8))) + assertThrowingLeadingZerosError(atPosition: .prerelease, inIdentifier: "0000", whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":9,"minor":8,"patch":7,"prerelease":"0000"}"#.utf8))) + + XCTAssertNoThrow(try SymbolGraph.SemanticVersion(major: 2, minor: 3, patch: 4, prerelease: "0")) + XCTAssertNoThrow(try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":8,"minor":9,"patch":1,"prelease":"0"}"#.utf8))) + + // MARK: build metadata + XCTAssertNoThrow(try SymbolGraph.SemanticVersion(major: 2, minor: 3, patch: 4, buildMetadata: "0567")) + XCTAssertNoThrow(try SymbolGraph.SemanticVersion(major: 8, minor: 9, patch: 1, buildMetadata: "0023")) + XCTAssertNoThrow(try SymbolGraph.SemanticVersion(major: 4, minor: 5, patch: 6, buildMetadata: "0000")) + XCTAssertNoThrow(try SymbolGraph.SemanticVersion(major: 7, minor: 8, patch: 9, buildMetadata: "0")) + XCTAssertNoThrow(try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":9,"minor":1,"patch":2,"buildMetadata":"0345"}"#.utf8))) + XCTAssertNoThrow(try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":6,"minor":7,"patch":8,"buildMetadata":"0091"}"#.utf8))) + XCTAssertNoThrow(try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":2,"minor":3,"patch":4,"buildMetadata":"0000"}"#.utf8))) + XCTAssertNoThrow(try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":5,"minor":6,"patch":7,"buildMetadata":"0"}"#.utf8))) + } + + func testOversizedNumericValues() { + func sum(_ summand1: String, _ summand2: String) -> Substring { + let paddedSummandLength = max(summand1.count, summand2.count) + 1 + let paddedSummand1 = zeroPadded(summand1, toLength: paddedSummandLength) + let paddedSummand2 = zeroPadded(summand2, toLength: paddedSummandLength) + + var result: [Character] = [] + result.reserveCapacity(paddedSummandLength) + + var carry: Int8 = 0 + + for (digit1, digit2) in zip(paddedSummand1.reversed(), paddedSummand2.reversed()) { + let digit1 = Int8(String(digit1))! + let digit2 = Int8(String(digit2))! + let columnSum = digit1 + digit2 + carry + carry = columnSum > 9 ? 1 : 0 + result.append(String(columnSum).last!) + } + + return Substring(result[...result.lastIndex(where: { $0 != "0" })!].reversed()) + + func zeroPadded(_ number: String, toLength paddedLength: Int) -> [Character] { + let paddingLength = paddedLength - number.count + var paddedNumber = [Character](repeating: "0", count: paddingLength) + paddedNumber.reserveCapacity(paddedLength) + paddedNumber.append(contentsOf: number) + return paddedNumber + } + } + + // MARK: infrastructure sanity check + + XCTAssertEqual(sum("123", "456"), "579") + XCTAssertEqual(sum("999", "999"), "1998") + XCTAssertEqual(sum("4545", "4545"), "9090") + XCTAssertEqual(sum("123456789", "98765"), "123555554") + XCTAssertEqual(sum("111111111111111111111111111111", "111111111111111111111111111111"), "222222222222222222222222222222") // 30 digits in each + XCTAssertEqual(sum("111111111111111111111111111111", "000000000011111111112222222222"), "111111111122222222223333333333") + XCTAssertEqual(sum("999999999999999999999999999999", "999999999999999999999999999999"), "1999999999999999999999999999998") // 30 digits in each + XCTAssertEqual(sum("\(UInt64.max)", "1"), "18446744073709551616") + XCTAssertEqual(sum("\(UInt64.max)", "\(Int32.max)"), "18446744075857035262") + XCTAssertEqual(sum("\(UInt64.max)", "\(UInt64.max)"), "36893488147419103230") + + // MARK: pre-release + + assertThrowingOversizedValueError(atPosition: .prerelease, inIdentifier: sum("\(UInt.max)", "1"), whenEvaluating: try .init(major: 1, minor: 3, patch: 5, prerelease: String(sum("\(UInt.max)", "1")))) + assertThrowingOversizedValueError(atPosition: .prerelease, inIdentifier: "8\(UInt.max)", whenEvaluating: try .init(major: 2, minor: 4, patch: 6, prerelease: "8\(UInt.max)")) + assertThrowingOversizedValueError(atPosition: .prerelease, inIdentifier: "\(UInt.max)\(UInt.max)", whenEvaluating: try .init(major: 3, minor: 5, patch: 7, prerelease: "\(UInt.max)\(UInt.max)")) + assertThrowingOversizedValueError(atPosition: .prerelease, inIdentifier: sum("\(UInt.max)", "1"), whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":9,"minor":8,"patch":7,"prerelease":"\#(sum("\(UInt.max)", "1"))"}"#.utf8))) + assertThrowingOversizedValueError(atPosition: .prerelease, inIdentifier: "3\(UInt.max)", whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":6,"minor":5,"patch":4,"prerelease":"3\#(UInt.max)"}"#.utf8))) + assertThrowingOversizedValueError(atPosition: .prerelease, inIdentifier: "\(UInt.max)\(UInt64.max)", whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":2,"minor":1,"patch":0,"prerelease":"\#(UInt.max)\#(UInt64.max)"}"#.utf8))) + + XCTAssertNoThrow(try SymbolGraph.SemanticVersion(major: 1, minor: 2, patch: 3, prerelease: "\(UInt.max)")) + XCTAssertNoThrow(try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":7,"minor":8,"patch":9,"prerelease":"\#(UInt.max)"}"#.utf8))) + + // MARK: build metadata + + XCTAssertNoThrow(try SymbolGraph.SemanticVersion(major: 1, minor: 2, patch: 3, buildMetadata: "\(UInt.max)")) + XCTAssertNoThrow(try SymbolGraph.SemanticVersion(major: 4, minor: 5, patch: 6, buildMetadata: String(sum("\(UInt.max)", "1")))) + XCTAssertNoThrow(try SymbolGraph.SemanticVersion(major: 7, minor: 8, patch: 9, buildMetadata: "\(UInt.max)0")) + XCTAssertNoThrow(try SymbolGraph.SemanticVersion(major: 1, minor: 2, patch: 3, buildMetadata: "\(UInt.max)\(Int.max)")) + XCTAssertNoThrow(try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":7,"minor":8,"patch":9,"buildMetadata":"\#(UInt.max)"}"#.utf8))) + XCTAssertNoThrow(try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":0,"minor":1,"patch":2,"buildMetadata":"\#(sum("\(UInt.max)", "1"))"}"#.utf8))) + XCTAssertNoThrow(try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":3,"minor":4,"patch":5,"buildMetadata":"\#(UInt.max)6"}"#.utf8))) + XCTAssertNoThrow(try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":7,"minor":8,"patch":9,"buildMetadata":"\#(UInt.max)\#(UInt.max)"}"#.utf8))) + } + + // TODO: Add tests for the precedence of error-throwing. + + func assertThrowingEmptyIdentifierError( + atPosition position: SymbolGraph.SemanticVersionError.IdentifierPosition, + whenEvaluating expression: @autoclosure () throws -> SymbolGraph.SemanticVersion + ) { + let positionString: String + switch position { + case .major: positionString = "major" + case .minor: positionString = "minor" + case .patch: positionString = "patch" + case .prerelease: positionString = "prerelease" + case .buildMetadata: positionString = "buildMetadata" + } + XCTAssertThrowsError( + try expression(), + "'SymbolGraph.SemanticVersionError.emptyIdentifier(position: .\(positionString))' should've been thrown, but no error is thrown" + ) { error in + guard let error = error as? SymbolGraph.SemanticVersionError, case .emptyIdentifier(position: position) = error else { + XCTFail(#"'SymbolGraph.SemanticVersionError.emptyIdentifier(position: .\#(positionString))' should've been thrown, but a different error is thrown instead; error description: "\#(error)""#) + return + } + let positionDescription: String + switch position { + case .major: positionDescription = "major version number" + case .minor: positionDescription = "minor version number" + case .patch: positionDescription = "patch version number" + case .prerelease: positionDescription = "pre-release" + case .buildMetadata: positionDescription = "build metadata" + } + XCTAssertEqual( + error.description, + "semantic version \(positionDescription) identifier cannot be empty" + ) + } + } + + func assertThrowingNonAlphanumericCharacterError( + atPosition position: SymbolGraph.SemanticVersionError.AlphanumericIdentifierPosition, + inIdentifier identifier: Substring, + whenEvaluating expression: @autoclosure () throws -> SymbolGraph.SemanticVersion + ) { + let positionString: String + switch position { + case .prerelease: positionString = "prerelease" + case .buildMetadata: positionString = "buildMetadata" + } + XCTAssertThrowsError( + try expression(), + "'SymbolGraph.SemanticVersionError.invalidCharacterInIdentifier(\(identifier), position: .\(positionString))' should've been thrown, but no error is thrown" + ) { error in + guard let error = error as? SymbolGraph.SemanticVersionError, case .invalidCharacterInIdentifier(identifier, position: position) = error else { + XCTFail((#"'SymbolGraph.SemanticVersionError.invalidCharacterInIdentifier(\#(identifier), position: .\#(positionString))' should've been thrown, but a different error is thrown instead; error description: "\#(error)""#)) + return + } + let positionDescription: String + switch position { + case .prerelease: positionDescription = "pre-release" + case .buildMetadata: positionDescription = "build metadata" + } + XCTAssertEqual( + error.description, + "semantic version \(positionDescription) identifier '\(identifier)' cannot contain characters other than ASCII alphanumerics and hyphen-minus ([0-9A-Za-z-])" + ) + } + } + + func assertThrowingNonNumericCharacterError( + atPosition position: SymbolGraph.SemanticVersionError.NumericIdentifierPosition, + inIdentifier identifier: Substring, + whenEvaluating expression: @autoclosure () throws -> SymbolGraph.SemanticVersion + ) { + let positionString: String + switch position { + case .major: positionString = "major" + case .minor: positionString = "minor" + case .patch: positionString = "patch" + case .prerelease: XCTFail("pre-release identifier with non-numeric characters should be regarded as alpha-numeric identifier"); positionString = "prerelease" + } + XCTAssertThrowsError( + try expression(), + "'SymbolGraph.SemanticVersionError.invalidCharacterInIdentifier(\(identifier), position: .\(positionString))' should've been thrown, but no error is thrown" + ) { error in + guard let error = error as? SymbolGraph.SemanticVersionError, case .invalidNumericIdentifier(identifier, position: position, errorKind: .nonNumericCharacter) = error else { + XCTFail((#"'SymbolGraph.SemanticVersionError.invalidNumericIdentifier(\#(identifier), position: \#(positionString), errorKind: .nonNumericCharacter)' should've been thrown, but a different error is thrown instead; error description: "\#(error)""#)) + return + } + let positionDescription: String + switch position { + case .major: positionDescription = "major version number" + case .minor: positionDescription = "minor version number" + case .patch: positionDescription = "patch version number" + case .prerelease: XCTFail("pre-release identifier with non-numeric characters should be regarded as alpha-numeric identifier"); positionDescription = "pre-release numeric" + } + XCTAssertEqual( + error.description, + "semantic version \(positionDescription) identifier '\(identifier)' cannot contain non-numeric characters" + ) + } + } + + func assertThrowingLeadingZerosError( + atPosition position: SymbolGraph.SemanticVersionError.NumericIdentifierPosition, + inIdentifier identifier: Substring, + whenEvaluating expression: @autoclosure () throws -> SymbolGraph.SemanticVersion + ) { + let positionString: String + switch position { + case .major: positionString = "major" + case .minor: positionString = "minor" + case .patch: positionString = "patch" + case .prerelease: positionString = "prerelease" + } + XCTAssertThrowsError( + try expression(), + "'SymbolGraph.SemanticVersionError.invalidCharacterInIdentifier(\(identifier), position: .\(positionString))' should've been thrown, but no error is thrown" + ) { error in + guard let error = error as? SymbolGraph.SemanticVersionError, case .invalidNumericIdentifier(identifier, position: position, errorKind: .leadingZeros) = error else { + XCTFail((#"'SymbolGraph.SemanticVersionError.invalidNumericIdentifier(\#(identifier), position: \#(positionString), errorKind: .leadingZeros)' should've been thrown, but a different error is thrown instead; error description: "\#(error)""#)) + return + } + let positionDescription: String + switch position { + case .major: positionDescription = "major version number" + case .minor: positionDescription = "minor version number" + case .patch: positionDescription = "patch version number" + case .prerelease: positionDescription = "pre-release numeric" + } + XCTAssertEqual( + error.description, + "semantic version \(positionDescription) identifier '\(identifier)' cannot contain leading '0'" + ) + } + } + + func assertThrowingOversizedValueError( + atPosition position: SymbolGraph.SemanticVersionError.NumericIdentifierPosition, + inIdentifier identifier: Substring, + whenEvaluating expression: @autoclosure () throws -> SymbolGraph.SemanticVersion + ) { + let positionString: String + switch position { + case .major: positionString = "major" + case .minor: positionString = "minor" + case .patch: positionString = "patch" + case .prerelease: positionString = "prerelease" + } + XCTAssertThrowsError( + try expression(), + "'SymbolGraph.SemanticVersionError.invalidCharacterInIdentifier(\(identifier), position: .\(positionString))' should've been thrown, but no error is thrown" + ) { error in + guard let error = error as? SymbolGraph.SemanticVersionError, case .invalidNumericIdentifier(identifier, position: position, errorKind: .oversizedValue) = error else { + XCTFail((#"'SymbolGraph.SemanticVersionError.invalidNumericIdentifier(\#(identifier), position: \#(positionString), errorKind: .oversizedValue)' should've been thrown, but a different error is thrown instead; error description: "\#(error)""#)) + return + } + let positionDescription: String + switch position { + case .major: positionDescription = "major version number" + case .minor: positionDescription = "minor version number" + case .patch: positionDescription = "patch version number" + case .prerelease: positionDescription = "pre-release numeric" + } + XCTAssertEqual( + error.description, + "semantic version \(positionDescription) identifier '\(identifier)' cannot be larger than 'UInt.max'" + ) + } + } +} diff --git a/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/PrecedenceTests.swift b/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/PrecedenceTests.swift new file mode 100644 index 0000000..2557299 --- /dev/null +++ b/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/PrecedenceTests.swift @@ -0,0 +1,256 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2022 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 PrecedenceTests: XCTestCase { + func assert(_ version1: SymbolGraph.SemanticVersion, precedes version2: SymbolGraph.SemanticVersion) { + XCTAssertLessThan(version1, version2) + XCTAssertLessThanOrEqual(version1, version2) + XCTAssertGreaterThan(version2, version1) + XCTAssertGreaterThanOrEqual(version2, version1) + XCTAssertNotEqual(version1, version2) + XCTAssertNotEqual(version2, version1) + // test false paths + XCTAssertFalse(version1 > version2) + XCTAssertFalse(version1 >= version2) + XCTAssertFalse(version2 < version1) + XCTAssertFalse(version2 <= version1) + } + + func assert(_ version1: SymbolGraph.SemanticVersion, equals version2: SymbolGraph.SemanticVersion) { + XCTAssertEqual(version1, version2) + XCTAssertEqual(version2, version1) + XCTAssertLessThanOrEqual(version1, version2) + XCTAssertLessThanOrEqual(version1, version2) + XCTAssertGreaterThanOrEqual(version1, version2) + XCTAssertGreaterThanOrEqual(version2, version1) + // test false paths + XCTAssertFalse(version1 != version2) + XCTAssertFalse(version2 != version1) + XCTAssertFalse(version1 < version2) + XCTAssertFalse(version2 < version1) + XCTAssertFalse(version1 > version2) + XCTAssertFalse(version2 > version1) + } + + func testVersionCorePrecedence() throws { + assert(try .init(major: 0, minor: 0, patch: 0), precedes: try .init(major: 0, minor: 0, patch: 1)) + assert(try .init(major: 0, minor: 0, patch: 0), precedes: try .init(major: 0, minor: 1, patch: 0)) + assert(try .init(major: 0, minor: 0, patch: 0), precedes: try .init(major: 1, minor: 0, patch: 0)) + assert(try .init(major: 1, minor: 2, patch: 3), precedes: try .init(major: 1, minor: 2, patch: 4)) + assert(try .init(major: 1, minor: 2, patch: 3), precedes: try .init(major: 1, minor: 3, patch: 3)) + assert(try .init(major: 1, minor: 2, patch: 3), precedes: try .init(major: 2, minor: 2, patch: 3)) + + assert(try .init(major: 0, minor: 0, patch: 0), equals: try .init(major: 0, minor: 0, patch: 0)) + assert(try .init(major: 1, minor: 2, patch: 3), equals: try .init(major: 1, minor: 2, patch: 3)) + assert(try .init(major: 3, minor: 2, patch: 1), equals: try .init(major: 3, minor: 2, patch: 1)) + } + + func testPrereleasePrecedence() throws { + // Test cases include different combinations of numeric and alpha-numeric identifiers, with different precedences, and try to capture edge cases. + + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta"), precedes: try .init(major: 1, minor: 2, patch: 3)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta"), precedes: try .init(major: 1, minor: 2, patch: 4)) + assert(try .init(major: 1, minor: 2, patch: 2), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta")) + assert(try .init(major: 1, minor: 2, patch: 3), precedes: try .init(major: 1, minor: 2, patch: 4, prerelease: "beta")) + assert(try .init(major: 1, minor: 2, patch: 3), precedes: try .init(major: 1, minor: 3, patch: 3, prerelease: "beta")) + assert(try .init(major: 1, minor: 2, patch: 3), precedes: try .init(major: 2, minor: 2, patch: 3, prerelease: "beta")) + + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta"), precedes: try .init(major: 1, minor: 2, patch: 4, prerelease: "beta")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "alpha"), precedes: try .init(major: 1, minor: 2, patch: 4, prerelease: "beta")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta"), precedes: try .init(major: 1, minor: 2, patch: 4, prerelease: "alpha")) + + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "betax")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta-x")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta1")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta-1")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta1x")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta-1x")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "betax1")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta-x1")) + + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta.x")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta.1")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta.1x")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta.x1")) + + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "a"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "alpha"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "alpha42"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "alpha-42"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "alpha.42"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta")) + + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "1bcd"), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "abcd")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abcd"), precedes: try .init(major: 4, minor: 5, patch: 7, prerelease: "1bcd")) + + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "456"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "alpha")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "456.alpha"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "alpha")) + + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "123"), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "987")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "123"), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "124")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "123.456.789"), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "123.456.987")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "123.456.789"), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "321.111.111")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "9.9.9.9.9.9"), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "10")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "9.9.9.9.9.9"), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "10.0.0.0.0")) + + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.def.123"), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.def.789")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.def.789"), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.ghi.123")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "123.def-ghi"), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "789.abc.def")) + + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "987"), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "123a")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "987654321"), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "0a")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "999999999"), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "0a")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "999999999.zzz.zz"), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "0a")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.def.123"), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.def.ghi")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.987.ghi"), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.123def.123")) + + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc-123def-123"), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc-987-ghi")) + + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "0a"), precedes: try .init(major: 4, minor: 5, patch: 7, prerelease: "987")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "0a"), precedes: try .init(major: 4, minor: 6, patch: 6, prerelease: "987")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "0a"), precedes: try .init(major: 5, minor: 5, patch: 6, prerelease: "987")) + + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta"), equals: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "123abc"), equals: try .init(major: 4, minor: 5, patch: 6, prerelease: "123abc")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "123-abc"), equals: try .init(major: 4, minor: 5, patch: 6, prerelease: "123-abc")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "123.abc"), equals: try .init(major: 4, minor: 5, patch: 6, prerelease: "123.abc")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc123"), equals: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc123")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc-123"), equals: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc-123")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.123"), equals: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.123")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc123.123abc.123-abc.abc-123"), equals: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc123.123abc.123-abc.abc-123")) + } + + func testBuildMetadataPrecedence() throws { + + // In addition to hardcoded different combinations of numeric and alpha-numeric identifiers, with different precedences, some build metadata is randomly generated at each run of the test. + + var randomBuildMetadata: String { + let lexicon = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-." + let length = UInt8.random(in: 1...(.max)) // don't be too long + var characters: [Character] = [] + characters.reserveCapacity(Int(length)) + for position in 1...length { + if let lastCharacter = characters.last, lastCharacter != ".", position != length { + characters.append(lexicon.randomElement()!) + } else { + characters.append(lexicon.dropLast().randomElement()!) + } + } + return String(characters) + } + + assert(try .init(major: 0, minor: 0, patch: 0, buildMetadata: "abc"), precedes: try .init(major: 0, minor: 0, patch: 1, buildMetadata: "abc")) + assert(try .init(major: 0, minor: 0, patch: 0, buildMetadata: "abc"), precedes: try .init(major: 0, minor: 0, patch: 1, buildMetadata: "bcd")) + assert(try .init(major: 0, minor: 0, patch: 0, buildMetadata: "bcd"), precedes: try .init(major: 0, minor: 0, patch: 1, buildMetadata: "abc")) + assert(try .init(major: 0, minor: 0, patch: 0, buildMetadata: "123"), precedes: try .init(major: 0, minor: 1, patch: 0, buildMetadata: "123")) + assert(try .init(major: 0, minor: 0, patch: 0, buildMetadata: "123"), precedes: try .init(major: 0, minor: 1, patch: 0, buildMetadata: "234")) + assert(try .init(major: 0, minor: 0, patch: 0, buildMetadata: "234"), precedes: try .init(major: 0, minor: 1, patch: 0, buildMetadata: "123")) + assert(try .init(major: 0, minor: 0, patch: 0, buildMetadata: "1a2b3c"), precedes: try .init(major: 1, minor: 0, patch: 0, buildMetadata: "1a2b3c")) + assert(try .init(major: 0, minor: 0, patch: 0, buildMetadata: "1a2b3c"), precedes: try .init(major: 1, minor: 0, patch: 0, buildMetadata: "3c2b1a")) + assert(try .init(major: 0, minor: 0, patch: 0, buildMetadata: "3c2b1a"), precedes: try .init(major: 1, minor: 0, patch: 0, buildMetadata: "1a2b3c")) + assert(try .init(major: 1, minor: 2, patch: 3, buildMetadata: "a1b2c3"), precedes: try .init(major: 1, minor: 2, patch: 4, buildMetadata: "a1b2c3")) + assert(try .init(major: 1, minor: 2, patch: 3, buildMetadata: "a1b2c3"), precedes: try .init(major: 1, minor: 2, patch: 4, buildMetadata: "c3b2a1")) + assert(try .init(major: 1, minor: 2, patch: 3, buildMetadata: "c3b2a1"), precedes: try .init(major: 1, minor: 2, patch: 4, buildMetadata: "a1b2c3")) + assert(try .init(major: 1, minor: 2, patch: 3, buildMetadata: "1-2-3"), precedes: try .init(major: 1, minor: 3, patch: 3, buildMetadata: "1-2-3")) + assert(try .init(major: 1, minor: 2, patch: 3, buildMetadata: "1-2-3"), precedes: try .init(major: 1, minor: 3, patch: 3, buildMetadata: "3-2-1")) + assert(try .init(major: 1, minor: 2, patch: 3, buildMetadata: "3-2-1"), precedes: try .init(major: 1, minor: 3, patch: 3, buildMetadata: "1-2-3")) + assert(try .init(major: 1, minor: 2, patch: 3, buildMetadata: "1.2.3"), precedes: try .init(major: 2, minor: 2, patch: 3, buildMetadata: "1.2.3")) + assert(try .init(major: 1, minor: 2, patch: 3, buildMetadata: "1.2.3"), precedes: try .init(major: 2, minor: 2, patch: 3, buildMetadata: "2.3.4")) + assert(try .init(major: 1, minor: 2, patch: 3, buildMetadata: "2.3.4"), precedes: try .init(major: 2, minor: 2, patch: 3, buildMetadata: "1.2.3")) + + assert(try .init(major: 0, minor: 0, patch: 0, buildMetadata: "a-b-c"), equals: try .init(major: 0, minor: 0, patch: 0, buildMetadata: "a-b-c")) + assert(try .init(major: 0, minor: 0, patch: 0, buildMetadata: "a-b-c"), equals: try .init(major: 0, minor: 0, patch: 0, buildMetadata: "c-b-a")) + assert(try .init(major: 0, minor: 0, patch: 0, buildMetadata: "c-b-a"), equals: try .init(major: 0, minor: 0, patch: 0, buildMetadata: "a-b-c")) + assert(try .init(major: 1, minor: 2, patch: 3, buildMetadata: "a.b.c"), equals: try .init(major: 1, minor: 2, patch: 3, buildMetadata: "a.b.c")) + assert(try .init(major: 1, minor: 2, patch: 3, buildMetadata: "a.b.c"), equals: try .init(major: 1, minor: 2, patch: 3, buildMetadata: "c.b.a")) + assert(try .init(major: 1, minor: 2, patch: 3, buildMetadata: "c.b.a"), equals: try .init(major: 1, minor: 2, patch: 3, buildMetadata: "a.b.c")) + assert(try .init(major: 3, minor: 2, patch: 1, buildMetadata: "a-1.b-2.c-3"), equals: try .init(major: 3, minor: 2, patch: 1, buildMetadata: "a-1.b-2.c-3")) + assert(try .init(major: 3, minor: 2, patch: 1, buildMetadata: "a-1.b-2.c-3"), equals: try .init(major: 3, minor: 2, patch: 1, buildMetadata: "3.c-2.b-1.a")) + assert(try .init(major: 3, minor: 2, patch: 1, buildMetadata: "3.c-2.b-1.a"), equals: try .init(major: 3, minor: 2, patch: 1, buildMetadata: "a-1.b-2.c-3")) + + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: "---"), precedes: try .init(major: 1, minor: 2, patch: 3, buildMetadata: "---")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: "-"), precedes: try .init(major: 1, minor: 2, patch: 3, buildMetadata: "---")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: "---"), precedes: try .init(major: 1, minor: 2, patch: 3, buildMetadata: "-")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: "-.-"), precedes: try .init(major: 1, minor: 2, patch: 4, buildMetadata: "-.-")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: "-.-.-"), precedes: try .init(major: 1, minor: 2, patch: 4, buildMetadata: "-.-")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: "-.-"), precedes: try .init(major: 1, minor: 2, patch: 4, buildMetadata: "-.-.-")) + assert(try .init(major: 1, minor: 2, patch: 2, buildMetadata: "000"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: "000")) + assert(try .init(major: 1, minor: 2, patch: 2, buildMetadata: "000"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: "0")) + assert(try .init(major: 1, minor: 2, patch: 2, buildMetadata: "0"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: "000")) + assert(try .init(major: 1, minor: 2, patch: 3, buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 4, prerelease: "beta", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 3, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, buildMetadata: randomBuildMetadata), precedes: try .init(major: 2, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata)) + + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 4, prerelease: "beta", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "alpha", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 4, prerelease: "beta", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 4, prerelease: "alpha", buildMetadata: randomBuildMetadata)) + + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "betax", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta-x", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta1", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta-1", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta1x", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta-1x", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "betax1", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta-x1", buildMetadata: randomBuildMetadata)) + + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta.x", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta.1", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta.1x", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta.x1", buildMetadata: randomBuildMetadata)) + + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "a", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "alpha", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "alpha42", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "alpha-42", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "alpha.42", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata)) + + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "1bcd", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "abcd", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abcd", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 7, prerelease: "1bcd", buildMetadata: randomBuildMetadata)) + + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "456", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "alpha", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "456.alpha", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "alpha", buildMetadata: randomBuildMetadata)) + + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "123", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "987", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "123", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "124", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "123.456.789", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "123.456.987", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "123.456.789", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "321.111.111", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "9.9.9.9.9.9", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "10", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "9.9.9.9.9.9", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "10.0.0.0.0", buildMetadata: randomBuildMetadata)) + + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.def.123", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.def.789", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.def.789", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.ghi.123", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "123.def-ghi", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "789.abc.def", buildMetadata: randomBuildMetadata)) + + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "987", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "123a", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "987654321", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "0a", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "999999999", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "0a", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "999999999.zzzzz.zzzzz", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "0a", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.def.123", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.def.ghi", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.987.ghi", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.123def.123", buildMetadata: randomBuildMetadata)) + + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc-123def-123", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc-987-ghi", buildMetadata: randomBuildMetadata)) + + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "0a", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 7, prerelease: "987", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "0a", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 6, patch: 6, prerelease: "987", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "0a", buildMetadata: randomBuildMetadata), precedes: try .init(major: 5, minor: 5, patch: 6, prerelease: "987", buildMetadata: randomBuildMetadata)) + + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata), equals: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "123abc", buildMetadata: randomBuildMetadata), equals: try .init(major: 4, minor: 5, patch: 6, prerelease: "123abc", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "123-abc", buildMetadata: randomBuildMetadata), equals: try .init(major: 4, minor: 5, patch: 6, prerelease: "123-abc", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "123.abc", buildMetadata: randomBuildMetadata), equals: try .init(major: 4, minor: 5, patch: 6, prerelease: "123.abc", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc123", buildMetadata: randomBuildMetadata), equals: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc123", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc-123", buildMetadata: randomBuildMetadata), equals: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc-123", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.123", buildMetadata: randomBuildMetadata), equals: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.123", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc123.123abc.123-abc.abc-123", buildMetadata: randomBuildMetadata), equals: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc123.123abc.123-abc.abc-123", buildMetadata: randomBuildMetadata)) + } +} diff --git a/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/SemanticsTests.swift b/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/SemanticsTests.swift new file mode 100644 index 0000000..23a80da --- /dev/null +++ b/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/SemanticsTests.swift @@ -0,0 +1,137 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2022 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 SemanticsTests: XCTestCase { + func testDenotingStableRelease() throws { + let stableVersions: [SymbolGraph.SemanticVersion] = [ + try .init(major: 1, minor: 0, patch: 0), + try .init(major: 1, minor: 0, patch: 1), + try .init(major: 1, minor: 2, patch: 3), + try .init(major: 987, minor: 654, patch: 321), + try .init(major: 3, minor: 2, patch: 1, buildMetadata: "abcde") + ] + + for stableVersion in stableVersions { + XCTAssertTrue(stableVersion.denotesStableRelease) + } + + let unstableVersions: [SymbolGraph.SemanticVersion] = [ + try .init(major: 0, minor: 0, patch: 0), + try .init(major: 0, minor: 0, patch: 1), + try .init(major: 0, minor: 1, patch: 2), + try .init(major: 0, minor: 9, patch: 8, prerelease: "gm"), + try .init(major: 1, minor: 0, patch: 0, prerelease: "beta"), + try .init(major: 9, minor: 9, patch: 9, prerelease: "alpha", buildMetadata: "xyz") + ] + + for unstableVersion in unstableVersions { + XCTAssertFalse(unstableVersion.denotesStableRelease) + } + } + + func testDenotingPrerelease() throws { + let prereleaseVersions: [SymbolGraph.SemanticVersion] = [ + try .init(major: 0, minor: 0, patch: 0, prerelease: "asd"), + try .init(major: 0, minor: 0, patch: 1, prerelease: "fgh"), + try .init(major: 0, minor: 1, patch: 0, prerelease: "jkl"), + try .init(major: 0, minor: 9, patch: 8, prerelease: "qwe", buildMetadata: "rty"), + try .init(major: 1, minor: 0, patch: 0, prerelease: "uio"), + try .init(major: 1, minor: 2, patch: 3, prerelease: "zxc") + ] + + for prereleaseVersion in prereleaseVersions { + XCTAssertTrue(prereleaseVersion.denotesPrerelease) + } + + let releaseVersions: [SymbolGraph.SemanticVersion] = [ + try .init(major: 0, minor: 0, patch: 0), + try .init(major: 0, minor: 0, patch: 1), + try .init(major: 0, minor: 1, patch: 0), + try .init(major: 0, minor: 9, patch: 8, buildMetadata: "rty"), + try .init(major: 1, minor: 0, patch: 0), + try .init(major: 1, minor: 2, patch: 3), + try .init(major: 0, minor: 0, patch: 0, buildMetadata: "-asd"), + try .init(major: 0, minor: 0, patch: 1, buildMetadata: "-fgh"), + try .init(major: 0, minor: 1, patch: 0, buildMetadata: "-jkl"), + try .init(major: 0, minor: 9, patch: 8, buildMetadata: "-qwe-rty"), + try .init(major: 1, minor: 0, patch: 0, buildMetadata: "-uio"), + try .init(major: 1, minor: 2, patch: 3, buildMetadata: "-zxc") + ] + + for releaseVersion in releaseVersions { + XCTAssertFalse(releaseVersion.denotesPrerelease) + } + } + + func testDenotingSourceBreakableRelease() throws { + let sortedBreakableVersions: [SymbolGraph.SemanticVersion] = [ + try .init(major: 0, minor: 0, patch: 0, prerelease: "123"), + try .init(major: 0, minor: 0, patch: 0, prerelease: "abc"), + try .init(major: 0, minor: 0, patch: 0), + try .init(major: 0, minor: 0, patch: 1, prerelease: "456"), + try .init(major: 0, minor: 0, patch: 1, prerelease: "def"), + try .init(major: 0, minor: 0, patch: 1), + try .init(major: 0, minor: 0, patch: 2, prerelease: "789"), + try .init(major: 0, minor: 0, patch: 2, prerelease: "ghi"), + try .init(major: 0, minor: 0, patch: 2), + try .init(major: 0, minor: 1, patch: 0, prerelease: "876"), + try .init(major: 0, minor: 1, patch: 0, prerelease: "jkl"), + try .init(major: 0, minor: 1, patch: 0), + try .init(major: 0, minor: 1, patch: 2, prerelease: "543"), + try .init(major: 0, minor: 1, patch: 2, prerelease: "mno"), + try .init(major: 0, minor: 1, patch: 2), + try .init(major: 1, minor: 0, patch: 0, prerelease: "212"), + try .init(major: 1, minor: 0, patch: 0, prerelease: "pqr"), + try .init(major: 1, minor: 0, patch: 0), + try .init(major: 2, minor: 0, patch: 0, prerelease: "345"), + try .init(major: 2, minor: 0, patch: 0, prerelease: "stu"), + try .init(major: 2, minor: 0, patch: 0), + try .init(major: 3, minor: 4, patch: 5, prerelease: "678"), + try .init(major: 3, minor: 4, patch: 5, prerelease: "vwx"), + try .init(major: 3, minor: 4, patch: 5) + ] + + for newVersionIndex in 1..