Skip to content

Commit 130805d

Browse files
nan-licursoragent
andcommitted
test: cover OperationRepo flush matched/unmatched delta routing
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 29ce8ad commit 130805d

2 files changed

Lines changed: 185 additions & 0 deletions

File tree

iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
3C30FE362F21FBE1001B9C25 /* EarlyTriggerTrackingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C30FE352F21FBE1001B9C25 /* EarlyTriggerTrackingTests.swift */; };
8383
3C3D34E92E95EAA5006A2924 /* LiveActivityConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3D34E82E95EAA5006A2924 /* LiveActivityConstants.swift */; };
8484
3C3D8D782E92DB7500C3E977 /* OSLiveActivityViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3D8D772E92DB7500C3E977 /* OSLiveActivityViewExtensions.swift */; };
85+
3C427AC9301BB28A0059B8B7 /* OSOperationRepoFlushTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C427AC8301BB28A0059B8B7 /* OSOperationRepoFlushTests.swift */; };
8586
3C4319092F4CE9D90075492D /* SessionEndOutcomesRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4319082F4CE9D90075492D /* SessionEndOutcomesRequestTests.swift */; };
8687
3C44673E296D099D0039A49E /* OneSignalMobileProvision.m in Sources */ = {isa = PBXBuildFile; fileRef = 912411FD1E73342200E41FD7 /* OneSignalMobileProvision.m */; };
8788
3C44673F296D09CC0039A49E /* OneSignalMobileProvision.h in Headers */ = {isa = PBXBuildFile; fileRef = 912411FC1E73342200E41FD7 /* OneSignalMobileProvision.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -1351,6 +1352,7 @@
13511352
3C30FE352F21FBE1001B9C25 /* EarlyTriggerTrackingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EarlyTriggerTrackingTests.swift; sourceTree = "<group>"; };
13521353
3C3D34E82E95EAA5006A2924 /* LiveActivityConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityConstants.swift; sourceTree = "<group>"; };
13531354
3C3D8D772E92DB7500C3E977 /* OSLiveActivityViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLiveActivityViewExtensions.swift; sourceTree = "<group>"; };
1355+
3C427AC8301BB28A0059B8B7 /* OSOperationRepoFlushTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSOperationRepoFlushTests.swift; sourceTree = "<group>"; };
13541356
3C4319082F4CE9D90075492D /* SessionEndOutcomesRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionEndOutcomesRequestTests.swift; sourceTree = "<group>"; };
13551357
3C448B9B2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OSBackgroundTaskHandlerImpl.h; sourceTree = "<group>"; };
13561358
3C448B9C2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OSBackgroundTaskHandlerImpl.m; sourceTree = "<group>"; };
@@ -2525,6 +2527,7 @@
25252527
isa = PBXGroup;
25262528
children = (
25272529
5BC1DE672C90C23E00CA8807 /* OSConsistencyManagerTests.swift */,
2530+
3C427AC8301BB28A0059B8B7 /* OSOperationRepoFlushTests.swift */,
25282531
3C23A21A2FCE0A52001D32E3 /* OneSignalIdentifiersFallbackTests.swift */,
25292532
3C23A21E2FCE0AA1001D32E3 /* OSResilientStorageTests.swift */,
25302533
3C23A21C2FCE0A83001D32E3 /* OSModelStoreRefreshTests.swift */,
@@ -4546,6 +4549,7 @@
45464549
5B053FC32CAE0843002F30C4 /* OSConsistencyManagerTests.swift in Sources */,
45474550
3C23A21F2FCE0AA1001D32E3 /* OSResilientStorageTests.swift in Sources */,
45484551
3C23A21D2FCE0A83001D32E3 /* OSModelStoreRefreshTests.swift in Sources */,
4552+
3C427AC9301BB28A0059B8B7 /* OSOperationRepoFlushTests.swift in Sources */,
45494553
3C23A21B2FCE0A52001D32E3 /* OneSignalIdentifiersFallbackTests.swift in Sources */,
45504554
);
45514555
runOnlyForDeploymentPostprocessing = 0;
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
/*
2+
Modified MIT License
3+
4+
Copyright 2026 OneSignal
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
1. The above copyright notice and this permission notice shall be included in
14+
all copies or substantial portions of the Software.
15+
16+
2. All copies of substantial portions of the Software may only be used in connection
17+
with services provided by OneSignal.
18+
19+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25+
THE SOFTWARE.
26+
*/
27+
28+
import Foundation
29+
import XCTest
30+
import OneSignalCore
31+
@testable import OneSignalOSCore
32+
33+
/// Covers `flushDeltaQueue` routing: matched deltas go to executors and leave the repo
34+
/// queue; unmatched names stay queued (and in order). Alternating match/unmatch is the
35+
/// regression case for mutating the queue while iterating it.
36+
final class OSOperationRepoFlushTests: XCTestCase {
37+
38+
private let knownDelta = "test_known_delta"
39+
private let unknownDelta = "test_unknown_delta"
40+
41+
override func setUp() {
42+
super.setUp()
43+
OneSignalIdentifiers.currentAppId = "test-app-id"
44+
resetOperationRepo()
45+
// Pause so the poller (started by addExecutor/start) cannot flush mid-setup.
46+
OSOperationRepo.sharedInstance.paused = true
47+
OSOperationRepo.sharedInstance.pollIntervalMilliseconds = 60_000
48+
}
49+
50+
override func tearDown() {
51+
resetOperationRepo()
52+
super.tearDown()
53+
}
54+
55+
func testFlush_sendsMatchedDeltasToExecutorAndClearsRepoQueue() {
56+
let executor = MockOperationExecutor(supportedDeltas: [knownDelta])
57+
let processExpectation = expectation(description: "processDeltaQueue")
58+
executor.onProcessDeltaQueue = { processExpectation.fulfill() }
59+
60+
let repo = OSOperationRepo.sharedInstance
61+
repo.addExecutor(executor)
62+
63+
let deltaA = makeDelta(name: knownDelta, property: "a")
64+
let deltaB = makeDelta(name: knownDelta, property: "b")
65+
repo.enqueueDelta(deltaA)
66+
repo.enqueueDelta(deltaB)
67+
waitUntil("both deltas enqueued") { repo.deltaQueue.count == 2 }
68+
69+
repo.paused = false
70+
repo.addFlushDeltaQueueToDispatchQueue()
71+
wait(for: [processExpectation], timeout: 2.0)
72+
73+
XCTAssertEqual(executor.enqueued.map(\.property), ["a", "b"])
74+
XCTAssertTrue(repo.deltaQueue.isEmpty)
75+
}
76+
77+
func testFlush_keepsUnmatchedDeltasInRepoQueue() {
78+
let executor = MockOperationExecutor(supportedDeltas: [knownDelta])
79+
let processExpectation = expectation(description: "processDeltaQueue")
80+
executor.onProcessDeltaQueue = { processExpectation.fulfill() }
81+
82+
let repo = OSOperationRepo.sharedInstance
83+
repo.addExecutor(executor)
84+
85+
let deltaA = makeDelta(name: unknownDelta, property: "a")
86+
let deltaB = makeDelta(name: unknownDelta, property: "b")
87+
repo.enqueueDelta(deltaA)
88+
repo.enqueueDelta(deltaB)
89+
waitUntil("both deltas enqueued") { repo.deltaQueue.count == 2 }
90+
91+
repo.paused = false
92+
repo.addFlushDeltaQueueToDispatchQueue()
93+
wait(for: [processExpectation], timeout: 2.0)
94+
95+
XCTAssertTrue(executor.enqueued.isEmpty)
96+
XCTAssertEqual(repo.deltaQueue.map(\.property), ["a", "b"])
97+
}
98+
99+
func testFlush_routesMatchedAndPreservesUnmatchedOrder() {
100+
let executor = MockOperationExecutor(supportedDeltas: [knownDelta])
101+
let processExpectation = expectation(description: "processDeltaQueue")
102+
executor.onProcessDeltaQueue = { processExpectation.fulfill() }
103+
104+
let repo = OSOperationRepo.sharedInstance
105+
repo.addExecutor(executor)
106+
107+
// Alternating names: removing matched entries mid-iteration used to skip or crash.
108+
repo.enqueueDelta(makeDelta(name: knownDelta, property: "known-1"))
109+
repo.enqueueDelta(makeDelta(name: unknownDelta, property: "unknown-1"))
110+
repo.enqueueDelta(makeDelta(name: knownDelta, property: "known-2"))
111+
repo.enqueueDelta(makeDelta(name: unknownDelta, property: "unknown-2"))
112+
repo.enqueueDelta(makeDelta(name: knownDelta, property: "known-3"))
113+
waitUntil("all deltas enqueued") { repo.deltaQueue.count == 5 }
114+
115+
repo.paused = false
116+
repo.addFlushDeltaQueueToDispatchQueue()
117+
wait(for: [processExpectation], timeout: 2.0)
118+
119+
XCTAssertEqual(executor.enqueued.map(\.property), ["known-1", "known-2", "known-3"])
120+
XCTAssertEqual(repo.deltaQueue.map(\.property), ["unknown-1", "unknown-2"])
121+
}
122+
123+
// MARK: - Helpers
124+
125+
private func resetOperationRepo() {
126+
let repo = OSOperationRepo.sharedInstance
127+
repo.deltaQueue.removeAll()
128+
repo.executors.removeAll()
129+
repo.deltasToExecutorMap.removeAll()
130+
repo.paused = false
131+
}
132+
133+
private func makeDelta(name: String, property: String) -> OSDelta {
134+
OSDelta(
135+
name: name,
136+
identityModelId: UUID().uuidString,
137+
model: OSModel(changeNotifier: OSEventProducer()),
138+
property: property,
139+
value: property
140+
)
141+
}
142+
143+
private func waitUntil(
144+
_ description: String,
145+
timeout: TimeInterval = 2.0,
146+
file: StaticString = #filePath,
147+
line: UInt = #line,
148+
_ condition: @escaping () -> Bool
149+
) {
150+
let exp = expectation(description: description)
151+
let timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { timer in
152+
if condition() {
153+
timer.invalidate()
154+
exp.fulfill()
155+
}
156+
}
157+
let result = XCTWaiter.wait(for: [exp], timeout: timeout)
158+
timer.invalidate()
159+
XCTAssertEqual(result, .completed, "Timed out waiting for: \(description)", file: file, line: line)
160+
}
161+
}
162+
163+
private final class MockOperationExecutor: OSOperationExecutor {
164+
let supportedDeltas: [String]
165+
private(set) var enqueued: [OSDelta] = []
166+
var onProcessDeltaQueue: (() -> Void)?
167+
168+
init(supportedDeltas: [String]) {
169+
self.supportedDeltas = supportedDeltas
170+
}
171+
172+
func enqueueDelta(_ delta: OSDelta) {
173+
enqueued.append(delta)
174+
}
175+
176+
func cacheDeltaQueue() {}
177+
178+
func processDeltaQueue(inBackground: Bool) {
179+
onProcessDeltaQueue?()
180+
}
181+
}

0 commit comments

Comments
 (0)