From 2e65c473216d92eb02110dd00bfbb7290fe268b6 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Fri, 5 Jul 2024 13:49:48 +0200 Subject: [PATCH] test: Basic E2E test for events upload (#155) * test: Basic E2E test for events upload * refactor: Rename test class * test: E2E test check single events * fix: E2E track test considers in-batch errors --------- Co-authored-by: Nicklas Lundin --- .../ConfidenceDemoApp/ConfidenceDemoApp.swift | 24 ----- Sources/Confidence/Confidence.swift | 21 +++-- .../Confidence/RemoteConfidenceClient.swift | 13 ++- .../ConfidenceIntegrationTest.swift | 41 +++++++++ .../Helpers/DebugLoggerFake.swift | 90 +++++++++++++++++++ 5 files changed, 156 insertions(+), 33 deletions(-) create mode 100644 Tests/ConfidenceTests/Helpers/DebugLoggerFake.swift diff --git a/ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift b/ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift index 38769765..073efd95 100644 --- a/ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift +++ b/ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift @@ -43,29 +43,5 @@ struct ConfidenceDemoApp: App { extension ConfidenceDemoApp { func setup(confidence: Confidence) async throws { try await confidence.fetchAndActivate() - try confidence.track( - eventName: "all-types", - data: [ - "my_string": ConfidenceValue(string: "hello_from_world"), - "my_timestamp": ConfidenceValue(timestamp: Date()), - "my_bool": ConfidenceValue(boolean: true), - "my_date": ConfidenceValue(date: DateComponents(year: 2024, month: 4, day: 3)), - "my_int": ConfidenceValue(integer: 2), - "my_double": ConfidenceValue(double: 3.14), - "my_list": ConfidenceValue(booleanList: [true, false]), - "my_struct": ConfidenceValue(structure: [ - "my_nested_struct": ConfidenceValue(structure: [ - "my_nested_nested_struct": ConfidenceValue(structure: [ - "my_nested_nested_nested_int": ConfidenceValue(integer: 666) - ]), - "my_nested_nested_list": ConfidenceValue(dateList: [ - DateComponents(year: 2024, month: 4, day: 4), - DateComponents(year: 2024, month: 4, day: 5) - ]) - ]), - "my_nested_string": ConfidenceValue(string: "nested_hello") - ]) - ] - ) } } diff --git a/Sources/Confidence/Confidence.swift b/Sources/Confidence/Confidence.swift index a3abefc2..dc2914cd 100644 --- a/Sources/Confidence/Confidence.swift +++ b/Sources/Confidence/Confidence.swift @@ -286,13 +286,13 @@ extension Confidence { // Can be configured internal var region: ConfidenceRegion = .global - internal var metadata: ConfidenceMetadata? internal var initialContext: ConfidenceStruct = [:] // Injectable for testing internal var flagApplier: FlagApplier? internal var storage: Storage? internal var flagResolver: ConfidenceResolveClient? + internal var debugLogger: DebugLogger? /** Initializes the builder with the given credentails. @@ -323,6 +323,11 @@ extension Confidence { return self } + internal func withDebugLogger(debugLogger: DebugLogger) -> Builder { + self.debugLogger = debugLogger + return self + } + public func withContext(initialContext: ConfidenceStruct) -> Builder { self.initialContext = initialContext return self @@ -338,12 +343,11 @@ extension Confidence { } public func build() -> Confidence { - var debugLogger: DebugLogger? - if loggerLevel != LoggerLevel.NONE { - debugLogger = DebugLoggerImpl(loggerLevel: loggerLevel) - debugLogger?.logContext(action: "InitialContext", context: initialContext) - } else { - debugLogger = nil + if debugLogger == nil { + if loggerLevel != LoggerLevel.NONE { + debugLogger = DebugLoggerImpl(loggerLevel: loggerLevel) + debugLogger?.logContext(action: "InitialContext", context: initialContext) + } } let options = ConfidenceClientOptions( credentials: ConfidenceClientCredentials.clientSecret(secret: clientSecret), @@ -353,7 +357,8 @@ extension Confidence { version: "0.2.4") // x-release-please-version let uploader = RemoteConfidenceClient( options: options, - metadata: metadata + metadata: metadata, + debugLogger: debugLogger ) let httpClient = NetworkClient(baseUrl: BaseUrlMapper.from(region: options.region)) let flagApplier = flagApplier ?? FlagApplierWithRetries( diff --git a/Sources/Confidence/RemoteConfidenceClient.swift b/Sources/Confidence/RemoteConfidenceClient.swift index 15bf28c5..67573571 100644 --- a/Sources/Confidence/RemoteConfidenceClient.swift +++ b/Sources/Confidence/RemoteConfidenceClient.swift @@ -46,11 +46,22 @@ public class RemoteConfidenceClient: ConfidenceClient { switch result { case .success(let successData): let status = successData.response.statusCode + let indecesWithError = successData.decodedData?.errors.map { error in + error.index + } ?? [] + let successEventNames = events.enumerated() + // Filter only events in batch that have no error reported from backend + .filter { index, _ in + return !(indecesWithError.contains(index)) + } + .map { _, event in + event.eventDefinition + } switch status { case 200: // clean up in case of success debugLogger?.logMessage( - message: "Event upload: HTTP status 200", + message: "Event upload: HTTP status 200. Events: \(successEventNames.joined(separator: ","))", isWarning: false ) return true diff --git a/Tests/ConfidenceTests/ConfidenceIntegrationTest.swift b/Tests/ConfidenceTests/ConfidenceIntegrationTest.swift index eeae2371..ad3e3036 100644 --- a/Tests/ConfidenceTests/ConfidenceIntegrationTest.swift +++ b/Tests/ConfidenceTests/ConfidenceIntegrationTest.swift @@ -44,6 +44,47 @@ class ConfidenceIntegrationTests: XCTestCase { XCTAssertNil(boolResult.errorMessage) } + func testTrackEventAllTypes() async throws { + guard let clientToken = self.clientToken else { + throw TestError.missingClientToken + } + + let logger = DebugLoggerFake() + let confidence = Confidence.Builder(clientSecret: clientToken) + .withDebugLogger(debugLogger: logger) + .build() + + try confidence.track( + eventName: "all-types", + data: [ + "my_string": ConfidenceValue(string: "hello_from_world"), + "my_timestamp": ConfidenceValue(timestamp: Date()), + "my_bool": ConfidenceValue(boolean: true), + "my_date": ConfidenceValue(date: DateComponents(year: 2024, month: 4, day: 3)), + "my_int": ConfidenceValue(integer: 2), + "my_double": ConfidenceValue(double: 3.14), + "my_list": ConfidenceValue(booleanList: [true, false]), + "my_struct": ConfidenceValue(structure: [ + "my_nested_struct": ConfidenceValue(structure: [ + "my_nested_nested_struct": ConfidenceValue(structure: [ + "my_nested_nested_nested_int": ConfidenceValue(integer: 666) + ]), + "my_nested_nested_list": ConfidenceValue(dateList: [ + DateComponents(year: 2024, month: 4, day: 4), + DateComponents(year: 2024, month: 4, day: 5) + ]) + ]), + "my_nested_string": ConfidenceValue(string: "nested_hello") + ]) + ] + ) + + confidence.flush() + try logger.waitUploadBatchSuccessCount(value: 1, timeout: 5.0) + XCTAssertEqual(logger.getUploadBatchSuccessCount(), 1) + XCTAssertEqual(logger.uploadedEvents, ["all-types"]) + } + func testConfidenceFeatureApplies() async throws { guard let clientToken = self.clientToken else { throw TestError.missingClientToken diff --git a/Tests/ConfidenceTests/Helpers/DebugLoggerFake.swift b/Tests/ConfidenceTests/Helpers/DebugLoggerFake.swift new file mode 100644 index 00000000..db9ddb55 --- /dev/null +++ b/Tests/ConfidenceTests/Helpers/DebugLoggerFake.swift @@ -0,0 +1,90 @@ +import Foundation + +@testable import Confidence + +internal class DebugLoggerFake: DebugLogger { + private let uploadBatchSuccessCounter = ThreadSafeCounter() + public var uploadedEvents: [String] = [] // Holds the "eventDefinition" name of each uploaded event + + func logEvent(action: String, event: ConfidenceEvent?) { + // no-op + } + + func logMessage(message: String, isWarning: Bool) { + if message.starts(with: "Event upload: HTTP status 200") { + uploadedEvents.append(contentsOf: parseEvents(fromString: message)) + uploadBatchSuccessCounter.increment() + } + } + + func logFlags(action: String, flag: String) { + // no-op + } + + func logContext(action: String, context: ConfidenceStruct) { + // no-op + } + + func getUploadBatchSuccessCount() -> Int { + return uploadBatchSuccessCounter.get() + } + + func waitUploadBatchSuccessCount(value: Int32, timeout: TimeInterval) throws { + try uploadBatchSuccessCounter.waitUntil(value: value, timeout: timeout) + } + + /** + Example + Input: "Event upload: HTTP status 200. Events: event-name1, event-name2" + Output: ["event-name1", "event-name2"] + */ + private func parseEvents(fromString message: String) -> [String] { + guard let eventsStart = message.range(of: "Events:") else { + return [] + } + + let startIndex = message.index(eventsStart.upperBound, offsetBy: 1) + let endIndex = message.endIndex + let eventsString = message[startIndex.. Int { + queue.sync { + return count + } + } + + func waitUntil(value: Int32, timeout: TimeInterval) throws { + let deadline = DispatchTime.now() + timeout + + repeat { + Thread.sleep(forTimeInterval: 0.1) // Shortcut to reduce CPU usage, probably needs refactoring + guard deadline > DispatchTime.now() else { + throw TimeoutError(message: "Timed out waiting for counter to reach \(value)") + } + if (queue.sync { + count >= value + }) { + return + } + } while true + } + } + + struct TimeoutError: Error { + let message: String + } +}