t3-code-android-nightly/apps/mobile/scripts/fixtures/NotificationCenterManagerRegression.swift

189 lines
6.5 KiB
Swift

import Foundation
// The registry runs on macOS without starting a simulator. Only the OS-facing
// types are replaced; the tests compile the dependency's actual Swift source.
public enum UIBackgroundFetchResult {
case noData
}
public struct UNNotificationPresentationOptions: OptionSet {
public let rawValue: Int
public init(rawValue: Int) { self.rawValue = rawValue }
}
public final class UNNotification: NSObject {}
public final class UNNotificationResponse: NSObject {
let identifier: String
init(_ identifier: String) { self.identifier = identifier }
}
public protocol UNUserNotificationCenterDelegate: AnyObject {}
public final class UNUserNotificationCenter: NSObject {
private static let instance = UNUserNotificationCenter()
public weak var delegate: UNUserNotificationCenterDelegate?
public static func current() -> UNUserNotificationCenter { instance }
}
private final class TestDelegate: NotificationDelegate {
private let lock = NSLock()
private var recordedEvents: [String] = []
var onEvent: ((String) -> Void)?
var onResponse: ((UNNotificationResponse) -> Bool)?
var events: [String] { lock.withLock { recordedEvents } }
private func record(_ event: String) {
lock.withLock { recordedEvents.append(event) }
onEvent?(event)
}
func didRegister(_ deviceToken: String) { record("registered") }
func didFailRegistration(_ error: Error) { record("failed") }
func openSettings(_ notification: UNNotification?) { record("settings") }
func willPresent(_ notification: UNNotification, completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) -> Bool {
record("present")
return false
}
func didReceive(_ userInfo: [AnyHashable: Any], completionHandler: @escaping (UIBackgroundFetchResult) -> Void) -> Bool {
record("background")
return false
}
func didReceive(_ response: UNNotificationResponse, completionHandler: @escaping () -> Void) -> Bool {
record(response.identifier)
return onResponse?(response) ?? false
}
}
private func require(_ condition: @autoclosure () -> Bool, _ message: String) throws {
if !condition() {
throw NSError(domain: "NotificationCenterManagerRegression", code: 1, userInfo: [NSLocalizedDescriptionKey: message])
}
}
private func deliver(_ identifier: String) {
NotificationCenterManager.shared.userNotificationCenter(
UNUserNotificationCenter.current(),
didReceive: UNNotificationResponse(identifier),
withCompletionHandler: {}
)
}
private func reentrantCallbacks() throws {
let manager = NotificationCenterManager.shared
let center = UNUserNotificationCenter.current()
let callbacks: [(String, () -> Void)] = [
("registered", { manager.didRegister("token") }),
("failed", { manager.didFailRegistration(NSError(domain: "test", code: 1)) }),
("settings", { manager.userNotificationCenter(center, openSettingsFor: nil) }),
("present", { manager.userNotificationCenter(center, willPresent: UNNotification(), withCompletionHandler: { _ in }) }),
("background", { manager.didReceive([:], completionHandler: { _ in }) })
]
for (event, callback) in callbacks {
let original = TestDelegate()
let replacement = TestDelegate()
original.onEvent = { [weak original] _ in
guard let original else { return }
manager.removeDelegate(original)
manager.addDelegate(replacement)
}
manager.addDelegate(original)
callback()
try require(original.events == [event], "The original delegate must receive the callback once")
try require(replacement.events.isEmpty, "New delegates must not join an in-flight callback snapshot")
callback()
try require(replacement.events == [event], "Reentrant registration must take effect for the next callback")
manager.removeDelegate(replacement)
}
}
private func registrationDuringDelivery() throws {
let manager = NotificationCenterManager.shared
let original = TestDelegate()
let receiver = TestDelegate()
receiver.onResponse = { _ in true }
original.onResponse = { _ in
manager.addDelegate(receiver)
return false
}
manager.addDelegate(original)
deliver("handoff")
try require(receiver.events == ["handoff"], "A delegate registering during delivery must not miss the pending response")
manager.removeDelegate(original)
manager.removeDelegate(receiver)
}
private func responseDuringReplay() throws {
let manager = NotificationCenterManager.shared
deliver("first")
let original = TestDelegate()
original.onResponse = { response in
if response.identifier == "first" {
deliver("second")
return true
}
return false
}
manager.addDelegate(original)
let receiver = TestDelegate()
receiver.onResponse = { _ in true }
manager.addDelegate(receiver)
try require(receiver.events == ["second"], "Finishing a replay must retain responses received during callbacks")
manager.removeDelegate(original)
manager.removeDelegate(receiver)
}
private func concurrentRegistrations() throws {
let manager = NotificationCenterManager.shared
let delegates = (0..<512).map { _ in
let delegate = TestDelegate()
delegate.onResponse = { _ in true }
return delegate
}
DispatchQueue.concurrentPerform(iterations: delegates.count) { index in
manager.addDelegate(delegates[index])
if index.isMultiple(of: 16) {
manager.didRegister("during-add")
deliver("during-add")
}
}
let before = delegates.map { $0.events.count }
manager.didRegister("after-add")
for (index, delegate) in delegates.enumerated() {
try require(delegate.events.count == before[index] + 1, "Concurrent registration must retain every delegate exactly once")
}
DispatchQueue.concurrentPerform(iterations: delegates.count) { index in
manager.removeDelegate(delegates[index])
if index.isMultiple(of: 16) {
manager.didRegister("during-remove")
deliver("during-remove")
}
}
let removed = delegates.map { $0.events.count }
manager.didRegister("after-remove")
try require(delegates.map { $0.events.count } == removed, "Removed delegates must not receive new callbacks")
}
@main
private enum RegressionTests {
static func main() {
do {
switch CommandLine.arguments.last {
case "reentrant": try reentrantCallbacks()
case "handoff": try registrationDuringDelivery()
case "pending": try responseDuringReplay()
case "concurrent": try concurrentRegistrations()
default: throw NSError(domain: "NotificationCenterManagerRegression", code: 2)
}
print("passed")
} catch {
FileHandle.standardError.write(Data("\(error.localizedDescription)\n".utf8))
exit(1)
}
}
}