diff --git a/Examples/destination_plugins/AdjustDestination.swift b/Examples/destination_plugins/AdjustDestination.swift index 10eff5fa..2c42909c 100644 --- a/Examples/destination_plugins/AdjustDestination.swift +++ b/Examples/destination_plugins/AdjustDestination.swift @@ -41,7 +41,7 @@ class AdjustDestination: NSObject, DestinationPlugin, RemoteNotifications { let timeline = Timeline() let type = PluginType.destination let key = "Adjust" - var analytics: Analytics? = nil + weak var analytics: Analytics? = nil private var settings: AdjustSettings? = nil diff --git a/Examples/destination_plugins/ComscoreDestination.swift b/Examples/destination_plugins/ComscoreDestination.swift index 9c7c2050..08bf33d6 100644 --- a/Examples/destination_plugins/ComscoreDestination.swift +++ b/Examples/destination_plugins/ComscoreDestination.swift @@ -44,7 +44,7 @@ class ComscoreDestination: DestinationPlugin { let timeline = Timeline() let type = PluginType.destination let key = "comScore" - var analytics: Analytics? = nil + weak var analytics: Analytics? = nil private var comscoreSettings: ComscoreSettings? private var comscoreEnrichment: ComscoreEnrichment? diff --git a/Examples/destination_plugins/ExampleDestination.swift b/Examples/destination_plugins/ExampleDestination.swift index 626ed6c6..c54de879 100644 --- a/Examples/destination_plugins/ExampleDestination.swift +++ b/Examples/destination_plugins/ExampleDestination.swift @@ -46,7 +46,7 @@ public class ExampleDestination: DestinationPlugin { public let type = PluginType.destination // TODO: Fill this out with your settings key that matches your destination in the Segment App public let key = "Example" - public var analytics: Analytics? = nil + public weak var analytics: Analytics? = nil private var exampleSettings: ExampleSettings? diff --git a/Examples/destination_plugins/FlurryDestination.swift b/Examples/destination_plugins/FlurryDestination.swift index 64d58b91..8bf37cea 100644 --- a/Examples/destination_plugins/FlurryDestination.swift +++ b/Examples/destination_plugins/FlurryDestination.swift @@ -46,7 +46,7 @@ class FlurryDestination: DestinationPlugin { let timeline = Timeline() let type = PluginType.destination let key = "Flurry" - var analytics: Analytics? = nil + weak var analytics: Analytics? = nil var screenTracksEvents = false diff --git a/Examples/destination_plugins/IntercomDestination.swift b/Examples/destination_plugins/IntercomDestination.swift index 759dda59..3c2d6bee 100644 --- a/Examples/destination_plugins/IntercomDestination.swift +++ b/Examples/destination_plugins/IntercomDestination.swift @@ -43,7 +43,7 @@ class IntercomDestination: DestinationPlugin { let timeline = Timeline() let type = PluginType.destination let key = "Intercom" - var analytics: Analytics? = nil + weak var analytics: Analytics? = nil private var intercomSettings: IntercomSettings? private var configurationLabels = [String: Any]() diff --git a/Examples/other_plugins/CellularCarrier.swift b/Examples/other_plugins/CellularCarrier.swift index 31fc74ea..6dffc067 100644 --- a/Examples/other_plugins/CellularCarrier.swift +++ b/Examples/other_plugins/CellularCarrier.swift @@ -54,7 +54,7 @@ import CoreTelephony class CellularCarrier: Plugin { var type: PluginType = .enrichment - var analytics: Analytics? + weak var analytics: Analytics? func execute(event: T?) -> T? { guard var workingEvent = event else { return event } diff --git a/Examples/other_plugins/ConsentTracking.swift b/Examples/other_plugins/ConsentTracking.swift index ae5c89b9..18a05c08 100644 --- a/Examples/other_plugins/ConsentTracking.swift +++ b/Examples/other_plugins/ConsentTracking.swift @@ -46,7 +46,7 @@ import UIKit */ class ConsentTracking: Plugin { let type = PluginType.before - var analytics: Analytics? = nil + weak var analytics: Analytics? = nil var queuedEvents = [RawEvent]() diff --git a/Examples/other_plugins/ConsoleLogger.swift b/Examples/other_plugins/ConsoleLogger.swift index 1e0d2726..de4699fe 100644 --- a/Examples/other_plugins/ConsoleLogger.swift +++ b/Examples/other_plugins/ConsoleLogger.swift @@ -42,7 +42,7 @@ import Segment class ConsoleLogger: Plugin { let type = PluginType.after let name: String - var analytics: Analytics? = nil + weak var analytics: Analytics? = nil var identifier: String? = nil diff --git a/Examples/other_plugins/IDFACollection.swift b/Examples/other_plugins/IDFACollection.swift index ac389ba5..4ee1550c 100644 --- a/Examples/other_plugins/IDFACollection.swift +++ b/Examples/other_plugins/IDFACollection.swift @@ -46,7 +46,7 @@ import AppTrackingTransparency */ class IDFACollection: Plugin { let type = PluginType.enrichment - var analytics: Analytics? = nil + weak var analytics: Analytics? = nil @Atomic private var alreadyAsked = false func execute(event: T?) -> T? { diff --git a/Examples/other_plugins/NotificationTracking.swift b/Examples/other_plugins/NotificationTracking.swift index 6c7bd470..471158ac 100644 --- a/Examples/other_plugins/NotificationTracking.swift +++ b/Examples/other_plugins/NotificationTracking.swift @@ -40,7 +40,7 @@ import Segment class NotificationTracking: Plugin { var type: PluginType = .utility - var analytics: Analytics? + weak var analytics: Analytics? func trackNotification(_ properties: [String: Any], fromLaunch launch: Bool) { if launch { diff --git a/Examples/other_plugins/UIKitScreenTracking.swift b/Examples/other_plugins/UIKitScreenTracking.swift index cfa5cdb7..387b0223 100644 --- a/Examples/other_plugins/UIKitScreenTracking.swift +++ b/Examples/other_plugins/UIKitScreenTracking.swift @@ -51,7 +51,7 @@ class UIKitScreenTracking: UtilityPlugin { static let controllerKey = "controller" let type = PluginType.utility - var analytics: Analytics? = nil + weak var analytics: Analytics? = nil init() { setupUIKitHooks() diff --git a/Sources/Segment/Plugins/Context.swift b/Sources/Segment/Plugins/Context.swift index 146a8f2b..fb38debf 100644 --- a/Sources/Segment/Plugins/Context.swift +++ b/Sources/Segment/Plugins/Context.swift @@ -9,7 +9,7 @@ import Foundation public class Context: PlatformPlugin { public let type: PluginType = .before - public var analytics: Analytics? + public weak var analytics: Analytics? internal var staticContext = staticContextData() internal static var device = VendorSystem.current diff --git a/Sources/Segment/Plugins/DestinationMetadataPlugin.swift b/Sources/Segment/Plugins/DestinationMetadataPlugin.swift index ade5d5bf..27f97bce 100644 --- a/Sources/Segment/Plugins/DestinationMetadataPlugin.swift +++ b/Sources/Segment/Plugins/DestinationMetadataPlugin.swift @@ -13,7 +13,7 @@ import Foundation */ public class DestinationMetadataPlugin: Plugin { public let type: PluginType = PluginType.enrichment - public var analytics: Analytics? + public weak var analytics: Analytics? private var analyticsSettings: Settings? = nil public func update(settings: Settings, type: UpdateType) { diff --git a/Sources/Segment/Plugins/DeviceToken.swift b/Sources/Segment/Plugins/DeviceToken.swift index daa8bdd5..4391fa5d 100644 --- a/Sources/Segment/Plugins/DeviceToken.swift +++ b/Sources/Segment/Plugins/DeviceToken.swift @@ -9,7 +9,7 @@ import Foundation public class DeviceToken: PlatformPlugin { public let type = PluginType.before - public var analytics: Analytics? + public weak var analytics: Analytics? public var token: String? = nil diff --git a/Sources/Segment/Plugins/Logger/SegmentLog.swift b/Sources/Segment/Plugins/Logger/SegmentLog.swift index 7a757d1a..bc3d5d98 100644 --- a/Sources/Segment/Plugins/Logger/SegmentLog.swift +++ b/Sources/Segment/Plugins/Logger/SegmentLog.swift @@ -11,7 +11,7 @@ import Foundation internal class SegmentLog: UtilityPlugin { public var filterKind = LogFilterKind.debug - var analytics: Analytics? + weak var analytics: Analytics? let type = PluginType.utility @@ -22,7 +22,7 @@ internal class SegmentLog: UtilityPlugin { // For internal use only. Note: This will contain the last created instance // of analytics when used in a multi-analytics environment. - internal static var sharedAnalytics: Analytics? = nil + internal static weak var sharedAnalytics: Analytics? = nil #if DEBUG internal static var globalLogger: SegmentLog { diff --git a/Sources/Segment/Plugins/Platforms/Linux/LinuxLifecycleMonitor.swift b/Sources/Segment/Plugins/Platforms/Linux/LinuxLifecycleMonitor.swift index 12efff1c..0f6a3151 100644 --- a/Sources/Segment/Plugins/Platforms/Linux/LinuxLifecycleMonitor.swift +++ b/Sources/Segment/Plugins/Platforms/Linux/LinuxLifecycleMonitor.swift @@ -10,6 +10,6 @@ import Foundation #if os(Linux) class LinuxLifecycleMonitor: PlatformPlugin { let type = PluginType.utility - var analytics: Analytics? + weak var analytics: Analytics? } #endif diff --git a/Sources/Segment/Plugins/Platforms/Mac/macOSLifecycleEvents.swift b/Sources/Segment/Plugins/Platforms/Mac/macOSLifecycleEvents.swift index 57509dcb..780c1a51 100644 --- a/Sources/Segment/Plugins/Platforms/Mac/macOSLifecycleEvents.swift +++ b/Sources/Segment/Plugins/Platforms/Mac/macOSLifecycleEvents.swift @@ -16,7 +16,7 @@ class macOSLifecycleEvents: PlatformPlugin, macOSLifecycle { static var buildKey = "SEGBuildKeyV2" let type = PluginType.before - var analytics: Analytics? + weak var analytics: Analytics? /// Since application:didFinishLaunchingWithOptions is not automatically called with Scenes / SwiftUI, /// this gets around by using a flag in user defaults to check for big events like application updating, diff --git a/Sources/Segment/Plugins/Platforms/Mac/macOSLifecycleMonitor.swift b/Sources/Segment/Plugins/Platforms/Mac/macOSLifecycleMonitor.swift index 3e34089f..bd4ec95b 100644 --- a/Sources/Segment/Plugins/Platforms/Mac/macOSLifecycleMonitor.swift +++ b/Sources/Segment/Plugins/Platforms/Mac/macOSLifecycleMonitor.swift @@ -46,7 +46,7 @@ class macOSLifecycleMonitor: PlatformPlugin { static var specificName = "Segment_macOSLifecycleMonitor" let type = PluginType.utility let name = specificName - var analytics: Analytics? + weak var analytics: Analytics? private var application: NSApplication private var appNotifications: [NSNotification.Name] = diff --git a/Sources/Segment/Plugins/Platforms/Vendors/AppleUtils.swift b/Sources/Segment/Plugins/Platforms/Vendors/AppleUtils.swift index 2f5bad90..4230cc76 100644 --- a/Sources/Segment/Plugins/Platforms/Vendors/AppleUtils.swift +++ b/Sources/Segment/Plugins/Platforms/Vendors/AppleUtils.swift @@ -172,7 +172,7 @@ internal class watchOSVendorSystem: VendorSystem { } override var requiredPlugins: [PlatformPlugin] { - return [watchOSLifecycleMonitor()] + return [watchOSLifecycleMonitor(), DeviceToken()] } private func deviceModel() -> String { diff --git a/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleEvents.swift b/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleEvents.swift index e6dc4fa2..3b0943a9 100644 --- a/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleEvents.swift +++ b/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleEvents.swift @@ -16,7 +16,7 @@ class iOSLifecycleEvents: PlatformPlugin, iOSLifecycle { static var buildKey = "SEGBuildKeyV2" let type = PluginType.before - var analytics: Analytics? + weak var analytics: Analytics? /// Since application:didFinishLaunchingWithOptions is not automatically called with Scenes / SwiftUI, /// this gets around by using a flag in user defaults to check for big events like application updating, diff --git a/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleMonitor.swift b/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleMonitor.swift index 3fb7eb90..8bc352e2 100644 --- a/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleMonitor.swift +++ b/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleMonitor.swift @@ -38,7 +38,7 @@ public extension iOSLifecycle { class iOSLifecycleMonitor: PlatformPlugin { let type = PluginType.utility - var analytics: Analytics? + weak var analytics: Analytics? private var application: UIApplication? = nil private var appNotifications: [NSNotification.Name] = [UIApplication.didEnterBackgroundNotification, diff --git a/Sources/Segment/Plugins/Platforms/watchOS/watchOSLifecycleEvents.swift b/Sources/Segment/Plugins/Platforms/watchOS/watchOSLifecycleEvents.swift index 6946045c..31453a3a 100644 --- a/Sources/Segment/Plugins/Platforms/watchOS/watchOSLifecycleEvents.swift +++ b/Sources/Segment/Plugins/Platforms/watchOS/watchOSLifecycleEvents.swift @@ -15,7 +15,7 @@ class watchOSLifecycleEvents: PlatformPlugin, watchOSLifecycle { static var buildKey = "SEGBuildKeyV2" let type = PluginType.before - var analytics: Analytics? + weak var analytics: Analytics? func applicationDidFinishLaunching(watchExtension: WKExtension) { if analytics?.configuration.values.trackApplicationLifecycleEvents == false { diff --git a/Sources/Segment/Plugins/Platforms/watchOS/watchOSLifecycleMonitor.swift b/Sources/Segment/Plugins/Platforms/watchOS/watchOSLifecycleMonitor.swift index c32b66f5..4bf30324 100644 --- a/Sources/Segment/Plugins/Platforms/watchOS/watchOSLifecycleMonitor.swift +++ b/Sources/Segment/Plugins/Platforms/watchOS/watchOSLifecycleMonitor.swift @@ -28,8 +28,8 @@ public extension watchOSLifecycle { class watchOSLifecycleMonitor: PlatformPlugin { - var type = PluginType.utility - var analytics: Analytics? + let type = PluginType.utility + weak var analytics: Analytics? var wasBackgrounded: Bool = false private var watchExtension = WKExtension.shared() diff --git a/Sources/Segment/Plugins/SegmentDestination.swift b/Sources/Segment/Plugins/SegmentDestination.swift index 532cc787..a94f7a0b 100644 --- a/Sources/Segment/Plugins/SegmentDestination.swift +++ b/Sources/Segment/Plugins/SegmentDestination.swift @@ -25,7 +25,7 @@ public class SegmentDestination: DestinationPlugin { public let type = PluginType.destination public let key: String = Constants.integrationName.rawValue public let timeline = Timeline() - public var analytics: Analytics? { + public weak var analytics: Analytics? { didSet { initialSetup() } @@ -54,8 +54,8 @@ public class SegmentDestination: DestinationPlugin { guard let analytics = self.analytics else { return } storage = analytics.storage httpClient = HTTPClient(analytics: analytics) - flushTimer = QueueTimer(interval: analytics.configuration.values.flushInterval) { - self.flush() + flushTimer = QueueTimer(interval: analytics.configuration.values.flushInterval) { [weak self] in + self?.flush() } // Add DestinationMetadata enrichment plugin add(plugin: DestinationMetadataPlugin()) diff --git a/Sources/Segment/Plugins/StartupQueue.swift b/Sources/Segment/Plugins/StartupQueue.swift index c29a08ed..8f316f8e 100644 --- a/Sources/Segment/Plugins/StartupQueue.swift +++ b/Sources/Segment/Plugins/StartupQueue.swift @@ -15,9 +15,11 @@ public class StartupQueue: Plugin, Subscriber { public let type: PluginType = .before - public var analytics: Analytics? = nil { + public weak var analytics: Analytics? = nil { didSet { - analytics?.store.subscribe(self, handler: runningUpdate) + analytics?.store.subscribe(self) { [weak self] (state: System) in + self?.runningUpdate(state: state) + } } } diff --git a/Sources/Segment/Startup.swift b/Sources/Segment/Startup.swift index 2b4bbb90..b52477ad 100644 --- a/Sources/Segment/Startup.swift +++ b/Sources/Segment/Startup.swift @@ -76,10 +76,10 @@ extension Analytics { // do the first one checkSettings() // set up return-from-background to do it again. - NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: OperationQueue.main) { (notification) in + NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: OperationQueue.main) { [weak self] (notification) in guard let app = notification.object as? UIApplication else { return } if app.applicationState == .background { - self.checkSettings() + self?.checkSettings() } } } @@ -100,8 +100,8 @@ extension Analytics { // now set up a timer to do it every 24 hrs. // mac apps change focus a lot more than iOS apps, so this // seems more appropriate here. - QueueTimer.schedule(interval: .days(1), queue: .main) { - self.checkSettings() + QueueTimer.schedule(interval: .days(1), queue: .main) { [weak self] in + self?.checkSettings() } } } diff --git a/Sources/Segment/Timeline.swift b/Sources/Segment/Timeline.swift index 0afbccc6..b6b17019 100644 --- a/Sources/Segment/Timeline.swift +++ b/Sources/Segment/Timeline.swift @@ -11,7 +11,7 @@ import Sovran // MARK: - Main Timeline -public class Timeline: Subscriber { +public class Timeline { internal let plugins: [PluginType: Mediator] public init() { diff --git a/Sources/Segment/Utilities/HTTPClient.swift b/Sources/Segment/Utilities/HTTPClient.swift index 7b18f1d2..318eabb9 100644 --- a/Sources/Segment/Utilities/HTTPClient.swift +++ b/Sources/Segment/Utilities/HTTPClient.swift @@ -24,7 +24,8 @@ public class HTTPClient { private var apiHost: String private var apiKey: String private var cdnHost: String - private let analytics: Analytics + + private weak var analytics: Analytics? init(analytics: Analytics, apiKey: String? = nil, apiHost: String? = nil, cdnHost: String? = nil) { self.analytics = analytics @@ -75,7 +76,7 @@ public class HTTPClient { let dataTask = session.uploadTask(with: urlRequest, fromFile: batch) { [weak self] (data, response, error) in if let error = error { - self?.analytics.log(message: "Error uploading request \(error.localizedDescription).") + self?.analytics?.log(message: "Error uploading request \(error.localizedDescription).") completion(.failure(error)) } else if let httpResponse = response as? HTTPURLResponse { switch (httpResponse.statusCode) { @@ -83,16 +84,16 @@ public class HTTPClient { completion(.success(true)) return case 300..<400: - self?.analytics.log(message: "Server responded with unexpected HTTP code \(httpResponse.statusCode).") + self?.analytics?.log(message: "Server responded with unexpected HTTP code \(httpResponse.statusCode).") completion(.failure(HTTPClientErrors.statusCode(code: httpResponse.statusCode))) case 429: - self?.analytics.log(message: "Server limited client with response code \(httpResponse.statusCode).") + self?.analytics?.log(message: "Server limited client with response code \(httpResponse.statusCode).") completion(.failure(HTTPClientErrors.statusCode(code: httpResponse.statusCode))) case 400..<500: - self?.analytics.log(message: "Server rejected payload with HTTP code \(httpResponse.statusCode).") + self?.analytics?.log(message: "Server rejected payload with HTTP code \(httpResponse.statusCode).") completion(.failure(HTTPClientErrors.statusCode(code: httpResponse.statusCode))) default: // All 500 codes - self?.analytics.log(message: "Server rejected payload with HTTP code \(httpResponse.statusCode).") + self?.analytics?.log(message: "Server rejected payload with HTTP code \(httpResponse.statusCode).") completion(.failure(HTTPClientErrors.statusCode(code: httpResponse.statusCode))) } } @@ -113,21 +114,21 @@ public class HTTPClient { let dataTask = session.dataTask(with: urlRequest) { [weak self] (data, response, error) in if let error = error { - self?.analytics.log(message: "Error fetching settings \(error.localizedDescription).") + self?.analytics?.log(message: "Error fetching settings \(error.localizedDescription).") completion(false, nil) return } if let httpResponse = response as? HTTPURLResponse { if httpResponse.statusCode > 300 { - self?.analytics.log(message: "Server responded with unexpected HTTP code \(httpResponse.statusCode).") + self?.analytics?.log(message: "Server responded with unexpected HTTP code \(httpResponse.statusCode).") completion(false, nil) return } } guard let data = data, let responseJSON = try? JSONDecoder().decode(Settings.self, from: data) else { - self?.analytics.log(message: "Error deserializing settings.") + self?.analytics?.log(message: "Error deserializing settings.") completion(false, nil) return } diff --git a/Sources/Segment/Utilities/Storage.swift b/Sources/Segment/Utilities/Storage.swift index 2045ce56..5c94a331 100644 --- a/Sources/Segment/Utilities/Storage.swift +++ b/Sources/Segment/Utilities/Storage.swift @@ -9,7 +9,6 @@ import Foundation import Sovran internal class Storage: Subscriber { - let store: Store let writeKey: String let userDefaults: UserDefaults? static let MAXFILESIZE = 475000 // Server accepts max 500k per batch @@ -21,11 +20,14 @@ internal class Storage: Subscriber { private var fileHandle: FileHandle? = nil init(store: Store, writeKey: String) { - self.store = store self.writeKey = writeKey self.userDefaults = UserDefaults(suiteName: "com.segment.storage.\(writeKey)") - store.subscribe(self, handler: userInfoUpdate) - store.subscribe(self, handler: systemUpdate) + store.subscribe(self) { [weak self] (state: UserInfo) in + self?.userInfoUpdate(state: state) + } + store.subscribe(self) { [weak self] (state: System) in + self?.systemUpdate(state: state) + } } func write(_ key: Storage.Constants, value: T?) { diff --git a/Tests/Segment-Tests/MemoryLeak_Tests.swift b/Tests/Segment-Tests/MemoryLeak_Tests.swift new file mode 100644 index 00000000..2f7e1f71 --- /dev/null +++ b/Tests/Segment-Tests/MemoryLeak_Tests.swift @@ -0,0 +1,106 @@ +// +// MemoryLeak_Tests.swift +// +// +// Created by Brandon Sneed on 10/17/22. +// + +import XCTest +@testable import Segment + +final class MemoryLeak_Tests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + 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 segmentLog = analytics.find(pluginType: SegmentLog.self)! + + let context = analytics.find(pluginType: Context.self)! + + #if !os(Linux) + let deviceToken = analytics.find(pluginType: DeviceToken.self)! + #endif + #if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) + let iosLifecycle = analytics.find(pluginType: iOSLifecycleEvents.self)! + let iosMonitor = analytics.find(pluginType: iOSLifecycleMonitor.self)! + #elseif os(watchOS) + let watchLifecycle = analytics.find(pluginType: watchOSLifecycleEvents.self)! + let watchMonitor = analytics.find(pluginType: watchOSLifecycleMonitor.self)! + #elseif os(macOS) + let macLifecycle = analytics.find(pluginType: macOSLifecycleEvents.self)! + let macMonitor = analytics.find(pluginType: macOSLifecycleMonitor.self)! + #endif + + analytics.remove(plugin: startupQueue) + analytics.remove(plugin: segmentLog) + analytics.remove(plugin: segmentDest) + segmentDest.remove(plugin: destMetadata) + + analytics.remove(plugin: context) + #if !os(Linux) + analytics.remove(plugin: deviceToken) + #endif + #if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) + analytics.remove(plugin: iosLifecycle) + analytics.remove(plugin: iosMonitor) + #elseif os(watchOS) + analytics.remove(plugin: watchLifecycle) + analytics.remove(plugin: watchMonitor) + #elseif os(macOS) + analytics.remove(plugin: macLifecycle) + analytics.remove(plugin: macMonitor) + #endif + + RunLoop.main.run(until: Date(timeIntervalSinceNow: 1)) + + checkIfLeaked(segmentLog) + checkIfLeaked(segmentDest) + checkIfLeaked(destMetadata) + checkIfLeaked(startupQueue) + + checkIfLeaked(context) + #if !os(Linux) + checkIfLeaked(deviceToken) + #endif + #if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) + checkIfLeaked(iosLifecycle) + checkIfLeaked(iosMonitor) + #elseif os(watchOS) + checkIfLeaked(watchLifecycle) + checkIfLeaked(watchMonitor) + #elseif os(macOS) + 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/Support/TestUtilities.swift b/Tests/Segment-Tests/Support/TestUtilities.swift index e37ddd59..c9c3f5f6 100644 --- a/Tests/Segment-Tests/Support/TestUtilities.swift +++ b/Tests/Segment-Tests/Support/TestUtilities.swift @@ -6,6 +6,7 @@ // import Foundation +import XCTest @testable import Segment extension UUID{ @@ -133,3 +134,14 @@ func waitUntilStarted(analytics: Analytics?) { } } } + +extension XCTestCase { + func checkIfLeaked(_ instance: AnyObject, file: StaticString = #filePath, line: UInt = #line) { + addTeardownBlock { [weak instance] in + if instance != nil { + print("Instance \(String(describing: instance)) is not nil") + } + XCTAssertNil(instance, "Instance should have been deallocated. Potential memory leak!", file: file, line: line) + } + } +}