Further isolate tests that should be isolated

This commit is contained in:
Max Radermacher 2025-04-03 17:45:19 -05:00 committed by GitHub
parent ce833bd48d
commit bb9f83f69c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 152 additions and 60 deletions

View File

@ -157,7 +157,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
// This should be the first thing we do.
let mainAppContext = MainAppContext()
SetCurrentAppContext(mainAppContext)
SetCurrentAppContext(mainAppContext, isRunningTests: false)
let debugLogger = DebugLogger.shared
debugLogger.enableTTYLoggingIfNeeded()

View File

@ -33,7 +33,7 @@ class DebugUISyncMessages: DebugUIPage {
}
private static func sendBlockListSyncMessage() {
Task { await SSKEnvironment.shared.blockingManagerRef.syncBlockList() }
_ = SSKEnvironment.shared.blockingManagerRef.syncBlockList()
}
private static func sendConfigurationSyncMessage() {

View File

@ -8,7 +8,7 @@ import XCTest
@testable import SignalUI
final class PhoneNumberCountryTest: XCTestCase {
final class PhoneNumberCountryTest: SignalBaseTest {
func testCountryCodesForSearchTerm() {
func countryCodes(forSearchTerm searchTerm: String?) -> [String] {
return PhoneNumberCountry.buildCountries(searchText: searchTerm).map(\.countryCode)

View File

@ -8,11 +8,13 @@ public import XCTest
@testable import SignalServiceKit
open class SignalBaseTest: XCTestCase {
private var oldContext: (any AppContext)!
@MainActor
public override func setUp() {
super.setUp()
let setupExpectation = expectation(description: "mock ssk environment setup completed")
self.oldContext = CurrentAppContext()
Task {
await MockSSKEnvironment.activate()
setupExpectation.fulfill()
@ -20,8 +22,9 @@ open class SignalBaseTest: XCTestCase {
waitForExpectations(timeout: 2)
}
@MainActor
open override func tearDown() {
MockSSKEnvironment.flushAndWait()
MockSSKEnvironment.deactivate(oldContext: self.oldContext)
super.tearDown()
}

View File

@ -57,11 +57,12 @@ class CollectionViewLogger: MediaGalleryCollectionViewUpdaterDelegate {
}
final class MediaGalleryCollectionViewUpdaterTest: SignalBaseTest {
final class MediaGalleryCollectionViewUpdaterTest: XCTestCase {
private var logger: CollectionViewLogger!
private var updater: MediaGalleryCollectionViewUpdater!
override func setUp() {
super.setUp()
logger = CollectionViewLogger()
}

View File

@ -33,6 +33,7 @@ class OWSContactsManagerTest: SignalBaseTest {
override func tearDown() {
mockUsernameLookupMananger.clearAllUsernames()
super.tearDown()
}
private func makeContactsManager() -> OWSContactsManager {

View File

@ -12,7 +12,7 @@ class NSEEnvironment {
init() {
self.appContext = NSEContext()
SetCurrentAppContext(self.appContext)
SetCurrentAppContext(self.appContext, isRunningTests: false)
appReadiness = AppReadinessImpl()
}

View File

@ -34,7 +34,14 @@ public class DependenciesBridge {
}
private static var _shared: DependenciesBridge?
static func setShared(_ dependenciesBridge: DependenciesBridge) {
#if TESTABLE_BUILD
static var hasShared: Bool {
return _shared != nil
}
#endif
static func setShared(_ dependenciesBridge: DependenciesBridge?, isRunningTests: Bool) {
owsPrecondition((_shared == nil && dependenciesBridge != nil) || isRunningTests)
Self._shared = dependenciesBridge
}

View File

@ -1366,7 +1366,7 @@ public class AppSetup {
wallpaperImageStore: wallpaperImageStore,
wallpaperStore: wallpaperStore
)
DependenciesBridge.setShared(dependenciesBridge)
DependenciesBridge.setShared(dependenciesBridge, isRunningTests: appContext.isRunningTests)
let proximityMonitoringManager = OWSProximityMonitoringManagerImpl()
let avatarBuilder = AvatarBuilder(appReadiness: appReadiness)

View File

@ -29,6 +29,14 @@ public class BlockingManager {
private let blockedGroupStore: BlockedGroupStore
private let blockedRecipientStore: BlockedRecipientStore
private let syncQueue = SerialTaskQueue()
#if TESTABLE_BUILD
func flushSyncQueueTask() -> Task<Void, any Error> {
return self.syncQueue.enqueue {}
}
#endif
init(
appReadiness: AppReadiness,
blockedGroupStore: BlockedGroupStore,
@ -48,7 +56,9 @@ public class BlockingManager {
private func syncIfNeeded() {
appReadiness.runNowOrWhenMainAppDidBecomeReadyAsync {
Task { await self.sendBlockListSyncMessage(force: false) }
self.syncQueue.enqueue {
await self.sendBlockListSyncMessage(force: false)
}
}
}
@ -66,7 +76,9 @@ public class BlockingManager {
private func setNeedsSync(tx: DBWriteTransaction) {
setChangeToken(fetchChangeToken(tx: tx) + 1, tx: tx)
tx.addSyncCompletion {
Task { await self.sendBlockListSyncMessage(force: false) }
self.syncQueue.enqueue {
await self.sendBlockListSyncMessage(force: false)
}
}
}
@ -407,8 +419,10 @@ public class BlockingManager {
}
}
public func syncBlockList() async {
await sendBlockListSyncMessage(force: true)
public func syncBlockList() -> Task<Void, any Error> {
return self.syncQueue.enqueue {
await self.sendBlockListSyncMessage(force: true)
}
}
private func sendBlockListSyncMessage(force: Bool) async {

View File

@ -688,7 +688,7 @@ public final class MessageReceiver {
let pendingTask = Self.buildPendingTask(label: "syncBlockList")
Task {
defer { pendingTask.complete() }
await SSKEnvironment.shared.blockingManagerRef.syncBlockList()
try? await SSKEnvironment.shared.blockingManagerRef.syncBlockList().value
}
case .configuration:

View File

@ -14,8 +14,8 @@ public class SSKEnvironment: NSObject {
@objc
public static var shared: SSKEnvironment { _shared! }
public static func setShared(_ env: SSKEnvironment, isRunningTests: Bool) {
owsPrecondition(_shared == nil || isRunningTests)
public static func setShared(_ env: SSKEnvironment?, isRunningTests: Bool) {
owsPrecondition((_shared == nil && env != nil) || isRunningTests)
_shared = env
}

View File

@ -11,10 +11,28 @@ import GRDB
public class MockSSKEnvironment {
/// Set up a mock SSK environment as well as ``DependenciesBridge``.
@MainActor
public static func activate() async {
public static func activate(
appReadiness: any AppReadiness = AppReadinessImpl(),
callMessageHandler: any CallMessageHandler = NoopCallMessageHandler(),
currentCallProvider: any CurrentCallProvider = CurrentCallNoOpProvider(),
notificationPresenter: any NotificationPresenter = NoopNotificationPresenterImpl(),
incrementalMessageTSAttachmentMigratorFactory: any IncrementalMessageTSAttachmentMigratorFactory = IncrementalMessageTSAttachmentMigratorFactoryMock(),
testDependencies: AppSetup.TestDependencies? = nil
) async {
owsPrecondition(!(CurrentAppContext() is TestAppContext))
owsPrecondition(!SSKEnvironment.hasShared)
owsPrecondition(!DependenciesBridge.hasShared)
let testAppContext = TestAppContext()
SetCurrentAppContext(testAppContext)
let appReadiness = AppReadinessImpl()
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.
_ = await AppSetup().start(
appContext: testAppContext,
@ -26,12 +44,12 @@ public class MockSSKEnvironment {
),
paymentsEvents: PaymentsEventsNoop(),
mobileCoinHelper: MobileCoinHelperMock(),
callMessageHandler: NoopCallMessageHandler(),
currentCallProvider: CurrentCallNoOpProvider(),
notificationPresenter: NoopNotificationPresenterImpl(),
incrementalMessageTSAttachmentMigratorFactory: IncrementalMessageTSAttachmentMigratorFactoryMock(),
callMessageHandler: callMessageHandler,
currentCallProvider: currentCallProvider,
notificationPresenter: notificationPresenter,
incrementalMessageTSAttachmentMigratorFactory: incrementalMessageTSAttachmentMigratorFactory,
messageBackupErrorPresenterFactory: NoOpMessageBackupErrorPresenterFactory(),
testDependencies: AppSetup.TestDependencies(
testDependencies: testDependencies ?? AppSetup.TestDependencies(
contactManager: FakeContactsManager(),
groupV2Updates: MockGroupV2Updates(),
groupsV2: MockGroupsV2(),
@ -56,7 +74,8 @@ public class MockSSKEnvironment {
).prepareDatabase()
}
public static func flushAndWait() {
@MainActor
private static func flushAndWait() {
AssertIsOnMainThread()
waitForMainQueue()
@ -68,6 +87,30 @@ public class MockSSKEnvironment {
waitForMainQueue()
}
public static func deactivateAsync(oldContext: any AppContext) async {
await withCheckedContinuation { continuation in
DispatchQueue.main.async {
SSKEnvironment.shared.databaseStorageRef.grdbStorage.pool.barrierWriteWithoutTransaction { _ in }
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

View File

@ -152,7 +152,8 @@ public func CurrentAppContext() -> any AppContext {
currentAppContext!
}
public func SetCurrentAppContext(_ appContext: any AppContext) {
public func SetCurrentAppContext(_ appContext: any AppContext, isRunningTests: Bool) {
owsPrecondition(currentAppContext == nil || isRunningTests)
currentAppContext = appContext
}

View File

@ -8,7 +8,7 @@ import Foundation
import XCTest
@testable import SignalServiceKit
final class PreKeyTaskTests: XCTestCase {
final class PreKeyTaskTests: SSKBaseTest {
private var mockTSAccountManager: MockTSAccountManager!
private var mockIdentityManager: PreKey.Mocks.IdentityManager!
@ -57,6 +57,7 @@ final class PreKeyTaskTests: XCTestCase {
override func tearDown() {
mockAPIClient.setPreKeysResult.ensureUnset()
super.tearDown()
}
//

View File

@ -23,6 +23,19 @@ class BlockingManagerTests: SSKBaseTest {
)
}
override func tearDown() {
let flushTask = blockingManager.flushSyncQueueTask()
let otherFlushTask = otherBlockingManager.flushSyncQueueTask()
let flushExpectation = self.expectation(description: "flush sync queues")
Task {
try! await flushTask.value
try! await otherFlushTask.value
flushExpectation.fulfill()
}
self.wait(for: [flushExpectation], timeout: 60)
super.tearDown()
}
func testAddBlockedAddress() {
// Setup
let aci = Aci.randomForTesting()

View File

@ -286,18 +286,11 @@ class MessageBackupIntegrationTests: XCTestCase {
/// by LibSignal. They should be equivalent; any disparity indicates that
/// some data was dropped or modified as part of the import/export process,
/// which should be idempotent.
@MainActor
private func runRoundTripTest(
testCaseFileUrl: URL,
failureLogOutput: LibSignalComparisonFailureLogOutput
) async throws {
/// A backup doesn't contain our own local identifiers. Rather, those
/// are determined as part of registration for a backup import, and are
/// already-known for a backup export.
///
/// Consequently, we can use any local identifiers for our test
/// purposes without worrying about the contents of each test case's
/// backup file.
let localIdentifiers: LocalIdentifiers = .forUnitTests
/// Backup files hardcode timestamps, some of which are interpreted
/// relative to "now". For example, "deleted" story distribution lists
@ -312,7 +305,32 @@ class MessageBackupIntegrationTests: XCTestCase {
/// that as our "now" during import.
let backupTimeMs = try await readBackupTimeMs(testCaseFileUrl: testCaseFileUrl)
let oldContext = CurrentAppContext()
await initializeApp(dateProvider: { Date(millisecondsSince1970: backupTimeMs) })
let result = await Result {
try await self._runRoundTripTest(
testCaseFileUrl: testCaseFileUrl,
backupTimeMs: backupTimeMs,
failureLogOutput: failureLogOutput
)
}
await deinitializeApp(oldContext: oldContext)
try result.get()
}
private func _runRoundTripTest(
testCaseFileUrl: URL,
backupTimeMs: UInt64,
failureLogOutput: LibSignalComparisonFailureLogOutput
) async throws {
/// A backup doesn't contain our own local identifiers. Rather, those
/// are determined as part of registration for a backup import, and are
/// already-known for a backup export.
///
/// Consequently, we can use any local identifiers for our test
/// purposes without worrying about the contents of each test case's
/// backup file.
let localIdentifiers: LocalIdentifiers = .forUnitTests
try await deps.messageBackupManager.importPlaintextBackup(
fileUrl: testCaseFileUrl,
@ -429,44 +447,28 @@ class MessageBackupIntegrationTests: XCTestCase {
@MainActor
private func initializeApp(dateProvider: DateProvider?) async {
let testAppContext = TestAppContext()
SetCurrentAppContext(testAppContext)
let appReadiness = AppReadinessMock()
/// 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 databaseStorage = try! SDSDatabaseStorage(
appReadiness: appReadiness,
databaseFileUrl: SDSDatabaseStorage.grdbDatabaseFileUrl,
keychainStorage: MockKeychainStorage()
)
/// We use crashy versions of dependencies that should never be called
/// during backups, and no-op implementations of payments because those
/// are bound to the SignalUI target.
_ = await AppSetup().start(
appContext: testAppContext,
await MockSSKEnvironment.activate(
appReadiness: appReadiness,
databaseStorage: databaseStorage,
paymentsEvents: PaymentsEventsNoop(),
mobileCoinHelper: MobileCoinHelperMock(),
callMessageHandler: CrashyMocks.MockCallMessageHandler(),
currentCallProvider: CrashyMocks.MockCurrentCallThreadProvider(),
notificationPresenter: CrashyMocks.MockNotificationPresenter(),
incrementalMessageTSAttachmentMigratorFactory: NoOpIncrementalMessageTSAttachmentMigratorFactory(),
messageBackupErrorPresenterFactory: NoOpMessageBackupErrorPresenterFactory(),
testDependencies: AppSetup.TestDependencies(
backupAttachmentDownloadManager: BackupAttachmentDownloadManagerMock(),
dateProvider: dateProvider,
networkManager: CrashyMocks.MockNetworkManager(appReadiness: appReadiness, libsignalNet: nil),
webSocketFactory: CrashyMocks.MockWebSocketFactory()
)
).prepareDatabase()
)
}
private func deinitializeApp(oldContext: any AppContext) async {
await MockSSKEnvironment.deactivateAsync(oldContext: oldContext)
}
}

View File

@ -8,10 +8,13 @@ public import XCTest
import CocoaLumberjack
public class SSKBaseTest: XCTestCase {
private var oldContext: (any AppContext)!
@MainActor
public override func setUp() {
DDLog.add(DDTTYLogger.sharedInstance!)
let setupExpectation = expectation(description: "mock ssk environment setup completed")
self.oldContext = CurrentAppContext()
Task {
await MockSSKEnvironment.activate()
setupExpectation.fulfill()
@ -19,8 +22,9 @@ public class SSKBaseTest: XCTestCase {
waitForExpectations(timeout: 2)
}
@MainActor
public override func tearDown() {
MockSSKEnvironment.flushAndWait()
MockSSKEnvironment.deactivate(oldContext: self.oldContext)
super.tearDown()
}

View File

@ -38,13 +38,15 @@ class SystemStoryManagerTest: SSKBaseTest {
}
override func tearDown() {
super.tearDown()
let flushExpectation = self.expectation(description: "flush")
DispatchQueue.main.async {
self.manager.chainedPromise.enqueue { .value(()) }.ensure {
self.manager = nil
flushExpectation.fulfill()
}.cauterize()
}
self.wait(for: [flushExpectation], timeout: 60)
super.tearDown()
}
// MARK: - Downloading

View File

@ -52,7 +52,7 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
// This should be the first thing we do.
let appContext = ShareAppExtensionContext(rootViewController: self)
SetCurrentAppContext(appContext)
SetCurrentAppContext(appContext, isRunningTests: false)
let debugLogger = DebugLogger.shared
debugLogger.enableTTYLoggingIfNeeded()