From d08eb3971aa0885686c0c7f9a97d242ac39d2098 Mon Sep 17 00:00:00 2001 From: Brian Michel Date: Fri, 12 Jan 2024 16:55:40 -0500 Subject: [PATCH 1/3] Add Windows Support To Version 9 (#2) * Windows Compiles * Start pulling things around to build tests * Add Windows Build action * We should allow streaming on Windows * Update package file * Fix tests * Add newlines * add macos testing * Fix tests on Darwin * Go to event source revision with windows support * Fix casing * start adding environment reporting support * Remove swift-crypto and use windows-native hasher * Update linking type * Respond to PR feedback * PR feedback (cherry picked from commit e624a384645317ec4223b5367302dc1608991874) --- .github/workflows/ci.yml | 15 ++++ LaunchDarkly.xcodeproj/project.pbxproj | 26 ++++++ LaunchDarkly/LaunchDarkly/LDClient.swift | 23 +++-- LaunchDarkly/LaunchDarkly/LDCommon.swift | 11 +++ .../Models/ConnectionInformation.swift | 4 + .../LaunchDarkly/Models/LDConfig.swift | 11 ++- .../Networking/DarklyService.swift | 6 ++ .../Networking/HTTPURLRequest.swift | 4 + .../Networking/HTTPURLResponse.swift | 4 + .../LaunchDarkly/Networking/URLResponse.swift | 4 + .../ServiceObjects/CwlSysctl.swift | 2 + .../ServiceObjects/DiagnosticReporter.swift | 4 + .../ServiceObjects/EnvironmentReporter.swift | 6 +- .../EnvironmentReporterBuilder.swift | 2 + .../SystemCapabilities.swift | 10 +++ .../WindowsEnvironmentReporter.swift | 55 ++++++++++++ .../ServiceObjects/EventReporter.swift | 4 + .../ServiceObjects/FlagSynchronizer.swift | 10 ++- .../LaunchDarkly/ServiceObjects/LDTimer.swift | 10 ++- LaunchDarkly/LaunchDarkly/Util.swift | 48 ++++++++++ .../Extensions/ThreadSpec.swift | 4 +- .../LaunchDarklyTests/LDClientSpec.swift | 87 ++++++++++--------- .../LaunchDarklyTests/Matcher/Match.swift | 40 +++++++++ .../Mocks/DarklyServiceMock.swift | 20 ++++- .../Mocks/LDEventSourceMock.swift | 5 ++ .../Models/Context/LDContextCodableSpec.swift | 6 +- .../Models/DiagnosticEventSpec.swift | 26 +++--- .../Models/LDConfigSpec.swift | 4 + .../Networking/DarklyServiceSpec.swift | 22 ++--- .../Networking/HTTPURLResponse.swift | 4 + .../Networking/URLRequestSpec.swift | 4 + .../Cache/DiagnosticCacheSpec.swift | 11 +-- .../Cache/FeatureFlagCacheSpec.swift | 6 +- .../DiagnosticReporterSpec.swift | 6 ++ .../WindowsEnvironmentReporterSpec.swift | 44 ++++++++++ .../ServiceObjects/EventReporterSpec.swift | 18 ++-- .../FlagChangeNotifierSpec.swift | 8 +- .../ServiceObjects/FlagSynchronizerSpec.swift | 51 ++++++++--- .../ServiceObjects/LDTimerSpec.swift | 8 +- .../SynchronizingErrorSpec.swift | 4 + .../ServiceObjects/ThrottlerSpec.swift | 22 ++--- Package.swift | 51 ++++++++--- 42 files changed, 574 insertions(+), 136 deletions(-) create mode 100644 LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/WindowsEnvironmentReporter.swift create mode 100644 LaunchDarkly/LaunchDarklyTests/Matcher/Match.swift create mode 100644 LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporting/WindowsEnvironmentReporterSpec.swift diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f42b0fd0..023692c43 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,7 @@ on: branches: [ v11, 'feat/**' ] paths-ignore: - '**.md' + workflow_dispatch: jobs: lint: @@ -131,6 +132,20 @@ jobs: - uses: ./.github/actions/test-swiftpm + test-swiftpm-windows: + name: Windows - Swift 5.9 - SPM + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + - name: Install Swift + uses: compnerd/gha-setup-swift@main + with: + branch: swift-5.9-release + tag: 5.9-RELEASE + - name: Run tests + run: swift test + contract-tests: runs-on: macos-15 diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index ebf09e7b6..7b7ed98e8 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -377,6 +377,12 @@ C443A41023186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = C443A40E23186A4F00145710 /* ConnectionModeChangeObserver.swift */; }; C443A41123186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = C443A40E23186A4F00145710 /* ConnectionModeChangeObserver.swift */; }; C443A41223186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = C443A40E23186A4F00145710 /* ConnectionModeChangeObserver.swift */; }; + BC0000000000000000000101 /* WindowsEnvironmentReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0000000000000000000100 /* WindowsEnvironmentReporter.swift */; }; + BC0000000000000000000102 /* WindowsEnvironmentReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0000000000000000000100 /* WindowsEnvironmentReporter.swift */; }; + BC0000000000000000000103 /* WindowsEnvironmentReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0000000000000000000100 /* WindowsEnvironmentReporter.swift */; }; + BC0000000000000000000104 /* WindowsEnvironmentReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0000000000000000000100 /* WindowsEnvironmentReporter.swift */; }; + BC0000000000000000000201 /* WindowsEnvironmentReporterSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0000000000000000000200 /* WindowsEnvironmentReporterSpec.swift */; }; + BC0000000000000000000302 /* Match.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0000000000000000000301 /* Match.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -582,6 +588,9 @@ C43C37E0236BA050003C1624 /* LDEvaluationDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDEvaluationDetail.swift; sourceTree = ""; }; C443A4092315AA4D00145710 /* NetworkReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkReporter.swift; sourceTree = ""; }; C443A40E23186A4F00145710 /* ConnectionModeChangeObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionModeChangeObserver.swift; sourceTree = ""; }; + BC0000000000000000000100 /* WindowsEnvironmentReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowsEnvironmentReporter.swift; sourceTree = ""; }; + BC0000000000000000000200 /* WindowsEnvironmentReporterSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowsEnvironmentReporterSpec.swift; sourceTree = ""; }; + BC0000000000000000000301 /* Match.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Match.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -782,6 +791,7 @@ B4265EB024E7390C001CFD2C /* TestUtil.swift */, A3FFE1122B7D4BA2009EF93F /* LDValueDecoderSpec.swift */, A3BA7D032BD2BD620000DB28 /* TestContext.swift */, + BC0000000000000000000300 /* Matcher */, ); name = LaunchDarklyTests; path = LaunchDarkly/LaunchDarklyTests; @@ -949,6 +959,7 @@ isa = PBXGroup; children = ( A35038842F4F96CA0032BA9F /* WatchOSEnvironmentReporterSpec.swift */, + BC0000000000000000000200 /* WindowsEnvironmentReporterSpec.swift */, A3047D622A606B6000F568E0 /* ApplicationInfoEnvironmentReporterSpec.swift */, A3047D5F2A606B6000F568E0 /* EnvironmentReporterChainBaseSpec.swift */, A3047D5E2A606B6000F568E0 /* IOSEnvironmentReporterSpec.swift */, @@ -990,6 +1001,7 @@ A358D6E12A4DE98300270C60 /* MacOSEnvironmentReporter.swift */, A358D6E62A4DE99B00270C60 /* WatchOSEnvironmentReporter.swift */, A358D6EB2A4DE9A600270C60 /* TVOSEnvironmentReporter.swift */, + BC0000000000000000000100 /* WindowsEnvironmentReporter.swift */, A358D6F12A4DEB4C00270C60 /* EnvironmentReporterBuilder.swift */, A358D6F62A4DF1D500270C60 /* SDKEnvironmentReporter.swift */, A35AD45F2A619E45005A8DCB /* SystemCapabilities.swift */, @@ -1016,6 +1028,14 @@ name = Frameworks; sourceTree = ""; }; + BC0000000000000000000300 /* Matcher */ = { + isa = PBXGroup; + children = ( + BC0000000000000000000301 /* Match.swift */, + ); + path = Matcher; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -1440,6 +1460,7 @@ A3BA7CEC2BD056920000DB28 /* Hook.swift in Sources */, 832D68A5224A38FC005F052A /* CacheConverter.swift in Sources */, A35AD4632A619E45005A8DCB /* SystemCapabilities.swift in Sources */, + BC0000000000000000000101 /* WindowsEnvironmentReporter.swift in Sources */, A358D6FA2A4DF1D500270C60 /* SDKEnvironmentReporter.swift in Sources */, 831188432113ADBE00D77CB5 /* LDCommon.swift in Sources */, A358D6D42A4DD48600270C60 /* EnvironmentReporterChainBase.swift in Sources */, @@ -1480,6 +1501,7 @@ 3D24061B2E0D90E000F91253 /* SdkMetadata.swift in Sources */, A310881D2837DC0400184942 /* Kind.swift in Sources */, A35AD4622A619E45005A8DCB /* SystemCapabilities.swift in Sources */, + BC0000000000000000000102 /* WindowsEnvironmentReporter.swift in Sources */, C443A40423145FBE00145710 /* ConnectionInformation.swift in Sources */, 832D68A4224A38FC005F052A /* CacheConverter.swift in Sources */, 831EF34C20655E730001C643 /* FlagChangeObserver.swift in Sources */, @@ -1603,6 +1625,7 @@ A3BA7CE92BD056920000DB28 /* Hook.swift in Sources */, 832D68A2224A38FC005F052A /* CacheConverter.swift in Sources */, A35AD4602A619E45005A8DCB /* SystemCapabilities.swift in Sources */, + BC0000000000000000000103 /* WindowsEnvironmentReporter.swift in Sources */, A358D6F72A4DF1D500270C60 /* SDKEnvironmentReporter.swift in Sources */, 835E1D401F63450A00184DB4 /* ObjcLDConfig.swift in Sources */, A358D6D12A4DD48600270C60 /* EnvironmentReporterChainBase.swift in Sources */, @@ -1629,11 +1652,13 @@ 83411A5F1FABDA8700E5CF39 /* mocks.generated.swift in Sources */, 83DDBF001FA2589900E428B6 /* FlagStoreSpec.swift in Sources */, A35038852F4F96CA0032BA9F /* WatchOSEnvironmentReporterSpec.swift in Sources */, + BC0000000000000000000201 /* WindowsEnvironmentReporterSpec.swift in Sources */, B4F689142497B2FC004D3CE0 /* DiagnosticEventSpec.swift in Sources */, 83396BC91F7C3711000E256E /* DarklyServiceSpec.swift in Sources */, 3D9A12582A73236800698B8D /* UtilSpec.swift in Sources */, 83EF67931F9945E800403126 /* EventSpec.swift in Sources */, A3BA7D042BD2BD620000DB28 /* TestContext.swift in Sources */, + BC0000000000000000000302 /* Match.swift in Sources */, 83B6E3F1222EFA3800FF2A6A /* ThreadSpec.swift in Sources */, 831AAE3020A9E75D00B46DBA /* ThrottlerSpec.swift in Sources */, 832D68AC224B3321005F052A /* CacheConverterSpec.swift in Sources */, @@ -1743,6 +1768,7 @@ A3BA7CEA2BD056920000DB28 /* Hook.swift in Sources */, 83D9EC952062DEAB004D7FA6 /* Date.swift in Sources */, A35AD4612A619E45005A8DCB /* SystemCapabilities.swift in Sources */, + BC0000000000000000000104 /* WindowsEnvironmentReporter.swift in Sources */, A358D6F82A4DF1D500270C60 /* SDKEnvironmentReporter.swift in Sources */, 832D68A3224A38FC005F052A /* CacheConverter.swift in Sources */, A358D6D22A4DD48600270C60 /* EnvironmentReporterChainBase.swift in Sources */, diff --git a/LaunchDarkly/LaunchDarkly/LDClient.swift b/LaunchDarkly/LaunchDarkly/LDClient.swift index 137faa549..925461edb 100644 --- a/LaunchDarkly/LaunchDarkly/LDClient.swift +++ b/LaunchDarkly/LaunchDarkly/LDClient.swift @@ -261,14 +261,14 @@ public class LDClient { os_log("%s stopped", log: config.logger, type: .debug, typeName(and: #function)) } - @objc private func didEnterBackground() { + private func didEnterBackground() { os_log("%s", log: config.logger, type: .debug, typeName(and: #function)) Thread.performOnMain { runMode = .background } } - @objc private func willEnterForeground() { + private func willEnterForeground() { os_log("%s", log: config.logger, type: .debug, typeName(and: #function)) Thread.performOnMain { runMode = .foreground @@ -764,7 +764,7 @@ public class LDClient { } } - @objc private func didCloseEventSource() { + private func didCloseEventSource() { os_log("%s", log: config.logger, type: .debug, typeName(and: #function)) self.connectionInformation = ConnectionInformation.lastSuccessfulConnectionCheck(connectionInformation: self.connectionInformation) } @@ -949,6 +949,8 @@ public class LDClient { private var initializedQueue = DispatchQueue(label: "com.launchdarkly.LDClient.initializedQueue") private var identifyQueue = SheddingQueue() + private var notificationTokens = [NSObjectProtocol]() + private init(serviceFactory: ClientServiceCreating, configuration: LDConfig, startContext: LDContext?, completion: (() -> Void)? = nil) { self.serviceFactory = serviceFactory self.hooks = Array(configuration.hooks) @@ -1003,13 +1005,22 @@ public class LDClient { service: service) if let backgroundNotification = SystemCapabilities.backgroundNotification { - NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: backgroundNotification, object: nil) + let background = NotificationCenter.default.addObserver(forName: backgroundNotification, object:nil, queue: OperationQueue.current, using: { [weak self] notification in + self?.didEnterBackground() + }) + notificationTokens.append(background) } if let foregroundNotification = SystemCapabilities.foregroundNotification { - NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: foregroundNotification, object: nil) + let foreground = NotificationCenter.default.addObserver(forName: foregroundNotification, object: nil, queue: OperationQueue.current, using: { [weak self] notification in + self?.willEnterForeground() + }) + notificationTokens.append(foreground) } - NotificationCenter.default.addObserver(self, selector: #selector(didCloseEventSource), name: Notification.Name(FlagSynchronizer.Constants.didCloseEventSourceName), object: nil) + let didClose = NotificationCenter.default.addObserver(forName: Notification.Name(FlagSynchronizer.Constants.didCloseEventSourceName), object: nil, queue: OperationQueue.current, using: { [weak self] _ in + self?.didCloseEventSource() + }) + notificationTokens.append(didClose) eventReporter = self.serviceFactory.makeEventReporter(config: configuration, service: service, onSyncComplete: onEventSyncComplete) service.resetFlagResponseCache(etag: cachedData.etag) diff --git a/LaunchDarkly/LaunchDarkly/LDCommon.swift b/LaunchDarkly/LaunchDarkly/LDCommon.swift index 0c5248489..0a05666cf 100644 --- a/LaunchDarkly/LaunchDarkly/LDCommon.swift +++ b/LaunchDarkly/LaunchDarkly/LDCommon.swift @@ -20,6 +20,16 @@ extension LDFlagKey { } /// An error thrown from APIs when an invalid argument is provided. +#if os(Linux) || os(Windows) +public class LDInvalidArgumentError: Error { + /// A description of the error. + public let localizedDescription: String + + init(_ description: String) { + self.localizedDescription = description + } +} +#else @objc public class LDInvalidArgumentError: NSObject, Error { /// A description of the error. public let localizedDescription: String @@ -28,6 +38,7 @@ extension LDFlagKey { self.localizedDescription = description } } +#endif struct DynamicKey: CodingKey { let intValue: Int? = nil diff --git a/LaunchDarkly/LaunchDarkly/Models/ConnectionInformation.swift b/LaunchDarkly/LaunchDarkly/Models/ConnectionInformation.swift index 0627d8064..cedddc11d 100644 --- a/LaunchDarkly/LaunchDarkly/Models/ConnectionInformation.swift +++ b/LaunchDarkly/LaunchDarkly/Models/ConnectionInformation.swift @@ -1,6 +1,10 @@ import Foundation import OSLog +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + public struct ConnectionInformation: Codable, CustomStringConvertible { public enum ConnectionMode: String, Codable { case streaming, offline, establishingStreamingConnection, polling diff --git a/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift index f4edfa3bf..84be45a39 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift @@ -1,6 +1,10 @@ import Foundation import OSLog +#if os(Linux) || os(Windows) +import FoundationNetworking +#endif + /// Defines the connection modes the SDK may be configured to use to retrieve feature flag data from LaunchDarkly. public enum LDStreamingMode { /** @@ -32,7 +36,10 @@ public enum LDStreamingMode { you can use targeting rules to enable "dark mode" for all customers who are using version 15 or greater, and ensure that customers on previous versions don't use the earlier, unfinished version of the feature. */ -@objc public enum AutoEnvAttributes: Int { +#if !os(Linux) && !os(Windows) +@objc +#endif +public enum AutoEnvAttributes: Int { /// Enables the Auto EnvironmentAttributes functionality. case enabled /// Disables the Auto EnvironmentAttributes functionality. @@ -439,8 +446,10 @@ public struct LDConfig { /// LaunchDarkly defined minima for selected configurable items public let minima: Minima + #if !os(Linux) && !os(Windows) /// An NSObject wrapper for the Swift LDConfig struct. Intended for use in mixed apps when Swift code needs to pass a config into an Objective-C method. public var objcLdConfig: ObjcLDConfig { ObjcLDConfig(self) } + #endif /// Initial set of hooks for the client. /// diff --git a/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift b/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift index 7d980c859..2d6158659 100644 --- a/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift +++ b/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift @@ -2,6 +2,10 @@ import Foundation import LDSwiftEventSource import OSLog +#if os(Linux) || os(Windows) +import FoundationNetworking +#endif + // swiftlint:disable:next large_tuple typealias ServiceResponse = (data: Data?, urlResponse: URLResponse?, error: Error?, etag: String?) typealias ServiceCompletionHandler = (ServiceResponse) -> Void @@ -70,11 +74,13 @@ final class DarklyService: DarklyServiceProvider { // URLSessionConfiguration is a class, but `.default` creates a new instance. This does not effect other session configuration. let sessionConfig = URLSessionConfiguration.default + #if !os(Linux) && !os(Windows) if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { sessionConfig.tlsMinimumSupportedProtocolVersion = .TLSv12 } else { sessionConfig.tlsMinimumSupportedProtocol = .tlsProtocol12 } + #endif // We always revalidate the cache which we handle manually sessionConfig.requestCachePolicy = .reloadIgnoringLocalCacheData diff --git a/LaunchDarkly/LaunchDarkly/Networking/HTTPURLRequest.swift b/LaunchDarkly/LaunchDarkly/Networking/HTTPURLRequest.swift index 4ece8e2b9..0d77c0b68 100644 --- a/LaunchDarkly/LaunchDarkly/Networking/HTTPURLRequest.swift +++ b/LaunchDarkly/LaunchDarkly/Networking/HTTPURLRequest.swift @@ -1,5 +1,9 @@ import Foundation +#if os(Linux) || os(Windows) +import FoundationNetworking +#endif + extension URLRequest { struct HTTPMethods { static let get = "GET" diff --git a/LaunchDarkly/LaunchDarkly/Networking/HTTPURLResponse.swift b/LaunchDarkly/LaunchDarkly/Networking/HTTPURLResponse.swift index d51d76af0..232ffc513 100644 --- a/LaunchDarkly/LaunchDarkly/Networking/HTTPURLResponse.swift +++ b/LaunchDarkly/LaunchDarkly/Networking/HTTPURLResponse.swift @@ -1,5 +1,9 @@ import Foundation +#if os(Linux) || os(Windows) +import FoundationNetworking +#endif + extension HTTPURLResponse { struct HeaderKeys { diff --git a/LaunchDarkly/LaunchDarkly/Networking/URLResponse.swift b/LaunchDarkly/LaunchDarkly/Networking/URLResponse.swift index 6469bd36e..1f00aa28f 100644 --- a/LaunchDarkly/LaunchDarkly/Networking/URLResponse.swift +++ b/LaunchDarkly/LaunchDarkly/Networking/URLResponse.swift @@ -1,5 +1,9 @@ import Foundation +#if os(Linux) || os(Windows) +import FoundationNetworking +#endif + extension URLResponse { var httpStatusCode: Int? { (self as? HTTPURLResponse)?.statusCode } var httpHeaderEtag: String? { (self as? HTTPURLResponse)?.headerEtag } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/CwlSysctl.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/CwlSysctl.swift index 457a66604..51e7f2f1f 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/CwlSysctl.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/CwlSysctl.swift @@ -1,3 +1,4 @@ +#if canImport(Darwin) // // CwlSysctl.swift // CwlUtils @@ -78,3 +79,4 @@ struct Sysctl { return try! Sysctl.stringForKeys([CTL_HW, HW_MODEL]) } } +#endif // canImport(Darwin) diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/DiagnosticReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/DiagnosticReporter.swift index 8970d1098..b13d0e96d 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/DiagnosticReporter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/DiagnosticReporter.swift @@ -1,6 +1,10 @@ import Foundation import OSLog +#if os(Linux) || os(Windows) +import FoundationNetworking +#endif + // sourcery: autoMockable protocol DiagnosticReporting { func setMode(_ runMode: LDClientRunMode, online: Bool) diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift index a72a79d0d..55d7d8d87 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift @@ -11,10 +11,10 @@ import UIKit #endif enum OperatingSystem: String { - case iOS, watchOS, macOS, tvOS, unknown + case iOS, watchOS, macOS, tvOS, windows, linux, unknown static var allOperatingSystems: [OperatingSystem] { - [.iOS, .watchOS, .macOS, .tvOS] + [.iOS, .watchOS, .macOS, .tvOS, .windows, .linux] } var isBackgroundEnabled: Bool { @@ -28,7 +28,7 @@ enum OperatingSystem: String { OperatingSystem.streamingEnabledOperatingSystems.contains(self) } static var streamingEnabledOperatingSystems: [OperatingSystem] { - [.iOS, .macOS, .tvOS] + [.iOS, .macOS, .tvOS, .windows] } } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/EnvironmentReporterBuilder.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/EnvironmentReporterBuilder.swift index 583ebb445..d28f65a1c 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/EnvironmentReporterBuilder.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/EnvironmentReporterBuilder.swift @@ -36,6 +36,8 @@ class EnvironmentReporterBuilder { reporters.append(MacOSEnvironmentReporter()) #elseif os(tvOS) reporters.append(TVOSEnvironmentReporter()) + #elseif os(Windows) + reporters.append(WindowsEnvironmentReporter()) #endif } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/SystemCapabilities.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/SystemCapabilities.swift index 61a4e97c5..8165aac13 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/SystemCapabilities.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/SystemCapabilities.swift @@ -29,5 +29,15 @@ class SystemCapabilities { static var foregroundNotification: Notification.Name? { UIApplication.willEnterForegroundNotification } static var systemName: String { UIDevice.current.systemName } static var operatingSystem: OperatingSystem { .tvOS } + #elseif os(Windows) + static var backgroundNotification: Notification.Name? { nil } + static var foregroundNotification: Notification.Name? { nil } + static var systemName: String { "Windows" } + static var operatingSystem: OperatingSystem { .windows } + #elseif os(Linux) + static var backgroundNotification: Notification.Name? { UIApplication.didEnterBackgroundNotification } + static var foregroundNotification: Notification.Name? { UIApplication.willEnterForegroundNotification } + static var systemName: String { "Linux" } + static var operatingSystem: OperatingSystem { .linux } #endif } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/WindowsEnvironmentReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/WindowsEnvironmentReporter.swift new file mode 100644 index 000000000..7af78b432 --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/WindowsEnvironmentReporter.swift @@ -0,0 +1,55 @@ +#if os(Windows) +import Foundation +import WinSDK + +class WindowsEnvironmentReporter: EnvironmentReporterChainBase { + override var applicationInfo: ApplicationInfo { + var info = ApplicationInfo() + info.applicationIdentifier(Bundle.main.object(forInfoDictionaryKey: "CFBundleIdentifier") as? String) + info.applicationVersion(Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String) + info.applicationName(Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String) + info.applicationVersionName(Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String) + + if info.applicationId == nil { + info = super.applicationInfo + } + return info + } + + override var manufacturer: String { + "unknown" + } + + override var systemVersion: String { + let version = ProcessInfo.processInfo.operatingSystemVersion + + return "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)" + } + + override var osFamily: String { + "Windows" + } + + override var deviceModel: String { + var status: SYSTEM_POWER_STATUS = .init() + if !GetSystemPowerStatus(&status) { + return "unknown" + } + + switch status.ACLineStatus { + // Not using AC power, probably a laptop on battery + case 0: + return "laptop" + // Using AC power, probably a laptop on AC power + case 1: + return "laptop" + // AC power status unknown, likely a desktop + case 255: + return "desktop" + // An unknown value, return unknown since we don't know + default: + return "unknown" + } + } +} +#endif diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift index e8d295928..60775124b 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EventReporter.swift @@ -1,6 +1,10 @@ import Foundation import OSLog +#if os(Linux) || os(Windows) +import FoundationNetworking +#endif + typealias EventSyncCompleteClosure = ((SynchronizingError?) -> Void) // sourcery: autoMockable protocol EventReporting { diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift index e25dcb1da..2ec4346ac 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagSynchronizer.swift @@ -3,6 +3,10 @@ import Dispatch import LDSwiftEventSource import OSLog +#if os(Linux) || os(Windows) +import FoundationNetworking +#endif + // sourcery: autoMockable protocol LDFlagSynchronizing { // sourcery: defaultMockValue = false @@ -163,7 +167,9 @@ class FlagSynchronizer: LDFlagSynchronizing, EventHandler { // signal completion immediately syncQueue.async { [self] in reportSyncComplete(.upToDate) } } - flagRequestTimer = LDTimer(withTimeInterval: pollingInterval, fireQueue: syncQueue, fireAt: fireAt, execute: processTimer) + flagRequestTimer = LDTimer(withTimeInterval: pollingInterval, fireQueue: syncQueue, fireAt: fireAt, execute: { [weak self] in + self?.processTimer() + }) os_log("%s", log: service.config.logger, type: .debug, typeName(and: #function)) } @@ -179,7 +185,7 @@ class FlagSynchronizer: LDFlagSynchronizing, EventHandler { flagRequestTimer = nil } - @objc private func processTimer() { + private func processTimer() { makeFlagRequest(isOnline: isOnline) } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/LDTimer.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/LDTimer.swift index 11e180bee..a38c28dae 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/LDTimer.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/LDTimer.swift @@ -22,9 +22,13 @@ final class LDTimer: TimeResponding { // the run loop retains the timer, so the property is weak to avoid a retain cycle. Setting the timer to a strong reference is important so that the timer doesn't get nil'd before it's added to the run loop. let timer: Timer if let at = fireAt { - timer = Timer(fireAt: at, interval: timeInterval, target: self, selector: #selector(timerFired), userInfo: nil, repeats: true) + timer = Timer(fire: at, interval: timeInterval, repeats: true, block: { [weak self] _ in + self?.timerFired() + }) } else { - timer = Timer(timeInterval: timeInterval, target: self, selector: #selector(timerFired), userInfo: nil, repeats: true) + timer = Timer(timeInterval: timeInterval, repeats: true, block: { [weak self] _ in + self?.timerFired() + }) } self.timer = timer RunLoop.main.add(timer, forMode: RunLoop.Mode.default) @@ -34,7 +38,7 @@ final class LDTimer: TimeResponding { timer?.invalidate() } - @objc private func timerFired() { + private func timerFired() { fireQueue.async { [weak self] in guard (self?.isCancelled ?? true) == false else { return } diff --git a/LaunchDarkly/LaunchDarkly/Util.swift b/LaunchDarkly/LaunchDarkly/Util.swift index aa7deeeea..0cfbe022a 100644 --- a/LaunchDarkly/LaunchDarkly/Util.swift +++ b/LaunchDarkly/LaunchDarkly/Util.swift @@ -1,4 +1,6 @@ +#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) import CommonCrypto +#endif import Foundation class Util { @@ -11,11 +13,18 @@ class Util { class func sha256(_ str: String) -> Data { let data = Data(str.utf8) + + #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) data.withUnsafeBytes { _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &digest) } return Data(digest) + #elseif os(Windows) + return data.sha256Digest + #else + fatalError("\(#function) is unimplemented!") + #endif } } @@ -28,3 +37,42 @@ extension String { return true } } + +#if os(Windows) +import WinSDK + +extension Data { + var sha256Digest: Data { + func handleError(_ status: WinSDK.NTSTATUS) { + // Failfast to mimic CryptoKit/Crypto semantics + if status < 0 { fatalError("Failed to create SHA256 digest using BCrypt APIs (NTSTATUS: \(status))") } + } + + let BCRYPT_SHA256_ALGORITHM = "SHA256" + + var algorithm: WinSDK.BCRYPT_ALG_HANDLE? + BCRYPT_SHA256_ALGORITHM.withCString(encodedAs: UTF16.self) { + handleError(WinSDK.BCryptOpenAlgorithmProvider(&algorithm, $0, nil, 0)) + } + defer { handleError(WinSDK.BCryptCloseAlgorithmProvider(algorithm, 0)) } + + var hash: BCRYPT_HASH_HANDLE? + handleError(WinSDK.BCryptCreateHash(algorithm, &hash, nil, 0, nil, 0, 0)) + defer { handleError(WinSDK.BCryptDestroyHash(hash)) } + + withUnsafeBytes { (buffer: UnsafeRawBufferPointer) in + let input = UnsafeMutablePointer(mutating: buffer.baseAddress?.bindMemory(to: UInt8.self, capacity: buffer.count)) + handleError(WinSDK.BCryptHashData(hash, input, ULONG(buffer.count), 0)) + } + + var result = Data(count: 32) // Size of SHA256 hash + result.withUnsafeMutableBytes { (buffer: UnsafeMutableRawBufferPointer) in + let output = buffer.baseAddress?.bindMemory(to: UInt8.self, capacity: buffer.count) + handleError(WinSDK.BCryptFinishHash(hash, output, ULONG(buffer.count), 0)) + } + + return result + } +} + +#endif diff --git a/LaunchDarkly/LaunchDarklyTests/Extensions/ThreadSpec.swift b/LaunchDarkly/LaunchDarklyTests/Extensions/ThreadSpec.swift index 37c67ad18..7193fd9b0 100644 --- a/LaunchDarkly/LaunchDarklyTests/Extensions/ThreadSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Extensions/ThreadSpec.swift @@ -4,11 +4,11 @@ import Nimble @testable import LaunchDarkly final class ThreadSpec: QuickSpec { - override func spec() { + override class func spec() { performOnMainSpec() } - private func performOnMainSpec() { + private class func performOnMainSpec() { var runCount = 0 var ranOnMainThread = false describe("performOnMain") { diff --git a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift index 7590ae2e4..78d983cc8 100644 --- a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift @@ -5,6 +5,10 @@ import Nimble import LDSwiftEventSource @testable import LaunchDarkly +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + final class LDClientSpec: QuickSpec { struct Constants { fileprivate static let alternateMockUrl = URL(string: "https://dummy.alternate.com")! @@ -13,7 +17,7 @@ final class LDClientSpec: QuickSpec { fileprivate static let updateThreshold: TimeInterval = 0.05 } - override func spec() { + override class func spec() { startSpec() moveToBackgroundSpec() identifySpec() @@ -32,7 +36,7 @@ final class LDClientSpec: QuickSpec { isInitializedSpec() } - private func startSpec() { + private class func startSpec() { describe("start") { startSpec(withTimeout: false) } @@ -62,7 +66,7 @@ final class LDClientSpec: QuickSpec { } } - private func startSpec(withTimeout: Bool) { + private class func startSpec(withTimeout: Bool) { var testContext: TestContext! context("when configured to start online") { @@ -252,7 +256,7 @@ final class LDClientSpec: QuickSpec { } } - func startCompletionSpec() { + class func startCompletionSpec() { var testContext: TestContext! var completed = false var didTimeOut: Bool? = nil @@ -329,21 +333,21 @@ final class LDClientSpec: QuickSpec { it("does complete without timeout") { testContext.start(completion: startCompletion) testContext.onSyncComplete?(.flagCollection((FeatureFlagCollection([:]), nil))) - expect(completed).toEventually(beTrue(), timeout: DispatchTimeInterval.seconds(2)) + expect(completed).toEventually(beTrue(), timeout: .seconds(2)) } it("does complete with timeout") { waitUntil(timeout: .seconds(3)) { done in testContext.start(timeOut: 5.0, timeOutCompletion: startTimeoutCompletion(done)) testContext.onSyncComplete?(.flagCollection((FeatureFlagCollection([:]), nil))) } - expect(completed).toEventually(beTrue(), timeout: DispatchTimeInterval.seconds(2)) + expect(completed).toEventually(beTrue(), timeout: .seconds(2)) expect(didTimeOut) == false } } } } - func moveToBackgroundSpec() { + class func moveToBackgroundSpec() { describe("moveToBackground") { var testContext: TestContext! context("when configured to allow background updates") { @@ -434,7 +438,7 @@ final class LDClientSpec: QuickSpec { } } - private func identifySpec() { + private class func identifySpec() { describe("identify") { it("when the client is online") { let testContext = TestContext(startOnline: true) @@ -578,7 +582,7 @@ final class LDClientSpec: QuickSpec { } } - private func setOnlineSpec() { + private class func setOnlineSpec() { describe("setOnline") { it("set online when the client is offline") { let testContext = TestContext() @@ -653,7 +657,7 @@ final class LDClientSpec: QuickSpec { } } - private func closeSpec() { + private class func closeSpec() { describe("stop") { it("when started and online") { let testContext = TestContext(startOnline: true) @@ -683,7 +687,7 @@ final class LDClientSpec: QuickSpec { } } - private func trackEventSpec() { + private class func trackEventSpec() { describe("track event") { it("records a custom event") { let testContext = TestContext() @@ -715,7 +719,7 @@ final class LDClientSpec: QuickSpec { } } - private func variationSpec() { + private class func variationSpec() { describe("variation") { var testContext: TestContext! beforeEach { @@ -802,10 +806,12 @@ final class LDClientSpec: QuickSpec { } } - private func observeSpec() { + private class func observeSpec() { + final class TestObserver {} var testContext: TestContext! var mockNotifier: FlagChangeNotifyingMock! var callCount: Int = 0 + var testObserver: TestObserver! describe("observe") { beforeEach { testContext = TestContext() @@ -813,68 +819,69 @@ final class LDClientSpec: QuickSpec { mockNotifier = FlagChangeNotifyingMock() testContext.subject.flagChangeNotifier = mockNotifier callCount = 0 + testObserver = TestObserver() } it("observe") { - testContext.subject.observe(key: "test-key", owner: self) { _ in callCount += 1 } + testContext.subject.observe(key: "test-key", owner: testObserver) { _ in callCount += 1 } let receivedObserver = mockNotifier.addFlagChangeObserverReceivedObserver expect(mockNotifier.addFlagChangeObserverCallCount) == 1 expect(receivedObserver?.flagKeys) == ["test-key"] - expect(receivedObserver?.owner) === self + expect(receivedObserver?.owner) === testObserver receivedObserver?.flagChangeHandler?(LDChangedFlag(key: "", oldValue: nil, newValue: nil)) expect(callCount) == 1 } it("observeKeys") { - testContext.subject.observe(keys: ["test-key"], owner: self) { _ in callCount += 1 } + testContext.subject.observe(keys: ["test-key"], owner: testObserver) { _ in callCount += 1 } let receivedObserver = mockNotifier.addFlagChangeObserverReceivedObserver expect(mockNotifier.addFlagChangeObserverCallCount) == 1 expect(receivedObserver?.flagKeys) == ["test-key"] - expect(receivedObserver?.owner) === self + expect(receivedObserver?.owner) === testObserver let changedFlags = ["test-key": LDChangedFlag(key: "", oldValue: nil, newValue: nil)] receivedObserver?.flagCollectionChangeHandler?(changedFlags) expect(callCount) == 1 } it("observeAll") { - testContext.subject.observeAll(owner: self) { _ in callCount += 1 } + testContext.subject.observeAll(owner: testObserver) { _ in callCount += 1 } let receivedObserver = mockNotifier.addFlagChangeObserverReceivedObserver expect(mockNotifier.addFlagChangeObserverCallCount) == 1 expect(receivedObserver?.flagKeys) == LDFlagKey.anyKey - expect(receivedObserver?.owner) === self + expect(receivedObserver?.owner) === testObserver let changedFlags = ["test-key": LDChangedFlag(key: "", oldValue: nil, newValue: nil)] receivedObserver?.flagCollectionChangeHandler?(changedFlags) expect(callCount) == 1 } it("observeFlagsUnchanged") { - testContext.subject.observeFlagsUnchanged(owner: self) { callCount += 1 } + testContext.subject.observeFlagsUnchanged(owner: testObserver) { callCount += 1 } let receivedObserver = mockNotifier.addFlagsUnchangedObserverReceivedObserver expect(mockNotifier.addFlagsUnchangedObserverCallCount) == 1 - expect(receivedObserver?.owner) === self + expect(receivedObserver?.owner) === testObserver receivedObserver?.flagsUnchangedHandler() expect(callCount) == 1 } it("observeConnectionModeChanged") { - testContext.subject.observeCurrentConnectionMode(owner: self) { _ in callCount += 1 } + testContext.subject.observeCurrentConnectionMode(owner: testObserver) { _ in callCount += 1 } let receivedObserver = mockNotifier.addConnectionModeChangedObserverReceivedObserver expect(mockNotifier.addConnectionModeChangedObserverCallCount) == 1 - expect(receivedObserver?.owner) === self + expect(receivedObserver?.owner) === testObserver receivedObserver?.connectionModeChangedHandler(ConnectionInformation.ConnectionMode.offline) expect(callCount) == 1 } it("stopObserving") { - testContext.subject.stopObserving(owner: self) + testContext.subject.stopObserving(owner: testObserver) expect(mockNotifier.removeObserverCallCount) == 1 - expect(mockNotifier.removeObserverReceivedOwner) === self + expect(mockNotifier.removeObserverReceivedOwner) === testObserver } } } - private func onSyncCompleteSpec() { + private class func onSyncCompleteSpec() { describe("on sync complete") { onSyncCompleteSuccessSpec() onSyncCompleteErrorSpec() } } - private func onSyncCompleteSuccessSpec() { + private class func onSyncCompleteSuccessSpec() { it("flag collection") { self.onSyncCompleteSuccessReplacingFlagsSpec() } @@ -886,7 +893,7 @@ final class LDClientSpec: QuickSpec { } } - private func onSyncCompleteSuccessReplacingFlagsSpec() { + private class func onSyncCompleteSuccessReplacingFlagsSpec() { let testContext = TestContext(startOnline: true) testContext.start() testContext.subject.flagChangeNotifier = ClientServiceMockFactory(config: testContext.config).makeFlagChangeNotifier() @@ -912,7 +919,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags) == [:] } - func onSyncCompleteStreamingPatchSpec() { + class func onSyncCompleteStreamingPatchSpec() { let stubFlags = FlagMaintainingMock.stubStoredItems() let testContext = TestContext(startOnline: true).withCached(flags: stubFlags.featureFlags) testContext.start() @@ -939,7 +946,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags == stubFlags.featureFlags).to(beTrue()) } - func onSyncCompleteDeleteFlagSpec() { + class func onSyncCompleteDeleteFlagSpec() { let stubFlags = FlagMaintainingMock.stubStoredItems() let testContext = TestContext(startOnline: true).withCached(flags: stubFlags.featureFlags) testContext.start() @@ -966,7 +973,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags == stubFlags.featureFlags).to(beTrue()) } - func onSyncCompleteErrorSpec() { + class func onSyncCompleteErrorSpec() { func runTest(_ ctx: String, _ err: SynchronizingError, testError: @escaping ((ConnectionInformation.LastConnectionFailureReason) -> Void)) { @@ -1010,7 +1017,7 @@ final class LDClientSpec: QuickSpec { runTest("there was a non-NSError error", .streamError(DummyError())) { _ in } } - private func runModeSpec() { + private class func runModeSpec() { describe("didEnterBackground notification") { context("after starting client") { context("when online") { @@ -1269,14 +1276,14 @@ final class LDClientSpec: QuickSpec { #endif } - private func streamingModeSpec() { + private class func streamingModeSpec() { var testContext: TestContext! describe("flag synchronizer streaming mode") { OperatingSystem.allOperatingSystems.forEach { os in it("on \(os) sets the flag synchronizer streaming mode") { // TODO(os-tests): We need to expand this to the other OSs - if os == .watchOS { + if os == .watchOS || os == .linux { return } @@ -1288,7 +1295,7 @@ final class LDClientSpec: QuickSpec { } } - private func flushSpec() { + private class func flushSpec() { describe("flush") { it("tells the event reporter to report events") { let testContext = TestContext() @@ -1299,7 +1306,7 @@ final class LDClientSpec: QuickSpec { } } - private func allFlagsSpec() { + private class func allFlagsSpec() { let stubFlags = FlagMaintainingMock.stubStoredItems() describe("allFlags") { it("returns all non-null flag values from store") { @@ -1316,7 +1323,7 @@ final class LDClientSpec: QuickSpec { } } - private func connectionInformationSpec() { + private class func connectionInformationSpec() { describe("ConnectionInformation") { it("when client was started in foreground") { let testContext = TestContext(startOnline: true) @@ -1338,7 +1345,7 @@ final class LDClientSpec: QuickSpec { } } - private func variationDetailSpec() { + private class func variationDetailSpec() { describe("variationDetail") { it("when flag doesn't exist") { let testContext = TestContext() @@ -1351,7 +1358,7 @@ final class LDClientSpec: QuickSpec { } } - private func isInitializedSpec() { + private class func isInitializedSpec() { describe("isInitialized") { it("when client was started but no flag update") { let testContext = TestContext(startOnline: true) @@ -1376,7 +1383,7 @@ final class LDClientSpec: QuickSpec { testContext.start() testContext.onSyncComplete?(.flagCollection((FeatureFlagCollection([:]), nil))) - expect(testContext.subject.isInitialized).toEventually(beTrue(), timeout: DispatchTimeInterval.seconds(2)) + expect(testContext.subject.isInitialized).toEventually(beTrue(), timeout: .seconds(2)) testContext.subject.close() expect(testContext.subject.isInitialized) == false diff --git a/LaunchDarkly/LaunchDarklyTests/Matcher/Match.swift b/LaunchDarkly/LaunchDarklyTests/Matcher/Match.swift new file mode 100644 index 000000000..5e6fd14ee --- /dev/null +++ b/LaunchDarkly/LaunchDarklyTests/Matcher/Match.swift @@ -0,0 +1,40 @@ +import Foundation +import Nimble + +/** + Used by the `toMatch` matcher. + + This is the return type for the closure. + */ +public enum ToMatchResult { + case matched + case failed(reason: String) +} + +/** + A Nimble matcher that takes in a closure for validation. + + Return `.matched` when the validation succeeds. + Return `.failed` with a failure reason when the validation fails. + */ +public func match() -> Matcher<() -> ToMatchResult> { + Matcher.define { actualExpression in + let optActual = try actualExpression.evaluate() + guard let actual = optActual else { + return MatcherResult(status: .fail, message: .fail("expected a closure, got ")) + } + + switch actual() { + case .matched: + return MatcherResult( + bool: true, + message: .expectedCustomValueTo("match", actual: "") + ) + case .failed(let reason): + return MatcherResult( + bool: false, + message: .expectedCustomValueTo("match", actual: " because <\(reason)>") + ) + } + } +} diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift index 88f4a14bd..5fb61b9a7 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift @@ -1,11 +1,17 @@ import Foundation import Quick import Nimble +#if !os(Linux) && !os(Windows) import OHHTTPStubs import OHHTTPStubsSwift +#endif import LDSwiftEventSource @testable import LaunchDarkly +#if os(Linux) || os(Windows) +import FoundationNetworking +#endif + final class DarklyServiceMock: DarklyServiceProvider { struct FlagKeys { static let bool = "bool-flag" @@ -46,7 +52,7 @@ final class DarklyServiceMock: DarklyServiceProvider { } struct Constants { - static let error = NSError(domain: NSURLErrorDomain, code: Int(CFNetworkErrors.cfurlErrorResourceUnavailable.rawValue), userInfo: nil) + static let error = NSError(domain: NSURLErrorDomain, code: Int(URLError.resourceUnavailable.rawValue), userInfo: nil) static let jsonErrorString = "Bad json data" static let errorData = jsonErrorString.data(using: .utf8)! @@ -97,7 +103,9 @@ final class DarklyServiceMock: DarklyServiceProvider { var context: LDContext var diagnosticCache: DiagnosticCaching? = nil + #if !os(Linux) && !os(Windows) var activationBlocks = [(testBlock: HTTPStubsTestBlock, callback: ((URLRequest, HTTPStubsDescriptor, HTTPStubsResponse) -> Void))]() + #endif init(config: LDConfig = LDConfig.stub, context: LDContext = LDContext.stub()) { self.config = config @@ -161,6 +169,8 @@ extension DarklyServiceMock { var flagHost: String? { config.baseUrl.host } + + #if !os(Linux) && !os(Windows) var flagRequestStubTest: HTTPStubsTestBlock { isScheme(Constants.schemeHttps) && isHost(flagHost!) } @@ -214,6 +224,7 @@ extension DarklyServiceMock { stubbedFlagResponse = (nil, response, Constants.error, nil) } } + #endif func flagStubName(statusCode: Int, useReport: Bool) -> String { "Flag request stub using method \(useReport ? URLRequest.HTTPMethods.report : URLRequest.HTTPMethods.get) with response status code \(statusCode)" @@ -224,6 +235,8 @@ extension DarklyServiceMock { var eventHost: String? { config.eventsUrl.host } + + #if !os(Windows) && !os(Linux) var eventRequestStubTest: HTTPStubsTestBlock { isScheme(Constants.schemeHttps) && isHost(eventHost!) && isMethodPOST() } @@ -239,6 +252,7 @@ extension DarklyServiceMock { activate?(request) } } + #endif /// Use when testing requires the mock service to provide a service response to the event request callback func stubEventResponse(success: Bool, responseOnly: Bool = false, errorOnly: Bool = false, responseDate: Date? = nil) { @@ -265,6 +279,7 @@ extension DarklyServiceMock { // MARK: Publish Diagnostic + #if !os(Linux) && !os(Windows) /// Use when testing requires the mock service to actually make an diagnostic request func stubDiagnosticRequest(success: Bool, onActivation activate: ((URLRequest, HTTPStubsDescriptor, HTTPStubsResponse) -> Void)? = nil) { let stubResponse: HTTPStubsResponseBlock = success ? { _ in @@ -297,8 +312,10 @@ extension DarklyServiceMock { return } } + #endif } +#if !os(Linux) && !os(Windows) /** * Matcher testing that the `NSURLRequest` is using the **REPORT** `HTTPMethod` * @@ -306,6 +323,7 @@ extension DarklyServiceMock { * is using the REPORT method */ public func isMethodREPORT() -> HTTPStubsTestBlock { { $0.httpMethod == URLRequest.HTTPMethods.report } } +#endif extension HTTPURLResponse { static func dateHeader(from date: Date?) -> [String: String]? { diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/LDEventSourceMock.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/LDEventSourceMock.swift index 0f08112ab..b9c57780c 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/LDEventSourceMock.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/LDEventSourceMock.swift @@ -1,5 +1,10 @@ import Foundation import LDSwiftEventSource + +#if os(Linux) || os(Windows) +import FoundationNetworking +#endif + @testable import LaunchDarkly extension EventHandler { diff --git a/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift index 7e74bb7d3..6afeb8ea4 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift @@ -50,7 +50,11 @@ final class LDContextCodableSpec: XCTestCase { let output = try jsonEncoder.encode(context) let outputJson = String(data: output, encoding: .utf8) - XCTAssertEqual(json, outputJson) + // ordering of the values in the string is not guaranteed, we should instead serialize the string + // back into a context and ensure that it matches the input context. + let contextFromOutput = try JSONDecoder().decode(LDContext.self, from: Data(try XCTUnwrap(outputJson).utf8)) + + XCTAssertEqual(context, contextFromOutput) } } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift index b76e4186e..835559fb8 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift @@ -5,7 +5,7 @@ import Nimble final class DiagnosticEventSpec: QuickSpec { - override func spec() { + override class func spec() { diagnosticIdSpec() diagnosticSdkSpec() diagnosticPlatformSpec() @@ -16,7 +16,7 @@ final class DiagnosticEventSpec: QuickSpec { diagnosticStatsSpec() } - private func diagnosticIdSpec() { + private class func diagnosticIdSpec() { context("DiagnosticId init") { context("with empty mobile key") { it("inits with correct values") { @@ -62,7 +62,7 @@ final class DiagnosticEventSpec: QuickSpec { } } - private func diagnosticSdkSpec() { + private class func diagnosticSdkSpec() { context("DiagnosticSdk") { context("without wrapper configured") { it("has correct values and encoding") { @@ -101,7 +101,7 @@ final class DiagnosticEventSpec: QuickSpec { } } - private func diagnosticPlatformSpec() { + private class func diagnosticPlatformSpec() { var environmentReporter: EnvironmentReportingMock! var diagnosticPlatform: DiagnosticPlatform! context("DiagnosticPlatform") { @@ -139,7 +139,7 @@ final class DiagnosticEventSpec: QuickSpec { } } - private func diagnosticStreamInitSpec() { + private class func diagnosticStreamInitSpec() { context("DiagnosticStreamInit") { it("inits with given values") { let streamInit = DiagnosticStreamInit(timestamp: 1000, durationMillis: 100, failed: true) @@ -169,7 +169,7 @@ final class DiagnosticEventSpec: QuickSpec { } } - private func customizedConfig() -> LDConfig { + private class func customizedConfig() -> LDConfig { var customConfig = LDConfig(mobileKey: "foobar", autoEnvAttributes: .disabled, isDebugBuild: true) customConfig.baseUrl = URL(string: "https://clientstream.launchdarkly.com")! customConfig.eventsUrl = URL(string: "https://app.launchdarkly.com")! @@ -192,7 +192,7 @@ final class DiagnosticEventSpec: QuickSpec { return customConfig } - private func diagnosticConfigSpec() { + private class func diagnosticConfigSpec() { let defaultConfig = LDConfig(mobileKey: "foobar", autoEnvAttributes: .disabled, isDebugBuild: true) let customConfig = customizedConfig() context("DiagnosticConfig") { @@ -311,7 +311,7 @@ final class DiagnosticEventSpec: QuickSpec { } } - private func diagnosticKindSpec() { + private class func diagnosticKindSpec() { context("DiagnosticKind") { // JSONEncoder will encode raw primitives on newer platforms, but not all supported platforms. For these // tests we wrap the kind in an object to allow us to test the encoding. @@ -334,7 +334,7 @@ final class DiagnosticEventSpec: QuickSpec { } } - private func diagnosticInitSpec() { + private class func diagnosticInitSpec() { let customConfig = customizedConfig() var now: Int64! var diagnosticId: DiagnosticId! @@ -376,7 +376,7 @@ final class DiagnosticEventSpec: QuickSpec { } } - private func diagnosticStatsSpec() { + private class func diagnosticStatsSpec() { var now: Int64! var diagnosticId: DiagnosticId! var diagnosticStats: DiagnosticStats! @@ -426,10 +426,10 @@ final class DiagnosticEventSpec: QuickSpec { } } - private let encoder = JSONEncoder() - private let decoder = JSONDecoder() + private static let encoder = JSONEncoder() + private static let decoder = JSONDecoder() - private func loadAndRestore(_ subject: T?) -> T? { + private class func loadAndRestore(_ subject: T?) -> T? { let encoded = try? encoder.encode(subject) return try? decoder.decode(T.self, from: encoded!) } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift index f5457044d..13d51d379 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift @@ -1,6 +1,10 @@ import Foundation import XCTest +#if os(Linux) || os(Windows) +import FoundationNetworking +#endif + @testable import LaunchDarkly final class LDConfigSpec: XCTestCase { diff --git a/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift b/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift index 19a917e48..affff3b87 100644 --- a/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift @@ -1,3 +1,4 @@ +#if !os(Linux) && !os(Windows) import Foundation import Quick import Nimble @@ -48,7 +49,7 @@ final class DarklyServiceSpec: QuickSpec { } } - override func spec() { + override class func spec() { getFeatureFlagsSpec() flagRequestEtagSpec() clearFlagRequestCacheSpec() @@ -62,7 +63,7 @@ final class DarklyServiceSpec: QuickSpec { } } - private func getFeatureFlagsSpec() { + private class func getFeatureFlagsSpec() { var testContext: TestContext! var requestEtag: String! @@ -410,7 +411,7 @@ final class DarklyServiceSpec: QuickSpec { } } - private func flagRequestEtagSpec() { + private class func flagRequestEtagSpec() { var originalFlagRequestEtag: String! var testContext: TestContext! describe("flagRequestEtag") { @@ -512,7 +513,7 @@ final class DarklyServiceSpec: QuickSpec { } } - private func clearFlagRequestCacheSpec() { + private class func clearFlagRequestCacheSpec() { describe("clearFlagResponseCache") { it("clears cached etag") { let testContext = TestContext() @@ -523,7 +524,7 @@ final class DarklyServiceSpec: QuickSpec { } } - private func createEventSourceSpec() { + private class func createEventSourceSpec() { var testContext: TestContext! describe("createEventSource") { @@ -543,7 +544,7 @@ final class DarklyServiceSpec: QuickSpec { let expectedContext = encodeToLDValue(testContext.context, userInfo: [LDContext.UserInfoKeys.includePrivateAttributes: true, LDContext.UserInfoKeys.redactAttributes: false]) expect(receivedArguments!.url.lastPathComponent.jsonValue) == expectedContext expect(receivedArguments!.httpHeaders).toNot(beEmpty()) - expect(receivedArguments!.connectMethod).to(be("GET")) + expect(receivedArguments!.connectMethod) == "GET" expect(receivedArguments!.connectBody).to(beNil()) } } @@ -568,7 +569,7 @@ final class DarklyServiceSpec: QuickSpec { } } - private func publishEventDataSpec() { + private class func publishEventDataSpec() { let testData = Data("abc".utf8) var testContext: TestContext! @@ -638,7 +639,7 @@ final class DarklyServiceSpec: QuickSpec { } } - private func diagnosticCacheSpec() { + private class func diagnosticCacheSpec() { describe("diagnosticCache") { it("does not create cache with empty mobile key") { let testContext = TestContext(mobileKey: "") @@ -659,11 +660,11 @@ final class DarklyServiceSpec: QuickSpec { } } - private func stubDiagnostic() -> DiagnosticStats { + private class func stubDiagnostic() -> DiagnosticStats { DiagnosticStats(id: DiagnosticId(diagnosticId: "test-id", sdkKey: LDConfig.Constants.mockMobileKey), creationDate: 1000, dataSinceDate: 100, droppedEvents: 0, eventsInLastBatch: 0, streamInits: []) } - private func publishDiagnosticSpec() { + private class func publishDiagnosticSpec() { var testContext: TestContext! describe("publishDiagnostic") { @@ -755,3 +756,4 @@ private extension String { return try? JSONDecoder().decode(LDValue.self, from: data) } } +#endif // !os(Linux) && !os(Windows) diff --git a/LaunchDarkly/LaunchDarklyTests/Networking/HTTPURLResponse.swift b/LaunchDarkly/LaunchDarklyTests/Networking/HTTPURLResponse.swift index ffb5910f2..b5c0ca0b0 100644 --- a/LaunchDarkly/LaunchDarklyTests/Networking/HTTPURLResponse.swift +++ b/LaunchDarkly/LaunchDarklyTests/Networking/HTTPURLResponse.swift @@ -1,6 +1,10 @@ import Foundation @testable import LaunchDarkly +#if os(Linux) || os(Windows) +import FoundationNetworking +#endif + extension HTTPURLResponse.StatusCodes { static let all = [ok, accepted, badRequest, unauthorized, methodNotAllowed, internalServerError, notImplemented] static let retry = LDConfig.reportRetryStatusCodes diff --git a/LaunchDarkly/LaunchDarklyTests/Networking/URLRequestSpec.swift b/LaunchDarkly/LaunchDarklyTests/Networking/URLRequestSpec.swift index 64471ad67..8c9cd8fd2 100644 --- a/LaunchDarkly/LaunchDarklyTests/Networking/URLRequestSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Networking/URLRequestSpec.swift @@ -1,6 +1,10 @@ import Foundation import XCTest +#if os(Linux) || os(Windows) +import FoundationNetworking +#endif + @testable import LaunchDarkly final class URLRequestSpec: XCTestCase { diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DiagnosticCacheSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DiagnosticCacheSpec.swift index 20e36eb13..212e5be61 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DiagnosticCacheSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/DiagnosticCacheSpec.swift @@ -4,7 +4,7 @@ import Nimble @testable import LaunchDarkly final class DiagnosticCacheSpec: QuickSpec { - override func spec() { + override class func spec() { context("DiagnosticCache") { getCurrentStatsAndResetSpec() incrementDroppedEventCountSpec() @@ -13,7 +13,7 @@ final class DiagnosticCacheSpec: QuickSpec { } } - private func getCurrentStatsAndResetSpec() { + private class func getCurrentStatsAndResetSpec() { context("getCurrentStatsAndReset") { it("has expected initial values") { let diagnosticCache = DiagnosticCache(sdkKey: "this_is_a_fake_key") @@ -55,7 +55,7 @@ final class DiagnosticCacheSpec: QuickSpec { } } - private func incrementDroppedEventCountSpec() { + private class func incrementDroppedEventCountSpec() { context("incrementDroppedEventCount") { it("increments dropped event count") { let diagnosticCache = DiagnosticCache(sdkKey: "this_is_a_fake_key") @@ -94,7 +94,7 @@ final class DiagnosticCacheSpec: QuickSpec { } } - private func recordEventsInLastBatchSpec() { + private class func recordEventsInLastBatchSpec() { context("recordEventsInLastBatch") { it("sets events in last batch") { let diagnosticCache = DiagnosticCache(sdkKey: "this_is_a_fake_key") @@ -116,7 +116,7 @@ final class DiagnosticCacheSpec: QuickSpec { } } - private func addStreamInitSpec() { + private class func addStreamInitSpec() { context("addStreamInit") { it("adds a stream init") { let diagnosticCache = DiagnosticCache(sdkKey: "this_is_a_fake_key") @@ -179,4 +179,5 @@ final class DiagnosticCacheSpec: QuickSpec { } } } + } diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift index 7987e267c..166deedab 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/FeatureFlagCacheSpec.swift @@ -18,9 +18,13 @@ final class FeatureFlagCacheSpec: XCTestCase { let flagCache = FeatureFlagCache(serviceFactory: serviceFactory, mobileKey: "abc", maxCachedContexts: 2) XCTAssertEqual(flagCache.maxCachedContexts, 2) XCTAssertEqual(serviceFactory.makeKeyedValueCacheCallCount, 1) - let bundleHashed = Util.sha256base64(Bundle.main.bundleIdentifier!) let keyHashed = Util.sha256base64("abc") + #if !os(Linux) && !os(Windows) + let bundleHashed = Util.sha256base64(Bundle.main.bundleIdentifier!) let expectedCacheKey = "com.launchdarkly.client.\(bundleHashed).\(keyHashed)" + #else + let expectedCacheKey = "com.launchdarkly.client.\(keyHashed)" + #endif XCTAssertEqual(serviceFactory.makeKeyedValueCacheReceivedCacheKey, expectedCacheKey) XCTAssertTrue(flagCache.keyedValueCache as? KeyedValueCachingMock === mockValueCache) } diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/DiagnosticReporterSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/DiagnosticReporterSpec.swift index 38e4b9d9d..fa34703f5 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/DiagnosticReporterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/DiagnosticReporterSpec.swift @@ -1,6 +1,11 @@ +#if !os(Linux) && !os(Windows) import Foundation import XCTest +#if os(Linux) || os(Windows) +import FoundationNetworking +#endif + @testable import LaunchDarkly final class DiagnosticReporterSpec: XCTestCase { @@ -118,3 +123,4 @@ final class DiagnosticReporterSpec: XCTestCase { tst.stop() } } +#endif diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporting/WindowsEnvironmentReporterSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporting/WindowsEnvironmentReporterSpec.swift new file mode 100644 index 000000000..5caf52530 --- /dev/null +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporting/WindowsEnvironmentReporterSpec.swift @@ -0,0 +1,44 @@ +#if os(Windows) +import Foundation +import WinSDK +import XCTest + +@testable import LaunchDarkly + +final class WindowsEnvironmentReporterSpec: XCTestCase { + func testDefaultReporterBehavior() { + let chain = EnvironmentReporterChainBase() + chain.setNext(WindowsEnvironmentReporter()) + + XCTAssertEqual(chain.osFamily, "Windows") + XCTAssertEqual(chain.deviceModel, expectedDeviceModel()) + XCTAssertEqual(chain.manufacturer, "unknown") + + let version = chain.systemVersion + let split = version.split(separator: ".") + XCTAssertEqual(split.count, 3) + } + + private func expectedDeviceModel() -> String { + var status: SYSTEM_POWER_STATUS = .init() + if !GetSystemPowerStatus(&status) { + return "unknown" + } + + switch status.ACLineStatus { + // Not using AC power, probably a laptop on battery + case 0: + return "laptop" + // Using AC power, probably a laptop on AC power + case 1: + return "laptop" + // AC power status unknown, likely a desktop + case 255: + return "desktop" + // An unknown value, return unknown since we don't know + default: + return "unknown" + } + } +} +#endif diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift index 588418717..92419f551 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EventReporterSpec.swift @@ -3,6 +3,10 @@ import Quick import Nimble @testable import LaunchDarkly +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + final class EventReporterSpec: QuickSpec { struct Constants { static let eventFlushInterval: TimeInterval = 10.0 @@ -62,7 +66,7 @@ final class EventReporterSpec: QuickSpec { } } - override func spec() { + override class func spec() { initSpec() isOnlineSpec() recordEventSpec() @@ -71,7 +75,7 @@ final class EventReporterSpec: QuickSpec { reportTimerSpec() } - private func initSpec() { + private class func initSpec() { describe("init") { var testContext: TestContext! beforeEach { @@ -87,7 +91,7 @@ final class EventReporterSpec: QuickSpec { } } - private func isOnlineSpec() { + private class func isOnlineSpec() { describe("isOnline") { var testContext: TestContext! beforeEach { @@ -163,7 +167,7 @@ final class EventReporterSpec: QuickSpec { } } - private func recordEventSpec() { + private class func recordEventSpec() { describe("recordEvent") { var testContext: TestContext! context("event store empty") { @@ -202,7 +206,7 @@ final class EventReporterSpec: QuickSpec { } } - private func reportEventsSpec() { + private class func reportEventsSpec() { describe("reportEvents") { var testContext: TestContext! var eventStubResponseDate: Date! @@ -474,7 +478,7 @@ final class EventReporterSpec: QuickSpec { } } - func testRecordFlagEvaluationEvents() { + class func testRecordFlagEvaluationEvents() { let context = LDContext.stub() let serviceMock = DarklyServiceMock() describe("recordFlagEvaluationEvents") { @@ -610,7 +614,7 @@ final class EventReporterSpec: QuickSpec { } } - private func reportTimerSpec() { + private class func reportTimerSpec() { describe("report timer fires") { var testContext: TestContext! afterEach { diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagChangeNotifierSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagChangeNotifierSpec.swift index 5c4bbe7b0..408c501b4 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagChangeNotifierSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagChangeNotifierSpec.swift @@ -134,7 +134,7 @@ final class FlagChangeNotifierSpec: QuickSpec { } } - override func spec() { + override class func spec() { describe("init") { it("no initial observers") { let notifier = FlagChangeNotifier(logger: OSLog(subsystem: "com.launchdarkly", category: "tests")) @@ -149,7 +149,7 @@ final class FlagChangeNotifierSpec: QuickSpec { notifyConnectionSpec() } - private func removeObserverSpec() { + private class func removeObserverSpec() { describe("removeObserver") { var testContext: TestContext! var removedOwner: FlagChangeHandlerOwnerMock! @@ -197,7 +197,7 @@ final class FlagChangeNotifierSpec: QuickSpec { } } - private func notifyObserverSpec() { + private class func notifyObserverSpec() { describe("notifyObservers") { var testContext: TestContext! beforeEach { @@ -395,7 +395,7 @@ final class FlagChangeNotifierSpec: QuickSpec { } } - private func notifyConnectionSpec() { + private class func notifyConnectionSpec() { describe("notifyConnectionModeChangedObservers") { it("removes and does not notify expired observers") { let testContext = TestContext() diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagSynchronizerSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagSynchronizerSpec.swift index 1421827d5..03ce239f9 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagSynchronizerSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagSynchronizerSpec.swift @@ -4,6 +4,10 @@ import Nimble import LDSwiftEventSource @testable import LaunchDarkly +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + final class FlagSynchronizerSpec: QuickSpec { struct Constants { fileprivate static let pollingInterval: TimeInterval = 1 @@ -34,7 +38,7 @@ final class FlagSynchronizerSpec: QuickSpec { } } - override func spec() { + override class func spec() { initSpec() changeIsOnlineSpec() streamingEventSpec() @@ -42,7 +46,7 @@ final class FlagSynchronizerSpec: QuickSpec { flagRequestSpec() } - func initSpec() { + class func initSpec() { describe("init") { it("starts up streaming offline using get flag requests") { let testContext = TestContext(streamingMode: .streaming, useReport: false) @@ -91,7 +95,7 @@ final class FlagSynchronizerSpec: QuickSpec { } } - func changeIsOnlineSpec() { + class func changeIsOnlineSpec() { describe("change isOnline") { var testContext: TestContext! @@ -242,7 +246,7 @@ final class FlagSynchronizerSpec: QuickSpec { } } - func streamingEventSpec() { + class func streamingEventSpec() { describe("streaming events") { streamingPingEventSpec() streamingPutEventSpec() @@ -253,7 +257,7 @@ final class FlagSynchronizerSpec: QuickSpec { } } - func streamingPingEventSpec() { + class func streamingPingEventSpec() { var testContext: TestContext! context("ping") { context("success") { @@ -264,7 +268,9 @@ final class FlagSynchronizerSpec: QuickSpec { syncResult = result done() } + #if !os(Linux) && !os(Windows) testContext.serviceMock.stubFlagResponse(statusCode: HTTPURLResponse.StatusCodes.ok) + #endif testContext.flagSynchronizer.isOnline = true testContext.providedEventHandler!.sendPing() } @@ -291,7 +297,9 @@ final class FlagSynchronizerSpec: QuickSpec { } done() } + #if !os(Linux) && !os(Windows) testContext.serviceMock.stubFlagResponse(statusCode: HTTPURLResponse.StatusCodes.ok, badData: true) + #endif testContext.flagSynchronizer.isOnline = true testContext.providedEventHandler!.sendPing() @@ -320,7 +328,9 @@ final class FlagSynchronizerSpec: QuickSpec { } done() } + #if !os(Linux) && !os(Windows) testContext.serviceMock.stubFlagResponse(statusCode: HTTPURLResponse.StatusCodes.internalServerError, responseOnly: true) + #endif testContext.flagSynchronizer.isOnline = true testContext.providedEventHandler!.sendPing() @@ -349,7 +359,9 @@ final class FlagSynchronizerSpec: QuickSpec { } done() } + #if !os(Linux) && !os(Windows) testContext.serviceMock.stubFlagResponse(statusCode: HTTPURLResponse.StatusCodes.internalServerError, errorOnly: true) + #endif testContext.flagSynchronizer.isOnline = true testContext.providedEventHandler!.sendPing() @@ -372,7 +384,7 @@ final class FlagSynchronizerSpec: QuickSpec { } } - func streamingPutEventSpec() { + class func streamingPutEventSpec() { var testContext: TestContext! var syncResult: FlagSyncResult? context("put") { @@ -428,7 +440,7 @@ final class FlagSynchronizerSpec: QuickSpec { } } - func streamingPatchEventSpec() { + class func streamingPatchEventSpec() { var testContext: TestContext! var syncResult: FlagSyncResult? context("patch") { @@ -486,7 +498,7 @@ final class FlagSynchronizerSpec: QuickSpec { } } - func streamingDeleteEventSpec() { + class func streamingDeleteEventSpec() { var testContext: TestContext! beforeEach { @@ -550,7 +562,7 @@ final class FlagSynchronizerSpec: QuickSpec { } } - func streamingOtherEventSpec() { + class func streamingOtherEventSpec() { var syncError: SynchronizingError? context("other events") { @@ -795,7 +807,7 @@ final class FlagSynchronizerSpec: QuickSpec { } } - private func streamingProcessingSpec() { + private class func streamingProcessingSpec() { var testContext: TestContext! var syncError: SynchronizingError? @@ -880,7 +892,7 @@ final class FlagSynchronizerSpec: QuickSpec { } } - func pollingTimerFiresSpec() { + class func pollingTimerFiresSpec() { describe("polling timer fires") { var testContext: TestContext! @@ -927,7 +939,7 @@ final class FlagSynchronizerSpec: QuickSpec { } } - func flagRequestSpec() { + class func flagRequestSpec() { describe("flag request") { var testContext: TestContext! context("using get method") { @@ -935,7 +947,9 @@ final class FlagSynchronizerSpec: QuickSpec { it("requests flags using a get request exactly one time") { waitUntil { done in testContext = TestContext(streamingMode: .streaming, useReport: false) { _ in done() } + #if !os(Linux) && !os(Windows) testContext.serviceMock.stubFlagResponse(statusCode: HTTPURLResponse.StatusCodes.ok) + #endif testContext.flagSynchronizer.isOnline = true testContext.providedEventHandler!.sendPing() @@ -951,7 +965,9 @@ final class FlagSynchronizerSpec: QuickSpec { for statusCode in HTTPURLResponse.StatusCodes.nonRetry { waitUntil { done in testContext = TestContext(streamingMode: .streaming, useReport: false) { _ in done() } + #if !os(Linux) && !os(Windows) testContext.serviceMock.stubFlagResponse(statusCode: statusCode) + #endif testContext.flagSynchronizer.isOnline = true testContext.providedEventHandler!.sendPing() @@ -967,7 +983,9 @@ final class FlagSynchronizerSpec: QuickSpec { for statusCode in HTTPURLResponse.StatusCodes.retry { waitUntil { done in testContext = TestContext(streamingMode: .streaming, useReport: false) { _ in done() } + #if !os(Linux) && !os(Windows) testContext.serviceMock.stubFlagResponse(statusCode: statusCode) + #endif testContext.flagSynchronizer.isOnline = true testContext.providedEventHandler!.sendPing() @@ -985,7 +1003,9 @@ final class FlagSynchronizerSpec: QuickSpec { it("requests flags using a get request exactly one time") { waitUntil { done in testContext = TestContext(streamingMode: .streaming, useReport: true) { _ in done() } + #if !os(Linux) && !os(Windows) testContext.serviceMock.stubFlagResponse(statusCode: HTTPURLResponse.StatusCodes.ok) + #endif testContext.flagSynchronizer.isOnline = true testContext.providedEventHandler!.sendPing() @@ -1001,7 +1021,9 @@ final class FlagSynchronizerSpec: QuickSpec { for statusCode in HTTPURLResponse.StatusCodes.nonRetry { waitUntil { done in testContext = TestContext(streamingMode: .streaming, useReport: true) { _ in done() } + #if !os(Linux) && !os(Windows) testContext.serviceMock.stubFlagResponse(statusCode: statusCode) + #endif testContext.flagSynchronizer.isOnline = true testContext.providedEventHandler!.sendPing() @@ -1017,7 +1039,9 @@ final class FlagSynchronizerSpec: QuickSpec { for statusCode in HTTPURLResponse.StatusCodes.retry { waitUntil { done in testContext = TestContext(streamingMode: .streaming, useReport: true) { _ in done() } + #if !os(Linux) && !os(Windows) testContext.serviceMock.stubFlagResponse(statusCode: statusCode) + #endif testContext.flagSynchronizer.isOnline = true testContext.providedEventHandler!.sendPing() @@ -1045,7 +1069,10 @@ final class FlagSynchronizerSpec: QuickSpec { } done() } + + #if !os(Linux) && !os(Windows) testContext.serviceMock.stubFlagResponse(statusCode: HTTPURLResponse.StatusCodes.ok) + #endif testContext.flagSynchronizer.testMakeFlagRequest() } diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/LDTimerSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/LDTimerSpec.swift index 88baefc9d..03d79080e 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/LDTimerSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/LDTimerSpec.swift @@ -18,13 +18,13 @@ final class LDTimerSpec: QuickSpec { } } - override func spec() { + override class func spec() { initSpec() timerFiredSpec() cancelSpec() } - private func initSpec() { + private class func initSpec() { describe("init") { it("creates a repeating timer") { let testContext = TestContext(execute: { }) @@ -38,7 +38,7 @@ final class LDTimerSpec: QuickSpec { } } - private func timerFiredSpec() { + private class func timerFiredSpec() { describe("timerFired") { it("calls execute on the fireQueue multiple times") { var fireCount = 0 @@ -66,7 +66,7 @@ final class LDTimerSpec: QuickSpec { } } - private func cancelSpec() { + private class func cancelSpec() { describe("cancel") { it("cancels the timer") { let testContext = TestContext(execute: { }) diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/SynchronizingErrorSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/SynchronizingErrorSpec.swift index 94dae68da..72eb27909 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/SynchronizingErrorSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/SynchronizingErrorSpec.swift @@ -1,6 +1,10 @@ import Foundation import XCTest +#if os(Linux) || os(Windows) +import FoundationNetworking +#endif + import LDSwiftEventSource @testable import LaunchDarkly diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/ThrottlerSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/ThrottlerSpec.swift index f3781457d..70fd22f8d 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/ThrottlerSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/ThrottlerSpec.swift @@ -10,22 +10,22 @@ final class ThrottlerSpec: QuickSpec { static let maxDelay: TimeInterval = 10.0 } - let dispatchQueue = DispatchQueue(label: "ThrottlerSpecQueue") + static let dispatchQueue = DispatchQueue(label: "ThrottlerSpecQueue") - func testThrottler(throttlingDisabled: Bool = false) -> Throttler { + class func testThrottler(throttlingDisabled: Bool = false) -> Throttler { return Throttler(logger: OSLog(subsystem: "com.launchdarkly", category: "tests"), maxDelay: Constants.maxDelay, isDebugBuild: throttlingDisabled, dispatcher: { self.dispatchQueue.sync(execute: $0) }) } - override func spec() { + override class func spec() { initSpec() runSpec() cancelSpec() } - func initSpec() { + class func initSpec() { describe("init") { it("with a maxDelay parameter") { let throttler = Throttler(logger: OSLog(subsystem: "com.launchdarkly", category: "tests"), maxDelay: Constants.maxDelay) @@ -46,7 +46,7 @@ final class ThrottlerSpec: QuickSpec { } } - func runSpec() { + class func runSpec() { describe("runThrottled") { context("throttling enabled") { firstRunsSpec() @@ -60,7 +60,7 @@ final class ThrottlerSpec: QuickSpec { } } - func firstRunsSpec() { + class func firstRunsSpec() { it("first runs immediate") { var hasRun = false let throttler = self.testThrottler() @@ -80,7 +80,7 @@ final class ThrottlerSpec: QuickSpec { } } - func immediateAfterDelaySpec() { + class func immediateAfterDelaySpec() { it("delay resets throttling") { let throttler = self.testThrottler() // First two run immediate @@ -99,7 +99,7 @@ final class ThrottlerSpec: QuickSpec { } } - func throttledRunSpec() { + class func throttledRunSpec() { it("sequential calls are throttled") { let throttler = self.testThrottler() // First two run immediate @@ -119,7 +119,7 @@ final class ThrottlerSpec: QuickSpec { } } - func maxDelaySpec() { + class func maxDelaySpec() { it("limits delay to maxDelay") { let throttler = Throttler(logger: OSLog(subsystem: "com.launchdarkly", category: "tests"), maxDelay: 1.0, isDebugBuild: false) (0..<10).forEach { _ in throttler.runThrottled { } } @@ -138,7 +138,7 @@ final class ThrottlerSpec: QuickSpec { } } - func throttlingDisabledRunSpec() { + class func throttlingDisabledRunSpec() { it("never throttles") { let throttler = self.testThrottler(throttlingDisabled: true) for _ in 0..<5 { @@ -152,7 +152,7 @@ final class ThrottlerSpec: QuickSpec { } } - func cancelSpec() { + class func cancelSpec() { it("can be cancelled") { let throttler = self.testThrottler() // Two immediate runs diff --git a/Package.swift b/Package.swift index 87d81d6a8..12d426489 100644 --- a/Package.swift +++ b/Package.swift @@ -2,6 +2,15 @@ import PackageDescription +// Temporarily needed to keep the Windows SPM build under the symbol count. +let linkType: Product.Library.LibraryType = { + #if os(Windows) + .dynamic + #else + .static + #endif +}() + let package = Package( name: "LaunchDarkly", platforms: [ @@ -13,12 +22,13 @@ let package = Package( products: [ .library( name: "LaunchDarkly", + type: linkType, targets: ["LaunchDarkly"]), ], dependencies: [ .package(url: "https://github.com/AliSoftware/OHHTTPStubs.git", .exact("9.1.0")), - .package(url: "https://github.com/Quick/Quick.git", .exact("4.0.0")), - .package(url: "https://github.com/Quick/Nimble.git", .exact("9.2.1")), + .package(url: "https://github.com/Quick/Quick.git", .exact("7.3.0")), + .package(url: "https://github.com/Quick/Nimble.git", .exact("13.0.0")), .package(url: "https://github.com/mattgallagher/CwlPreconditionTesting", .exact("2.1.2")), .package(name: "LDSwiftEventSource", url: "https://github.com/LaunchDarkly/swift-eventsource.git", .exact("3.3.0")), ], @@ -29,21 +39,42 @@ let package = Package( .product(name: "LDSwiftEventSource", package: "LDSwiftEventSource"), ], path: "LaunchDarkly/LaunchDarkly", - exclude: ["Support"], + exclude: osSpecificExcludes(), resources: [ .process("PrivacyInfo.xcprivacy") ]), .testTarget( name: "LaunchDarklyTests", - dependencies: [ - "LaunchDarkly", - .product(name: "OHHTTPStubsSwift", package: "OHHTTPStubs"), - .product(name: "Quick", package: "Quick"), - .product(name: "CwlPreconditionTesting", package: "CwlPreconditionTesting"), - .product(name: "Nimble", package: "Nimble") - ], + dependencies: osSpecificLDTestsDependencies(), path: "LaunchDarkly", exclude: ["LaunchDarklyTests/Info.plist", "LaunchDarklyTests/.swiftlint.yml"], sources: ["GeneratedCode", "LaunchDarklyTests"]), ], swiftLanguageVersions: [.v5]) + +func osSpecificLDTestsDependencies() -> [Target.Dependency] { + #if os(Linux) || os(Windows) + [ + "LaunchDarkly", + .product(name: "Quick", package: "Quick"), + .product(name: "Nimble", package: "Nimble") + ] + #else + [ + "LaunchDarkly", + .product(name: "OHHTTPStubsSwift", package: "OHHTTPStubs"), + .product(name: "Quick", package: "Quick"), + .product(name: "CwlPreconditionTesting", package: "CwlPreconditionTesting"), + .product(name: "Nimble", package: "Nimble") + ] + #endif +} + +func osSpecificExcludes() -> [String] { + var exclusions = ["Support"] + #if os(Linux) || os(Windows) + exclusions.append("ObjectiveC") + #endif + + return exclusions +} From ab8f7b746ed57d2affad782257aa6a39908d58ae Mon Sep 17 00:00:00 2001 From: Brian Michel Date: Fri, 19 Jan 2024 20:17:31 -0500 Subject: [PATCH 2/3] Integrate the AnyURLSession library (#3) (cherry picked from commit f5b7abf0f8fbdad4022779a79a2281ce00f68a63) --- .../LaunchDarkly/Networking/DarklyService.swift | 14 ++++++++++---- Package.swift | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift b/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift index 2d6158659..a42bac436 100644 --- a/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift +++ b/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift @@ -3,7 +3,12 @@ import LDSwiftEventSource import OSLog #if os(Linux) || os(Windows) -import FoundationNetworking +import class FoundationNetworking.URLResponse +import class FoundationNetworking.HTTPURLResponse +import struct FoundationNetworking.URLRequest + +import class FoundationNetworking.URLSessionConfiguration +import AnyURLSession #endif // swiftlint:disable:next large_tuple @@ -120,9 +125,10 @@ final class DarklyService: DarklyServiceProvider { } self.session.dataTask(with: request) { [weak self] data, response, error in - DispatchQueue.main.async { - self?.processEtag(from: (data: data, urlResponse: response, error: error, etag: self?.flagRequestEtag)) - completion?((data: data, urlResponse: response, error: error, etag: self?.flagRequestEtag)) + DispatchQueue.main.async { [weak self] in + let etag = self?.flagRequestEtag + self?.processEtag(from: (data: data, urlResponse: response, error: error, etag: etag)) + completion?((data: data, urlResponse: response, error: error, etag: etag)) } }.resume() } diff --git a/Package.swift b/Package.swift index 12d426489..c30995a44 100644 --- a/Package.swift +++ b/Package.swift @@ -30,7 +30,7 @@ let package = Package( .package(url: "https://github.com/Quick/Quick.git", .exact("7.3.0")), .package(url: "https://github.com/Quick/Nimble.git", .exact("13.0.0")), .package(url: "https://github.com/mattgallagher/CwlPreconditionTesting", .exact("2.1.2")), - .package(name: "LDSwiftEventSource", url: "https://github.com/LaunchDarkly/swift-eventsource.git", .exact("3.3.0")), + .package(name: "LDSwiftEventSource", url: "https://github.com/thebrowsercompany/swift-eventsource.git", .branchItem("main-bcny")), ], targets: [ .target( From e9477d92e9a471b171cdc43ef4e0f2e5defd94d2 Mon Sep 17 00:00:00 2001 From: Brian Michel Date: Wed, 10 Jun 2026 15:51:59 -0400 Subject: [PATCH 3/3] Fix v11 Windows replay build issues --- .../LaunchDarkly/ServiceObjects/ClientServiceFactory.swift | 1 - LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift | 7 +++---- .../LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift | 6 +++++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift index 79b162f5e..fcef52ff1 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift @@ -89,7 +89,6 @@ final class ClientServiceFactory: ClientServiceCreating { config.headerTransform = { delegate?(url, $0) ?? $0 } config.headers = httpHeaders config.method = connectMethod - config.logger = self.logger if let errorHandler = errorHandler { config.connectionErrorHandler = errorHandler } diff --git a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift index 78d983cc8..47d8a94f3 100644 --- a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift @@ -699,16 +699,15 @@ final class LDClientSpec: QuickSpec { expect(receivedEvent?.data) == "abc" expect(receivedEvent?.metricValue) == 5.0 } - context("does not record when events are turned off") { + it("does not record when events are turned off") { var config = LDConfig.stub(mobileKey: LDConfig.Constants.mockMobileKey, autoEnvAttributes: .disabled, isDebugBuild: false) config.sendEvents = false let testContext = TestContext(newConfig: config) testContext.start() - let priorRecordedEvents = testContext.eventReporterMock.recordCallCount + expect(testContext.subject.eventReporter is NullEventReporter) == true testContext.subject.track(key: "customEvent", data: "abc", metricValue: 5.0) - expect(testContext.eventReporterMock.recordCallCount) == priorRecordedEvents } - context("does not record when client was stopped") { + it("does not record when client was stopped") { let testContext = TestContext() testContext.start() testContext.subject.close() diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift index d954e4fd9..65ce5008a 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift @@ -76,7 +76,11 @@ final class ClientServiceMockFactory: ClientServiceCreating { makeEventReporterReceivedService = service onEventSyncComplete = onSyncComplete - return EventReportingMock() + if config.sendEvents { + return EventReportingMock() + } else { + return NullEventReporter() + } } func makeEventReporter(config: LDConfig, service: DarklyServiceProvider) -> EventReporting {