diff --git a/Sources/EmbraceCore/Embrace.swift b/Sources/EmbraceCore/Embrace.swift index 1b4e8bb5..6d2a66bb 100644 --- a/Sources/EmbraceCore/Embrace.swift +++ b/Sources/EmbraceCore/Embrace.swift @@ -143,7 +143,7 @@ To start the SDK you first need to configure it using an `Embrace.Options` insta self.upload = Embrace.createUpload(options: options, deviceId: deviceId.hex) self.captureServices = try CaptureServices(options: options, storage: storage, upload: upload) self.config = Embrace.createConfig(options: options, deviceId: deviceId.hex) - self.sessionController = SessionController(storage: storage, upload: upload) + self.sessionController = SessionController(storage: storage, upload: upload, config: config) self.sessionLifecycle = Embrace.createSessionLifecycle(controller: sessionController) self.metadata = MetadataHandler(storage: storage, sessionController: sessionController) self.logController = logControllable ?? LogController( @@ -182,6 +182,9 @@ To start the SDK you first need to configure it using an `Embrace.Options` insta throw EmbraceSetupError.invalidThread("Embrace must be started on the main thread") } + // must be called on main thread in order to fetch the app state + sessionLifecycle.setup() + Embrace.synchronizationQueue.sync { guard started == false else { Embrace.logger.warning("Embrace was already started!") diff --git a/Sources/EmbraceCore/Internal/Embrace+Setup.swift b/Sources/EmbraceCore/Internal/Embrace+Setup.swift index 2d7fc559..afa8ab73 100644 --- a/Sources/EmbraceCore/Internal/Embrace+Setup.swift +++ b/Sources/EmbraceCore/Internal/Embrace+Setup.swift @@ -4,6 +4,7 @@ import Foundation import EmbraceCommonInternal +import EmbraceConfigInternal import EmbraceOTelInternal import EmbraceStorageInternal import EmbraceUploadInternal diff --git a/Sources/EmbraceCore/Session/Lifecycle/Implementations/iOSSessionLifecycle.swift b/Sources/EmbraceCore/Session/Lifecycle/Implementations/iOSSessionLifecycle.swift index 67ecd348..7503c999 100644 --- a/Sources/EmbraceCore/Session/Lifecycle/Implementations/iOSSessionLifecycle.swift +++ b/Sources/EmbraceCore/Session/Lifecycle/Implementations/iOSSessionLifecycle.swift @@ -5,6 +5,7 @@ #if os(iOS) import Foundation import EmbraceCommonInternal +import EmbraceConfigInternal import UIKit // ignoring linting rule to have a lowercase letter first on the class name diff --git a/Sources/EmbraceCore/Session/ProcessUptime/DefaultProcessUptimeProvider.swift b/Sources/EmbraceCore/Session/ProcessUptime/DefaultProcessUptimeProvider.swift new file mode 100644 index 00000000..4b7e5600 --- /dev/null +++ b/Sources/EmbraceCore/Session/ProcessUptime/DefaultProcessUptimeProvider.swift @@ -0,0 +1,11 @@ +// +// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. +// + +import Foundation + +class DefaultProcessUptimeProvider: ProcessUptimeProvider { + func uptime(since date: Date = Date()) -> TimeInterval? { + return ProcessMetadata.uptime(since: date) + } +} diff --git a/Sources/EmbraceCore/Session/ProcessUptime/ProcessUptimeProvider.swift b/Sources/EmbraceCore/Session/ProcessUptime/ProcessUptimeProvider.swift new file mode 100644 index 00000000..ee138f63 --- /dev/null +++ b/Sources/EmbraceCore/Session/ProcessUptime/ProcessUptimeProvider.swift @@ -0,0 +1,9 @@ +// +// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. +// + +import Foundation + +protocol ProcessUptimeProvider { + func uptime(since date: Date) -> TimeInterval? +} diff --git a/Sources/EmbraceCore/Session/SessionControllable.swift b/Sources/EmbraceCore/Session/SessionControllable.swift index feb47e61..4e7b9454 100644 --- a/Sources/EmbraceCore/Session/SessionControllable.swift +++ b/Sources/EmbraceCore/Session/SessionControllable.swift @@ -13,7 +13,7 @@ protocol SessionControllable: AnyObject { var currentSession: SessionRecord? { get } @discardableResult - func startSession(state: SessionState) -> SessionRecord + func startSession(state: SessionState) -> SessionRecord? @discardableResult func endSession() -> Date diff --git a/Sources/EmbraceCore/Session/SessionController.swift b/Sources/EmbraceCore/Session/SessionController.swift index b61647e2..939d307f 100644 --- a/Sources/EmbraceCore/Session/SessionController.swift +++ b/Sources/EmbraceCore/Session/SessionController.swift @@ -4,6 +4,7 @@ import Foundation import EmbraceCommonInternal +import EmbraceConfigInternal import EmbraceStorageInternal import EmbraceUploadInternal import EmbraceOTelInternal @@ -35,23 +36,35 @@ class SessionController: SessionControllable { weak var storage: EmbraceStorage? weak var upload: EmbraceUpload? + weak var config: EmbraceConfig? + + private var backgroundSessionsEnabled: Bool { + return config?.isBackgroundSessionEnabled == true + } + let heartbeat: SessionHeartbeat let queue: DispatchQueue + let processUptimeProvider: ProcessUptimeProvider internal var notificationCenter = NotificationCenter.default init( storage: EmbraceStorage, upload: EmbraceUpload?, - heartbeatInterval: TimeInterval = SessionHeartbeat.defaultInterval + config: EmbraceConfig?, + heartbeatInterval: TimeInterval = SessionHeartbeat.defaultInterval, + processUptimeProvider: ProcessUptimeProvider = DefaultProcessUptimeProvider() ) { self.storage = storage self.upload = upload + self.config = config let heartbeatQueue = DispatchQueue(label: "com.embrace.session_heartbeat") self.heartbeat = SessionHeartbeat(queue: heartbeatQueue, interval: heartbeatInterval) self.queue = DispatchQueue(label: "com.embrace.session_controller_upload") + self.processUptimeProvider = processUptimeProvider + self.heartbeat.callback = { [weak self] in let heartbeat = Date() self?.currentSession?.lastHeartbeatTime = heartbeat @@ -65,24 +78,40 @@ class SessionController: SessionControllable { } @discardableResult - func startSession(state: SessionState) -> SessionRecord { + func startSession(state: SessionState) -> SessionRecord? { return startSession(state: state, startTime: Date()) } @discardableResult - func startSession(state: SessionState, startTime: Date = Date()) -> SessionRecord { + func startSession(state: SessionState, startTime: Date = Date()) -> SessionRecord? { // end current session first if currentSession != nil { endSession() } + // detect cold start + let isColdStart = withinColdStartInterval(startTime: startTime) + + // Don't start background session if the config is disabled. + // + // Note: There's an exception for the cold start session: + // We start the session anyways and we drop it when it ends if + // it's still considered a background session. + // Due to how iOS works we can't know for sure the state when the + // app starts, so we need to delay the logic! + // + // + + if isColdStart == false && + state == .background && + backgroundSessionsEnabled == false { + return nil + } + // - + // we lock after end session to avoid a deadlock return lock.locked { - // detect cold start - let isColdStart = withinColdStartInterval(startTime: startTime) - // create session span let newId = SessionIdentifier.random let span = SessionSpanUtils.span(id: newId, startTime: startTime, state: state, coldStart: isColdStart) @@ -121,11 +150,22 @@ class SessionController: SessionControllable { return lock.locked { // stop heartbeat heartbeat.stop() + let now = Date() + + // If the session is a background session and background sessions + // are disabled in the config, we drop the session! + // + + if currentSession?.coldStart == true && + currentSession?.state == SessionState.background.rawValue && + backgroundSessionsEnabled == false { + delete() + return now + } + // - // post notification notificationCenter.post(name: .embraceSessionWillEnd, object: currentSession) - let now = Date() currentSessionSpan?.end(time: now) SessionSpanUtils.setCleanExit(span: currentSessionSpan, cleanExit: true) @@ -173,9 +213,9 @@ class SessionController: SessionControllable { extension SessionController { static let allowedColdStartInterval: TimeInterval = 5.0 - /// - Returns: `true` if ``ProcessMetadata.uptime`` is less than or equal to the allowed cold start interval. See ``iOSAppListener.minimumColdStartInterval`` + /// - Returns: `true` if ``ProcessMetadata.uptime`` is less than or equal to the allowed cold start interval. private func withinColdStartInterval(startTime: Date) -> Bool { - guard let uptime = ProcessMetadata.uptime(since: startTime), uptime >= 0 else { + guard let uptime = processUptimeProvider.uptime(since: startTime), uptime >= 0 else { return false } @@ -194,4 +234,20 @@ extension SessionController { Embrace.logger.warning("Error trying to update session:\n\(error.localizedDescription)") } } + + private func delete() { + guard let storage = storage, + let session = currentSession else { + return + } + + do { + try storage.delete(record: session) + } catch { + Embrace.logger.warning("Error trying to delete session:\n\(error.localizedDescription)") + } + + currentSession = nil + currentSessionSpan = nil + } } diff --git a/Tests/EmbraceCoreTests/Internal/Logs/EmbraceLogAttributesBuilderTests.swift b/Tests/EmbraceCoreTests/Internal/Logs/EmbraceLogAttributesBuilderTests.swift index 518b081d..3bb0a535 100644 --- a/Tests/EmbraceCoreTests/Internal/Logs/EmbraceLogAttributesBuilderTests.swift +++ b/Tests/EmbraceCoreTests/Internal/Logs/EmbraceLogAttributesBuilderTests.swift @@ -11,7 +11,7 @@ import EmbraceCommonInternal class EmbraceLogAttributesBuilderTests: XCTestCase { private var sut: EmbraceLogAttributesBuilder! private var storage: MockMetadataFetcher! - private var controller: SpySessionController! + private var controller: MockSessionController! private var result: [String: String]! // MARK: - Test Build Alone @@ -177,11 +177,12 @@ private extension EmbraceLogAttributesBuilderTests { sessionWithId sessionId: SessionIdentifier = .random, sessionState: SessionState = .foreground ) { - controller = SpySessionController(currentSession: .with(id: sessionId, state: sessionState)) + controller = MockSessionController() + controller.currentSession = .with(id: sessionId, state: sessionState) } func givenSessionControllerWithNoSession() { - controller = SpySessionController(currentSession: nil) + controller = MockSessionController() } func givenMetadataFetcher(with metadata: [MetadataRecord]? = nil) { diff --git a/Tests/EmbraceCoreTests/Internal/Logs/LogControllerTests.swift b/Tests/EmbraceCoreTests/Internal/Logs/LogControllerTests.swift index 50570775..470b6b47 100644 --- a/Tests/EmbraceCoreTests/Internal/Logs/LogControllerTests.swift +++ b/Tests/EmbraceCoreTests/Internal/Logs/LogControllerTests.swift @@ -12,7 +12,7 @@ import EmbraceCommonInternal class LogControllerTests: XCTestCase { private var sut: LogController! private var storage: SpyStorage? - private var sessionController: SpySessionController! + private var sessionController: MockSessionController! private var upload: SpyEmbraceLogUploader! override func setUp() { diff --git a/Tests/EmbraceCoreTests/Mocks/remote_config_background_enabled.json b/Tests/EmbraceCoreTests/Mocks/remote_config_background_enabled.json new file mode 100644 index 00000000..29d2a3f0 --- /dev/null +++ b/Tests/EmbraceCoreTests/Mocks/remote_config_background_enabled.json @@ -0,0 +1,5 @@ +{ + "background": { + "threshold": 100 + } +} diff --git a/Tests/EmbraceCoreTests/Public/Metadata/MetadataHandlerTests.swift b/Tests/EmbraceCoreTests/Public/Metadata/MetadataHandlerTests.swift index 02baae4e..97849062 100644 --- a/Tests/EmbraceCoreTests/Public/Metadata/MetadataHandlerTests.swift +++ b/Tests/EmbraceCoreTests/Public/Metadata/MetadataHandlerTests.swift @@ -210,7 +210,7 @@ final class MetadataHandlerTests: XCTestCase { // start new session let newSession = sessionController.startSession(state: .foreground) - let secondSessionId = newSession.id + let secondSessionId = newSession!.id try storage.addSession( id: secondSessionId, state: .foreground, diff --git a/Tests/EmbraceCoreTests/Session/SessionControllerTests.swift b/Tests/EmbraceCoreTests/Session/SessionControllerTests.swift index e7e6026b..782849be 100644 --- a/Tests/EmbraceCoreTests/Session/SessionControllerTests.swift +++ b/Tests/EmbraceCoreTests/Session/SessionControllerTests.swift @@ -9,6 +9,7 @@ import EmbraceStorageInternal @testable import EmbraceUploadInternal import EmbraceCommonInternal import EmbraceOTelInternal +import EmbraceConfigInternal import TestSupport final class SessionControllerTests: XCTestCase { @@ -24,29 +25,30 @@ final class SessionControllerTests: XCTestCase { ) static let testRedundancyOptions = EmbraceUpload.RedundancyOptions(automaticRetryCount: 0) - var testOptions: EmbraceUpload.Options! + var uploadTestOptions: EmbraceUpload.Options! + var queue: DispatchQueue! var module: EmbraceUpload! override func setUpWithError() throws { - let urlSessionconfig = URLSessionConfiguration.ephemeral - urlSessionconfig.httpMaximumConnectionsPerHost = .max - urlSessionconfig.protocolClasses = [EmbraceHTTPMock.self] + let uploadUrlSessionconfig = URLSessionConfiguration.ephemeral + uploadUrlSessionconfig.httpMaximumConnectionsPerHost = .max + uploadUrlSessionconfig.protocolClasses = [EmbraceHTTPMock.self] - testOptions = EmbraceUpload.Options( + uploadTestOptions = EmbraceUpload.Options( endpoints: testEndpointOptions(testName: testName), cache: EmbraceUpload.CacheOptions(named: testName), metadata: Self.testMetadataOptions, redundancy: Self.testRedundancyOptions, - urlSessionConfiguration: urlSessionconfig + urlSessionConfiguration: uploadUrlSessionconfig ) - self.queue = DispatchQueue(label: "com.test.embrace.queue", attributes: .concurrent) - upload = try EmbraceUpload(options: testOptions, logger: MockLogger(), queue: queue) + self.queue = DispatchQueue(label: "com.test.embrace.upload.queue", attributes: .concurrent) + upload = try EmbraceUpload(options: uploadTestOptions, logger: MockLogger(), queue: queue) storage = try EmbraceStorage.createInMemoryDb() - // we pass nil so we only use the upload module in the relevant tests - controller = SessionController(storage: storage, upload: nil) + // we pass nil so we only use the upload/config module in the relevant tests + controller = SessionController(storage: storage, upload: nil, config: nil) } override func tearDownWithError() throws { @@ -60,19 +62,14 @@ final class SessionControllerTests: XCTestCase { let b = controller.startSession(state: .foreground) let c = controller.startSession(state: .foreground) - XCTAssertNotEqual(a.id, b.id) - XCTAssertNotEqual(a.id, c.id) - XCTAssertNotEqual(b.id, c.id) + XCTAssertNotEqual(a!.id, b!.id) + XCTAssertNotEqual(a!.id, c!.id) + XCTAssertNotEqual(b!.id, c!.id) } func test_startSession_setsForegroundState() throws { let a = controller.startSession(state: .foreground) - XCTAssertEqual(a.state, "foreground") - } - - func test_startSession_setsBackgroundState() throws { - let a = controller.startSession(state: .background) - XCTAssertEqual(a.state, "background") + XCTAssertEqual(a!.state, "foreground") } // MARK: startSession @@ -80,7 +77,7 @@ final class SessionControllerTests: XCTestCase { func test_startSession_setsCurrentSession_andPostsDidStartNotification() throws { let notificationExpectation = expectation(forNotification: .embraceSessionDidStart, object: nil) - let session = controller.startSession(state: .foreground) + let session = controller.startSession(state: .foreground)! XCTAssertNotNil(session.startTime) XCTAssertNotNil(controller.currentSessionSpan) XCTAssertEqual(controller.currentSession?.id, session.id) @@ -121,19 +118,10 @@ final class SessionControllerTests: XCTestCase { let sessions: [SessionRecord] = try storage.fetchAll() XCTAssertEqual(sessions.count, 1) - XCTAssertEqual(sessions.first?.id, session.id) + XCTAssertEqual(sessions.first?.id, session!.id) XCTAssertEqual(sessions.first?.state, "foreground") } - func test_startSession_saves_backgroundSession() throws { - let session = controller.startSession(state: .background) - - let sessions: [SessionRecord] = try storage.fetchAll() - XCTAssertEqual(sessions.count, 1) - XCTAssertEqual(sessions.first?.id, session.id) - XCTAssertEqual(sessions.first?.state, "background") - } - func test_startSession_startsSessionSpan() throws { let spanProcessor = MockSpanProcessor() EmbraceOTel.setup(spanProcessors: [spanProcessor]) @@ -143,7 +131,7 @@ final class SessionControllerTests: XCTestCase { if let spanData = spanProcessor.startedSpans.first { XCTAssertEqual( spanData.startTime.timeIntervalSince1970, - session.startTime.timeIntervalSince1970, + session!.startTime.timeIntervalSince1970, accuracy: 0.001 ) XCTAssertFalse(spanData.hasEnded) @@ -159,7 +147,7 @@ final class SessionControllerTests: XCTestCase { let session = controller.startSession(state: .foreground) XCTAssertNotNil(controller.currentSessionSpan) - XCTAssertNil(session.endTime) + XCTAssertNil(session!.endTime) let endTime = controller.endSession() @@ -175,13 +163,13 @@ final class SessionControllerTests: XCTestCase { func test_endSession_saves_foregroundSession() throws { let session = controller.startSession(state: .foreground) - XCTAssertNil(session.endTime) + XCTAssertNil(session!.endTime) let endTime = controller.endSession() let sessions: [SessionRecord] = try storage.fetchAll() XCTAssertEqual(sessions.count, 1) - XCTAssertEqual(sessions.first!.id, session.id) + XCTAssertEqual(sessions.first!.id, session!.id) XCTAssertEqual(sessions.first!.state, "foreground") XCTAssertEqual(sessions.first!.endTime!.timeIntervalSince1970, endTime.timeIntervalSince1970, accuracy: 0.001) } @@ -203,7 +191,7 @@ final class SessionControllerTests: XCTestCase { EmbraceHTTPMock.mock(url: testSessionsUrl()) // given a started session - let controller = SessionController(storage: storage, upload: upload) + let controller = SessionController(storage: storage, upload: upload, config: nil) controller.startSession(state: .foreground) // when ending the session @@ -227,7 +215,7 @@ final class SessionControllerTests: XCTestCase { EmbraceHTTPMock.mock(url: testSessionsUrl(), errorCode: 500) // given a started session - let controller = SessionController(storage: storage, upload: upload) + let controller = SessionController(storage: storage, upload: upload, config: nil) controller.startSession(state: .foreground) // when ending the session and the upload fails @@ -259,17 +247,9 @@ final class SessionControllerTests: XCTestCase { XCTAssertEqual(controller.currentSession?.state, "background") } - func test_update_assignsState_toForeground_whenPresent() throws { - controller.startSession(state: .background) - XCTAssertEqual(controller.currentSession?.state, "background") - - controller.update(state: .foreground) - XCTAssertEqual(controller.currentSession?.state, "foreground") - } - func test_update_assignsAppTerminated_toFalse_whenPresent() throws { var session = controller.startSession(state: .foreground) - session.appTerminated = true + session!.appTerminated = true controller.update(appTerminated: false) XCTAssertEqual(controller.currentSession?.appTerminated, false) @@ -277,7 +257,7 @@ final class SessionControllerTests: XCTestCase { func test_update_assignsAppTerminated_toTrue_whenPresent() throws { var session = controller.startSession(state: .foreground) - session.appTerminated = false + session!.appTerminated = false controller.update(appTerminated: true) XCTAssertEqual(controller.currentSession?.appTerminated, true) @@ -285,39 +265,155 @@ final class SessionControllerTests: XCTestCase { func test_update_changesTo_appTerminated_saveInStorage() throws { var session = controller.startSession(state: .foreground) - session.appTerminated = false + session!.appTerminated = false controller.update(appTerminated: true) let sessions: [SessionRecord] = try storage.fetchAll() XCTAssertEqual(sessions.count, 1) - XCTAssertEqual(sessions.first?.id, session.id) + XCTAssertEqual(sessions.first?.id, session!.id) XCTAssertEqual(sessions.first?.state, "foreground") XCTAssertEqual(sessions.first?.appTerminated, true) } func test_update_changesTo_sessionState_saveInStorage() throws { var session = controller.startSession(state: .foreground) - session.appTerminated = false + session!.appTerminated = false controller.update(state: .background) let sessions: [SessionRecord] = try storage.fetchAll() XCTAssertEqual(sessions.count, 1) - XCTAssertEqual(sessions.first?.id, session.id) + XCTAssertEqual(sessions.first?.id, session!.id) XCTAssertEqual(sessions.first?.state, "background") XCTAssertEqual(sessions.first?.appTerminated, false) } + // MARK: background + func test_startup_background_enabled() throws { + + // given background sessions enabled + try mockSuccessfulResponse() + + let configUrlSessionconfig = URLSessionConfiguration.ephemeral + configUrlSessionconfig.httpMaximumConnectionsPerHost = .max + configUrlSessionconfig.protocolClasses = [EmbraceHTTPMock.self] + + let configTestOptions = EmbraceConfig.Options( + apiBaseUrl: configBaseUrl, + queue: DispatchQueue(label: "com.test.embrace.config.queue", attributes: .concurrent), + appId: TestConstants.appId, + deviceId: TestConstants.deviceId, + osVersion: TestConstants.osVersion, + sdkVersion: TestConstants.sdkVersion, + appVersion: TestConstants.appVersion, + userAgent: TestConstants.userAgent, + urlSessionConfiguration: configUrlSessionconfig + ) + + let config = EmbraceConfig( + options: configTestOptions, + notificationCenter: NotificationCenter.default, + logger: MockLogger() + ) + wait(delay: .defaultTimeout) + + let controller = SessionController( + storage: storage, + upload: nil, + config: config, + processUptimeProvider: MockProcessUptimeProvider() + ) + + // when starting a cold start session in the background + let session = controller.startSession(state: .background) + + // then the session is created + XCTAssertNotNil(session) + + // when the session ends + controller.endSession() + + // then the session is stored + let sessions: [SessionRecord] = try storage.fetchAll() + XCTAssertEqual(sessions.count, 1) + XCTAssertEqual(sessions.first?.id, session!.id) + XCTAssertEqual(sessions.first?.state, "background") + } + + func mockSuccessfulResponse() throws { + var url = try XCTUnwrap(URL(string: "\(configBaseUrl)/v2/config")) + + if #available(iOS 16.0, *) { + url.append(queryItems: [ + .init(name: "appId", value: TestConstants.appId), + .init(name: "osVersion", value: TestConstants.osVersion), + .init(name: "appVersion", value: TestConstants.appVersion), + .init(name: "deviceId", value: TestConstants.deviceId), + .init(name: "sdkVersion", value: TestConstants.sdkVersion) + ]) + } else { + XCTFail("This will fail on versions prior to iOS 16.0") + } + + let path = Bundle.module.path( + forResource: "remote_config_background_enabled", + ofType: "json", + inDirectory: "Mocks" + )! + let data = try Data(contentsOf: URL(fileURLWithPath: path)) + EmbraceHTTPMock.mock(url: url, response: .withData(data, statusCode: 200)) + } + + func test_startup_background_disabled() throws { + + // given background sessions disabled + let controller = SessionController( + storage: storage, + upload: nil, + config: nil, + processUptimeProvider: MockProcessUptimeProvider() + ) + + // when starting a cold start session in the background + let session = controller.startSession(state: .background) + + // then the session is created + XCTAssertNotNil(session) + + // when the session ends + controller.endSession() + + // then the session is not stored + let sessions: [SessionRecord] = try storage.fetchAll() + XCTAssertEqual(sessions.count, 0) + } + + func test_background_disabled() throws { + // given background sessions disabled + let controller = SessionController( + storage: storage, + upload: nil, + config: nil, + processUptimeProvider: MockProcessUptimeProvider(uptime: 60) + ) + + // when starting a session in the background + let session = controller.startSession(state: .background) + + // then the session is not created + XCTAssertNil(session) + } + // MARK: heartbeat func test_heartbeat() throws { // given a session controller with a 1 second heartbeat invertal - let controller = SessionController(storage: storage, upload: nil, heartbeatInterval: 1) + let controller = SessionController(storage: storage, upload: nil, config: nil, heartbeatInterval: 1) // when starting a session let session = controller.startSession(state: .foreground) - var lastDate = session.lastHeartbeatTime + var lastDate = session!.lastHeartbeatTime // then the heartbeat time is updated every second for _ in 1...3 { @@ -343,4 +439,8 @@ private extension SessionControllerTests { func testLogsUrl(testName: String = #function) -> URL { URL(string: "https://embrace.\(testName).com/session_controller/logs")! } + + private var configBaseUrl: String { + "https://embrace.\(testName).com/config" + } } diff --git a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/MockProcessUptimeProvider.swift b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/MockProcessUptimeProvider.swift new file mode 100644 index 00000000..3012e241 --- /dev/null +++ b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/MockProcessUptimeProvider.swift @@ -0,0 +1,22 @@ +// +// Copyright © 2024 Embrace Mobile, Inc. All rights reserved. +// + +import Foundation +@testable import EmbraceCore + +class MockProcessUptimeProvider: ProcessUptimeProvider { + var uptime: TimeInterval + + convenience init() { + self.init(uptime: 0) + } + + init(uptime: TimeInterval = 0) { + self.uptime = uptime + } + + func uptime(since date: Date) -> TimeInterval? { + return uptime + } +} diff --git a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/MockSessionController.swift b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/MockSessionController.swift index 5c66adf6..d363bc84 100644 --- a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/MockSessionController.swift +++ b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/MockSessionController.swift @@ -21,12 +21,12 @@ class MockSessionController: SessionControllable { var currentSession: SessionRecord? @discardableResult - func startSession(state: SessionState) -> SessionRecord { + func startSession(state: SessionState) -> SessionRecord? { return startSession(state: state, startTime: Date()) } @discardableResult - func startSession(state: SessionState, startTime: Date = Date()) -> SessionRecord { + func startSession(state: SessionState, startTime: Date = Date()) -> SessionRecord? { if currentSession != nil { endSession() } diff --git a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/SpySessionController.swift b/Tests/EmbraceCoreTests/TestSupport/TestDoubles/SpySessionController.swift deleted file mode 100644 index 75ce9416..00000000 --- a/Tests/EmbraceCoreTests/TestSupport/TestDoubles/SpySessionController.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright © 2023 Embrace Mobile, Inc. All rights reserved. -// - -import Foundation -import EmbraceStorageInternal -import EmbraceCommonInternal - -@testable import EmbraceCore - -class SpySessionController: SessionControllable { - var currentSession: SessionRecord? - - init(currentSession: SessionRecord? = nil) { - self.currentSession = currentSession - } - - func startSession(state: SessionState) -> SessionRecord { - return currentSession! - } - - func endSession() -> Date { Date() } - func update(state: SessionState) {} - func update(appTerminated: Bool) {} -}