From 44cab1eabf89a9345f332588010faadb01b6e5d0 Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Thu, 16 May 2024 08:16:39 -0700 Subject: [PATCH] Add Windows support (#348) * Everything compiles * Remove SSH agent * Add windows vendor and tests * Update windows ci and leave comment * Disable testing and leave comment * Use spaces * Update json encoder version * Make `shared()` public. * disable windows test suite temporarily * Updated tests for windows compat * another test update * turn win test runner back on * try dynamic lib on windows. * try a different build runner * try some settings-fu * and again ... * disable tests for Windows. * Added link to issue. * Update gitignore. --------- Co-authored-by: Brian Michel Co-authored-by: Alan --- .github/workflows/swift.yml | 19 + .gitignore | 6 +- .../other_plugins/NotificationTracking.swift | 5 +- Package.resolved | 4 +- Package.swift | 6 +- Sources/Segment/Analytics.swift | 78 ++-- Sources/Segment/Configuration.swift | 36 +- Sources/Segment/ObjC/ObjCAnalytics.swift | 30 +- Sources/Segment/ObjC/ObjCConfiguration.swift | 21 +- Sources/Segment/ObjC/ObjCEvents.swift | 116 ++--- Sources/Segment/ObjC/ObjCPlugin.swift | 15 +- .../Platforms/Vendors/WindowsUtils.swift | 75 ++++ .../Segment/Plugins/SegmentDestination.swift | 32 +- Sources/Segment/Startup.swift | 27 +- Sources/Segment/Utilities/Atomic.swift | 10 +- .../Utilities/Networking/HTTPClient.swift | 40 +- .../Segment/Utilities/OutputFileStream.swift | 18 +- Sources/Segment/Utilities/Utils.swift | 2 +- Tests/Segment-Tests/Analytics_Tests.swift | 408 ++++++++---------- Tests/Segment-Tests/HTTPClient_Tests.swift | 2 +- Tests/Segment-Tests/MemoryLeak_Tests.swift | 32 +- Tests/Segment-Tests/ObjC_Tests.swift | 80 ++-- Tests/Segment-Tests/StressTests.swift | 12 +- .../Segment-Tests/Support/TestUtilities.swift | 40 +- .../WindowsVendorSystem_Tests.swift | 38 ++ 25 files changed, 633 insertions(+), 519 deletions(-) create mode 100644 Sources/Segment/Plugins/Platforms/Vendors/WindowsUtils.swift create mode 100644 Tests/Segment-Tests/WindowsVendorSystem_Tests.swift diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index cded69c6..e3fed886 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -47,6 +47,25 @@ jobs: - name: Run tests run: swift test --enable-test-discovery + build_and_test_spm_windows: + needs: cancel_previous + runs-on: windows-latest + steps: + - uses: SwiftyLab/setup-swift@latest + with: + swift-version: "5.10" + - uses: actions/checkout@v2 + - name: Build + run: swift build + # + # Disable tests right now. There's an SPM issue where link errors generate + # a bad exit code even though the tests run/work properly. + # + # See: https://forums.swift.org/t/linker-warnings-on-windows-with-swift-argument-parser/71443/2 + # + # - name: Run tests + # run: swift test --enable-test-discovery + build_and_test_ios: needs: cancel_previous runs-on: macos-14 diff --git a/.gitignore b/.gitignore index a2da4812..87e4c200 100644 --- a/.gitignore +++ b/.gitignore @@ -92,11 +92,13 @@ iOSInjectionProject/ Package.resolved *.xcuserdatad /.swiftpm/xcode/xcshareddata - -# XCFramework Segment-Package_XCFramework *.zip Segment.xcframework.zip Segment.xcframework XCFrameworkOutput *.sha256 +.fleet +.idea +.vscode +.editorconfig diff --git a/Examples/other_plugins/NotificationTracking.swift b/Examples/other_plugins/NotificationTracking.swift index 8346b1ec..294adbab 100644 --- a/Examples/other_plugins/NotificationTracking.swift +++ b/Examples/other_plugins/NotificationTracking.swift @@ -33,7 +33,7 @@ // MARK: Common -#if !os(Linux) && !os(macOS) +#if !os(Linux) && !os(macOS) && !os(Windows) import Foundation import Segment @@ -41,7 +41,7 @@ import Segment class NotificationTracking: Plugin { var type: PluginType = .utility weak var analytics: Analytics? - + func trackNotification(_ properties: [String: Codable], fromLaunch launch: Bool) { if launch { analytics?.track(name: "Push Notification Tapped", properties: properties) @@ -95,4 +95,3 @@ extension NotificationTracking: iOSLifecycle { } #endif - diff --git a/Package.resolved b/Package.resolved index 413ef649..e051e505 100644 --- a/Package.resolved +++ b/Package.resolved @@ -12,7 +12,7 @@ { "identity" : "sovran-swift", "kind" : "remoteSourceControl", - "location" : "https://github.com/segmentio/Sovran-Swift.git", + "location" : "https://github.com/segmentio/sovran-swift.git", "state" : { "revision" : "a342b905f6baa64499cabdf61ccc185ec476b7b2", "version" : "1.1.1" @@ -20,4 +20,4 @@ } ], "version" : 2 -} +} \ No newline at end of file diff --git a/Package.swift b/Package.swift index 173bdb9c..70fc4b79 100644 --- a/Package.swift +++ b/Package.swift @@ -33,9 +33,11 @@ let package = Package( .product(name: "Sovran", package: "sovran-swift"), .product(name: "JSONSafeEncoding", package: "jsonsafeencoding-swift") ], - resources: [.process("Resources")]), + resources: [.process("Resources")] + ), .testTarget( name: "Segment-Tests", - dependencies: ["Segment"]), + dependencies: ["Segment"] + ), ] ) diff --git a/Sources/Segment/Analytics.swift b/Sources/Segment/Analytics.swift index feff5d50..f296cc5c 100644 --- a/Sources/Segment/Analytics.swift +++ b/Sources/Segment/Analytics.swift @@ -20,22 +20,23 @@ public class Analytics { } internal var store: Store internal var storage: Storage - + /// Enabled/disables debug logging to trace your data going through the SDK. public static var debugLogsEnabled = false - + public var timeline: Timeline - + static internal let deadInstance = "DEADINSTANCE" static internal weak var firstInstance: Analytics? = nil + @Atomic static internal var activeWriteKeys = [String]() - + /** This method isn't a traditional singleton implementation. It's provided here to ease migration from analytics-ios to analytics-swift. Rather than return a singleton, it returns the first instance of Analytics created, OR an instance who's writekey is "DEADINSTANCE". - + In the case of a dead instance, an assert will be thrown when in DEBUG builds to assist developers in knowning that `shared()` is being called too soon. */ @@ -45,16 +46,16 @@ public class Analytics { return a } } - + #if DEBUG if isUnitTesting == false { assert(true == false, "An instance of Analytice does not exist!") } #endif - + return Analytics(configuration: Configuration(writeKey: deadInstance)) } - + /// Initialize this instance of Analytics with a given configuration setup. /// - Parameters: /// - configuration: The configuration to use @@ -75,15 +76,15 @@ public class Analytics { operatingMode: configuration.values.operatingMode ) timeline = Timeline() - + // provide our default state store.provide(state: System.defaultState(configuration: configuration, from: storage)) store.provide(state: UserInfo.defaultState(from: storage, anonIdGenerator: configuration.values.anonymousIdGenerator)) storage.analytics = self - + checkSharedInstance() - + // Get everything running platformStartup() } @@ -91,24 +92,24 @@ public class Analytics { deinit { Self.removeActiveWriteKey(configuration.values.writeKey) } - + internal func process(incomingEvent: E) { guard enabled == true else { return } let event = incomingEvent.applyRawEventData(store: store) - + _ = timeline.process(incomingEvent: event) - + let flushPolicies = configuration.values.flushPolicies for policy in flushPolicies { policy.updateState(event: event) - + if (policy.shouldFlush() == true) { flush() policy.reset() } } } - + /// Process a raw event through the system. Useful when one needs to queue and replay events at a later time. /// - Parameters: /// - event: An event conforming to RawEvent that will be processed. @@ -160,7 +161,7 @@ extension Analytics { } return "" } - + /// Returns the userId that was specified in the last identify call. public var userId: String? { if let userInfo: UserInfo = store.currentState() { @@ -168,12 +169,12 @@ extension Analytics { } return nil } - + /// Returns the current operating mode this instance was given. public var operatingMode: OperatingMode { return configuration.values.operatingMode } - + /// Adjusts the flush interval post configuration. public var flushInterval: TimeInterval { get { @@ -186,7 +187,7 @@ extension Analytics { } } } - + /// Adjusts the flush-at count post configuration. public var flushAt: Int { get { @@ -199,14 +200,14 @@ extension Analytics { } } } - + /// Returns a list of currently active flush policies. public var flushPolicies: [FlushPolicy] { get { configuration.values.flushPolicies } } - + /// Returns the traits that were specified in the last identify call. public func traits() -> T? { if let userInfo: UserInfo = store.currentState() { @@ -214,7 +215,7 @@ extension Analytics { } return nil } - + /// Returns the traits that were specified in the last identify call, as a dictionary. public func traits() -> [String: Any]? { if let userInfo: UserInfo = store.currentState() { @@ -222,7 +223,7 @@ extension Analytics { } return nil } - + /// Tells this instance of Analytics to flush any queued events up to Segment.com. This command will also /// be sent to each plugin present in the system. A completion handler can be optionally given and will be /// called when flush has completed. @@ -247,7 +248,7 @@ extension Analytics { completion?() } } - + /// Resets this instance of Analytics to a clean slate. Traits, UserID's, anonymousId, etc are all cleared or reset. This /// command will also be sent to each plugin present in the system. public func reset() { @@ -258,13 +259,13 @@ extension Analytics { } } } - + /// Retrieve the version of this library in use. /// - Returns: A string representing the version in "BREAKING.FEATURE.FIX" format. public func version() -> String { return Analytics.version() } - + /// Retrieve the version of this library in use. /// - Returns: A string representing the version in "BREAKING.FEATURE.FIX" format. public static func version() -> String { @@ -282,7 +283,7 @@ extension Analytics { } return settings } - + /// Manually enable a destination plugin. This is useful when a given DestinationPlugin doesn't have any Segment tie-ins at all. /// This will allow the destination to be processed in the same way within this library. /// - Parameters: @@ -306,17 +307,17 @@ extension Analytics { return storage.dataStore.hasData } - + /// Provides a list of finished, but unsent events. public var pendingUploads: [URL]? { return storage.read(Storage.Constants.events)?.dataFiles } - + /// Purge all pending event upload files. public func purgeStorage() { storage.dataStore.reset() } - + /// Purge a single event upload file. public func purgeStorage(fileURL: URL) { guard let dataFiles = storage.read(Storage.Constants.events)?.dataFiles else { return } @@ -324,7 +325,7 @@ extension Analytics { try? FileManager.default.removeItem(at: fileURL) } } - + /// Wait until the Analytics object has completed startup. /// This method is primarily useful for command line utilities where /// it's desirable to wait until the system is up and running @@ -344,7 +345,7 @@ extension Analytics { Call openURL as needed or when instructed to by either UIApplicationDelegate or UISceneDelegate. This is necessary to track URL referrers across events. This method will also iterate any plugins that are watching for openURL events. - + Example: ``` func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { @@ -359,12 +360,12 @@ extension Analytics { guard let dict = jsonProperties.dictionaryValue else { return } openURL(url, options: dict) } - + /** Call openURL as needed or when instructed to by either UIApplicationDelegate or UISceneDelegate. This is necessary to track URL referrers across events. This method will also iterate any plugins that are watching for openURL events. - + Example: ``` func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { @@ -375,14 +376,14 @@ extension Analytics { */ public func openURL(_ url: URL, options: [String: Any] = [:]) { store.dispatch(action: UserInfo.SetReferrerAction(url: url)) - + // let any conforming plugins know apply { plugin in if let p = plugin as? OpeningURLs { p.openURL(url, options: options) } } - + var jsonProperties: JSON? = nil if let json = try? JSON(options) { jsonProperties = json @@ -411,7 +412,7 @@ extension Analytics { Self.firstInstance = self } } - + /// Determines if an instance is dead. internal var isDead: Bool { return configuration.values.writeKey == Self.deadInstance @@ -455,4 +456,3 @@ extension OperatingMode { } } } - diff --git a/Sources/Segment/Configuration.swift b/Sources/Segment/Configuration.swift index e6bca8d7..b061c680 100644 --- a/Sources/Segment/Configuration.swift +++ b/Sources/Segment/Configuration.swift @@ -7,7 +7,7 @@ import Foundation import JSONSafeEncoding -#if os(Linux) +#if os(Linux) || os(Windows) import FoundationNetworking #endif @@ -28,7 +28,7 @@ public enum OperatingMode { case synchronous /// The operation of the Analytics client are asynchronous. case asynchronous - + static internal let defaultQueue = DispatchQueue(label: "com.segment.operatingModeQueue", qos: .utility) } @@ -69,7 +69,7 @@ public class Configuration { var anonymousIdGenerator: AnonymousIdGenerator = SegmentAnonymousId() var httpSession: (() -> any HTTPSession) = HTTPSessions.urlSession } - + internal var values: Values /// Initialize a configuration object to pass along to an Analytics instance. @@ -83,7 +83,7 @@ public class Configuration { settings.integrations = try? JSON([ "Segment.io": true ]) - + self.defaultSettings(settings) } } @@ -92,7 +92,7 @@ public class Configuration { // MARK: - Analytics Configuration public extension Configuration { - + /// Sets a reference to your application. This can be useful in instances /// where referring back to your application is necessary, such as within plugins /// or async code. The default value is `nil`. @@ -104,7 +104,7 @@ public extension Configuration { values.application = value return self } - + /// Opt-in/out of tracking lifecycle events. The default value is `false`. /// /// - Parameter enabled: A bool value @@ -114,7 +114,7 @@ public extension Configuration { values.trackApplicationLifecycleEvents = enabled return self } - + /// Set the number of events necessary to automatically flush. The default /// value is `20`. /// @@ -125,7 +125,7 @@ public extension Configuration { values.flushAt = count return self } - + /// Set a time interval (in seconds) by which to trigger an automatic flush. /// The default value is `30`. /// @@ -136,7 +136,7 @@ public extension Configuration { values.flushInterval = interval return self } - + /// Sets a default set of Settings. Normally these will come from Segment's /// api.segment.com/v1/projects//settings, however in instances such /// as first app launch, it can be useful to have a pre-set batch of settings to @@ -160,7 +160,7 @@ public extension Configuration { values.defaultSettings = settings return self } - + /// Enable/Disable the automatic adding of Segment as a destination. /// This can be useful in instances such as Consent Management, or in device /// mode only setups. The default value is `true`. @@ -172,7 +172,7 @@ public extension Configuration { values.autoAddSegmentDestination = value return self } - + /// Sets an alternative API host. This is useful when a proxy is in use, or /// events need to be routed to certain locales at all times (such as the EU). /// The default value is `api.segment.io/v1`. @@ -184,7 +184,7 @@ public extension Configuration { values.apiHost = value return self } - + /// Sets an alternative CDN host for settings retrieval. This is useful when /// a proxy is in use, or settings need to be queried from certain locales at /// all times (such as the EU). The default value is `cdn-settings.segment.com/v1`. @@ -196,7 +196,7 @@ public extension Configuration { values.cdnHost = value return self } - + /// Sets a block to be used when generating outgoing HTTP requests. Useful in /// proxying, or adding additional header information for outbound traffic. /// @@ -207,7 +207,7 @@ public extension Configuration { values.requestFactory = value return self } - + /// Sets an error handler to be called when errors are encountered by the Segment /// library. See `AnalyticsError` for a list of possible errors that can be /// encountered. @@ -219,13 +219,13 @@ public extension Configuration { values.errorHandler = value return self } - + @discardableResult func flushPolicies(_ policies: [FlushPolicy]) -> Configuration { values.flushPolicies = policies return self } - + /// Informs the Analytics instance of its operating mode/context. /// Use `.server` when operating in a web service, or when synchronous operation /// is desired. Use `.client` when operating in a long lived process, @@ -235,7 +235,7 @@ public extension Configuration { values.operatingMode = mode return self } - + /// Specify a custom queue to use when performing a flush operation. The default /// value is a Segment owned background queue. @discardableResult @@ -250,7 +250,7 @@ public extension Configuration { values.userAgent = userAgent return self } - + /// This option specifies how NaN/Infinity are handled when encoding JSON. /// The default is .zero. See JSONSafeEncoder.NonConformingFloatEncodingStrategy for more informatino. @discardableResult diff --git a/Sources/Segment/ObjC/ObjCAnalytics.swift b/Sources/Segment/ObjC/ObjCAnalytics.swift index d5883ec0..39a2bf71 100644 --- a/Sources/Segment/ObjC/ObjCAnalytics.swift +++ b/Sources/Segment/ObjC/ObjCAnalytics.swift @@ -1,11 +1,11 @@ // // ObjCAnalytics.swift -// +// // // Created by Cody Garvin on 6/10/21. // -#if !os(Linux) +#if !os(Linux) && !os(Windows) import Foundation import JSONSafeEncoding @@ -16,12 +16,12 @@ import JSONSafeEncoding public class ObjCAnalytics: NSObject { /// The underlying Analytics object we're working with public let analytics: Analytics - + @objc public init(configuration: ObjCConfiguration) { self.analytics = Analytics(configuration: configuration.configuration) } - + /// Get a workable ObjC instance by wrapping a Swift instance /// Useful when you want additional flexibility or to share /// a single instance between ObjC<>Swift. @@ -38,7 +38,7 @@ extension ObjCAnalytics { public func track(name: String) { track(name: name, properties: nil) } - + @objc(track:properties:) public func track(name: String, properties: [String: Any]?) { @@ -77,7 +77,7 @@ extension ObjCAnalytics { analytics.process(incomingEvent: event) } } - + /// Track a screen change with a title, category and other properties. /// - Parameters: /// - title: The title of the screen being tracked. @@ -85,7 +85,7 @@ extension ObjCAnalytics { public func screen(title: String) { screen(title: title, category: nil, properties: nil) } - + /// Track a screen change with a title, category and other properties. /// - Parameters: /// - title: The title of the screen being tracked. @@ -120,7 +120,7 @@ extension ObjCAnalytics { public func group(groupId: String, traits: [String: Any]?) { analytics.group(groupId: groupId, traits: traits) } - + @objc(alias:) /// The alias method is used to merge two user identities, effectively connecting two sets of user data /// as one. This is an advanced method, but it is required to manage user identities successfully in some of our destinations. @@ -139,27 +139,27 @@ extension ObjCAnalytics { public var anonymousId: String { return analytics.anonymousId } - + @objc public var userId: String? { return analytics.userId } - + @objc public func traits() -> [String: Any]? { return analytics.traits() } - + @objc public func flush() { analytics.flush() } - + @objc public func reset() { analytics.reset() } - + @objc public func settings() -> [String: Any]? { var result: [String: Any]? = nil @@ -177,12 +177,12 @@ extension ObjCAnalytics { } return result } - + @objc public func openURL(_ url: URL, options: [String: Any] = [:]) { analytics.openURL(url, options: options) } - + @objc public func version() -> String { return analytics.version() diff --git a/Sources/Segment/ObjC/ObjCConfiguration.swift b/Sources/Segment/ObjC/ObjCConfiguration.swift index 87117991..af1abcd8 100644 --- a/Sources/Segment/ObjC/ObjCConfiguration.swift +++ b/Sources/Segment/ObjC/ObjCConfiguration.swift @@ -1,11 +1,11 @@ // // ObjCConfiguration.swift -// +// // // Created by Brandon Sneed on 8/13/21. // -#if !os(Linux) +#if !os(Linux) && !os(Windows) import Foundation import JSONSafeEncoding @@ -13,7 +13,7 @@ import JSONSafeEncoding @objc(SEGConfiguration) public class ObjCConfiguration: NSObject { internal var configuration: Configuration - + /// Sets a reference to your application. This can be useful in instances /// where referring back to your application is necessary, such as within plugins /// or async code. The default value is `nil`. @@ -26,7 +26,7 @@ public class ObjCConfiguration: NSObject { configuration.application(value) } } - + /// Opt-in/out of tracking lifecycle events. The default value is `false`. @objc public var trackApplicationLifecycleEvents: Bool { @@ -37,7 +37,7 @@ public class ObjCConfiguration: NSObject { configuration.trackApplicationLifecycleEvents(value) } } - + /// Set the number of events necessary to automatically flush. The default /// value is `20`. @objc @@ -49,7 +49,7 @@ public class ObjCConfiguration: NSObject { configuration.flushAt(value) } } - + /// Set a time interval (in seconds) by which to trigger an automatic flush. /// The default value is `30`. @objc @@ -61,7 +61,7 @@ public class ObjCConfiguration: NSObject { configuration.flushInterval(value) } } - + /// Sets a default set of Settings. Normally these will come from Segment's /// api.segment.com/v1/projects//settings, however in instances such /// as first app launch, it can be useful to have a pre-set batch of settings to @@ -98,7 +98,7 @@ public class ObjCConfiguration: NSObject { } } } - + /// Enable/Disable the automatic adding of Segment as a destination. /// This can be useful in instances such as Consent Management, or in device /// mode only setups. The default value is `true`. @@ -111,7 +111,7 @@ public class ObjCConfiguration: NSObject { configuration.autoAddSegmentDestination(value) } } - + /// Sets an alternative API host. This is useful when a proxy is in use, or /// events need to be routed to certain locales at all times (such as the EU). /// The default value is `api.segment.io/v1`. @@ -137,7 +137,7 @@ public class ObjCConfiguration: NSObject { configuration.cdnHost(value) } } - + /// Sets a block to be used when generating outgoing HTTP requests. Useful in /// proxying, or adding additional header information for outbound traffic. /// @@ -163,4 +163,3 @@ public class ObjCConfiguration: NSObject { } #endif - diff --git a/Sources/Segment/ObjC/ObjCEvents.swift b/Sources/Segment/ObjC/ObjCEvents.swift index 019a7b6a..ca5d48f5 100644 --- a/Sources/Segment/ObjC/ObjCEvents.swift +++ b/Sources/Segment/ObjC/ObjCEvents.swift @@ -1,11 +1,11 @@ // // ObjCEvents.swift -// +// // // Created by Brandon Sneed on 4/17/23. // -#if !os(Linux) +#if !os(Linux) && !os(Windows) import Foundation @@ -17,27 +17,27 @@ internal protocol ObjCEvent { @objc(SEGDestinationMetadata) public class ObjCDestinationMetadata: NSObject { internal var _metadata: DestinationMetadata - + public var bundled: [String] { get { return _metadata.bundled } set(v) { _metadata.bundled = v } } - + public var unbundled: [String] { get { return _metadata.unbundled } set(v) { _metadata.unbundled = v } } - + public var bundledIds: [String] { get { return _metadata.bundledIds } set(v) { _metadata.bundledIds = v } } - + internal init?(_metadata: DestinationMetadata?) { guard let m = _metadata else { return nil } self._metadata = m } - + init(bundled: [String], unbundled: [String], bundledIds: [String]) { _metadata = DestinationMetadata(bundled: bundled, unbundled: unbundled, bundledIds: bundledIds) } @@ -50,7 +50,7 @@ public protocol ObjCRawEvent: NSObjectProtocol { var timestamp: String? { get } var anonymousId: String? { get set } var userId: String? { get set } - + var context: [String: Any]? { get set } var integrations: [String: Any]? { get set } @@ -83,46 +83,46 @@ internal func objcEventFromEvent(_ event: T?) -> ObjCRawEvent? { @objc(SEGTrackEvent) public class ObjCTrackEvent: NSObject, ObjCEvent, ObjCRawEvent { internal var _event: TrackEvent - + // RawEvent components - + public var type: String? { return _event.type } public var messageId: String? { return _event.messageId } public var timestamp: String? { return _event.timestamp } - + public var anonymousId: String? { get { return _event.anonymousId } set(v) { _event.anonymousId = v} } - + public var userId: String? { get { return _event.anonymousId } set(v) { _event.anonymousId = v} } - + public var context: [String: Any]? { get { return _event.context?.dictionaryValue } set(v) { _event.context = try? JSON(nilOrObject: v)} } - + public var integrations: [String: Any]? { get { return _event.context?.dictionaryValue } set(v) { _event.context = try? JSON(nilOrObject: v)} } - + public var metadata: ObjCDestinationMetadata? { get { return ObjCDestinationMetadata(_metadata: _event._metadata) } set(v) { _event._metadata = v?._metadata } } - + // Event Specific - + @objc public var event: String { get { return _event.event } set(v) { _event.event = v } } - + @objc public var properties: [String: Any]? { get { return _event.properties?.dictionaryValue } @@ -133,7 +133,7 @@ public class ObjCTrackEvent: NSObject, ObjCEvent, ObjCRawEvent { public init(name: String, properties: [String: Any]? = nil) { _event = TrackEvent(event: name, properties: try? JSON(nilOrObject: properties)) } - + internal init(event: EventType) { self._event = event } @@ -142,9 +142,9 @@ public class ObjCTrackEvent: NSObject, ObjCEvent, ObjCRawEvent { @objc(SEGIdentifyEvent) public class ObjCIdentifyEvent: NSObject, ObjCEvent, ObjCRawEvent { internal var _event: IdentifyEvent - + // RawEvent components - + public var type: String? { return _event.type } public var messageId: String? { return _event.messageId } public var timestamp: String? { return _event.timestamp } @@ -153,29 +153,29 @@ public class ObjCIdentifyEvent: NSObject, ObjCEvent, ObjCRawEvent { get { return _event.anonymousId } set(v) { _event.anonymousId = v} } - + public var userId: String? { get { return _event.anonymousId } set(v) { _event.anonymousId = v} } - + public var context: [String: Any]? { get { return _event.context?.dictionaryValue } set(v) { _event.context = try? JSON(nilOrObject: v)} } - + public var integrations: [String: Any]? { get { return _event.context?.dictionaryValue } set(v) { _event.context = try? JSON(nilOrObject: v)} } - + public var metadata: ObjCDestinationMetadata? { get { return ObjCDestinationMetadata(_metadata: _event._metadata) } set(v) { _event._metadata = v?._metadata } } - + // Event Specific - + @objc public var traits: [String: Any]? { get { return _event.traits?.dictionaryValue } @@ -186,7 +186,7 @@ public class ObjCIdentifyEvent: NSObject, ObjCEvent, ObjCRawEvent { public init(userId: String, traits: [String: Any]? = nil) { _event = IdentifyEvent(userId: userId, traits: try? JSON(nilOrObject: traits)) } - + internal init(event: EventType) { self._event = event } @@ -195,9 +195,9 @@ public class ObjCIdentifyEvent: NSObject, ObjCEvent, ObjCRawEvent { @objc(SEGScreenEvent) public class ObjCScreenEvent: NSObject, ObjCEvent, ObjCRawEvent { internal var _event: ScreenEvent - + // RawEvent components - + public var type: String? { return _event.type } public var messageId: String? { return _event.messageId } public var timestamp: String? { return _event.timestamp } @@ -206,41 +206,41 @@ public class ObjCScreenEvent: NSObject, ObjCEvent, ObjCRawEvent { get { return _event.anonymousId } set(v) { _event.anonymousId = v} } - + public var userId: String? { get { return _event.anonymousId } set(v) { _event.anonymousId = v} } - + public var context: [String: Any]? { get { return _event.context?.dictionaryValue } set(v) { _event.context = try? JSON(nilOrObject: v)} } - + public var integrations: [String: Any]? { get { return _event.context?.dictionaryValue } set(v) { _event.context = try? JSON(nilOrObject: v)} } - + public var metadata: ObjCDestinationMetadata? { get { return ObjCDestinationMetadata(_metadata: _event._metadata) } set(v) { _event._metadata = v?._metadata } } - + // Event Specific - + @objc public var name: String? { get { return _event.name } set(v) { _event.name = v} } - + @objc public var category: String? { get { return _event.category } set(v) { _event.category = v} } - + @objc public var properties: [String: Any]? { get { return _event.properties?.dictionaryValue } @@ -251,7 +251,7 @@ public class ObjCScreenEvent: NSObject, ObjCEvent, ObjCRawEvent { public init(name: String, category: String?, properties: [String: Any]? = nil) { _event = ScreenEvent(title: name, category: category, properties: try? JSON(nilOrObject: properties)) } - + internal init(event: EventType) { self._event = event } @@ -260,9 +260,9 @@ public class ObjCScreenEvent: NSObject, ObjCEvent, ObjCRawEvent { @objc(SEGGroupEvent) public class ObjCGroupEvent: NSObject, ObjCEvent, ObjCRawEvent { internal var _event: GroupEvent - + // RawEvent components - + public var type: String? { return _event.type } public var messageId: String? { return _event.messageId } public var timestamp: String? { return _event.timestamp } @@ -271,35 +271,35 @@ public class ObjCGroupEvent: NSObject, ObjCEvent, ObjCRawEvent { get { return _event.anonymousId } set(v) { _event.anonymousId = v} } - + public var userId: String? { get { return _event.anonymousId } set(v) { _event.anonymousId = v} } - + public var context: [String: Any]? { get { return _event.context?.dictionaryValue } set(v) { _event.context = try? JSON(nilOrObject: v)} } - + public var integrations: [String: Any]? { get { return _event.context?.dictionaryValue } set(v) { _event.context = try? JSON(nilOrObject: v)} } - + public var metadata: ObjCDestinationMetadata? { get { return ObjCDestinationMetadata(_metadata: _event._metadata) } set(v) { _event._metadata = v?._metadata } } - + // Event Specific - + @objc public var groupId: String? { get { return _event.groupId } set(v) { _event.groupId = v} } - + @objc public var traits: [String: Any]? { get { return _event.traits?.dictionaryValue } @@ -310,7 +310,7 @@ public class ObjCGroupEvent: NSObject, ObjCEvent, ObjCRawEvent { public init(groupId: String?, traits: [String: Any]? = nil) { _event = GroupEvent(groupId: groupId, traits: try? JSON(nilOrObject: traits)) } - + internal init(event: EventType) { self._event = event } @@ -319,9 +319,9 @@ public class ObjCGroupEvent: NSObject, ObjCEvent, ObjCRawEvent { @objc(SEGAliasEvent) public class ObjCAliasEvent: NSObject, ObjCEvent, ObjCRawEvent { internal var _event: AliasEvent - + // RawEvent components - + public var type: String? { return _event.type } public var messageId: String? { return _event.messageId } public var timestamp: String? { return _event.timestamp } @@ -330,29 +330,29 @@ public class ObjCAliasEvent: NSObject, ObjCEvent, ObjCRawEvent { get { return _event.anonymousId } set(v) { _event.anonymousId = v} } - + public var userId: String? { get { return _event.anonymousId } set(v) { _event.anonymousId = v} } - + public var context: [String: Any]? { get { return _event.context?.dictionaryValue } set(v) { _event.context = try? JSON(nilOrObject: v)} } - + public var integrations: [String: Any]? { get { return _event.context?.dictionaryValue } set(v) { _event.context = try? JSON(nilOrObject: v)} } - + public var metadata: ObjCDestinationMetadata? { get { return ObjCDestinationMetadata(_metadata: _event._metadata) } set(v) { _event._metadata = v?._metadata } } - + // Event Specific - + @objc public var previousId: String? { get { return _event.previousId } @@ -363,7 +363,7 @@ public class ObjCAliasEvent: NSObject, ObjCEvent, ObjCRawEvent { public init(newId: String?) { _event = AliasEvent(newId: newId) } - + internal init(event: EventType) { self._event = event } diff --git a/Sources/Segment/ObjC/ObjCPlugin.swift b/Sources/Segment/ObjC/ObjCPlugin.swift index 1396d670..0db9312c 100644 --- a/Sources/Segment/ObjC/ObjCPlugin.swift +++ b/Sources/Segment/ObjC/ObjCPlugin.swift @@ -1,12 +1,12 @@ // // ObjCPlugin.swift -// +// // // Created by Brandon Sneed on 4/17/23. // -#if !os(Linux) +#if !os(Linux) && !os(Windows) import Foundation @@ -39,7 +39,7 @@ public class ObjCEventPlugin: NSObject, EventPlugin, ObjCPlugin { #endif return event } - + public func execute(event: T?) -> T? where T : RawEvent { let objcEvent = objcEventFromEvent(event) let result = execute(event: objcEvent) @@ -51,12 +51,12 @@ public class ObjCEventPlugin: NSObject, EventPlugin, ObjCPlugin { @objc(SEGBlockPlugin) public class ObjCBlockPlugin: ObjCEventPlugin { let block: (ObjCRawEvent?) -> ObjCRawEvent? - + @objc(executeEvent:) public override func execute(event: ObjCRawEvent?) -> ObjCRawEvent? { return block(event) } - + @objc(initWithBlock:) public init(block: @escaping (ObjCRawEvent?) -> ObjCRawEvent?) { self.block = block @@ -73,11 +73,11 @@ extension ObjCAnalytics { analytics.add(plugin: p) } } - + @objc(addPlugin:destinationKey:) public func add(plugin: ObjCPlugin?, destinationKey: String) { guard let d = analytics.find(key: destinationKey) else { return } - + if let p = plugin as? ObjCPluginShim { _ = d.add(plugin: p.instance()) } else if let p = plugin as? ObjCEventPlugin { @@ -87,4 +87,3 @@ extension ObjCAnalytics { } #endif - diff --git a/Sources/Segment/Plugins/Platforms/Vendors/WindowsUtils.swift b/Sources/Segment/Plugins/Platforms/Vendors/WindowsUtils.swift new file mode 100644 index 00000000..0bcc4fcd --- /dev/null +++ b/Sources/Segment/Plugins/Platforms/Vendors/WindowsUtils.swift @@ -0,0 +1,75 @@ +import Foundation + +#if os(Windows) + +import WinSDK + +internal class WindowsVendorSystem: VendorSystem { + override var manufacturer: String { + return "unknown" + } + + override var type: String { + return "Windows" + } + + override var model: String { + return "unknown" + } + + override var name: String { + return "unknown" + } + + override var identifierForVendor: String? { + return nil + } + + override var systemName: String { + // If the name is larger than 256 characters, we might get an error. + var size: DWORD = 256 + var buffer = [CHAR](repeating: 0, count: Int(size)) + guard GetComputerNameA(&buffer, &size) else { + return "unknown" + } + + return String(cString: buffer) + } + + override var systemVersion: String { + let version = ProcessInfo.processInfo.operatingSystemVersion + return "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)" + } + + override var screenSize: ScreenSize { + var rect: RECT = .init(left: 0, top: 0, right: 0, bottom: 0) + guard SystemParametersInfoA(UInt32(SPI_GETWORKAREA), 0, &rect, 0) else { + return ScreenSize(width: 0, height: 0) + } + + return ScreenSize(width: rect.width, height: rect.height) + } + + override var userAgent: String? { + return "unknown" + } + + override var connection: ConnectionStatus { + return .unknown + } + + override var requiredPlugins: [any PlatformPlugin] { + [] + } +} + +extension RECT { + internal var width: Double { + Double(right - left) + } + + internal var height: Double { + Double(bottom - top) + } +} +#endif diff --git a/Sources/Segment/Plugins/SegmentDestination.swift b/Sources/Segment/Plugins/SegmentDestination.swift index 3c0367da..c279ace6 100644 --- a/Sources/Segment/Plugins/SegmentDestination.swift +++ b/Sources/Segment/Plugins/SegmentDestination.swift @@ -8,8 +8,8 @@ import Foundation import Sovran -#if os(Linux) -// Whoever is doing swift/linux development over there +#if os(Linux) || os(Windows) +// Whoever is doing swift/linux/Windows development over there // decided that it'd be a good idea to split out a TON // of stuff into another framework that NO OTHER PLATFORM // has; I guess to be special. :man-shrugging: @@ -28,7 +28,7 @@ public class SegmentDestination: DestinationPlugin, Subscriber, FlushCompletion case apiHost = "apiHost" case apiKey = "apiKey" } - + public let type = PluginType.destination public let key: String = Constants.integrationName.rawValue public let timeline = Timeline() @@ -46,23 +46,23 @@ public class SegmentDestination: DestinationPlugin, Subscriber, FlushCompletion typealias CleanupClosure = () -> Void var cleanup: CleanupClosure? = nil } - + internal var httpClient: HTTPClient? private var uploads = [UploadTaskInfo]() private let uploadsQueue = DispatchQueue(label: "uploadsQueue.segment.com") private var storage: Storage? - + @Atomic internal var eventCount: Int = 0 - + internal func initialSetup() { guard let analytics = self.analytics else { return } storage = analytics.storage httpClient = HTTPClient(analytics: analytics) - + // Add DestinationMetadata enrichment plugin add(plugin: DestinationMetadataPlugin()) } - + public func update(settings: Settings, type: UpdateType) { guard let analytics = analytics else { return } let segmentInfo = settings.integrationSettings(forKey: self.key) @@ -93,7 +93,7 @@ public class SegmentDestination: DestinationPlugin, Subscriber, FlushCompletion } } } - + // MARK: - Event Handling Methods public func execute(event: T?) -> T? { guard let event = event else { return nil } @@ -103,14 +103,14 @@ public class SegmentDestination: DestinationPlugin, Subscriber, FlushCompletion } return result } - + // MARK: - Abstracted Lifecycle Methods internal func enterForeground() { } - + internal func enterBackground() { flush() } - + // MARK: - Event Parsing Methods private func queueEvent(event: T) { guard let storage = self.storage else { return } @@ -120,7 +120,7 @@ public class SegmentDestination: DestinationPlugin, Subscriber, FlushCompletion count += 1 } } - + public func flush() { // unused .. see flush(group:completion:) } @@ -142,7 +142,7 @@ public class SegmentDestination: DestinationPlugin, Subscriber, FlushCompletion let hasData = storage.dataStore.hasData analytics.log(message: "Uploads in-progress: \(pendingUploads)") - + if pendingUploads == 0 { if type == .file, hasData { flushFiles(group: group) @@ -309,7 +309,7 @@ extension SegmentDestination { analytics?.log(message: "Cleaned up \(before - after) non-running uploads.") } } - + internal var pendingUploads: Int { var uploadsCount = 0 uploadsQueue.sync { @@ -317,7 +317,7 @@ extension SegmentDestination { } return uploadsCount } - + internal func add(uploadTask: UploadTaskInfo) { uploadsQueue.sync { uploads.append(uploadTask) diff --git a/Sources/Segment/Startup.swift b/Sources/Segment/Startup.swift index b6cfa3bc..67eb767e 100644 --- a/Sources/Segment/Startup.swift +++ b/Sources/Segment/Startup.swift @@ -9,10 +9,10 @@ import Foundation import Sovran extension Analytics: Subscriber { - + internal func platformStartup() { add(plugin: StartupQueue()) - + // add segment destination plugin unless // asked not to via configuration. if configuration.values.autoAddSegmentDestination { @@ -20,31 +20,31 @@ extension Analytics: Subscriber { segmentDestination.analytics = self add(plugin: segmentDestination) } - + // Setup platform specific plugins if let platformPlugins = platformPlugins() { for plugin in platformPlugins { add(plugin: plugin) } } - + for policy in configuration.values.flushPolicies { policy.configure(analytics: self) } - + // plugins will receive any settings we currently have as they are added. // ... but lets go check if we have new stuff .... // start checking periodically for settings changes from segment.com setupSettingsCheck() } - + internal func platformPlugins() -> [PlatformPlugin]? { var plugins = [PlatformPlugin]() - + // add context plugin as well as it's platform specific internally. // this must come first. plugins.append(Context()) - + plugins += VendorSystem.current.requiredPlugins // setup lifecycle if desired @@ -62,8 +62,11 @@ extension Analytics: Subscriber { // placeholder - not sure what this is yet //plugins.append(LinuxLifecycleMonitor()) #endif + #if os(Windows) + // placeholder - not sure what this is yet + #endif } - + if plugins.isEmpty { return nil } else { @@ -114,4 +117,10 @@ extension Analytics { checkSettings() } } +#elseif os(Windows) +extension Analytics { + internal func setupSettingsCheck() { + checkSettings() + } +} #endif diff --git a/Sources/Segment/Utilities/Atomic.swift b/Sources/Segment/Utilities/Atomic.swift index e3bbf266..ce7fce00 100644 --- a/Sources/Segment/Utilities/Atomic.swift +++ b/Sources/Segment/Utilities/Atomic.swift @@ -22,7 +22,7 @@ import Foundation @propertyWrapper public class Atomic { - #if os(Linux) + #if os(Linux) || os(Windows) let swiftLock: NSLock #else internal typealias os_unfair_lock_t = UnsafeMutablePointer @@ -32,7 +32,7 @@ public class Atomic { internal var value: T public init(wrappedValue value: T) { - #if os(Linux) + #if os(Linux) || os(Windows) self.swiftLock = NSLock() #else self.unfairLock = UnsafeMutablePointer.allocate(capacity: 1) @@ -42,7 +42,7 @@ public class Atomic { } deinit { - #if !os(Linux) + #if !os(Linux) && !os(Windows) unfairLock.deallocate() #endif } @@ -69,7 +69,7 @@ public class Atomic { extension Atomic { internal func lock() { - #if os(Linux) + #if os(Linux) || os(Windows) swiftLock.lock() #else os_unfair_lock_lock(unfairLock) @@ -77,7 +77,7 @@ extension Atomic { } internal func unlock() { - #if os(Linux) + #if os(Linux) || os(Windows) swiftLock.unlock() #else os_unfair_lock_unlock(unfairLock) diff --git a/Sources/Segment/Utilities/Networking/HTTPClient.swift b/Sources/Segment/Utilities/Networking/HTTPClient.swift index 51f43932..cf8b1c7d 100644 --- a/Sources/Segment/Utilities/Networking/HTTPClient.swift +++ b/Sources/Segment/Utilities/Networking/HTTPClient.swift @@ -6,7 +6,7 @@ // import Foundation -#if os(Linux) +#if os(Linux) || os(Windows) import FoundationNetworking #endif @@ -25,26 +25,26 @@ public class HTTPClient { private var apiHost: String private var apiKey: String private var cdnHost: String - + private weak var analytics: Analytics? - + init(analytics: Analytics) { self.analytics = analytics - + self.apiKey = analytics.configuration.values.writeKey self.apiHost = analytics.configuration.values.apiHost self.cdnHost = analytics.configuration.values.cdnHost self.session = analytics.configuration.values.httpSession() } - + func segmentURL(for host: String, path: String) -> URL? { let s = "https://\(host)\(path)" let result = URL(string: s) return result } - - + + /// Starts an upload of events. Responds appropriately if successful or not. If not, lets the respondant /// know if the task should be retried or not based on the response. /// - Parameters: @@ -58,14 +58,14 @@ public class HTTPClient { completion(.failure(HTTPClientErrors.failedToOpenBatch)) return nil } - + let urlRequest = configuredRequest(for: uploadURL, method: "POST") let dataTask = session.uploadTask(with: urlRequest, fromFile: batch) { [weak self] (data, response, error) in guard let self else { return } handleResponse(data: data, response: response, error: error, completion: completion) } - + dataTask.resume() return dataTask } @@ -123,9 +123,9 @@ public class HTTPClient { completion(false, nil) return } - + let urlRequest = configuredRequest(for: settingsURL, method: "GET") - + let dataTask = session.dataTask(with: urlRequest) { [weak self] (data, response, error) in if let error = error { self?.analytics?.reportInternalError(AnalyticsError.networkUnknown(error)) @@ -146,7 +146,7 @@ public class HTTPClient { completion(false, nil) return } - + do { let responseJSON = try JSONDecoder.default.decode(Settings.self, from: data) completion(true, responseJSON) @@ -155,12 +155,12 @@ public class HTTPClient { completion(false, nil) return } - + } - + dataTask.resume() } - + deinit { // finish any tasks that may be processing session.finishTasksAndInvalidate() @@ -177,26 +177,26 @@ extension HTTPClient { } return returnHeader } - + internal static func getDefaultAPIHost() -> String { return Self.defaultAPIHost } - + internal static func getDefaultCDNHost() -> String { return Self.defaultCDNHost } - + internal func configuredRequest(for url: URL, method: String) -> URLRequest { var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 60) request.httpMethod = method request.addValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") request.addValue("analytics-ios/\(Analytics.version())", forHTTPHeaderField: "User-Agent") request.addValue("gzip", forHTTPHeaderField: "Accept-Encoding") - + if let requestFactory = analytics?.configuration.values.requestFactory { request = requestFactory(request) } - + return request } } diff --git a/Sources/Segment/Utilities/OutputFileStream.swift b/Sources/Segment/Utilities/OutputFileStream.swift index 4d631d5d..5825c995 100644 --- a/Sources/Segment/Utilities/OutputFileStream.swift +++ b/Sources/Segment/Utilities/OutputFileStream.swift @@ -1,6 +1,6 @@ // // OutputFileStream.swift -// +// // // Created by Brandon Sneed on 10/15/22. // @@ -11,6 +11,8 @@ import Foundation #if os(Linux) import Glibc +#elseif os(Windows) +import WinSDK #else import Darwin.C #endif @@ -23,16 +25,16 @@ internal class OutputFileStream { case unableToCreate(String) case unableToClose(String) } - + var fileHandle: FileHandle? = nil let fileURL: URL - + init(fileURL: URL) throws { self.fileURL = fileURL let path = fileURL.path guard path.isEmpty == false else { throw OutputStreamError.invalidPath(path) } } - + /// Create attempts to create + open func create() throws { let path = fileURL.path @@ -47,7 +49,7 @@ internal class OutputFileStream { } } } - + /// Open simply opens the file, no attempt at creation is made. func open() throws { if fileHandle != nil { return } @@ -65,7 +67,7 @@ internal class OutputFileStream { throw OutputStreamError.unableToOpen(fileURL.path) } } - + func write(_ data: Data) throws { guard data.isEmpty == false else { return } if #available(macOS 10.15.4, iOS 13.4, macCatalyst 13.4, tvOS 13.4, watchOS 13.4, *) { @@ -79,14 +81,14 @@ internal class OutputFileStream { fileHandle?.write(data) } } - + func write(_ string: String) throws { guard string.isEmpty == false else { return } if let data = string.data(using: .utf8) { try write(data) } } - + func close() throws { do { let existing = fileHandle diff --git a/Sources/Segment/Utilities/Utils.swift b/Sources/Segment/Utilities/Utils.swift index 423f938c..72c20b9b 100644 --- a/Sources/Segment/Utilities/Utils.swift +++ b/Sources/Segment/Utilities/Utils.swift @@ -7,7 +7,7 @@ import Foundation -#if os(Linux) +#if os(Linux) || os(Windows) extension DispatchQueue { func asyncAndWait(execute workItem: DispatchWorkItem) { async { diff --git a/Tests/Segment-Tests/Analytics_Tests.swift b/Tests/Segment-Tests/Analytics_Tests.swift index 5d920615..086baf8b 100644 --- a/Tests/Segment-Tests/Analytics_Tests.swift +++ b/Tests/Segment-Tests/Analytics_Tests.swift @@ -2,44 +2,44 @@ import XCTest @testable import Segment final class Analytics_Tests: XCTestCase { - + func testBaseEventCreation() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let myDestination = MyDestination() myDestination.add(plugin: GooberPlugin()) - + analytics.add(plugin: ZiggyPlugin()) analytics.add(plugin: myDestination) - + let traits = MyTraits(email: "brandon@redf.net") analytics.identify(userId: "brandon", traits: traits) waitUntilStarted(analytics: analytics) checkIfLeaked(analytics) } - + func testPluginConfigure() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let ziggy = ZiggyPlugin() let myDestination = MyDestination() let goober = GooberPlugin() myDestination.add(plugin: goober) - + analytics.add(plugin: ziggy) analytics.add(plugin: myDestination) - + XCTAssertNotNil(ziggy.analytics) XCTAssertNotNil(myDestination.analytics) XCTAssertNotNil(goober.analytics) waitUntilStarted(analytics: analytics) } - + func testPluginRemove() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let myDestination = MyDestination() myDestination.add(plugin: GooberPlugin()) - + let expectation = XCTestExpectation(description: "Ziggy Expectation") let ziggy = ZiggyPlugin() ziggy.completion = { @@ -47,24 +47,24 @@ final class Analytics_Tests: XCTestCase { } analytics.add(plugin: ziggy) analytics.add(plugin: myDestination) - + let traits = MyTraits(email: "brandon@redf.net") analytics.identify(userId: "brandon", traits: traits) analytics.remove(plugin: ziggy) wait(for: [expectation], timeout: .infinity) } - + func testDestinationInitialUpdateOnlyOnce() { // need to clear settings for this one. UserDefaults.standard.removePersistentDomain(forName: "com.segment.storage.test") - + let expectation = XCTestExpectation(description: "MyDestination Expectation") let myDestination = MyDestination { expectation.fulfill() return true } - + var settings = Settings(writeKey: "test") if let existing = settings.integrations?.dictionaryValue { var newIntegrations = existing @@ -74,18 +74,18 @@ final class Analytics_Tests: XCTestCase { let configuration = Configuration(writeKey: "test") configuration.defaultSettings(settings) let analytics = Analytics(configuration: configuration) - + let ziggy1 = ZiggyPlugin() analytics.add(plugin: myDestination) analytics.add(plugin: ziggy1) - + waitUntilStarted(analytics: analytics) - + analytics.track(name: "testDestinationEnabled") - + let ziggy2 = ZiggyPlugin() analytics.add(plugin: ziggy2) - + let dest = analytics.find(key: myDestination.key) XCTAssertNotNil(dest) XCTAssertTrue(dest is MyDestination) @@ -95,21 +95,21 @@ final class Analytics_Tests: XCTestCase { XCTAssertEqual(myDestination.receivedInitialUpdate, 1) XCTAssertEqual(ziggy1.receivedInitialUpdate, 1) XCTAssertEqual(ziggy2.receivedInitialUpdate, 1) - + checkIfLeaked(analytics) } - + func testDestinationEnabled() { // need to clear settings for this one. UserDefaults.standard.removePersistentDomain(forName: "com.segment.storage.test") - + let expectation = XCTestExpectation(description: "MyDestination Expectation") let myDestination = MyDestination { expectation.fulfill() return true } - + var settings = Settings(writeKey: "test") if let existing = settings.integrations?.dictionaryValue { var newIntegrations = existing @@ -119,26 +119,26 @@ final class Analytics_Tests: XCTestCase { let configuration = Configuration(writeKey: "test") configuration.defaultSettings(settings) let analytics = Analytics(configuration: configuration) - + analytics.add(plugin: myDestination) - + waitUntilStarted(analytics: analytics) - + analytics.track(name: "testDestinationEnabled") - + let dest = analytics.find(key: myDestination.key) XCTAssertNotNil(dest) XCTAssertTrue(dest is MyDestination) wait(for: [expectation], timeout: .infinity) } - - // Linux doesn't support XCTExpectFailure -#if !os(Linux) + + // Linux & Windows don't support XCTExpectFailure +#if !os(Linux) && !os(Windows) func testDestinationNotEnabled() { // need to clear settings for this one. UserDefaults.standard.removePersistentDomain(forName: "com.segment.storage.test") - + let expectation = XCTestExpectation(description: "MyDestination Expectation") let myDestination = MyDestination(disabled: true) { expectation.fulfill() @@ -148,50 +148,40 @@ final class Analytics_Tests: XCTestCase { let configuration = Configuration(writeKey: "testDestNotEnabled") let analytics = Analytics(configuration: configuration) - + analytics.add(plugin: myDestination) - + waitUntilStarted(analytics: analytics) - + analytics.track(name: "testDestinationEnabled") - + XCTExpectFailure { wait(for: [expectation], timeout: 1.0) } } #endif - + func testAnonymousId() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let anonId = analytics.anonymousId - + XCTAssertTrue(anonId != "") XCTAssertTrue(anonId.count == 36) // it's a UUID y0. waitUntilStarted(analytics: analytics) } - + func testContext() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) - -#if !os(watchOS) && !os(Linux) - /* Disabling this for now; Newer SDKs, it's getting even more delay-ful. - // prime the pump for userAgent, since it's retrieved async. - let vendorSystem = VendorSystem.current - while vendorSystem.userAgent == nil { - RunLoop.main.run(until: Date.distantPast) - } - */ -#endif - + waitUntilStarted(analytics: analytics) - + // add a referrer analytics.openURL(URL(string: "https://google.com")!) - + analytics.track(name: "token check") - + let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent let context = trackEvent?.context?.dictionaryValue // Verify that context isn't empty here. @@ -203,48 +193,32 @@ final class Analytics_Tests: XCTestCase { XCTAssertNotNil(context?["timezone"], "timezone missing!") XCTAssertNotNil(context?["library"], "library missing!") XCTAssertNotNil(context?["device"], "device missing!") - + let referrer = context?["referrer"] as! [String: Any] XCTAssertEqual(referrer["url"] as! String, "https://google.com") - - // this key not present on watchOS (doesn't have webkit) -#if !os(watchOS) - /* Disabling this for now; Newer SDKs, it's getting even more delay-ful. */ - //XCTAssertNotNil(context?["userAgent"], "userAgent missing!") -#endif - - // these keys not present on linux -#if !os(Linux) + + // these keys not present on linux or Windows +#if !os(Linux) && !os(Windows) XCTAssertNotNil(context?["app"], "app missing!") XCTAssertNotNil(context?["locale"], "locale missing!") #endif } - - + + func testContextWithUserAgent() { let configuration = Configuration(writeKey: "test") configuration.userAgent("testing user agent") let analytics = Analytics(configuration: configuration) let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) - -#if !os(watchOS) && !os(Linux) - /* Disabling this for now; Newer SDKs, it's getting even more delay-ful. - // prime the pump for userAgent, since it's retrieved async. - let vendorSystem = VendorSystem.current - while vendorSystem.userAgent == nil { - RunLoop.main.run(until: Date.distantPast) - } - */ -#endif - + waitUntilStarted(analytics: analytics) - + // add a referrer analytics.openURL(URL(string: "https://google.com")!) - + analytics.track(name: "token check") - + let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent let context = trackEvent?.context?.dictionaryValue // Verify that context isn't empty here. @@ -256,172 +230,168 @@ final class Analytics_Tests: XCTestCase { XCTAssertNotNil(context?["timezone"], "timezone missing!") XCTAssertNotNil(context?["library"], "library missing!") XCTAssertNotNil(context?["device"], "device missing!") - + let referrer = context?["referrer"] as! [String: Any] XCTAssertEqual(referrer["url"] as! String, "https://google.com") - - /* Disabling this for now; Newer SDKs, it's getting even more delay-ful. - XCTAssertEqual(context?["userAgent"] as! String, "testing user agent") - */ // these keys not present on linux -#if !os(Linux) +#if !os(Linux) && !os(Windows) XCTAssertNotNil(context?["app"], "app missing!") XCTAssertNotNil(context?["locale"], "locale missing!") #endif } - + func testDeviceToken() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) - + waitUntilStarted(analytics: analytics) - + analytics.setDeviceToken("1234") analytics.track(name: "token check") - + let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent let device = trackEvent?.context?.dictionaryValue let token = device?[keyPath: "device.token"] as? String XCTAssertTrue(token == "1234") } - + #if os(iOS) || os(tvOS) || os(visionOS) || targetEnvironment(macCatalyst) func testDeviceTokenData() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) - + waitUntilStarted(analytics: analytics) - + let dataToken = UUID().asData() analytics.registeredForRemoteNotifications(deviceToken: dataToken) analytics.track(name: "token check") - + let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent let device = trackEvent?.context?.dictionaryValue let token = device?[keyPath: "device.token"] as? String XCTAssertTrue(token?.count == 32) // it's a uuid w/o the dashes. 36 becomes 32. } #endif - + func testTrack() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) - + waitUntilStarted(analytics: analytics) - + analytics.track(name: "test track") - + let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent XCTAssertTrue(trackEvent?.event == "test track") XCTAssertTrue(trackEvent?.type == "track") } - + func testIdentify() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) - + waitUntilStarted(analytics: analytics) - + analytics.identify(userId: "brandon", traits: MyTraits(email: "blah@blah.com")) - + let identifyEvent: IdentifyEvent? = outputReader.lastEvent as? IdentifyEvent XCTAssertTrue(identifyEvent?.userId == "brandon") let traits = identifyEvent?.traits?.dictionaryValue XCTAssertTrue(traits?["email"] as? String == "blah@blah.com") } - + func testUserIdAndTraitsPersistCorrectly() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) - + waitUntilStarted(analytics: analytics) - + analytics.identify(userId: "brandon", traits: MyTraits(email: "blah@blah.com")) - + let identifyEvent: IdentifyEvent? = outputReader.lastEvent as? IdentifyEvent XCTAssertTrue(identifyEvent?.userId == "brandon") let traits = identifyEvent?.traits?.dictionaryValue XCTAssertTrue(traits?["email"] as? String == "blah@blah.com") - + analytics.track(name: "test") - + let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent XCTAssertTrue(trackEvent?.userId == "brandon") let trackTraits = trackEvent?.context?.dictionaryValue?["traits"] as? [String: Any] XCTAssertNil(trackTraits) - + let analyticsTraits: MyTraits? = analytics.traits() XCTAssertEqual("blah@blah.com", analyticsTraits?.email) } - - + + func testScreen() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) - + waitUntilStarted(analytics: analytics) - + analytics.screen(title: "screen1", category: "category1") - + let screen1Event: ScreenEvent? = outputReader.lastEvent as? ScreenEvent XCTAssertTrue(screen1Event?.name == "screen1") XCTAssertTrue(screen1Event?.category == "category1") - + analytics.screen(title: "screen2", category: "category2", properties: MyTraits(email: "blah@blah.com")) - + let screen2Event: ScreenEvent? = outputReader.lastEvent as? ScreenEvent XCTAssertTrue(screen2Event?.name == "screen2") XCTAssertTrue(screen2Event?.category == "category2") let props = screen2Event?.properties?.dictionaryValue XCTAssertTrue(props?["email"] as? String == "blah@blah.com") } - + func testGroup() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) - + waitUntilStarted(analytics: analytics) - + analytics.group(groupId: "1234") - + let group1Event: GroupEvent? = outputReader.lastEvent as? GroupEvent XCTAssertTrue(group1Event?.groupId == "1234") - + analytics.group(groupId: "4567", traits: MyTraits(email: "blah@blah.com")) - + let group2Event: GroupEvent? = outputReader.lastEvent as? GroupEvent XCTAssertTrue(group2Event?.groupId == "4567") let props = group2Event?.traits?.dictionaryValue XCTAssertTrue(props?["email"] as? String == "blah@blah.com") } - + func testReset() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) - + waitUntilStarted(analytics: analytics) - + analytics.identify(userId: "brandon", traits: MyTraits(email: "blah@blah.com")) - + let identifyEvent: IdentifyEvent? = outputReader.lastEvent as? IdentifyEvent XCTAssertTrue(identifyEvent?.userId == "brandon") let traits = identifyEvent?.traits?.dictionaryValue XCTAssertTrue(traits?["email"] as? String == "blah@blah.com") - + let currentAnonId = analytics.anonymousId let currentUserInfo: UserInfo? = analytics.store.currentState() - + analytics.reset() - + let newAnonId = analytics.anonymousId let newUserInfo: UserInfo? = analytics.store.currentState() XCTAssertNotEqual(currentAnonId, newAnonId) @@ -429,15 +399,15 @@ final class Analytics_Tests: XCTestCase { XCTAssertNotEqual(currentUserInfo?.userId, newUserInfo?.userId) XCTAssertNotEqual(currentUserInfo?.traits, newUserInfo?.traits) } - + func testFlush() { // Use a specific writekey to this test so we do not collide with other cached items. let analytics = Analytics(configuration: Configuration(writeKey: "testFlush_do_not_reuse_this_writekey").flushInterval(9999).flushAt(9999)) - + waitUntilStarted(analytics: analytics) - + analytics.storage.hardReset(doYouKnowHowToUseThis: true) - + analytics.identify(userId: "brandon", traits: MyTraits(email: "blah@blah.com")) let currentBatchCount = analytics.storage.read(.events)!.dataFiles!.count @@ -450,99 +420,99 @@ final class Analytics_Tests: XCTestCase { // 1 new temp file XCTAssertTrue(newBatchCount == currentBatchCount + 1, "New Count (\(newBatchCount)) should be \(currentBatchCount) + 1") } - + func testEnabled() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) - + waitUntilStarted(analytics: analytics) - + analytics.track(name: "enabled") - + let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent XCTAssertTrue(trackEvent!.event == "enabled") - + outputReader.lastEvent = nil analytics.enabled = false analytics.track(name: "notEnabled") - + let noEvent = outputReader.lastEvent XCTAssertNil(noEvent) - + analytics.enabled = true analytics.track(name: "enabled") - + let newEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent XCTAssertTrue(newEvent!.event == "enabled") } - + func testSetFlushIntervalAfter() { let analytics = Analytics(configuration: Configuration(writeKey: "1234")) let intervalPolicy = IntervalBasedFlushPolicy(interval: 35) analytics.add(flushPolicy: intervalPolicy) - + waitUntilStarted(analytics: analytics) - + XCTAssertTrue(intervalPolicy.flushTimer!.interval == 35) - + analytics.flushInterval = 60 - + RunLoop.main.run(until: Date.distantPast) - + XCTAssertTrue(intervalPolicy.flushTimer!.interval == 60) } - + func testSetFlushAtAfter() { let analytics = Analytics(configuration: Configuration(writeKey: "1234")) let countPolicy = CountBasedFlushPolicy(count: 23) analytics.add(flushPolicy: countPolicy) - + waitUntilStarted(analytics: analytics) - + XCTAssertTrue(analytics.configuration.values.flushAt == 23) - + analytics.flushAt = 1 - + let event = TrackEvent(event: "blah", properties: nil) - + countPolicy.updateState(event: event) - + RunLoop.main.run(until: Date.distantPast) - + XCTAssertTrue(countPolicy.shouldFlush() == true) XCTAssertTrue(analytics.configuration.values.flushAt == 1) } - + func testPurgeStorage() { // Use a specific writekey to this test so we do not collide with other cached items. let analytics = Analytics(configuration: Configuration(writeKey: "testFlush_do_not_reuse_this_writekey_either") .flushInterval(9999) .flushAt(9999) .operatingMode(.synchronous)) - + waitUntilStarted(analytics: analytics) - + analytics.storage.hardReset(doYouKnowHowToUseThis: true) - + analytics.identify(userId: "brandon", traits: MyTraits(email: "blah@blah.com")) - + let currentPendingCount = analytics.pendingUploads!.count - + XCTAssertEqual(currentPendingCount, 1) - + analytics.flush() analytics.track(name: "test") - + analytics.flush() analytics.track(name: "test") - + analytics.flush() analytics.track(name: "test") let newPendingCount = analytics.pendingUploads!.count XCTAssertEqual(newPendingCount, 1) - + let pending = analytics.pendingUploads! analytics.purgeStorage(fileURL: pending.first!) XCTAssertNil(analytics.pendingUploads) @@ -550,48 +520,48 @@ final class Analytics_Tests: XCTestCase { analytics.purgeStorage() XCTAssertNil(analytics.pendingUploads) } - + func testVersion() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) - + waitUntilStarted(analytics: analytics) - + analytics.track(name: "whataversion") - + let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent let context = trackEvent?.context?.dictionaryValue let eventVersion = context?[keyPath: "library.version"] as? String let analyticsVersion = analytics.version() - + XCTAssertEqual(eventVersion, analyticsVersion) } - + class AnyDestination: DestinationPlugin { var timeline: Timeline let type: PluginType let key: String weak var analytics: Analytics? - + init(key: String) { self.key = key self.type = .destination self.timeline = Timeline() } } - + // Test to ensure bundled and unbundled integrations are populated correctly func testDestinationMetadata() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let mixpanel = AnyDestination(key: "Mixpanel") let outputReader = OutputReaderPlugin() - + // we want the output reader on the segment plugin // cuz that's the only place the metadata is getting added. let segmentDest = analytics.find(pluginType: SegmentDestination.self) segmentDest?.add(plugin: outputReader) - + analytics.add(plugin: mixpanel) var settings = Settings(writeKey: "123") let integrations = try? JSON([ @@ -607,30 +577,30 @@ final class Analytics_Tests: XCTestCase { ]) settings.integrations = integrations analytics.store.dispatch(action: System.UpdateSettingsAction(settings: settings)) - + waitUntilStarted(analytics: analytics) - - + + analytics.track(name: "sampleEvent") - + let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent let metadata = trackEvent?._metadata - + XCTAssertEqual(metadata?.bundled, ["Mixpanel"]) XCTAssertEqual(metadata?.unbundled.sorted(), ["Amplitude", "Customer.io"]) } - + // Test to ensure bundled and active integrations are populated correctly func testDestinationMetadataUnbundled() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) let mixpanel = AnyDestination(key: "Mixpanel") let outputReader = OutputReaderPlugin() - + // we want the output reader on the segment plugin // cuz that's the only place the metadata is getting added. let segmentDest = analytics.find(pluginType: SegmentDestination.self) segmentDest?.add(plugin: outputReader) - + analytics.add(plugin: mixpanel) var settings = Settings(writeKey: "123") let integrations = try? JSON([ @@ -646,19 +616,19 @@ final class Analytics_Tests: XCTestCase { ]) settings.integrations = integrations analytics.store.dispatch(action: System.UpdateSettingsAction(settings: settings)) - + waitUntilStarted(analytics: analytics) - - + + analytics.track(name: "sampleEvent") - + let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent let metadata = trackEvent?._metadata - + XCTAssertEqual(metadata?.bundled, ["Mixpanel"]) XCTAssertEqual(metadata?.unbundled.sorted(), ["Amplitude", "Customer.io", "dest1"]) } - + func testRequestFactory() { let config = Configuration(writeKey: "testSequential").requestFactory { request in XCTAssertEqual(request.value(forHTTPHeaderField: "Accept-Encoding"), "gzip") @@ -678,16 +648,16 @@ final class Analytics_Tests: XCTestCase { analytics.storage.hardReset(doYouKnowHowToUseThis: true) let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) - + waitUntilStarted(analytics: analytics) - + analytics.track(name: "something") - + analytics.flush() - + RunLoop.main.run(until: Date(timeIntervalSinceNow: 5)) } - + func testEnrichment() { var sourceHit: Bool = false let sourceEnrichment: EnrichmentClosure = { event in @@ -695,67 +665,67 @@ final class Analytics_Tests: XCTestCase { sourceHit = true return event } - + var destHit: Bool = true let destEnrichment: EnrichmentClosure = { event in print("destination enrichment applied") destHit = true return event } - + let config = Configuration(writeKey: "testEnrichments") let analytics = Analytics(configuration: config) analytics.storage.hardReset(doYouKnowHowToUseThis: true) let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) - + analytics.add(enrichment: sourceEnrichment) - + let segment = analytics.find(pluginType: SegmentDestination.self) segment?.add(enrichment: destEnrichment) - + waitUntilStarted(analytics: analytics) - + analytics.track(name: "something") - + analytics.flush() - + RunLoop.main.run(until: Date(timeIntervalSinceNow: 5)) - + XCTAssertTrue(sourceHit) XCTAssertTrue(destHit) } - + func testSharedInstance() { Analytics.firstInstance = nil - + let dead = Analytics.shared() XCTAssertTrue(dead.isDead) - + let alive = Analytics(configuration: Configuration(writeKey: "1234")) XCTAssertFalse(alive.isDead) - + let shared = Analytics.shared() XCTAssertFalse(shared.isDead) - + XCTAssertTrue(alive === shared) - + let alive2 = Analytics(configuration: Configuration(writeKey: "ABCD")) let shared2 = Analytics.shared() XCTAssertFalse(alive2 === shared2) XCTAssertTrue(shared2 === shared) } - + func testAsyncOperatingMode() throws { // Use a specific writekey to this test so we do not collide with other cached items. let analytics = Analytics(configuration: Configuration(writeKey: "testFlush_asyncMode") .flushInterval(9999) .flushAt(9999) .operatingMode(.asynchronous)) - + waitUntilStarted(analytics: analytics) - + analytics.storage.hardReset(doYouKnowHowToUseThis: true) let expectation = XCTestExpectation() @@ -775,16 +745,16 @@ final class Analytics_Tests: XCTestCase { XCTAssertNil(analytics.pendingUploads) } - + func testSyncOperatingMode() throws { // Use a specific writekey to this test so we do not collide with other cached items. let analytics = Analytics(configuration: Configuration(writeKey: "testFlush_syncMode") .flushInterval(9999) .flushAt(9999) .operatingMode(.synchronous)) - + waitUntilStarted(analytics: analytics) - + analytics.storage.hardReset(doYouKnowHowToUseThis: true) let expectation = XCTestExpectation() @@ -803,18 +773,18 @@ final class Analytics_Tests: XCTestCase { // put another event in the pipe. analytics.track(name: "completion test2") analytics.flush() - + // flush shouldn't return until all uploads are done, cuz // it's running in sync mode. XCTAssertNil(analytics.pendingUploads) } - + func testFindAll() throws { let analytics = Analytics(configuration: Configuration(writeKey: "testFindAll") .flushInterval(9999) .flushAt(9999) .operatingMode(.synchronous)) - + analytics.add(plugin: ZiggyPlugin()) analytics.add(plugin: ZiggyPlugin()) analytics.add(plugin: ZiggyPlugin()) @@ -822,14 +792,14 @@ final class Analytics_Tests: XCTestCase { let myDestination = MyDestination() myDestination.add(plugin: GooberPlugin()) myDestination.add(plugin: GooberPlugin()) - + analytics.add(plugin: myDestination) - + waitUntilStarted(analytics: analytics) - + let ziggysFound = analytics.findAll(pluginType: ZiggyPlugin.self) let goobersFound = myDestination.findAll(pluginType: GooberPlugin.self) - + XCTAssertEqual(ziggysFound!.count, 3) XCTAssertEqual(goobersFound!.count, 2) } @@ -870,7 +840,7 @@ final class Analytics_Tests: XCTestCase { } // Linux doesn't know what URLProtocol is and on watchOS it somehow works differently and isn't hit. - #if !os(Linux) && !os(watchOS) + #if !os(Linux) && !os(watchOS) && !os(Windows) func testFailedSegmentResponse() throws { //register our network blocker (returns 400 response) guard URLProtocol.registerClass(FailedNetworkCalls.self) else { diff --git a/Tests/Segment-Tests/HTTPClient_Tests.swift b/Tests/Segment-Tests/HTTPClient_Tests.swift index f71f42dd..6fe317ba 100644 --- a/Tests/Segment-Tests/HTTPClient_Tests.swift +++ b/Tests/Segment-Tests/HTTPClient_Tests.swift @@ -5,7 +5,7 @@ // Created by Brandon Sneed on 1/21/21. // -#if !os(Linux) +#if !os(Linux) && !os(Windows) import XCTest @testable import Segment diff --git a/Tests/Segment-Tests/MemoryLeak_Tests.swift b/Tests/Segment-Tests/MemoryLeak_Tests.swift index cff3e72e..7a8ba984 100644 --- a/Tests/Segment-Tests/MemoryLeak_Tests.swift +++ b/Tests/Segment-Tests/MemoryLeak_Tests.swift @@ -1,6 +1,6 @@ // // MemoryLeak_Tests.swift -// +// // // Created by Brandon Sneed on 10/17/22. // @@ -20,19 +20,19 @@ final class MemoryLeak_Tests: XCTestCase { func testLeaksVerbose() throws { let analytics = Analytics(configuration: Configuration(writeKey: "1234")) - + waitUntilStarted(analytics: analytics) analytics.track(name: "test") - + RunLoop.main.run(until: Date(timeIntervalSinceNow: 1)) - + let segmentDest = analytics.find(pluginType: SegmentDestination.self)! let destMetadata = segmentDest.timeline.find(pluginType: DestinationMetadataPlugin.self)! let startupQueue = analytics.find(pluginType: StartupQueue.self)! - + let context = analytics.find(pluginType: Context.self)! - - #if !os(Linux) + + #if !os(Linux) && !os(Windows) let deviceToken = analytics.find(pluginType: DeviceToken.self)! #endif #if os(iOS) || os(tvOS) || os(visionOS) || targetEnvironment(macCatalyst) @@ -45,7 +45,7 @@ final class MemoryLeak_Tests: XCTestCase { let macLifecycle = analytics.find(pluginType: macOSLifecycleEvents.self)! let macMonitor = analytics.find(pluginType: macOSLifecycleMonitor.self)! #endif - + // test that enrichment closure isn't leaked. was previously a retain loop. analytics.add { event in return event @@ -54,9 +54,9 @@ final class MemoryLeak_Tests: XCTestCase { analytics.remove(plugin: startupQueue) analytics.remove(plugin: segmentDest) segmentDest.remove(plugin: destMetadata) - + analytics.remove(plugin: context) - #if !os(Linux) + #if !os(Linux) && !os(Windows) analytics.remove(plugin: deviceToken) #endif #if os(iOS) || os(tvOS) || os(visionOS) || targetEnvironment(macCatalyst) @@ -75,9 +75,9 @@ final class MemoryLeak_Tests: XCTestCase { checkIfLeaked(segmentDest) checkIfLeaked(destMetadata) checkIfLeaked(startupQueue) - + checkIfLeaked(context) - #if !os(Linux) + #if !os(Linux) && !os(Windows) checkIfLeaked(deviceToken) #endif #if os(iOS) || os(tvOS) || os(visionOS) || targetEnvironment(macCatalyst) @@ -90,16 +90,16 @@ final class MemoryLeak_Tests: XCTestCase { checkIfLeaked(macLifecycle) checkIfLeaked(macMonitor) #endif - + checkIfLeaked(analytics) } - + func testLeaksSimple() throws { let analytics = Analytics(configuration: Configuration(writeKey: "1234")) - + waitUntilStarted(analytics: analytics) analytics.track(name: "test") - + RunLoop.main.run(until: Date(timeIntervalSinceNow: 1)) checkIfLeaked(analytics) diff --git a/Tests/Segment-Tests/ObjC_Tests.swift b/Tests/Segment-Tests/ObjC_Tests.swift index 55d232c0..8198946c 100644 --- a/Tests/Segment-Tests/ObjC_Tests.swift +++ b/Tests/Segment-Tests/ObjC_Tests.swift @@ -5,7 +5,7 @@ // Created by Brandon Sneed on 8/13/21. // -#if !os(Linux) +#if !os(Linux) && !os(Windows) import XCTest @testable import Segment @@ -24,60 +24,60 @@ class ObjC_Tests: XCTestCase { NOTE: These tests only cover non-trivial methods. Most ObjC methods pass straight through to their swift counterparts however, there are some where some data conversion needs to happen in order to be made accessible. - + */ func testWrapping() { let a = Analytics(configuration: Configuration(writeKey: "WRITE_KEY")) let objc = ObjCAnalytics(wrapping: a) - + XCTAssertTrue(objc.analytics === a) } - + func testNonTrivialConfiguration() { let config = ObjCConfiguration(writeKey: "WRITE_KEY") config.defaultSettings = ["integrations": ["Amplitude": true]] - + let defaults = config.defaultSettings let integrations = defaults["integrations"] as? [String: Any] - + XCTAssertTrue(integrations != nil) XCTAssertTrue(integrations?["Amplitude"] as? Bool == true) } - + func testNonTrivialAnalytics() { Storage.hardSettingsReset(writeKey: "WRITE_KEY") let config = ObjCConfiguration(writeKey: "WRITE_KEY") config.defaultSettings = ["integrations": ["Amplitude": true]] - + let analytics = ObjCAnalytics(configuration: config) analytics.reset() - + analytics.identify(userId: "testPerson", traits: ["email" : "blah@blah.com"]) - + waitUntilStarted(analytics: analytics.analytics) - + let settings = analytics.settings() let integrations = settings?["integrations"] as? [String: Any] - + XCTAssertTrue(integrations != nil) XCTAssertTrue(integrations?["Amplitude"] as? Bool == true) - + let traits = analytics.traits() XCTAssertTrue(traits != nil) XCTAssertTrue(traits?["email"] as? String == "blah@blah.com") - + let userId = analytics.userId XCTAssertTrue(userId == "testPerson") } - + func testTraitsAndUserIdOptionality() { let config = ObjCConfiguration(writeKey: "WRITE_KEY") let analytics = ObjCAnalytics(configuration: config) analytics.reset() - + analytics.identify(userId: nil, traits: ["email" : "blah@blah.com"]) - + waitUntilStarted(analytics: analytics.analytics) let userId = analytics.userId XCTAssertNil(userId) @@ -85,66 +85,66 @@ class ObjC_Tests: XCTestCase { XCTAssertTrue(traits != nil) XCTAssertTrue(traits?["email"] as? String == "blah@blah.com") } - + func testObjCMiddlewares() { var sourceHit: Bool = false var destHit: Bool = false - + Storage.hardSettingsReset(writeKey: "WRITE_KEY") - + let config = ObjCConfiguration(writeKey: "WRITE_KEY") let analytics = ObjCAnalytics(configuration: config) analytics.analytics.storage.hardReset(doYouKnowHowToUseThis: true) - + analytics.reset() - + let outputReader = OutputReaderPlugin() analytics.analytics.add(plugin: outputReader) - + let sourcePlugin = ObjCBlockPlugin { event in print("source enrichment applied") sourceHit = true return event } analytics.add(plugin: sourcePlugin) - + let destPlugin = ObjCBlockPlugin { event in print("destination enrichment applied") destHit = true return event } analytics.add(plugin: destPlugin, destinationKey: "Segment.io") - + waitUntilStarted(analytics: analytics.analytics) - + analytics.identify(userId: "batman") - + analytics.flush() - + RunLoop.main.run(until: Date(timeIntervalSinceNow: 5)) - + XCTAssertTrue(sourceHit) XCTAssertTrue(destHit) - + let lastEvent = outputReader.lastEvent XCTAssertTrue(lastEvent is IdentifyEvent) XCTAssertTrue((lastEvent as! IdentifyEvent).userId == "batman") } - + func testObjCDictionaryPassThru() { Storage.hardSettingsReset(writeKey: "WRITE_KEY2") - + let config = ObjCConfiguration(writeKey: "WRITE_KEY2") let analytics = ObjCAnalytics(configuration: config) analytics.analytics.storage.hardReset(doYouKnowHowToUseThis: true) - + analytics.reset() - + let outputReader = OutputReaderPlugin() analytics.analytics.add(plugin: outputReader) - + waitUntilStarted(analytics: analytics.analytics) - + let dict = [ "ancientAliens": [ "guy1": "hair guy", @@ -152,7 +152,7 @@ class ObjC_Tests: XCTestCase { "guy3": "old bald guy", "guy4": 4] as [String : Any], "channel": "hIsToRy cHaNnEL"] as [String : Any] - + analytics.track(name: "test", properties: dict) RunLoop.main.run(until: Date.distantPast) let trackEvent = outputReader.lastEvent as? TrackEvent @@ -160,7 +160,7 @@ class ObjC_Tests: XCTestCase { XCTAssertNotNil(trackEvent) XCTAssertTrue(props?.count == 2) XCTAssertTrue((props?["ancientAliens"] as? [String: Any])?.count == 4) - + analytics.identify(userId: "test", traits: dict) RunLoop.main.run(until: Date.distantPast) let identifyEvent = outputReader.lastEvent as? IdentifyEvent @@ -176,7 +176,7 @@ class ObjC_Tests: XCTestCase { XCTAssertNotNil(identifyEvent2) XCTAssertTrue(traits2?.count == 2) XCTAssertTrue((traits2?["ancientAliens"] as? [String: Any])?.count == 4) - + analytics.screen(title: "blah", category: nil, properties: dict) RunLoop.main.run(until: Date.distantPast) let screenEvent = outputReader.lastEvent as? ScreenEvent @@ -184,7 +184,7 @@ class ObjC_Tests: XCTestCase { XCTAssertNotNil(screenEvent) XCTAssertTrue(props2?.count == 2) XCTAssertTrue((props2?["ancientAliens"] as? [String: Any])?.count == 4) - + analytics.group(groupId: "123", traits: dict) RunLoop.main.run(until: Date.distantPast) let groupEvent = outputReader.lastEvent as? GroupEvent diff --git a/Tests/Segment-Tests/StressTests.swift b/Tests/Segment-Tests/StressTests.swift index c91c6d1c..70168a01 100644 --- a/Tests/Segment-Tests/StressTests.swift +++ b/Tests/Segment-Tests/StressTests.swift @@ -5,7 +5,7 @@ // Created by Brandon Sneed on 11/4/21. // -#if !os(Linux) && !os(tvOS) && !os(watchOS) +#if !os(Linux) && !os(tvOS) && !os(watchOS) && !os(Windows) import XCTest @testable import Segment @@ -239,15 +239,15 @@ class StressTests: XCTestCase { "User-Agent": "analytics-ios/\(Analytics.version())"] let blockSession = URLSession(configuration: configuration, delegate: nil, delegateQueue: nil) segment?.httpClient?.session = blockSession - + let writeQueue1 = DispatchQueue(label: "write queue 1") let writeQueue2 = DispatchQueue(label: "write queue 2") let flushQueue = DispatchQueue(label: "flush queue") - + @Atomic var ready = false @Atomic var queue1Done = false @Atomic var queue2Done = false - + writeQueue1.async { while (ready == false) { usleep(1) } var eventsWritten = 0 @@ -261,7 +261,7 @@ class StressTests: XCTestCase { print("queue 1 wrote \(eventsWritten) events.") _queue1Done.set(true) } - + writeQueue2.async { while (ready == false) { usleep(1) } var eventsWritten = 0 @@ -275,7 +275,7 @@ class StressTests: XCTestCase { print("queue 2 wrote \(eventsWritten) events.") _queue2Done.set(true) } - + flushQueue.async { while (ready == false) { usleep(1) } var counter = 0 diff --git a/Tests/Segment-Tests/Support/TestUtilities.swift b/Tests/Segment-Tests/Support/TestUtilities.swift index 9a0ca5fb..5136a2b5 100644 --- a/Tests/Segment-Tests/Support/TestUtilities.swift +++ b/Tests/Segment-Tests/Support/TestUtilities.swift @@ -31,7 +31,7 @@ class GooberPlugin: EventPlugin { init() { self.type = .enrichment } - + func identify(event: IdentifyEvent) -> IdentifyEvent? { var newEvent = IdentifyEvent(existing: event) newEvent.userId = "goober" @@ -43,30 +43,30 @@ class ZiggyPlugin: EventPlugin { let type: PluginType weak var analytics: Analytics? var receivedInitialUpdate: Int = 0 - + var completion: (() -> Void)? - + required init() { self.type = .enrichment } - + func update(settings: Settings, type: UpdateType) { if type == .initial { receivedInitialUpdate += 1 } } - + func identify(event: IdentifyEvent) -> IdentifyEvent? { var newEvent = IdentifyEvent(existing: event) newEvent.userId = "ziggy" return newEvent //return nil } - + func shutdown() { completion?() } } -#if !os(Linux) +#if !os(Linux) && !os(Windows) @objc(SEGMyDestination) public class ObjCMyDestination: NSObject, ObjCPlugin, ObjCPluginShim { @@ -81,10 +81,10 @@ class MyDestination: DestinationPlugin { let key: String weak var analytics: Analytics? let trackCompletion: (() -> Bool)? - + let disabled: Bool var receivedInitialUpdate: Int = 0 - + init(disabled: Bool = false, trackCompletion: (() -> Bool)? = nil) { self.key = "MyDestination" self.type = .destination @@ -92,7 +92,7 @@ class MyDestination: DestinationPlugin { self.trackCompletion = trackCompletion self.disabled = disabled } - + func update(settings: Settings, type: UpdateType) { if type == .initial { receivedInitialUpdate += 1 } if disabled == false { @@ -100,7 +100,7 @@ class MyDestination: DestinationPlugin { analytics?.manuallyEnableDestination(plugin: self) } } - + func track(event: TrackEvent) -> TrackEvent? { var returnEvent: TrackEvent? = event if let completion = trackCompletion { @@ -118,11 +118,11 @@ class OutputReaderPlugin: Plugin { var events = [RawEvent]() var lastEvent: RawEvent? = nil - + init() { self.type = .after } - + func execute(event: T?) -> T? where T : RawEvent { lastEvent = event if let t = lastEvent as? TrackEvent { @@ -191,7 +191,7 @@ extension XCTestCase { } } -#if !os(Linux) +#if !os(Linux) && !os(Windows) class RestrictedHTTPSession: HTTPSession { let sesh: URLSession @@ -251,23 +251,23 @@ class RestrictedHTTPSession: HTTPSession { class BlockNetworkCalls: URLProtocol { var initialURL: URL? = nil override class func canInit(with request: URLRequest) -> Bool { - + return true } - + override class func canonicalRequest(for request: URLRequest) -> URLRequest { return request } - + override var cachedResponse: CachedURLResponse? { return nil } - + override func startLoading() { client?.urlProtocol(self, didReceive: HTTPURLResponse(url: URL(string: "http://api.segment.com")!, statusCode: 200, httpVersion: nil, headerFields: ["blocked": "true"])!, cacheStoragePolicy: .notAllowed) client?.urlProtocolDidFinishLoading(self) } - + override func stopLoading() { - + } } diff --git a/Tests/Segment-Tests/WindowsVendorSystem_Tests.swift b/Tests/Segment-Tests/WindowsVendorSystem_Tests.swift new file mode 100644 index 00000000..ac79b995 --- /dev/null +++ b/Tests/Segment-Tests/WindowsVendorSystem_Tests.swift @@ -0,0 +1,38 @@ +import XCTest +@testable import Segment + +#if os(Windows) + +final class WindowsVendorSystem_Tests: XCTestCase { + func testScreenSizeReturnsNonEmpty() { + let system = WindowsVendorSystem() + + let screen = system.screenSize + + XCTAssertNotEqual(screen.width, 0) + XCTAssertNotEqual(screen.height, 0) + } + + func testNameReturnsNonEmpty() { + let system = WindowsVendorSystem() + + let name = system.systemName + + XCTAssertNotEqual(name, "unknown") + } + + func testVersionNumberIsWellFormatted() { + let system = WindowsVendorSystem() + + let version = system.systemVersion + + let components = version.split(separator: ".") + + XCTAssertEqual(components.count, 3) + + // Ensure that the version components are all numeric + XCTAssertTrue(components.allSatisfy({ Int($0) != nil })) + } +} + +#endif