@@ -10,6 +10,8 @@ class TestClass: NSObject {
1010 print ( testClassHi)
1111 return testClassHi
1212 }
13+
14+ @objc dynamic func doNothing( ) { }
1315}
1416
1517class TestSubclass : TestClass {
@@ -93,8 +95,100 @@ final class InterposeKitTests: XCTestCase {
9395 XCTAssertEqual ( testObj. sayHi ( ) , testClassHi + testSubclass)
9496 }
9597
98+ func testInterposedCleanup( ) throws {
99+ var deallocated = false
100+
101+ try autoreleasepool {
102+ let tracker = LifetimeTracker {
103+ deallocated = true
104+ }
105+
106+ // Swizzle test class
107+ let interposer = try Interpose ( TestClass . self) {
108+ try $0. hook ( #selector( TestClass . doNothing) , { store in { `self` in
109+ tracker. keep ( )
110+ let origCall = store ( ( @convention ( c) ( AnyObject, Selector) - > Void) . self)
111+ return origCall ( `self`, store. selector)
112+ } as @convention ( block) ( AnyObject ) -> Void } )
113+ }
114+
115+ // Dealloc interposer without removing hooks
116+ _ = interposer
117+ }
118+
119+ // Unreverted block should not be deallocated
120+ XCTAssertFalse ( deallocated)
121+ }
122+
123+ func testRevertedCleanup( ) throws {
124+ var deallocated = false
125+
126+ try autoreleasepool {
127+ let tracker = LifetimeTracker {
128+ deallocated = true
129+ }
130+
131+ // Swizzle test class
132+ let interposer = try Interpose ( TestClass . self) {
133+ try $0. hook ( #selector( TestClass . doNothing) , { store in { `self` in
134+ tracker. keep ( )
135+ let origCall = store ( ( @convention ( c) ( AnyObject, Selector) - > Void) . self)
136+ return origCall ( `self`, store. selector)
137+ } as @convention ( block) ( AnyObject ) -> Void } )
138+ }
139+
140+ try interposer. revert ( )
141+ }
142+
143+ // Verify that the block was deallocated
144+ XCTAssertTrue ( deallocated)
145+ }
146+
147+ func testImpRemoveBlockWorks( ) {
148+ var deallocated = false
149+
150+ let imp : IMP = autoreleasepool {
151+ let tracker = LifetimeTracker {
152+ deallocated = true
153+ }
154+
155+ let block : @convention ( block) ( AnyObject ) -> Void = { _ in
156+ // retain `tracker` inside a block
157+ tracker. keep ( )
158+ }
159+
160+ return imp_implementationWithBlock ( block)
161+ }
162+
163+ // `imp` retains `block` which retains `tracker`
164+ XCTAssertFalse ( deallocated)
165+
166+ // Detach `block` from `imp`
167+ imp_removeBlock ( imp)
168+
169+ // `block` and `tracker` should be deallocated now
170+ XCTAssertTrue ( deallocated)
171+ }
172+
173+ class LifetimeTracker {
174+ let deinitCalled : ( ) -> Void
175+
176+ init ( deinitCalled: @escaping ( ) -> Void ) {
177+ self . deinitCalled = deinitCalled
178+ }
179+
180+ deinit {
181+ deinitCalled ( )
182+ }
183+
184+ func keep( ) { }
185+ }
186+
96187 static var allTests = [
97188 ( " testClassOverrideAndRevert " , testClassOverrideAndRevert) ,
98- ( " testSubclassOverride " , testSubclassOverride)
189+ ( " testSubclassOverride " , testSubclassOverride) ,
190+ ( " testInterposedCleanup " , testInterposedCleanup) ,
191+ ( " testRevertedCleanup " , testRevertedCleanup) ,
192+ ( " testImpRemoveBlockWorks " , testImpRemoveBlockWorks)
99193 ]
100194}
0 commit comments