Skip to content

Commit

Permalink
test: TaskManager refactor and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
fabriziodemaria committed Dec 10, 2024
1 parent d6b4c9d commit a7ab0f3
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 18 deletions.
30 changes: 12 additions & 18 deletions Sources/Confidence/Confidence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,7 @@ public class Confidence: ConfidenceEventSender {
// Synchronization and task management resources
private var cancellables = Set<AnyCancellable>()
private let cacheQueue = DispatchQueue(label: "com.confidence.queue.cache")
private var currentFetchTask: Task<(), Never>? {
didSet {
if let oldTask = oldValue {
oldTask.cancel()
}
}
}
private var taskManager = TaskManager()

// Internal for testing
internal let remoteFlagResolver: ConfidenceResolveClient
Expand Down Expand Up @@ -161,7 +155,7 @@ public class Confidence: ConfidenceEventSender {
}

public func putContextAndWait(key: String, value: ConfidenceValue) async {
self.currentFetchTask = Task {
taskManager.currentTask = Task {
let newContext = contextManager.updateContext(withValues: [key: value], removedKeys: [])
do {
try await self.fetchAndActivate()
Expand All @@ -174,7 +168,7 @@ public class Confidence: ConfidenceEventSender {
}

public func putContextAndWait(context: ConfidenceStruct, removedKeys: [String] = []) async {
self.currentFetchTask = Task {
taskManager.currentTask = Task {
let newContext = contextManager.updateContext(withValues: context, removedKeys: removedKeys)
do {
try await self.fetchAndActivate()
Expand All @@ -187,7 +181,7 @@ public class Confidence: ConfidenceEventSender {
}

public func putContextAndWait(context: ConfidenceStruct) async {
self.currentFetchTask = Task {
taskManager.currentTask = Task {
let newContext = contextManager.updateContext(withValues: context, removedKeys: [])
do {
try await fetchAndActivate()
Expand All @@ -204,7 +198,7 @@ public class Confidence: ConfidenceEventSender {
}

public func removeContextAndWait(key: String) async {
self.currentFetchTask = Task {
taskManager.currentTask = Task {
let newContext = contextManager.updateContext(withValues: [:], removedKeys: [key])
do {
try await self.fetchAndActivate()
Expand All @@ -231,31 +225,31 @@ public class Confidence: ConfidenceEventSender {
}

public func putContext(key: String, value: ConfidenceValue) {
self.currentFetchTask = Task {
taskManager.currentTask = Task {
await putContextAndWait(key: key, value: value)
}
}

public func putContext(context: ConfidenceStruct) {
self.currentFetchTask = Task {
taskManager.currentTask = Task {
await putContextAndWait(context: context)
}
}

public func putContext(context: ConfidenceStruct, removeKeys removedKeys: [String] = []) {
self.currentFetchTask = Task {
taskManager.currentTask = Task {
await putContextAndWait(context: context, removedKeys: removedKeys)
}
}

public func removeContext(key: String) {
self.currentFetchTask = Task {
taskManager.currentTask = Task {
await removeContextAndWait(key: key)
}
}

public func putContext(context: ConfidenceStruct, removedKeys: [String]) {
self.currentFetchTask = Task {
taskManager.currentTask = Task {
let newContext = contextManager.updateContext(withValues: context, removedKeys: removedKeys)
do {
try await self.fetchAndActivate()
Expand All @@ -274,7 +268,7 @@ public class Confidence: ConfidenceEventSender {
Ensures all the already-started context changes prior to this function have been reconciliated
*/
public func awaitReconciliation() async {
while let task = self.currentFetchTask {
while let task = taskManager.currentTask {
// If current task is cancelled, return
if task.isCancelled {
return
Expand All @@ -287,7 +281,7 @@ public class Confidence: ConfidenceEventSender {
}
// If current task finished successfully
// and the set task has not changed, we are done waiting
if self.currentFetchTask == task {
if taskManager.currentTask == task {
return
}
}
Expand Down
30 changes: 30 additions & 0 deletions Sources/Confidence/TaskManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Foundation

class TaskManager {
public var currentTask: Task<(), Never>? {
didSet {
if let oldTask = oldValue {
oldTask.cancel()
}
}
}
public func awaitReconciliation() async {
while let task = self.currentTask {
// If current task is cancelled, return
if task.isCancelled {
return
}
// Wait for result of current task
await task.value
// If current task gets cancelled, check again if a new task was set
if task.isCancelled {
continue
}
// If current task finished successfully
// and the set task has not changed, we are done waiting
if self.currentTask == task {
return
}
}
}
}
96 changes: 96 additions & 0 deletions Tests/ConfidenceTests/TaskManagerTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import Foundation
import XCTest
@testable import Confidence

class TaskManagerTests: XCTestCase {
func testAwaitReconciliationCancelTask() async throws {
let signalManager = SignalManager()
let reconciliationExpectation = XCTestExpectation(description: "reconciliationExpectation")
let cancelTaskExpectation = XCTestExpectation(description: "cancelTaskExpectation")
let taskManager = TaskManager()

let tenSeconds = Task {
do {
try await Task.sleep(nanoseconds: 10_000_000_000)
await signalManager.setSignal1(true)
} catch {
cancelTaskExpectation.fulfill()
}
}
taskManager.currentTask = tenSeconds
Task {
await taskManager.awaitReconciliation()
reconciliationExpectation.fulfill()
}
// Ensures the currentTask is set and has started
try await Task.sleep(nanoseconds: 100_000_000)
tenSeconds.cancel()
await fulfillment(of: [cancelTaskExpectation, reconciliationExpectation], timeout: 1)

let finalSignal1 = await signalManager.getSignal1()

XCTAssertEqual(finalSignal1, false)
}

func testOverrideTask() async throws {
let signalManager = SignalManager()
let reconciliationExpectation = XCTestExpectation(description: "reconciliationExpectation")
let cancelTaskExpectation = XCTestExpectation(description: "cancelTaskExpectation")
let secondTaskExpectation = XCTestExpectation(description: "secondTaskExpectation")
let taskManager = TaskManager()

let tenSeconds1 = Task {
do {
try await Task.sleep(nanoseconds: 10_000_000_000)
await signalManager.setSignal1(true)
} catch {
cancelTaskExpectation.fulfill()
}
}
taskManager.currentTask = tenSeconds1
// Ensures the currentTask is set and has started
try await Task.sleep(nanoseconds: 100_000_000)

let tenSeconds2 = Task {
await signalManager.setSignal2(true)
secondTaskExpectation.fulfill()
}
taskManager.currentTask = tenSeconds2

// Ensures the currentTask is set and has started
try await Task.sleep(nanoseconds: 100_000_000)
Task {
await taskManager.awaitReconciliation()
reconciliationExpectation.fulfill()
}
await fulfillment(of: [cancelTaskExpectation, reconciliationExpectation, secondTaskExpectation], timeout: 1)

let finalSignal1 = await signalManager.getSignal1()
let finalSignal2 = await signalManager.getSignal2()

XCTAssertEqual(finalSignal1, false)
XCTAssertEqual(finalSignal2, true)
}

private actor SignalManager {
private var _signal1 = false
private var _signal2 = false

// Functions to access and mutate `signal1` and `signal2`
func setSignal1(_ value: Bool) {
_signal1 = value
}

func setSignal2(_ value: Bool) {
_signal2 = value
}

func getSignal1() -> Bool {
return _signal1
}

func getSignal2() -> Bool {
return _signal2
}
}
}

0 comments on commit a7ab0f3

Please sign in to comment.