Path: blob/main/SignalServiceKit/TestUtils/MockSSKEnvironment.swift
1 views
//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import GRDB
#if TESTABLE_BUILD
public class MockSSKEnvironment {
/// Set up a mock SSK environment as well as ``DependenciesBridge``.
@MainActor
public static func activate(
appReadiness: any AppReadiness = AppReadinessImpl(),
callMessageHandler: any CallMessageHandler = NoopCallMessageHandler(),
currentCallProvider: any CurrentCallProvider = CurrentCallNoOpProvider(),
notificationPresenter: any NotificationPresenter = NoopNotificationPresenterImpl(),
testDependencies: AppSetup.TestDependencies? = nil,
) async {
let sampleDatabase = await initializeSampleDatabase()
_ = await _activate(
appReadiness: appReadiness,
callMessageHandler: callMessageHandler,
currentCallProvider: currentCallProvider,
notificationPresenter: notificationPresenter,
testDependencies: testDependencies,
sampleDatabase: sampleDatabase,
)
}
@MainActor
private static func _activate(
appReadiness: any AppReadiness = AppReadinessImpl(),
callMessageHandler: any CallMessageHandler = NoopCallMessageHandler(),
currentCallProvider: any CurrentCallProvider = CurrentCallNoOpProvider(),
keychainStorage: MockKeychainStorage = MockKeychainStorage(),
notificationPresenter: any NotificationPresenter = NoopNotificationPresenterImpl(),
testDependencies: AppSetup.TestDependencies? = nil,
sampleDatabase: SampleDatabase?,
) async -> SampleDatabase {
owsPrecondition(!(CurrentAppContext() is TestAppContext))
owsPrecondition(!SSKEnvironment.hasShared)
owsPrecondition(!DependenciesBridge.hasShared)
let testAppContext = TestAppContext()
SetCurrentAppContext(testAppContext, isRunningTests: true)
/// Note that ``SDSDatabaseStorage/grdbDatabaseFileUrl``, through a few
/// layers of abstraction, uses the "current app context" to decide
/// where to put the database,
///
/// For a ``TestAppContext`` as configured above, this will be a
/// subdirectory of our temp directory unique to the instantiation of
/// the app context.
let databaseUrl = SDSDatabaseStorage.grdbDatabaseFileUrl
let keychainStorage: MockKeychainStorage
if let sampleDatabase {
sampleDatabase.copyTo(databaseUrl)
keychainStorage = sampleDatabase.keychainStorage.clone()
} else {
keychainStorage = MockKeychainStorage()
}
let finalContinuation = await AppSetup().start(
appContext: testAppContext,
databaseStorage: try! SDSDatabaseStorage(
appReadiness: appReadiness,
databaseFileUrl: databaseUrl,
keychainStorage: keychainStorage,
),
).migrateDatabaseSchema().initGlobals(
appContext: testAppContext,
appReadiness: appReadiness,
backupArchiveErrorPresenterFactory: NoOpBackupArchiveErrorPresenterFactory(),
deviceBatteryLevelManager: nil,
deviceSleepManager: nil,
paymentsEvents: PaymentsEventsNoop(),
mobileCoinHelper: MobileCoinHelperMock(),
callMessageHandler: callMessageHandler,
currentCallProvider: currentCallProvider,
notificationPresenter: notificationPresenter,
testDependencies: testDependencies ?? AppSetup.TestDependencies(
contactManager: FakeContactsManager(),
groupV2Updates: MockGroupV2Updates(),
groupsV2: MockGroupsV2(),
messageSender: { FakeMessageSender(accountChecker: $0) },
networkManager: OWSFakeNetworkManager(appReadiness: appReadiness, libsignalNet: nil),
paymentsCurrencies: MockPaymentsCurrencies(),
paymentsHelper: MockPaymentsHelper(),
pendingReceiptRecorder: NoopPendingReceiptRecorder(),
profileManager: OWSFakeProfileManager(),
reachabilityManager: MockSSKReachabilityManager(),
remoteConfigManager: StubbableRemoteConfigManager(),
signalService: OWSSignalServiceMock(),
storageServiceManager: FakeStorageServiceManager(),
syncManager: OWSMockSyncManager(),
systemStoryManager: SystemStoryManagerMock(),
versionedProfiles: MockVersionedProfiles(),
webSocketFactory: WebSocketFactoryMock(),
),
).migrateDatabaseData()
finalContinuation.runLaunchTasksIfNeededAndReloadCaches()
return SampleDatabase(fileUrl: databaseUrl, keychainStorage: keychainStorage)
}
struct SampleDatabase {
var fileUrl: URL
var keychainStorage: MockKeychainStorage
func copyTo(_ databaseUrl: URL) {
try! FileManager.default.copyItem(at: self.fileUrl, to: databaseUrl)
}
}
@MainActor
private static var sampleDatabase: SampleDatabase?
@MainActor
private static func initializeSampleDatabase() async -> SampleDatabase {
if let sampleDatabase {
return sampleDatabase
}
let oldContext = CurrentAppContext()
let result = await MockSSKEnvironment._activate(sampleDatabase: nil)
try! SSKEnvironment.shared.databaseStorageRef.grdbStorage.syncTruncatingCheckpoint()
self.sampleDatabase = result
await MockSSKEnvironment.deactivateAsync(oldContext: oldContext)
return result
}
@MainActor
private static func flushAndWait() {
AssertIsOnMainThread()
waitForMainQueue()
// Wait for all pending readers/writers to finish.
SSKEnvironment.shared.databaseStorageRef.grdbStorage.pool.barrierWriteWithoutTransaction { _ in }
// Wait for the MessageProcessor to finish.
SSKEnvironment.shared.messageProcessorRef.serialQueueForTests.sync {}
// Wait for the main queue *again* in case more work was scheduled.
waitForMainQueue()
}
public static func deactivateAsync(oldContext: any AppContext) async {
await withCheckedContinuation { continuation in
DispatchQueue.main.async {
SSKEnvironment.shared.databaseStorageRef.grdbStorage.pool.barrierWriteWithoutTransaction { _ in }
SSKEnvironment.shared.messageProcessorRef.serialQueueForTests.async {
DispatchQueue.main.async {
continuation.resume()
}
}
}
}
_deactivate(oldContext: oldContext)
}
@MainActor
public static func deactivate(oldContext: any AppContext) {
flushAndWait()
_deactivate(oldContext: oldContext)
}
private static func _deactivate(oldContext: any AppContext) {
SetCurrentAppContext(oldContext, isRunningTests: true)
SSKEnvironment.setShared(nil, isRunningTests: true)
DependenciesBridge.setShared(nil, isRunningTests: true)
}
private static func waitForMainQueue() {
// Spin the main run loop to flush any remaining async work.
var done = false
DispatchQueue.main.async { done = true }
while !done {
CFRunLoopRunInMode(.defaultMode, 0.0, true)
}
}
}
#endif