Skip to content

Commit 496e9af

Browse files
committed
[Test] Add RetainCycleTests to reproduce retain-cycle bug.
1 parent ca811ee commit 496e9af

File tree

3 files changed

+249
-4
lines changed

3 files changed

+249
-4
lines changed

SwiftTask.xcodeproj/project.pbxproj

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
1F6A8CA319A4E4F200369A5D /* SwiftTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F46DEE3199EDF1000F97868 /* SwiftTaskTests.swift */; };
1717
1F6A8CA619A5090E00369A5D /* AlamofireTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F5FA35619A374E600975FB9 /* AlamofireTests.swift */; };
1818
1FA4634919A8D73300DD8729 /* Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FA4631719A8D70A00DD8729 /* Alamofire.swift */; };
19+
48511C5B19C17563002FE03C /* RetainCycleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48511C5A19C17563002FE03C /* RetainCycleTests.swift */; };
1920
48797D6619B42CEF0085D80F /* SwiftState.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1FA4634319A8D70A00DD8729 /* SwiftState.framework */; };
2021
48797D6719B42CF30085D80F /* SwiftState.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1FA4634319A8D70A00DD8729 /* SwiftState.framework */; };
2122
48CD5A3C19AEEBDF0042B9F1 /* SwiftTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F46DEFA199EDF8100F97868 /* SwiftTask.swift */; };
@@ -60,6 +61,7 @@
6061
1F5FA35619A374E600975FB9 /* AlamofireTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlamofireTests.swift; sourceTree = "<group>"; };
6162
1FA4631719A8D70A00DD8729 /* Alamofire.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alamofire.swift; sourceTree = "<group>"; };
6263
1FA4633019A8D70A00DD8729 /* SwiftState.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; path = SwiftState.xcodeproj; sourceTree = "<group>"; };
64+
48511C5A19C17563002FE03C /* RetainCycleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RetainCycleTests.swift; sourceTree = "<group>"; };
6365
48CD5A0C19AEE3570042B9F1 /* SwiftTask.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftTask.framework; sourceTree = BUILT_PRODUCTS_DIR; };
6466
/* End PBXFileReference section */
6567

@@ -136,6 +138,7 @@
136138
1F20250119ADA8FD00DE0495 /* BasicTests.swift */,
137139
1F46DEE3199EDF1000F97868 /* SwiftTaskTests.swift */,
138140
1F2024FE19AD97A700DE0495 /* CustomOperatorTests.swift */,
141+
48511C5A19C17563002FE03C /* RetainCycleTests.swift */,
139142
1F5FA35619A374E600975FB9 /* AlamofireTests.swift */,
140143
1F46DEE1199EDF1000F97868 /* Supporting Files */,
141144
);
@@ -188,7 +191,7 @@
188191
children = (
189192
1FA4634319A8D70A00DD8729 /* SwiftState.framework */,
190193
1FA4634519A8D70A00DD8729 /* SwiftStateTests.xctest */,
191-
4872D5C819B423D600F326B5 /* SwiftState-iOS.framework */,
194+
4872D5C819B423D600F326B5 /* SwiftState.framework */,
192195
);
193196
name = Products;
194197
sourceTree = "<group>";
@@ -328,7 +331,7 @@
328331
remoteRef = 1FA4634419A8D70A00DD8729 /* PBXContainerItemProxy */;
329332
sourceTree = BUILT_PRODUCTS_DIR;
330333
};
331-
4872D5C819B423D600F326B5 /* SwiftState-iOS.framework */ = {
334+
4872D5C819B423D600F326B5 /* SwiftState.framework */ = {
332335
isa = PBXReferenceProxy;
333336
fileType = wrapper.framework;
334337
path = SwiftState.framework;
@@ -379,6 +382,7 @@
379382
1F20250219ADA8FD00DE0495 /* BasicTests.swift in Sources */,
380383
1F6A8CA619A5090E00369A5D /* AlamofireTests.swift in Sources */,
381384
1F6A8CA319A4E4F200369A5D /* SwiftTaskTests.swift in Sources */,
385+
48511C5B19C17563002FE03C /* RetainCycleTests.swift in Sources */,
382386
1F46DEFD199EE2C200F97868 /* _TestCase.swift in Sources */,
383387
);
384388
runOnlyForDeploymentPostprocessing = 0;

SwiftTaskTests/RetainCycleTests.swift

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
//
2+
// RetainCycleTests.swift
3+
// SwiftTask
4+
//
5+
// Created by Yasuhiro Inami on 2014/09/11.
6+
// Copyright (c) 2014年 Yasuhiro Inami. All rights reserved.
7+
//
8+
9+
import SwiftTask
10+
import XCTest
11+
12+
class Player
13+
{
14+
var completionHandler: (Void -> Void)?
15+
16+
deinit
17+
{
18+
println("deinit: Player")
19+
}
20+
21+
func doSomething(completion: (Void -> Void)? = nil)
22+
{
23+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 100_000_000), dispatch_get_main_queue()) { /*[weak self] in */
24+
25+
// NOTE: callback (either as argument or stored property) must be captured by dispatch_queue
26+
27+
if let completion = completion {
28+
completion() // NOTE: interestingly, self is also captured by just calling completion() if [weak self] is not set
29+
}
30+
else {
31+
self.completionHandler?()
32+
}
33+
}
34+
}
35+
36+
func cancel()
37+
{
38+
// no operation, just for capturing test
39+
}
40+
}
41+
42+
class RetainCycleTests: _TestCase
43+
{
44+
typealias Task = SwiftTask.Task<Float, String, ErrorString>
45+
46+
// weak properties for inspection
47+
weak var task: Task?
48+
weak var player: Player?
49+
50+
func _testPlayer_completionAsArgument_notConfigured()
51+
{
52+
var expect = self.expectationWithDescription(__FUNCTION__)
53+
54+
//
55+
// retain cycle:
56+
// ("x->" = will be released shortly)
57+
//
58+
// 1. dispatch_queue x-> task
59+
// dispatch_queue (via player impl) x-> completion -> fulfill -> task
60+
//
61+
// 2. dispatch_queue x-> player
62+
// dispatch_queue (via player impl) x-> player (via completion capturing)
63+
//
64+
self.task = Task { (progress, fulfill, reject, configure) in
65+
66+
let player = Player()
67+
self.player = player
68+
69+
// comment-out: no configuration test
70+
// configure.cancel = { player.cancel() }
71+
72+
player.doSomething {
73+
fulfill("OK")
74+
}
75+
76+
}
77+
78+
XCTAssertNotNil(self.task, "self.task (weak) should NOT be nil because of retain cycle: task <- dispatch_queue.")
79+
XCTAssertNotNil(self.player, "self.player (weak) should NOT nil because player is not retained by dispatch_queue.")
80+
81+
println("then")
82+
83+
self.task!.then { (value: String) -> Void in
84+
85+
XCTAssertEqual(value, "OK")
86+
expect.fulfill()
87+
88+
}
89+
90+
self.wait {
91+
XCTAssertNil(self.task)
92+
XCTAssertNil(self.player)
93+
}
94+
}
95+
96+
func _testPlayer_completionAsArgument_configured()
97+
{
98+
var expect = self.expectationWithDescription(__FUNCTION__)
99+
100+
//
101+
// retain cycle:
102+
// ("x->" = will be released shortly)
103+
//
104+
// 1. dispatch_queue x-> task
105+
// dispatch_queue (via player impl) x-> completion -> fulfill -> task
106+
//
107+
// 2. dispatch_queue x-> player
108+
// dispatch_queue (via player impl) x-> player (via completion capturing)
109+
//
110+
// 3. task -> player
111+
// task -> task.machine -> configure (via pause/resume addEventHandler) -> configure.cancel -> player
112+
//
113+
self.task = Task { (progress, fulfill, reject, configure) in
114+
115+
let player = Player()
116+
self.player = player
117+
118+
configure.cancel = { player.cancel() }
119+
120+
player.doSomething {
121+
fulfill("OK")
122+
}
123+
124+
}
125+
126+
XCTAssertNotNil(self.task, "self.task (weak) should NOT be nil because of retain cycle: task <- dispatch_queue.")
127+
XCTAssertNotNil(self.player, "self.player (weak) should NOT be nil because of retain cycle: player <- configure <- task.")
128+
129+
println("then")
130+
131+
self.task!.then { (value: String) -> Void in
132+
133+
XCTAssertEqual(value, "OK")
134+
expect.fulfill()
135+
136+
}
137+
138+
self.wait {
139+
XCTAssertNil(self.task)
140+
XCTAssertNil(self.player)
141+
}
142+
}
143+
144+
func _testPlayer_completionAsStoredProperty_notConfigured()
145+
{
146+
var expect = self.expectationWithDescription(__FUNCTION__)
147+
148+
//
149+
// retain cycle:
150+
// ("x->" = will be released shortly)
151+
//
152+
// 1. dispatch_queue x-> player -> task
153+
// dispatch_queue (via player impl) x-> player -> player.completionHandler -> fulfill -> task
154+
//
155+
self.task = Task { (progress, fulfill, reject, configure) in
156+
157+
let player = Player()
158+
self.player = player
159+
160+
// comment-out: no configuration test
161+
// configure.cancel = { player.cancel() }
162+
163+
player.completionHandler = {
164+
fulfill("OK")
165+
}
166+
player.doSomething()
167+
168+
}
169+
170+
XCTAssertNotNil(self.task, "self.task (weak) should not be nil because of retain cycle: task <- player <- dispatch_queue.")
171+
XCTAssertNotNil(self.player, "self.player (weak) should not be nil because of retain cycle: player <- configure <- task.")
172+
173+
println("then")
174+
175+
self.task!.then { (value: String) -> Void in
176+
177+
XCTAssertEqual(value, "OK")
178+
expect.fulfill()
179+
180+
}
181+
182+
self.wait {
183+
XCTAssertNil(self.task)
184+
XCTAssertNil(self.player)
185+
}
186+
}
187+
188+
func testPlayer_completionAsStoredProperty_configured()
189+
{
190+
var expect = self.expectationWithDescription(__FUNCTION__)
191+
192+
//
193+
// retain cycle:
194+
// ("x->" = will be released shortly)
195+
//
196+
// 1. dispatch_queue x-> player -> task
197+
// dispatch_queue (via player impl) -> player -> player.completionHandler -> fulfill -> task
198+
//
199+
// 2. task -> player
200+
// task -> task.machine -> configure (via pause/resume addEventHandler) -> configure.pause/resume/cancel -> player
201+
//
202+
self.task = Task { (progress, fulfill, reject, configure) in
203+
204+
let player = Player()
205+
self.player = player
206+
207+
configure.cancel = { player.cancel() }
208+
209+
player.completionHandler = {
210+
fulfill("OK")
211+
}
212+
player.doSomething()
213+
214+
}
215+
216+
XCTAssertNotNil(self.task, "self.task (weak) should not be nil because of retain cycle: task <- player <- dispatch_queue.")
217+
XCTAssertNotNil(self.player, "self.player (weak) should not be nil because of retain cycle: player <- configure <- task.")
218+
219+
println("then")
220+
221+
self.task!.then { (value: String) -> Void in
222+
223+
XCTAssertEqual(value, "OK")
224+
expect.fulfill()
225+
226+
}
227+
228+
// TODO: this test will fail
229+
self.wait {
230+
XCTAssertNil(self.task)
231+
XCTAssertNil(self.player)
232+
}
233+
}
234+
}

SwiftTaskTests/_TestCase.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,16 @@ class _TestCase: XCTestCase
2525
super.tearDown()
2626
}
2727

28-
func wait()
28+
func wait(handler: (Void -> Void)? = nil)
2929
{
30-
self.waitForExpectationsWithTimeout(3) { error in println("wait error = \(error)") }
30+
self.waitForExpectationsWithTimeout(3) { error in
31+
32+
println("wait error = \(error)")
33+
34+
if let handler = handler {
35+
handler()
36+
}
37+
}
3138
}
3239

3340
var isAsync: Bool { return false }

0 commit comments

Comments
 (0)