I have a project that is a mixture of Obj-C and Swift. A singleton instance is instantiated twice: once in the test target and once in the App target under test. I’m starting an investigation into this. It seems combinations of file target membership and dynamic linking change the outcome. I’ll come back with more information after I experiment further.
withUnsafePointer(to:) used on a class instance is almost always going to do the wrong thing. It'll give you a pointer to the memory on the stack that contains the reference to the class instance, not to the class instance itself. When that stack frame is lost, so is the pointer. Unmanaged is the correct way to pass pointers to classes in Swift.
Yep, you have the exact issue that @lukasa pointed out.
Using Unmanaged.passUnretained(instance).toOpaque() will work correctly, because it's looking at the address of the object itself (on the heap), not the address of the client variable (on the stack).
Also, you might be interested in using ObjectIdentifier(client), which is great for debug info like this.
I'm trying to understand if I do in fact have two instances of a singleton running when executing my tests. My test project is only a handful of lines of code.
[DEBUG] Within client init: 0x0000600000008270
[DEBUG] AppDelegate: 0x0000600000008270
Test Suite 'HelloWorldSwiftTests' started at 2024-07-24 05:53:49.152.
Test Case '-[HelloWorldSwiftTests.HelloWorldSwiftTests testExample]' started.
[DEBUG] Within client init: 0x0000600000008350
[DEBUG] Algorithm, perform: 0x0000600000008350
[TEST] client: 0x0000600000008350
[DEBUG] Algorithm, perform: 0x0000600000008350
Test Case '-[HelloWorldSwiftTests.HelloWorldSwiftTests testExample]' passed (0.001 seconds).
Test Suite 'HelloWorldSwiftTests' passed at 2024-07-24 05:53:49.154.
Executed 1 test, with 0 failures (0 unexpected) in 0.001 (0.002) seconds
Agove is the output from the following code.
import Foundation
final class Algorithm {
func calculate() -> Int {
let client = Client.shared
return perform(client: client) + 3
}
func perform(client: Client) -> Int {
assert(Client.shared === client)
let pointer = Unmanaged.passUnretained(client).toOpaque()
print("[DEBUG] Algorithm, perform: \(pointer)")
return client.establish() + 3
}
}
final class Client {
static let shared = Client()
private init() {
let pointer = Unmanaged.passUnretained(self).toOpaque()
print("[DEBUG] Within client init: \(pointer)")
}
func establish() -> Int {
return 4
}
}
And in the test
import XCTest
import HelloWorldSwift
final class HelloWorldSwiftTests: XCTestCase {
func testExample() throws {
let algorithm = Algorithm()
XCTAssertEqual(algorithm.calculate(), 10)
let client = Client.shared
let pointer = Unmanaged.passUnretained(client).toOpaque()
print("[TEST] client: \(pointer)")
XCTAssertEqual(algorithm.perform(client: client), 7)
}
}
Note: I am not using testable import. Instead, both client.swift and algorithm.swift are target members of all targets.
Pausing execution of the running tests reveals some runtime confusion. My understanding here is that the source is compiled in both modules, hence the Module name prefix to help distinguish them.
error: <EXPR>:8:8: error: ambiguous use of 'shared'
Client.shared
^
HelloWorldSwiftTests.Client (internal):2:25: note: found this candidate
internal static let shared: HelloWorldSwiftTests.Client
^
HelloWorldSwift.Client (internal):2:25: note: found this candidate
internal static let shared: HelloWorldSwift.Client
I need to understand this in terms of
Module boundaries
Pointers
More than one instance by accident?
Dynamic linking
Xcode running the app and test target in same process or not? If so, does dynamic linking ensure only one pointer recognised by each process?
If the same file is in two targets, then yes, you will instantiate two singletons. That's because the types are actually different, so you get one singleton for each of them.