I'm trying to understand the behavior I observe when running this test (see GitHub repo for Xcode project):
import XCTest
import Combine
class Item {
deinit {
// Randomly called by Future.deinit
// How does this instance's retain count go to zero?
// Shouldn't the Store retain this, preventing it from being freed?
// The Store itself is never deinitialized
print("item deinit")
}
}
class Store {
static let shared = Store()
private let item: Item = Item()
private let queue = DispatchQueue(label: "Store.queue")
deinit {
// Let's rule out the Store instance being deinitialized
fatalError("store deinit")
}
func fetchItem(completion: @escaping (Item) -> Void) {
queue.async {
completion(self.item)
}
}
}
final class BadAccessPlaygroundTests: XCTestCase {
func testBadAccess() {
let expectation = expectation(description: "expectation")
var task: AnyCancellable?
task = Future<Item, Never> { promise in
Store.shared.fetchItem() { result in
promise(.success(result))
}
}
.sink(
receiveCompletion: { _ in
expectation.fulfill()
task?.cancel()
}, receiveValue: { _ in }
)
wait(for: [expectation])
}
}
With Xcode 15 (15A240d) on an M2 Mac Studio and an iPhone 15 Pro, I am running the test with 10000 iterations until failure (running it only once or a few times is highly unlikely to trigger the described behavior). I expect the test to never fail or crash. However:
Item is often deinitialized in a random test iteration:
Later, it may run into an EXC_BAD_ACCESS: see screenshot
I thought perhaps it was due to something environmental with XCTest, but I have tried porting the code to a similar implementation in the app itself and was able to hit an EXC_BAD_ACCESS eventually.
I'd love to know if I'm misunderstanding something, doing something unsafe or undefined.