Created
April 7, 2022 00:02
-
-
Save eoghain/f93aad351017a06e3fe1d6c453221cc3 to your computer and use it in GitHub Desktop.
Feature Flags
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
/// Struct identifying a Feature Flag that is used to enable/disable access to a feature | |
/// | |
/// example: | |
/// ``` | |
/// // Only allow this feature on iPhones in the US if the flag is enabled | |
/// let featureFlag = FeatureFlag(name: "EnableMyFeature", localeRestrictions: ["en_US"], deviceTypes: [.phone]) | |
/// guard featureFlag.isEnabled else { return } | |
/// ``` | |
struct FeatureFlag { | |
// MARK: - Properties | |
// MARK: Public | |
/// Flag name - matches the Key in UserDefaults & config.json file | |
var name: String | |
/// Default value to use when we don't have a value in UserDefaults (default: true) | |
var defaultValue: Bool | |
/// Human readable name used in Settings for overwriting (default: name value) | |
var displayName: String? | |
/// Optionally restrict to certain locales (leave empty for worldwide) | |
var localeRestrictions: [String] | |
/// Optionally restrict to certain languages (leave empty for all languages) | |
var languageRestrictions: [String] | |
/// Optionally restrict to a range of iOS versions | |
var minimumOSVersion: String? | |
var maximumOSVersion: String? | |
/// Optionally restrict to certain device types (leave empty for all languages) | |
var deviceRestrictions: [UIUserInterfaceIdiom] | |
/// Requires the user to have an account | |
var isAccountRequired: Bool | |
/// Show in the debug UI? (default: true) | |
var showInSettings: Bool | |
/// Ask to restart when changed in DebugSettings (default: false) | |
var needsRelaunch: Bool | |
// MARK: Computed | |
/// Is the current device valid for this FeatureFlag | |
var supportsCurrentDevice: Bool { | |
return isSupportedDeviceType() | |
} | |
/// Is the current OSVersion valid for this FeatureFlag | |
var supportsCurrentOSVersion: Bool { | |
return isSupportedOSVersion() | |
} | |
/// Verifies the flag is valid for the locale, language, device, os, and user and finally returns the set value | |
var isEnabled: Bool { | |
guard isSupportedLocale() else { return false } | |
guard isSupportedLanguage() else { return false } | |
guard isSupportedOSVersion() else { return false } | |
guard isSupportedDeviceType() else { return false } | |
guard isSupportedAccount() else { return false } | |
let flag = userDefaults.bool(forKey: self.name) | |
return isKillSwitch ? !flag : flag | |
} | |
// MARK: Private | |
/// Use this FeatureFlag with an inverted value (default: false) | |
/// | |
/// If this is set the value used for determining the isEnabled state will be inverted!!!! | |
/// | |
/// * If the UserDefaults value is **true** then this FeatureFlag will be disabled | |
/// * If the UserDefaults value is **false** then this FeatureFlag will be enabled | |
private var isKillSwitch: Bool | |
// Dependency injection | |
private var userDefaults: UserDefaults | |
private var uiDevice: UIDevice | |
// MARK: - Initialization | |
/** | |
Init with defaults to allow caller to only supply what is necessary. | |
- note: Name must be *Unique*, it will be stored in UserDefaults | |
- important: If **isKillSwitch** is true the value used for determining the isEnabled state will be inverted!!!! | |
* If the UserDefaults value is **true** then this FeatureFlag will be disabled | |
* If the UserDefaults value is **false** then this FeatureFlag will be enabled | |
- parameter name: The name of the JSON Key used in config.json for this feature flag, must be *Unique* (i.e. "EnableInviteFriendToFollowTopic") | |
- parameter isKillSwitch: Inverts the value from UserDefaults | |
- parameter defaultValue: The value to use if we don't find anything in UserDefaults | |
- parameter displayName: The human readable name for display in settings (i.e. "Invite Flow") | |
- parameter localeRestrictions: Array of locales where this feature can be enabled [] = all (i.e. ["en_US", "en_CA"]) | |
- parameter languageRestrictions: Array of languages for this feature [] = all (i.e. ["en", "de"]) | |
- parameter minimumOSVersion: String (i.e. "1.2.3") | |
- parameter maximumOSVersion: String (i.e. "1.4.5") | |
- parameter deviceRestrictions: Array of devices for this feature [] = all (i.e. [.phone, .pad]) | |
- parameter isAccountRequired: Should the user have an account to use this feature | |
- parameter showInSettings: Allow the value to be overridden in the settings tool? | |
- parameter needsRelaunch: Used when changing the value in the *Debug Settings Tool* | |
- parameter mockUserDefaults: Used to inject a mock version of UserDefaults to be used for *Testing* | |
- parameter mockUIDevice: Used to inject a mock version of UIDevice to be used for *Testing* | |
*/ | |
init( | |
name: String, | |
isKillSwitch: Bool = false, | |
defaultValue: Bool = true, | |
displayName: String? = nil, | |
localeRestrictions: [String] = [], | |
languageRestrictions: [String] = [], | |
minimumOSVersion: String? = nil, | |
maximumOSVersion: String? = nil, | |
deviceRestrictions: [UIUserInterfaceIdiom] = [], | |
isAccountRequired: Bool = false, | |
showInSettings: Bool = true, | |
needsRelaunch: Bool = false, | |
mockUserDefaults: UserDefaults = UserDefaults.standard, | |
mockUIDevice: UIDevice = UIDevice.current | |
) { | |
self.name = name | |
self.isKillSwitch = isKillSwitch | |
self.defaultValue = defaultValue | |
self.displayName = displayName ?? name // default to name if not supplied | |
self.localeRestrictions = localeRestrictions | |
self.languageRestrictions = languageRestrictions | |
self.minimumOSVersion = minimumOSVersion | |
self.maximumOSVersion = maximumOSVersion | |
self.deviceRestrictions = deviceRestrictions | |
self.isAccountRequired = isAccountRequired | |
self.showInSettings = showInSettings | |
self.needsRelaunch = needsRelaunch | |
self.userDefaults = mockUserDefaults | |
self.uiDevice = mockUIDevice | |
// Store default value in UserDefaults Registration Domin (last domain checked so it will be overridden by stored values) | |
// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/UserDefaults/AboutPreferenceDomains/AboutPreferenceDomains.html#//apple_ref/doc/uid/10000059i-CH2-SW1 | |
userDefaults.register(defaults: [name: defaultValue]) | |
} | |
// MARK: - Public Methods | |
/// Sets the value into UserDefaults for this FeatureFlag | |
/// - Parameter newValue: value used to determine if FeatureFlag isEnabled | |
func setValue(_ newValue: Bool) { | |
userDefaults.set(newValue, forKey: name) | |
} | |
var notEnabledReason: String { | |
var messages = [String]() | |
if !isSupportedLocale() { | |
messages.append("Disabled because \(currentLocale()) not in \(localeRestrictions)") | |
} | |
if !isSupportedLanguage() { | |
messages.append("Disabled because \(preferredLanguage()) not in \(languageRestrictions)") | |
} | |
if !isSupportedOSVersion() { | |
let osVersion = ProcessInfo.processInfo.operatingSystemVersion | |
let osVersionString = "\(osVersion.majorVersion).\(osVersion.minorVersion).\(osVersion.patchVersion)" | |
messages.append("Disabled because OS Version \(osVersionString) not between \(String(describing: minimumOSVersion)) and \(String(describing: maximumOSVersion))") | |
} | |
if !isSupportedDeviceType() { | |
messages.append("Disabled because \(uiDevice.userInterfaceIdiom) not in \(deviceRestrictions)") | |
} | |
if !isSupportedAccount() { | |
messages.append("Disabled because account required") | |
} | |
if userDefaults.bool(forKey: self.name) == false { | |
messages.append("Disabled because UserDefaults value is false") | |
} | |
return messages.joined(separator: "\n") | |
} | |
// MARK: - Internal Checks | |
private func isSupportedLocale() -> Bool { | |
// No restrictions so always true | |
guard localeRestrictions.isEmpty == false else { return true } | |
// If we don't find our current locale in restriction list then it's not supported | |
guard localeRestrictions.contains(currentLocale()) else { return false } | |
return true | |
} | |
private func isSupportedLanguage() -> Bool { | |
// No restrictions so always true | |
guard languageRestrictions.isEmpty == false else { return true } | |
// If we don't find our current language in restriction list then it's not supported | |
guard languageRestrictions.contains(preferredLanguage()) else { return false } | |
return true | |
} | |
private func isSupportedOSVersion() -> Bool { | |
let osVersion = ProcessInfo.processInfo.operatingSystemVersion | |
let osVersionString = "\(osVersion.majorVersion).\(osVersion.minorVersion).\(osVersion.patchVersion)" | |
let minimumOSVersion = self.minimumOSVersion ?? osVersionString | |
let maximumOSVersion = self.maximumOSVersion ?? osVersionString | |
guard minimumOSVersion.versionCompare(osVersionString) != .orderedDescending, | |
maximumOSVersion.versionCompare(osVersionString) != .orderedAscending else { return false } | |
return true | |
} | |
private func isSupportedDeviceType() -> Bool { | |
// No restrictions so always true | |
guard deviceRestrictions.isEmpty == false else { return true } | |
// If we don't find our current userInterfaceIdiom in deviceRestrictions then it's not supported | |
guard deviceRestrictions.contains(uiDevice.userInterfaceIdiom) else { return false } | |
return true | |
} | |
// MARK: Helper Methods | |
private func currentLocale() -> String { | |
return NSLocale.current.identifier | |
} | |
private func preferredLanguage() -> String { | |
return NSLocale.preferredLanguages.first ?? "" | |
} | |
} | |
// MARK: - Objective-C Shim | |
// Obj-c Shim concept taken from this blog post | |
// https://www.steveonstuff.com/2022/01/13/migrating-from-objc-to-swift | |
/// FeatureFlag_ObjC is a shim for using the FeatureFlag struct in Objective-C. | |
@objc | |
class FeatureFlag_ObjC: NSObject { | |
let featureFlag: FeatureFlag | |
init(featureFlag: FeatureFlag) { | |
self.featureFlag = featureFlag | |
super.init() | |
} | |
/// Flag name - matches the Key in UserDefaults & config.json file | |
@objc var name: String { | |
return featureFlag.name | |
} | |
/// Default value to use when we don't have a value in UserDefaults (default: true) | |
@objc var defaultValue: Bool { | |
return featureFlag.defaultValue | |
} | |
/// Verifies the flag is valid for the locale, language, device, os, and user and finally returns the set value | |
@objc var isEnabled: Bool { | |
return featureFlag.isEnabled | |
} | |
/// Sets the value into UserDefaults for this FeatureFlag | |
/// - Parameter newValue: value used to determine if FeatureFlag isEnabled | |
@objc func setValue(_ newValue: Bool) { | |
self.featureFlag.setValue(newValue) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
/// Feature Flags | |
/// | |
/// How to create a Feature Flag: | |
/// 1. Create a static let Feature Flag in the **Feature Flags** section | |
/// 2. Add the static name to the allFlags array | |
/// 3. If you need Objective-C access to this FeatureFlag add a line to the FeatureFlags_ObjC class below in the **ObjC Feature Flags** section | |
struct FeatureFlags { | |
// Array of all flags, add your flag here as well as creating the static version below | |
static let allFlags: [FeatureFlag] = [ | |
inviteFlow, | |
passwordStrengthMeter | |
] | |
// MARK: Feature Flags | |
static let sample = FeatureFlag( | |
name: "SampleFeatureFlag", | |
displayName: "Sample Feature Flag", | |
localeRestrictions: ["en_US", "en_CA"] | |
) | |
// DefaultValue should be false for KillSwitches to let the server supplied value overwrite if it exists. | |
static let sampleKillSwitch = FeatureFlag( | |
name: "SampleKillSwitch", | |
isKillSwitch: true, | |
defaultValue: false, | |
displayName: "Sample Kill Switch", | |
languageRestrictions: ["en"]) | |
} | |
// MARK: - Objective-C Shim | |
// Obj-c Shim concept taken from this blog post | |
// https://www.steveonstuff.com/2022/01/13/migrating-from-objc-to-swift | |
/// FeatureFlags_ObjC is a shim for using the FeatureFlags struct in Objective-C. | |
@objc | |
class FeatureFlags_ObjC: NSObject { | |
@objc static var allFlags: [FeatureFlag_ObjC] { | |
// Return shim'd flags | |
return FeatureFlags.allFlags.map { FeatureFlag_ObjC(featureFlag: $0) } | |
} | |
// MARK: ObjC Feature Flags | |
@objc static let inviteFlow = FeatureFlag_ObjC(featureFlag: FeatureFlags.inviteFlow) | |
@objc static let personalizeForYou = FeatureFlag_ObjC(featureFlag: FeatureFlags.personalizeForYou) | |
@objc static let historyEnabled = FeatureFlag_ObjC(featureFlag: FeatureFlags.historyEnabled) | |
@objc static let passwordStrengthMeter = FeatureFlag_ObjC(featureFlag: FeatureFlags.passwordStrengthMeter) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import XCTest | |
class FeatureFlagTests: XCTestCase { | |
private var userDefaults: UserDefaults! | |
override func setUpWithError() throws { | |
try? super.setUpWithError() | |
// Create local injectable UserDefaults object | |
userDefaults = UserDefaults(suiteName: #file) | |
userDefaults.removePersistentDomain(forName: #file) | |
} | |
func testDefaultDisplayName() { | |
let flagName = "ThisIsATest" | |
let featureFlag = FeatureFlag(name: flagName, mockUserDefaults: userDefaults) | |
XCTAssertTrue(featureFlag.displayName == flagName, "Display name not defaulting when left empty") | |
} | |
func testDisplayName() { | |
let flagName = "ThisIsATest" | |
let displayName = "This is a test feature flag" | |
let featureFlag = FeatureFlag(name: flagName, displayName: displayName, mockUserDefaults: userDefaults) | |
XCTAssertTrue(featureFlag.displayName == displayName, "Display name getting overwritten even when supplied") | |
} | |
func testEnabled() { | |
let flagName = "ThisIsATest" | |
userDefaults.set(true, forKey: flagName) | |
let featureFlag = FeatureFlag(name: flagName, mockUserDefaults: userDefaults) | |
XCTAssertTrue(featureFlag.isEnabled, featureFlag.disabledReason) | |
} | |
func testValueFromUserDefaults() { | |
let flagName = "ThisIsATest" | |
let flagName2 = "ThisIsAnotherTest" | |
userDefaults.set(false, forKey: flagName) | |
userDefaults.set(true, forKey: flagName2) | |
let defaultEnabled = FeatureFlag(name: flagName, mockUserDefaults: userDefaults) | |
let defaultDisabled = FeatureFlag(name: flagName2, defaultValue: false, mockUserDefaults: userDefaults) | |
XCTAssertFalse(defaultEnabled.isEnabled, defaultEnabled.disabledReason) | |
XCTAssertTrue(defaultDisabled.isEnabled, defaultDisabled.disabledReason) | |
} | |
func testSupportedLocale() { | |
let flagName = "ThisIsATest" | |
userDefaults.set(true, forKey: flagName) | |
MockLocale.swizzleCurrentLocale() | |
MockLocale.mockLocaleIdentifier = "en_US" | |
let usFlag = FeatureFlag(name: flagName, localeRestrictions: ["en_US"], mockUserDefaults: userDefaults) | |
let caFlag = FeatureFlag(name: flagName, localeRestrictions: ["en_CA"], mockUserDefaults: userDefaults) | |
XCTAssertTrue(usFlag.isEnabled, usFlag.disabledReason) | |
XCTAssertFalse(caFlag.isEnabled, "Locale restricted FeatureFlag is enabled when it shouldn't be") | |
MockLocale.swizzleCurrentLocale() | |
MockLocale.mockLocaleIdentifier = nil | |
} | |
func testSupportedLanguage() { | |
let flagName = "ThisIsATest" | |
userDefaults.set(true, forKey: flagName) | |
let enFlag = FeatureFlag(name: flagName, languageRestrictions: ["en"], mockUserDefaults: userDefaults) | |
let deFlag = FeatureFlag(name: flagName, languageRestrictions: ["de"], mockUserDefaults: userDefaults) | |
// preferred language when running tests should always be "en" | |
XCTAssertTrue(enFlag.isEnabled, enFlag.disabledReason) | |
XCTAssertFalse(deFlag.isEnabled, "Language restricted FeatureFlag is enabled when it shouldn't be") | |
} | |
func testSupportedOS() { | |
let flagName = "ThisIsATest" | |
userDefaults.set(true, forKey: flagName) | |
let osVersion = ProcessInfo.processInfo.operatingSystemVersion | |
let osVersionString = "\(osVersion.majorVersion).\(osVersion.minorVersion).\(osVersion.patchVersion)" | |
let minOSString = "\(osVersion.majorVersion - 1).\(osVersion.minorVersion).\(osVersion.patchVersion)" | |
let maxOSString = "\(osVersion.majorVersion + 1).\(osVersion.minorVersion).\(osVersion.patchVersion)" | |
let supported = FeatureFlag(name: flagName, minimumOSVersion: osVersionString, maximumOSVersion: osVersionString, mockUserDefaults: userDefaults) | |
let belowMinimum = FeatureFlag(name: flagName, minimumOSVersion: maxOSString, mockUserDefaults: userDefaults) | |
let overMaximum = FeatureFlag(name: flagName, maximumOSVersion: minOSString, mockUserDefaults: userDefaults) | |
XCTAssertTrue(supported.isEnabled, supported.disabledReason) | |
XCTAssertFalse(belowMinimum.isEnabled, "Below minimum OS Version FeatureFlag is enabled when it shouldn't be") | |
XCTAssertFalse(overMaximum.isEnabled, "Over maximum OS Version FeatureFlag is enabled when it shouldn't be") | |
XCTAssertTrue(supported.supportsCurrentOSVersion, supported.disabledReason) | |
XCTAssertFalse(overMaximum.supportsCurrentOSVersion, overMaximum.disabledReason) | |
} | |
func testSupportedDeviceType() { | |
let flagName = "ThisIsATest" | |
userDefaults.set(true, forKey: flagName) | |
let mockDevice = MockDevice() | |
mockDevice.mockUserInterfaceIdiom = .pad | |
let phoneFlag = FeatureFlag(name: flagName, deviceRestrictions: [.phone], mockUserDefaults: userDefaults, mockUIDevice: mockDevice) | |
let padFlag = FeatureFlag(name: flagName, deviceRestrictions: [.pad], mockUserDefaults: userDefaults, mockUIDevice: mockDevice) | |
let multiFlag = FeatureFlag(name: flagName, deviceRestrictions: [.phone, .pad], mockUserDefaults: userDefaults, mockUIDevice: mockDevice) | |
XCTAssertFalse(phoneFlag.isEnabled, "Device restricted FeatureFlag is enabled when it shouldn't be") | |
XCTAssertTrue(padFlag.isEnabled, padFlag.disabledReason) | |
XCTAssertTrue(multiFlag.isEnabled, multiFlag.disabledReason) | |
XCTAssertTrue(padFlag.supportsCurrentDevice, padFlag.disabledReason) | |
XCTAssertFalse(phoneFlag.supportsCurrentDevice, "Device restricted FeatureFlag is enabled when it shouldn't be") | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class MockDevice: UIDevice { | |
var mockUserInterfaceIdiom: UIUserInterfaceIdiom = .phone | |
override var userInterfaceIdiom: UIUserInterfaceIdiom { | |
return mockUserInterfaceIdiom | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
class MockLocale { | |
static var mockLocaleIdentifier: String? | |
// MARK: - | |
// Swizzle current locale to be able to test it with different values | |
static func swizzleCurrentLocale() { | |
let originalMethod = class_getClassMethod(NSLocale.self, #selector(getter: NSLocale.current))! | |
let swizzledMethod = class_getClassMethod(Self.self, #selector(Self.mockCurrentLocale))! | |
method_exchangeImplementations(originalMethod, swizzledMethod) | |
} | |
@objc | |
private static func mockCurrentLocale() -> NSLocale { | |
guard let mockLocaleIdentifier = mockLocaleIdentifier, !mockLocaleIdentifier.isEmpty else { | |
fatalError("mockLocaleIdentifier required to be set to a non-nil, non-empty value.") | |
} | |
return NSLocale(localeIdentifier: mockLocaleIdentifier) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// MARK: - String Extension for semantic version comparison | |
extension String { | |
func versionCompare(_ otherVersion: String) -> ComparisonResult { | |
let versionDelimiter = "." | |
var version = self | |
var otherVersion = otherVersion | |
// Separate strings by "." (i.e. "1.0.0" -> ["1", "0", "0"]) | |
var versionComponents = self.components(separatedBy: versionDelimiter) | |
var otherVersionComponents = otherVersion.components(separatedBy: versionDelimiter) | |
let sectionDiffCount = versionComponents.count - otherVersionComponents.count | |
// The 2 strings have different numbers of sections (i.e. "1.0" and "1.0.0") | |
if sectionDiffCount != 0 { | |
// Create additional Zeros to be added to the shorter string | |
let additionalZeros = Array(repeating: "0", count: abs(sectionDiffCount)) | |
if sectionDiffCount > 0 { | |
otherVersionComponents.append(contentsOf: additionalZeros) | |
} else { | |
versionComponents.append(contentsOf: additionalZeros) | |
} | |
// Convert arrays back into strings (i.e. ["1","0","0"] -> "1.0.0" | |
version = versionComponents.joined(separator: versionDelimiter) | |
otherVersion = otherVersionComponents.joined(separator: versionDelimiter) | |
} | |
// Do numeric string comparison | |
return version.compare(otherVersion, options: .numeric) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import XCTest | |
class StringVersionComparisonTest: XCTestCase { | |
func testMatching() { | |
XCTAssertTrue("1.0.0".versionCompare("1.0.0") == .orderedSame, "Exact same version numbers don't match") | |
XCTAssertTrue("0.1.0".versionCompare("0.1.0") == .orderedSame, "Exact same version numbers don't match") | |
XCTAssertTrue("1".versionCompare("1") == .orderedSame, "Exact same version numbers don't match") | |
XCTAssertTrue("1.0".versionCompare("1.0") == .orderedSame, "Exact same version numbers don't match") | |
XCTAssertTrue("1.0.1".versionCompare("1.0.1") == .orderedSame, "Exact same version numbers don't match") | |
} | |
func testNewer() { | |
XCTAssertTrue("1.0.0".versionCompare("2.0.0") == .orderedAscending, "Versions should be Ascending but aren't") | |
XCTAssertTrue("1.0.0".versionCompare("1.0.1") == .orderedAscending, "Versions should be Ascending but aren't") | |
XCTAssertTrue("1.0.0".versionCompare("1.1.0") == .orderedAscending, "Versions should be Ascending but aren't") | |
XCTAssertTrue("14.6.80".versionCompare("14.7.80") == .orderedAscending, "Versions should be Ascending but aren't") | |
} | |
func testOlder() { | |
XCTAssertTrue("2.0.0".versionCompare("1.0.0") == .orderedDescending, "Versions should be Descending but aren't") | |
XCTAssertTrue("2.2.2".versionCompare("2.2.1") == .orderedDescending, "Versions should be Descending but aren't") | |
XCTAssertTrue("2.2.2".versionCompare("2.1.2") == .orderedDescending, "Versions should be Descending but aren't") | |
XCTAssertTrue("14.7.80".versionCompare("14.6.80") == .orderedDescending, "Versions should be Descending but aren't") | |
} | |
func testMismatched() { | |
XCTAssertTrue("1.0.0".versionCompare("1") == .orderedSame, "Exact same version numbers don't match") | |
XCTAssertTrue("1.0.0".versionCompare("1.0") == .orderedSame, "Exact same version numbers don't match") | |
XCTAssertTrue("14.6.80".versionCompare("14.7") == .orderedAscending, "Versions should be Ascending but aren't") | |
XCTAssertTrue("14.6.80".versionCompare("15") == .orderedAscending, "Versions should be Ascending but aren't") | |
XCTAssertTrue("14.7".versionCompare("14.6.80") == .orderedDescending, "Versions should be Descending but aren't") | |
XCTAssertTrue("15".versionCompare("14.6.80") == .orderedDescending, "Versions should be Descending but aren't") | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment