Files
ios-mail/Modules/InboxTesting/Sources/BaseTestCase.swift
T
Mateusz Szklarek 38c46c165d Refactor: Replace nonisolated(unsafe) Calendar with @TaskLocal
The `DateEnvironment.calendar` property was previously marked with `nonisolated(unsafe)`,
which disables crucial compiler safety checks for a mutable global variable.
This pattern is unsafe and can lead to data races and unpredictable behavior in
concurrent environments.

This commit replaces `nonisolated(unsafe)` with the modern `@TaskLocal`
property wrapper available in Swift 6.1. This provides a much safer API
for managing context-specific data.

The key benefits of this change are:
- **Thread Safety:** `@TaskLocal` is inherently safe. The property is read-only by default, and modifications are confined to the specific task's scope.
- **Scoped Modifications:** Changes to the calendar are made via the `withValue` function, which guarantees that the modification only applies within a local scope and does not leak, preventing global state pollution.
- **Improved Clarity:** The API now clearly expresses its intent. Consumers must explicitly create a local scope to use a different calendar, making the code more predictable and easier to reason about.
2025-07-16 16:35:40 +02:00

81 lines
3.2 KiB
Swift

// Copyright (c) 2024 Proton Technologies AG
//
// This file is part of Proton Mail.
//
// Proton Mail is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail. If not, see https://www.gnu.org/licenses/.
import InboxCore
import XCTest
open class BaseTestCase: XCTestCase {
private var originalMainScheduler: DispatchQueueScheduler!
private var originalDispatchOnMain: ((DispatchWorkItem) -> Void)!
private var originalDispatchOnMainAfter: Dispatcher.DispatchAfterType!
private var originalGlobalQueue: ((DispatchQoS.QoSClass) -> DispatchQueueScheduler)!
private var originalTimeInSeconds: ((Int) -> DispatchQueueTimeStride)!
private var original_swift_task_enqueueGlobal_hook: ConcurrencyEnvironment.Hook!
open override func setUp() {
super.setUp()
originalMainScheduler = Dispatcher.mainScheduler
originalDispatchOnMain = Dispatcher.dispatchOnMain
originalDispatchOnMainAfter = Dispatcher.dispatchOnMainAfter
originalGlobalQueue = Dispatcher.globalQueue
originalTimeInSeconds = Dispatcher.timeInSeconds
original_swift_task_enqueueGlobal_hook = ConcurrencyEnvironment.swift_task_enqueueGlobal_hook
Dispatcher.mainScheduler = AnyScheduler(DispatchQueueImmediateScheduler())
Dispatcher.dispatchOnMain = { task in task.perform() }
Dispatcher.dispatchOnMainAfter = { _, task in task.perform() }
Dispatcher.globalQueue = { _ in .init(DispatchQueueImmediateScheduler()) }
Dispatcher.timeInSeconds = { _ in .seconds(0) }
ConcurrencyEnvironment.swift_task_enqueueGlobal_hook = { job, _ in
TestExecutor.shared.enqueue(job)
}
}
open override func tearDown() {
Dispatcher.mainScheduler = originalMainScheduler
Dispatcher.dispatchOnMain = originalDispatchOnMain
Dispatcher.dispatchOnMainAfter = originalDispatchOnMainAfter
Dispatcher.globalQueue = originalGlobalQueue
Dispatcher.timeInSeconds = originalTimeInSeconds
ConcurrencyEnvironment.swift_task_enqueueGlobal_hook = original_swift_task_enqueueGlobal_hook
originalMainScheduler = nil
originalDispatchOnMain = nil
originalDispatchOnMainAfter = nil
originalGlobalQueue = nil
originalTimeInSeconds = nil
original_swift_task_enqueueGlobal_hook = nil
super.tearDown()
}
}
private final class TestExecutor: SerialExecutor {
static let shared = TestExecutor()
func enqueue(_ job: consuming ExecutorJob) {
job.runSynchronously(on: asUnownedSerialExecutor())
}
func asUnownedSerialExecutor() -> UnownedSerialExecutor {
UnownedSerialExecutor(ordinary: self)
}
}