1import Foundation
2
3// The registry runs on macOS without starting a simulator. Only the OS-facing
4// types are replaced; the tests compile the dependency's actual Swift source.
5public enum UIBackgroundFetchResult {
6 case noData
7}
8
9public struct UNNotificationPresentationOptions: OptionSet {
10 public let rawValue: Int
11 public init(rawValue: Int) { self.rawValue = rawValue }
12}
13
14public final class UNNotification: NSObject {}
15
16public final class UNNotificationResponse: NSObject {
17 let identifier: String
18 init(_ identifier: String) { self.identifier = identifier }
19}
20
21public protocol UNUserNotificationCenterDelegate: AnyObject {}
22
23public final class UNUserNotificationCenter: NSObject {
24 private static let instance = UNUserNotificationCenter()
25 public weak var delegate: UNUserNotificationCenterDelegate?
26 public static func current() -> UNUserNotificationCenter { instance }
27}
28
29private final class TestDelegate: NotificationDelegate {
30 private let lock = NSLock()
31 private var recordedEvents: [String] = []
32 var onEvent: ((String) -> Void)?
33 var onResponse: ((UNNotificationResponse) -> Bool)?
34
35 var events: [String] { lock.withLock { recordedEvents } }
36
37 private func record(_ event: String) {
38 lock.withLock { recordedEvents.append(event) }
39 onEvent?(event)
40 }
41
42 func didRegister(_ deviceToken: String) { record("registered") }
43 func didFailRegistration(_ error: Error) { record("failed") }
44 func openSettings(_ notification: UNNotification?) { record("settings") }
45
46 func willPresent(_ notification: UNNotification, completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) -> Bool {
47 record("present")
48 return false
49 }
50
51 func didReceive(_ userInfo: [AnyHashable: Any], completionHandler: @escaping (UIBackgroundFetchResult) -> Void) -> Bool {
52 record("background")
53 return false
54 }
55
56 func didReceive(_ response: UNNotificationResponse, completionHandler: @escaping () -> Void) -> Bool {
57 record(response.identifier)
58 return onResponse?(response) ?? false
59 }
60}
61
62private func require(_ condition: @autoclosure () -> Bool, _ message: String) throws {
63 if !condition() {
64 throw NSError(domain: "NotificationCenterManagerRegression", code: 1, userInfo: [NSLocalizedDescriptionKey: message])
65 }
66}
67
68private func deliver(_ identifier: String) {
69 NotificationCenterManager.shared.userNotificationCenter(
70 UNUserNotificationCenter.current(),
71 didReceive: UNNotificationResponse(identifier),
72 withCompletionHandler: {}
73 )
74}
75
76private func reentrantCallbacks() throws {
77 let manager = NotificationCenterManager.shared
78 let center = UNUserNotificationCenter.current()
79 let callbacks: [(String, () -> Void)] = [
80 ("registered", { manager.didRegister("token") }),
81 ("failed", { manager.didFailRegistration(NSError(domain: "test", code: 1)) }),
82 ("settings", { manager.userNotificationCenter(center, openSettingsFor: nil) }),
83 ("present", { manager.userNotificationCenter(center, willPresent: UNNotification(), withCompletionHandler: { _ in }) }),
84 ("background", { manager.didReceive([:], completionHandler: { _ in }) })
85 ]
86
87 for (event, callback) in callbacks {
88 let original = TestDelegate()
89 let replacement = TestDelegate()
90 original.onEvent = { [weak original] _ in
91 guard let original else { return }
92 manager.removeDelegate(original)
93 manager.addDelegate(replacement)
94 }
95 manager.addDelegate(original)
96 callback()
97 try require(original.events == [event], "The original delegate must receive the callback once")
98 try require(replacement.events.isEmpty, "New delegates must not join an in-flight callback snapshot")
99 callback()
100 try require(replacement.events == [event], "Reentrant registration must take effect for the next callback")
101 manager.removeDelegate(replacement)
102 }
103}
104
105private func registrationDuringDelivery() throws {
106 let manager = NotificationCenterManager.shared
107 let original = TestDelegate()
108 let receiver = TestDelegate()
109 receiver.onResponse = { _ in true }
110 original.onResponse = { _ in
111 manager.addDelegate(receiver)
112 return false
113 }
114 manager.addDelegate(original)
115 deliver("handoff")
116 try require(receiver.events == ["handoff"], "A delegate registering during delivery must not miss the pending response")
117 manager.removeDelegate(original)
118 manager.removeDelegate(receiver)
119}
120
121private func responseDuringReplay() throws {
122 let manager = NotificationCenterManager.shared
123 deliver("first")
124 let original = TestDelegate()
125 original.onResponse = { response in
126 if response.identifier == "first" {
127 deliver("second")
128 return true
129 }
130 return false
131 }
132 manager.addDelegate(original)
133 let receiver = TestDelegate()
134 receiver.onResponse = { _ in true }
135 manager.addDelegate(receiver)
136 try require(receiver.events == ["second"], "Finishing a replay must retain responses received during callbacks")
137 manager.removeDelegate(original)
138 manager.removeDelegate(receiver)
139}
140
141private func concurrentRegistrations() throws {
142 let manager = NotificationCenterManager.shared
143 let delegates = (0..<512).map { _ in
144 let delegate = TestDelegate()
145 delegate.onResponse = { _ in true }
146 return delegate
147 }
148 DispatchQueue.concurrentPerform(iterations: delegates.count) { index in
149 manager.addDelegate(delegates[index])
150 if index.isMultiple(of: 16) {
151 manager.didRegister("during-add")
152 deliver("during-add")
153 }
154 }
155 let before = delegates.map { $0.events.count }
156 manager.didRegister("after-add")
157 for (index, delegate) in delegates.enumerated() {
158 try require(delegate.events.count == before[index] + 1, "Concurrent registration must retain every delegate exactly once")
159 }
160 DispatchQueue.concurrentPerform(iterations: delegates.count) { index in
161 manager.removeDelegate(delegates[index])
162 if index.isMultiple(of: 16) {
163 manager.didRegister("during-remove")
164 deliver("during-remove")
165 }
166 }
167 let removed = delegates.map { $0.events.count }
168 manager.didRegister("after-remove")
169 try require(delegates.map { $0.events.count } == removed, "Removed delegates must not receive new callbacks")
170}
171
172@main
173private enum RegressionTests {
174 static func main() {
175 do {
176 switch CommandLine.arguments.last {
177 case "reentrant": try reentrantCallbacks()
178 case "handoff": try registrationDuringDelivery()
179 case "pending": try responseDuringReplay()
180 case "concurrent": try concurrentRegistrations()
181 default: throw NSError(domain: "NotificationCenterManagerRegression", code: 2)
182 }
183 print("passed")
184 } catch {
185 FileHandle.standardError.write(Data("\(error.localizedDescription)\n".utf8))
186 exit(1)
187 }
188 }
189}