Skip to content

Commit c83a40e

Browse files
committed
Add unit tests
1 parent dce19d0 commit c83a40e

File tree

2 files changed

+173
-0
lines changed

2 files changed

+173
-0
lines changed

LaunchDarkly.xcodeproj/project.pbxproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
50EE85C32EA0487F007CC662 /* TimeoutExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50EE85C12EA0487F007CC662 /* TimeoutExecutor.swift */; };
4343
50EE85C42EA0487F007CC662 /* TimeoutExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50EE85C12EA0487F007CC662 /* TimeoutExecutor.swift */; };
4444
50EE85C52EA0487F007CC662 /* TimeoutExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50EE85C12EA0487F007CC662 /* TimeoutExecutor.swift */; };
45+
50EE85C72EA0749C007CC662 /* TimeoutExecutorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50EE85C62EA0749C007CC662 /* TimeoutExecutorSpec.swift */; };
4546
830BF933202D188E006DF9B1 /* HTTPURLRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 830BF932202D188E006DF9B1 /* HTTPURLRequest.swift */; };
4647
830DB3AC22380A3E00D65D25 /* HTTPHeadersSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 830DB3AB22380A3E00D65D25 /* HTTPHeadersSpec.swift */; };
4748
830DB3AE2239B54900D65D25 /* URLResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 830DB3AD2239B54900D65D25 /* URLResponse.swift */; };
@@ -434,6 +435,7 @@
434435
3D3AB9472A570F3A003AECF1 /* ModifierSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModifierSpec.swift; sourceTree = "<group>"; };
435436
3D9A12572A73236800698B8D /* UtilSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtilSpec.swift; sourceTree = "<group>"; };
436437
50EE85C12EA0487F007CC662 /* TimeoutExecutor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimeoutExecutor.swift; sourceTree = "<group>"; };
438+
50EE85C62EA0749C007CC662 /* TimeoutExecutorSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeoutExecutorSpec.swift; sourceTree = "<group>"; };
437439
830BF932202D188E006DF9B1 /* HTTPURLRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPURLRequest.swift; sourceTree = "<group>"; };
438440
830DB3AB22380A3E00D65D25 /* HTTPHeadersSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPHeadersSpec.swift; sourceTree = "<group>"; };
439441
830DB3AD2239B54900D65D25 /* URLResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLResponse.swift; sourceTree = "<group>"; };
@@ -634,6 +636,7 @@
634636
831D8B751F72A48900ED65E8 /* ServiceObjects */ = {
635637
isa = PBXGroup;
636638
children = (
639+
50EE85C62EA0749C007CC662 /* TimeoutExecutorSpec.swift */,
637640
A3047D5B2A606A0000F568E0 /* EnvironmentReporting */,
638641
B46F344025E6DB7D0078D45F /* DiagnosticReporterSpec.swift */,
639642
83CFE7CD1F7AD81D0010544E /* EventReporterSpec.swift */,
@@ -1615,6 +1618,7 @@
16151618
838AB53F1F72A7D5006F03F5 /* FlagSynchronizerSpec.swift in Sources */,
16161619
A3FFE1132B7D4BA2009EF93F /* LDValueDecoderSpec.swift in Sources */,
16171620
A3570F5A28527B8200CF241A /* LDContextCodableSpec.swift in Sources */,
1621+
50EE85C72EA0749C007CC662 /* TimeoutExecutorSpec.swift in Sources */,
16181622
837406D421F760640087B22B /* LDTimerSpec.swift in Sources */,
16191623
832307A61F7D8D720029815A /* URLRequestSpec.swift in Sources */,
16201624
A3BA7D022BD192240000DB28 /* LDClientHookSpec.swift in Sources */,
@@ -2027,6 +2031,7 @@
20272031
isa = XCBuildConfiguration;
20282032
buildSettings = {
20292033
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)";
2034+
DEVELOPMENT_TEAM = 53D32B66PT;
20302035
INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarklyTests/Info.plist";
20312036
PRODUCT_BUNDLE_IDENTIFIER = com.launchdarkly.DarklyTests;
20322037
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -2037,6 +2042,7 @@
20372042
isa = XCBuildConfiguration;
20382043
buildSettings = {
20392044
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)";
2045+
DEVELOPMENT_TEAM = 53D32B66PT;
20402046
INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarklyTests/Info.plist";
20412047
PRODUCT_BUNDLE_IDENTIFIER = com.launchdarkly.DarklyTests;
20422048
PRODUCT_NAME = "$(TARGET_NAME)";
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import XCTest
2+
3+
@testable import LaunchDarkly
4+
5+
final class TimeoutExecutorSpec: XCTestCase {
6+
7+
// Helper: create a specific queue and tag it so we can assert where completion ran.
8+
private func makeTaggedQueue(label: String = "com.test.timeout.queue") -> DispatchQueue {
9+
let q = DispatchQueue(label: label)
10+
let key = DispatchSpecificKey<String>()
11+
q.setSpecific(key: key, value: label)
12+
// stash both so tests can read them
13+
queueKey = key
14+
queueLabel = label
15+
return q
16+
}
17+
18+
private var queueKey = DispatchSpecificKey<String>()
19+
private var queueLabel = "com.test.timeout.queue"
20+
21+
// MARK: - Tests
22+
23+
func test_ResultBeforeTimeout_CallsCompletionWithResult() {
24+
let exp = expectation(description: "completion called with result")
25+
let callbackQueue = makeTaggedQueue()
26+
27+
TimeoutExecutor.run(
28+
timeout: 1.0,
29+
queue: callbackQueue,
30+
operation: { done in
31+
// Finish well before timeout
32+
DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) {
33+
done("OK")
34+
}
35+
},
36+
timeoutValue: "TIMEOUT"
37+
) { result in
38+
XCTAssertEqual(result, "OK")
39+
// Assert queue
40+
XCTAssertEqual(DispatchQueue.getSpecific(key: self.queueKey), self.queueLabel)
41+
exp.fulfill()
42+
}
43+
44+
wait(for: [exp], timeout: 2.0)
45+
}
46+
47+
func test_TimeoutWins_CallsCompletionWithTimeoutValue() {
48+
let exp = expectation(description: "completion called with timeout")
49+
let callbackQueue = makeTaggedQueue()
50+
51+
TimeoutExecutor.run(
52+
timeout: 0.2,
53+
queue: callbackQueue,
54+
operation: { _ in
55+
// Complete after the timeout
56+
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) { /* never calls done */ }
57+
},
58+
timeoutValue: "TIMEOUT"
59+
) { result in
60+
XCTAssertEqual(result, "TIMEOUT")
61+
// Assert queue
62+
XCTAssertEqual(DispatchQueue.getSpecific(key: self.queueKey), self.queueLabel)
63+
exp.fulfill()
64+
}
65+
66+
wait(for: [exp], timeout: 2.0)
67+
}
68+
69+
func test_Race_ResultAndTimeout_CompletionCalledOnce() {
70+
let exp = expectation(description: "completion called once")
71+
exp.expectedFulfillmentCount = 1
72+
73+
let callbackQueue = makeTaggedQueue()
74+
var callCount = 0
75+
let countLock = NSLock()
76+
77+
TimeoutExecutor.run(
78+
timeout: 0.15,
79+
queue: callbackQueue,
80+
operation: { done in
81+
// Schedule completion very close to timeout to create a race.
82+
DispatchQueue.global().asyncAfter(deadline: .now() + 0.14) {
83+
done("OK")
84+
}
85+
},
86+
timeoutValue: "TIMEOUT"
87+
) { _ in
88+
countLock.lock(); callCount += 1; countLock.unlock()
89+
exp.fulfill()
90+
}
91+
92+
wait(for: [exp], timeout: 2.0)
93+
XCTAssertEqual(callCount, 1, "Completion should be called exactly once")
94+
}
95+
96+
func test_CompletionsRunsOnSpecifiedQueue() {
97+
let exp = expectation(description: "completion on specified queue")
98+
let callbackQueue = makeTaggedQueue(label: "com.test.specific.queue")
99+
100+
TimeoutExecutor.run(
101+
timeout: 1.0,
102+
queue: callbackQueue,
103+
operation: { done in
104+
DispatchQueue.global().async { done("OK") }
105+
},
106+
timeoutValue: "TIMEOUT"
107+
) { result in
108+
XCTAssertEqual(result, "OK")
109+
// Verify we're on our queue
110+
XCTAssertEqual(DispatchQueue.getSpecific(key: self.queueKey), self.queueLabel)
111+
exp.fulfill()
112+
}
113+
114+
wait(for: [exp], timeout: 2.0)
115+
}
116+
117+
func test_NoCompletion_NoTimeoutScheduled_OperationStillRuns() {
118+
// We can’t directly assert no timeout is scheduled, but we can ensure:
119+
// - no completion is called (test would fail if it did)
120+
// - the operation body executed (via a flag/expectation)
121+
let opExp = expectation(description: "operation executed")
122+
123+
TimeoutExecutor.run(
124+
timeout: 0.1,
125+
queue: .main,
126+
operation: { done in
127+
// Simulate some work and signal we ran.
128+
DispatchQueue.global().asyncAfter(deadline: .now() + 0.05) {
129+
// We pass a value to `done`—it should be ignored since completion is nil.
130+
opExp.fulfill()
131+
done("IGNORED")
132+
}
133+
},
134+
timeoutValue: "TIMEOUT",
135+
completion: nil // <- optional completion
136+
)
137+
138+
// If the executor accidentally called a completion, this test would hang or require extra plumbing.
139+
wait(for: [opExp], timeout: 1.0)
140+
}
141+
142+
func test_LongOperation_ResultAfterTimeout_Ignored() {
143+
let exp = expectation(description: "timeout fired and late result ignored")
144+
let callbackQueue = makeTaggedQueue()
145+
var observedResults: [String] = []
146+
let lock = NSLock()
147+
148+
TimeoutExecutor.run(
149+
timeout: 0.1,
150+
queue: callbackQueue,
151+
operation: { done in
152+
// Complete after timeout
153+
DispatchQueue.global().asyncAfter(deadline: .now() + 0.3) { done("LATE") }
154+
},
155+
timeoutValue: "TIMEOUT"
156+
) { result in
157+
lock.lock(); observedResults.append(result); lock.unlock()
158+
exp.fulfill()
159+
}
160+
161+
wait(for: [exp], timeout: 2.0)
162+
// Give a little extra time for any accidental second call
163+
Thread.sleep(forTimeInterval: 0.3)
164+
lock.lock(); defer { lock.unlock() }
165+
XCTAssertEqual(observedResults, ["TIMEOUT"])
166+
}
167+
}

0 commit comments

Comments
 (0)