I have a task aggregator which aggregates and executes tasks in a synchronized manner. It's thread safe and all that. Here's what it looks like
class DaddysMagicalTaskAggregator {
private let tasks: ThreadSafeValueContainer<[KewlTask]>
private let cancelled: ThreadSafeValueContainer<Bool>
private var completion: ((Result<Bool, Error>) -> Void)?
private var alreadyCompleted: Result<Bool, Error>?
private var getTasks: [KewlTask] {
return tasks.value ?? []
}
private var isCancelled: Bool {
return cancelled.value ?? true
}
init(queue: DispatchQueue = DispatchQueue(label: "DaddysMagicalTaskAggregator")) {
self.tasks = ThreadSafeValueContainer(value: [], queue: queue)
self.cancelled = ThreadSafeValueContainer(value: false, queue: queue)
}
/// Add a task to the list of tasks
func addTask(_ task: KewlTask) -> DaddysMagicalTaskAggregator {
self.tasks.value = getTasks + [task]
return self
}
/// Add tasks to the list of tasks
func addTasks(_ tasks: [KewlTask]) -> DaddysMagicalTaskAggregator {
self.tasks.value = getTasks + tasks
return self
}
/// Start executing tasks
@discardableResult
func run() -> DaddysMagicalTaskAggregator {
guard !isCancelled else {
return self
}
guard !getTasks.isEmpty else {
alreadyCompleted = .success(true)
completion?(.success(true))
return self
}
var currentTasks = getTasks
let taskToExecute = currentTasks.removeFirst()
self.tasks.value = currentTasks
taskToExecute.execute { (result) in
switch result {
case .success:
self.run()
case.failure(let error):
self.taskFailed(with: error)
}
}
return self
}
private func taskFailed(with error: Error) {
tasks.value = []
alreadyCompleted = .failure(error)
completion?(.failure(error))
completion = nil
}
/// Add a completion block which executes after all the tasks have executed or upon failing a task.
func onCompletion(_ completion: @escaping (Result<Bool, Error>) -> Void) {
if let result = alreadyCompleted {
completion(result)
} else {
self.completion = completion
}
}
/// Cancel all tasks
func cancelAllTasks(with error: Error) {
cancelled.value = true
taskFailed(with: error)
}
}
public class KewlTask {
private let closure: ((KewlTask) -> Void)
private var completion: ((Result<Bool, Error>) -> Void)?
public init(_ closure: @escaping (KewlTask) -> Void) {
self.closure = closure
}
public func execute(_ completion: @escaping (Result<Bool, Error>) -> Void) {
self.completion = completion
closure(self)
}
/// Task succeeded
func succeeded() {
completion?(.success(true))
}
/// Task failed with given error
func failed(with error: Error) {
completion?(.failure(error))
}
/// Take action based on the result received
func processResult(_ result: Result<Bool, Error>) {
switch result {
case .success:
succeeded()
case .failure(let error):
failed(with: error)
}
}
}
public class ThreadSafeContainer {
fileprivate let queue: DispatchQueue
public init(queue: DispatchQueue) {
self.queue = queue
}
}
public class ThreadSafeValueContainer<T>: ThreadSafeContainer {
private var _value: T
public init(value: T, queue: DispatchQueue) {
self._value = value
super.init(queue: queue)
}
public var value: T? {
get {
return queue.sync { [weak self] in
self?._value
}
}
set(newValue) {
queue.sync { [weak self] in
guard let newValue = newValue else { return }
self?._value = newValue
}
}
}
}
It works as expected. However, when I write a test to make sure it deallocates, the test keeps failing even after the expectation is fulfilled.
Please look at the test code below
import XCTest
class AggregatorTests: XCTestCase {
func testTaskAggregatorShouldDeallocatateUponSuccess() {
class TaskContainer {
let aggregator: CustomKewlAggregator
init(aggregator: CustomKewlAggregator = CustomKewlAggregator()) {
self.aggregator = aggregator
}
}
class CustomKewlAggregator: DaddysMagicalTaskAggregator {
var willDeinit: (() -> Void)?
deinit {
willDeinit?()
}
}
let myExpecation = self.expectation(description: "Task Aggregator should deallocate")
var container: TaskContainer? = TaskContainer()
let t1 = KewlTask { t in
t.succeeded()
}
let t2 = KewlTask {
$0.succeeded()
}
container?.aggregator.willDeinit = {
myExpecation.fulfill()
}
container?.aggregator
.addTasks([t1, t2])
.run()
.onCompletion { (result) in
container = nil
}
waitForExpectations(timeout: 4, handler: nil)
}
}
I've added breakpoints and everything to ensure the expectation fulfillment code does execute.
It doesn't seem to be an xCode issue since I've tested it on XCode 11.7, 12.1, 12.2, and 12.4.
Any idea what's going on here? To me, it looks like a bug in XCTests.