Skip to content

Commit

Permalink
test: WIP testing cancellable tasks
Browse files Browse the repository at this point in the history
  • Loading branch information
fabriziodemaria committed Dec 9, 2024
1 parent ac9346b commit a0c439c
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 16 deletions.
61 changes: 45 additions & 16 deletions Sources/Confidence/Confidence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public class Confidence: ConfidenceEventSender {
private var cancellables = Set<AnyCancellable>()
private let cacheQueue = DispatchQueue(label: "com.confidence.queue.cache")
private var currentFetchTask: Task<(), Never>?
private var currentFetchTask2: Task<(), Never>?

// Internal for testing
internal let remoteFlagResolver: ConfidenceResolveClient
Expand Down Expand Up @@ -67,6 +68,7 @@ public class Confidence: ConfidenceEventSender {
}
let savedFlags = try storage.load(defaultValue: FlagResolution.EMPTY)
cache = savedFlags
print(">>> In-mem flags are now \(savedFlags.context)")
}
}

Expand Down Expand Up @@ -105,6 +107,7 @@ public class Confidence: ConfidenceEventSender {
resolveToken: resolvedFlags.resolveToken ?? ""
)
try storage.save(data: resolution)
print(">>> Storage flags are now \(context)")
}

/**
Expand Down Expand Up @@ -165,7 +168,7 @@ public class Confidence: ConfidenceEventSender {
debugLogger?.logMessage(message: "Error when putting context: \(error)", isWarning: true)
}
}
await awaitReconciliation()
await self.currentFetchTask?.value
}

public func putContextAndWait(context: ConfidenceStruct, removedKeys: [String] = []) async {
Expand All @@ -179,15 +182,22 @@ public class Confidence: ConfidenceEventSender {
debugLogger?.logMessage(message: "Error when putting context: \(error)", isWarning: true)
}
}
await awaitReconciliation()
await self.currentFetchTask?.value
}

public func putContextAndWait(context: ConfidenceStruct) async {
self.currentFetchTask?.cancel()
if let current = self.currentFetchTask {
print(">>>> cancelling task \(self.currentFetchTask.hashValue) for \(context)")
current.cancel()
}
self.currentFetchTask = Task {
let newContext = contextManager.updateContext(withValues: context, removedKeys: [])
do {
try await fetchAndActivate()
let test = Task {
try await fetchAndActivate()
}
try await test.value
print(">> Activated \(context) - \(test.isCancelled)")
debugLogger?.logContext(
action: "PutContext",
context: newContext)
Expand All @@ -197,7 +207,8 @@ public class Confidence: ConfidenceEventSender {
isWarning: true)
}
}
await awaitReconciliation()
print(">>>> added task for \(self.currentFetchTask.hashValue)")
await self.currentFetchTask?.value
}

public func removeContextAndWait(key: String) async {
Expand All @@ -215,7 +226,7 @@ public class Confidence: ConfidenceEventSender {
isWarning: true)
}
}
await awaitReconciliation()
await self.currentFetchTask?.value
}

/**
Expand All @@ -229,36 +240,44 @@ public class Confidence: ConfidenceEventSender {
}

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

public func putContext(context: ConfidenceStruct) {
self.currentFetchTask?.cancel()
self.currentFetchTask = Task {
print(">> Starting! \(context)")
if let current = self.currentFetchTask {
print(">> Cancelling! \(context)")
current.cancel()
}
self.currentFetchTask2?.cancel()
let test = Task {

Check failure on line 256 in Sources/Confidence/Confidence.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Operator Usage Whitespace Violation: Operators should be surrounded by a single whitespace when they are being used (operator_usage_whitespace)
await putContextAndWait(context: context)
print(">> Done! \(context)")
}
print(">>>> added sync task for \(test.hashValue)")
self.currentFetchTask2 = test
}

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

public func removeContext(key: String) {
self.currentFetchTask?.cancel()
self.currentFetchTask = Task {
self.currentFetchTask2?.cancel()
self.currentFetchTask2 = Task {
await removeContextAndWait(key: key)
}
}

public func putContext(context: ConfidenceStruct, removedKeys: [String]) {
self.currentFetchTask?.cancel()
self.currentFetchTask = Task {
self.currentFetchTask2?.cancel()
self.currentFetchTask2 = Task {
let newContext = contextManager.updateContext(withValues: context, removedKeys: removedKeys)
do {
try await self.fetchAndActivate()
Expand All @@ -277,8 +296,17 @@ public class Confidence: ConfidenceEventSender {
Ensures all the already-started context changes prior to this function have been reconciliated
*/
public func awaitReconciliation() async {
if let task2 = self.currentFetchTask2 {
print(">>>> waiting for sync \(task2.hashValue)")
await task2.value
print(">>>> waited for sync \(task2.hashValue)")
return
}
if let task = self.currentFetchTask {
print(">>>> waiting for \(task.hashValue)")
await task.value
print(">>>> waited for \(task.hashValue)")
return
}
}

Expand Down Expand Up @@ -364,6 +392,7 @@ private class ContextManager {
map.updateValue(entry.value, forKey: entry.key)
}
self.context = map
print(">>> In-mem context is now \(self.context)")
return self.context
}
}
Expand Down
55 changes: 55 additions & 0 deletions Tests/ConfidenceTests/ConfidenceTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,61 @@ class ConfidenceTest: XCTestCase {
XCTAssertEqual(flagApplier.applyCallCount, 1)
}

func testAwaitReconciliationFailingTask() async throws {
class FakeClient: XCTestCase, ConfidenceResolveClient {
var resolveStats: Int = 0
var resolvedValues: [ResolvedValue] = []
func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult {
self.resolveStats += 1
if (resolveStats == 1) {

Check failure on line 499 in Tests/ConfidenceTests/ConfidenceTest.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Control Statement Violation: `if`, `for`, `guard`, `switch`, `while`, and `catch` statements shouldn't unnecessarily wrap their conditionals or arguments in parentheses (control_statement)
print(">> Before Sleep")
try await Task.sleep(nanoseconds: 10_000_000)
print(">> After Sleep")
}
if (ctx["hello"] == .init(string: "world")) {

Check failure on line 504 in Tests/ConfidenceTests/ConfidenceTest.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Control Statement Violation: `if`, `for`, `guard`, `switch`, `while`, and `catch` statements shouldn't unnecessarily wrap their conditionals or arguments in parentheses (control_statement)
print(">> Resolve call returns for context \(ctx)")
return .init(resolvedValues: resolvedValues, resolveToken: "token")
} else {
print(">> Resolve call returns for context \(ctx)")
return .init(resolvedValues: [], resolveToken: "token")
}
}
}

let client = FakeClient()
client.resolvedValues = [
ResolvedValue(
variant: "control",
value: .init(structure: ["size": .init(integer: 3)]),
flag: "flag",
resolveReason: .match)
]

let confidence = Confidence.Builder(clientSecret: "test")
.withContext(initialContext: ["targeting_key": .init(string: "user2")])
.withFlagResolverClient(flagResolver: client)
.withFlagApplier(flagApplier: flagApplier)
.build()
confidence.putContext(context: ["hello": .init(string: "not-world")])
Task {
confidence.putContext(context: ["hello": .init(string: "world")])
}
await confidence.awaitReconciliation()
print(">> Final eval context \(confidence.getContext())")
let evaluation = confidence.getEvaluation(
key: "flag.size",
defaultValue: 0)

XCTAssertEqual(client.resolveStats, 2)
XCTAssertEqual(evaluation.value, 3)
XCTAssertNil(evaluation.errorCode)
XCTAssertNil(evaluation.errorMessage)
XCTAssertEqual(evaluation.reason, .match)
XCTAssertEqual(evaluation.variant, "control")
await fulfillment(of: [flagApplier.applyExpectation], timeout: 1)
XCTAssertEqual(flagApplier.applyCallCount, 1)
}

func testResolveBooleanFlag() async throws {
class FakeClient: ConfidenceResolveClient {
var resolveStats: Int = 0
Expand Down

0 comments on commit a0c439c

Please sign in to comment.