Skip to content

Commit

Permalink
test: Basic E2E test for events upload (#155)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
fabriziodemaria and nicklasl authored Jul 5, 2024
1 parent 917743b commit 2e65c47
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 33 deletions.
24 changes: 0 additions & 24 deletions ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
])
]
)
}
}
21 changes: 13 additions & 8 deletions Sources/Confidence/Confidence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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),
Expand All @@ -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(
Expand Down
13 changes: 12 additions & 1 deletion Sources/Confidence/RemoteConfidenceClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 41 additions & 0 deletions Tests/ConfidenceTests/ConfidenceIntegrationTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
90 changes: 90 additions & 0 deletions Tests/ConfidenceTests/Helpers/DebugLoggerFake.swift
Original file line number Diff line number Diff line change
@@ -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..<endIndex]

return eventsString.components(separatedBy: ",")
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
}

private class ThreadSafeCounter {
private let queue = DispatchQueue(label: "ThreadSafeCounterQueue")
private var count = 0

func increment() {
queue.sync {
count += 1
}
}

func get() -> 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
}
}

0 comments on commit 2e65c47

Please sign in to comment.