Skip to content

Commit 9917468

Browse files
committed
Initial tests for object hooks
1 parent 61c7257 commit 9917468

8 files changed

Lines changed: 129 additions & 115 deletions

File tree

Sources/InterposeKit/Hooks/Hook.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ extension Hook: CustomDebugStringConvertible {
184184
description.append(" hook for -[\(self.class) \(self.selector)]")
185185

186186
if case .object(let object) = self.scope {
187-
description.append(" on \(ObjectIdentifier(object))")
187+
description.append(" on \(Unmanaged.passUnretained(object).toOpaque())")
188188
}
189189

190190
if let originalIMP = self.strategy.storedOriginalIMP {

Sources/InterposeKit/Hooks/HookStrategy/ObjectHookStrategy/ObjectHookStrategy.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ final class ObjectHookStrategy: HookStrategy {
4444

4545
if let _ = checkObjectPosingAsDifferentClass(self.object) {
4646
if object_isKVOActive(self.object) {
47-
throw InterposeError.keyValueObservationDetected(object)
47+
throw InterposeError.kvoDetected(object)
4848
}
4949
// TODO: Handle the case where the object is posing as different class but not the interpose subclass
5050
}
@@ -160,6 +160,8 @@ final class ObjectHookStrategy: HookStrategy {
160160
nextHook?.originalIMP = self.storedOriginalIMP
161161
}
162162

163+
self.storedOriginalIMP = nil
164+
163165
// FUTURE: remove class pair!
164166
// This might fail if we get KVO observed.
165167
// objc_disposeClassPair does not return a bool but logs if it fails.

Sources/InterposeKit/InterposeError.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public enum InterposeError: LocalizedError {
5454
/// Object-based hooking does not work if an object is using KVO.
5555
/// The KVO mechanism also uses subclasses created at runtime but doesn't check for additional overrides.
5656
/// Adding a hook eventually crashes the KVO management code so we reject hooking altogether in this case.
57-
case keyValueObservationDetected(AnyObject)
57+
case kvoDetected(AnyObject)
5858

5959
/// Object is lying about it's actual class metadata.
6060
/// This usually happens when other swizzling libraries (like Aspects) also interfere with a class.
@@ -91,7 +91,7 @@ extension InterposeError: Equatable {
9191
return "Failed to allocate class pair: \(klass), \(subclassName)"
9292
case .unableToAddMethod(let klass, let selector):
9393
return "Unable to add method: -[\(klass) \(selector)]"
94-
case .keyValueObservationDetected(let obj):
94+
case .kvoDetected(let obj):
9595
return "Unable to hook object that uses Key Value Observing: \(obj)"
9696
case .objectPosingAsDifferentClass(let obj, let actualClass):
9797
return "Unable to hook \(type(of: obj)) posing as \(NSStringFromClass(actualClass))/"

Tests/InterposeKitTests/ClassHookTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ final class ClassHookTests: XCTestCase {
2828
hookSignature: (@convention(block) (NSObject) -> Int).self
2929
) { hook in
3030
return { `self` in
31-
1 + hook.original(self, hook.selector)
31+
hook.original(self, hook.selector) + 1
3232
}
3333
}
3434

@@ -57,7 +57,7 @@ final class ClassHookTests: XCTestCase {
5757
hookSignature: (@convention(block) (NSObject) -> Int).self
5858
) { hook in
5959
return { `self` in
60-
1 + hook.original(self, hook.selector)
60+
hook.original(self, hook.selector) + 1
6161
}
6262
}
6363

@@ -97,7 +97,7 @@ final class ClassHookTests: XCTestCase {
9797
hookSignature: (@convention(block) (NSObject) -> Int).self
9898
) { hook in
9999
return { `self` in
100-
1 + hook.original(self, hook.selector)
100+
hook.original(self, hook.selector) + 1
101101
}
102102
}
103103

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import InterposeKit
2+
import XCTest
3+
4+
fileprivate class ExampleClass: NSObject {
5+
@objc dynamic var intValue = 1
6+
}
7+
8+
final class ObjectHookTests: XCTestCase {
9+
10+
override func setUpWithError() throws {
11+
Interpose.isLoggingEnabled = true
12+
}
13+
14+
func testLifecycle_applyHook() throws {
15+
let testObject = ExampleClass()
16+
let controlObject = ExampleClass()
17+
18+
let hook = try testObject.applyHook(
19+
for: #selector(getter: ExampleClass.intValue),
20+
methodSignature: (@convention(c) (NSObject, Selector) -> Int).self,
21+
hookSignature: (@convention(block) (NSObject) -> Int).self
22+
) { hook in
23+
return { `self` in
24+
hook.original(self, hook.selector) + 1
25+
}
26+
}
27+
28+
XCTAssertEqual(testObject.intValue, 2)
29+
XCTAssertEqual(controlObject.intValue, 1)
30+
31+
XCTAssertEqual(hook.state, .active)
32+
XCTAssertMatchesRegex(
33+
hook.debugDescription,
34+
#"^Active hook for -\[ExampleClass intValue\] on 0x[0-9a-fA-F]+ \(originalIMP: 0x[0-9a-fA-F]+\)$"#
35+
)
36+
37+
try hook.revert()
38+
39+
XCTAssertEqual(testObject.intValue, 1)
40+
XCTAssertEqual(controlObject.intValue, 1)
41+
42+
XCTAssertEqual(hook.state, .pending)
43+
XCTAssertMatchesRegex(
44+
hook.debugDescription,
45+
#"^Pending hook for -\[ExampleClass intValue\] on 0x[0-9a-fA-F]+$"#
46+
)
47+
}
48+
49+
// Hooking fails on an object that has KVO activated.
50+
func testKVO_observationBeforeHooking() throws {
51+
let object = ExampleClass()
52+
53+
var didInvokeObserver = false
54+
let token = object.observe(\.intValue) { _, _ in
55+
didInvokeObserver = true
56+
}
57+
58+
XCTAssertEqual(object.intValue, 1)
59+
XCTAssertEqual(didInvokeObserver, false)
60+
61+
object.intValue = 2
62+
XCTAssertEqual(object.intValue, 2)
63+
XCTAssertEqual(didInvokeObserver, true)
64+
65+
XCTAssertThrowsError(
66+
try object.applyHook(
67+
for: #selector(getter: ExampleClass.intValue),
68+
methodSignature: (@convention(c) (NSObject, Selector) -> Int).self,
69+
hookSignature: (@convention(block) (NSObject) -> Int).self
70+
) { hook in
71+
return { `self` in
72+
hook.original(self, hook.selector) + 1
73+
}
74+
},
75+
expected: InterposeError.kvoDetected(object)
76+
)
77+
XCTAssertEqual(object.intValue, 2)
78+
79+
_ = token
80+
}
81+
82+
// KVO works just fine on an object that has already been hooked.
83+
func testKVO_observationAfterHooking() throws {
84+
let object = ExampleClass()
85+
86+
let hook = try object.applyHook(
87+
for: #selector(getter: ExampleClass.intValue),
88+
methodSignature: (@convention(c) (NSObject, Selector) -> Int).self,
89+
hookSignature: (@convention(block) (NSObject) -> Int).self
90+
) { hook in
91+
return { `self` in
92+
hook.original(self, hook.selector) + 1
93+
}
94+
}
95+
XCTAssertEqual(object.intValue, 2)
96+
97+
try hook.revert()
98+
XCTAssertEqual(object.intValue, 1)
99+
100+
try hook.apply()
101+
XCTAssertEqual(object.intValue, 2)
102+
103+
var didInvokeObserver = false
104+
let token = object.observe(\.intValue) { _, _ in
105+
didInvokeObserver = true
106+
}
107+
108+
XCTAssertEqual(object.intValue, 2)
109+
XCTAssertEqual(didInvokeObserver, false)
110+
111+
object.intValue = 2
112+
XCTAssertEqual(object.intValue, 3)
113+
XCTAssertEqual(didInvokeObserver, true)
114+
115+
_ = token
116+
}
117+
118+
}

Tests/InterposeKitTests/ToBePolished/KVOTests.swift

Lines changed: 0 additions & 76 deletions
This file was deleted.

Tests/InterposeKitTests/ToBePolished/ObjectInterposeTests.swift

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,6 @@ import XCTest
44

55
final class ObjectInterposeTests: XCTestCase {
66

7-
func testInterposeSingleObject() throws {
8-
let testObj = TestClass()
9-
let testObj2 = TestClass()
10-
11-
XCTAssertEqual(testObj.sayHi(), testClassHi)
12-
XCTAssertEqual(testObj2.sayHi(), testClassHi)
13-
14-
let hook = try testObj.applyHook(
15-
for: #selector(TestClass.sayHi),
16-
methodSignature: (@convention(c) (NSObject, Selector) -> String).self,
17-
hookSignature: (@convention(block) (NSObject) -> String).self
18-
) { hook in
19-
return { `self` in
20-
print("Before Interposing \(self)")
21-
let string = hook.original(self, hook.selector)
22-
print("After Interposing \(self)")
23-
return string + testString
24-
}
25-
}
26-
27-
XCTAssertEqual(testObj.sayHi(), testClassHi + testString)
28-
XCTAssertEqual(testObj2.sayHi(), testClassHi)
29-
try hook.revert()
30-
XCTAssertEqual(testObj.sayHi(), testClassHi)
31-
XCTAssertEqual(testObj2.sayHi(), testClassHi)
32-
try hook.apply()
33-
XCTAssertEqual(testObj.sayHi(), testClassHi + testString)
34-
XCTAssertEqual(testObj2.sayHi(), testClassHi)
35-
}
36-
377
func testInterposeSingleObjectInt() throws {
388
let testObj = TestClass()
399
let returnIntDefault = testObj.returnInt()

Tests/InterposeKitTests/UtilitiesTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@ final class UtilitiesTests: XCTestCase {
4040
let object = ExampleClass()
4141
XCTAssertFalse(object_isKVOActive(object))
4242

43-
var token1: NSKeyValueObservation? = object.observe(\.intValue, options: []) { _, _ in }
43+
var token1: NSKeyValueObservation? = object.observe(\.intValue) { _, _ in }
4444
XCTAssertTrue(object_isKVOActive(object))
4545

46-
var token2: NSKeyValueObservation? = object.observe(\.intValue, options: []) { _, _ in }
46+
var token2: NSKeyValueObservation? = object.observe(\.intValue) { _, _ in }
4747
XCTAssertTrue(object_isKVOActive(object))
4848

4949
_ = token1

0 commit comments

Comments
 (0)