Skip to content

Commit

Permalink
chore: change to throw an exception instead of using fatalError from …
Browse files Browse the repository at this point in the history
…BKTClient.shared (#18)
  • Loading branch information
duyhungtnn authored Jul 26, 2023
1 parent b444efe commit 881de7f
Show file tree
Hide file tree
Showing 6 changed files with 283 additions and 133 deletions.
23 changes: 18 additions & 5 deletions Bucketeer/Sources/Public/BKTClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,10 @@ public class BKTClient {
}

extension BKTClient {
public static func initialize(config: BKTConfig, user: BKTUser, timeoutMillis: Int64 = 5000, completion: ((BKTError?) -> Void)? = nil) {
precondition(Thread.isMainThread, "the initialize method must be called on main thread")
public static func initialize(config: BKTConfig, user: BKTUser, timeoutMillis: Int64 = 5000, completion: ((BKTError?) -> Void)? = nil) throws {
guard (Thread.isMainThread) else {
throw BKTError.illegalState(message: "the initialize method must be called on main thread")
}
concurrentQueue.sync {
guard BKTClient.default == nil else {
config.logger?.warn(message: "BKTClient is already initialized. Not sure if the initial fetch has finished")
Expand All @@ -98,14 +100,25 @@ extension BKTClient {
}
}

public static func destroy() {
precondition(Thread.isMainThread, "the destroy method must be called on main thread")
public static func destroy() throws {
guard (Thread.isMainThread) else {
throw BKTError.illegalState(message: "the destroy method must be called on main thread")
}
BKTClient.default?.resetTasks()
BKTClient.default = nil
}

// Please make sure the BKTClient is initialize before access it
public static var shared: BKTClient {
return BKTClient.default ?? { fatalError("BKTClient is already initialized. Not sure if the initial fetch has finished") }()
get throws {
// We do not want to crash the SDK's consumer app on runtime by using fatalError().
// So let the app has a chance to catch this exception
// The same behavior with the Android SDK
guard BKTClient.default != nil else {
throw BKTError.illegalState(message: "BKTClient is not initialized")
}
return BKTClient.default
}
}

public func stringVariation(featureId: String, defaultValue: String) -> String {
Expand Down
51 changes: 51 additions & 0 deletions BucketeerTests/BKTClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,57 @@ import XCTest
// swiftlint:disable type_body_length
final class BKTClientTests: XCTestCase {

func testMainThreadRequired() throws {
let expectation = self.expectation(description: "")
expectation.expectedFulfillmentCount = 4

let config = BKTConfig.mock1
let user = try BKTUser.Builder().with(id: USER_ID).build()
let threadQueue = DispatchQueue(label: "threads")

threadQueue.async {
do {
try BKTClient.initialize(
config: config,
user: user, completion: { _ in
}
)
} catch {
// Should catch error, because we didn't on the main thread
expectation.fulfill()
}

DispatchQueue.main.sync {
do {
try BKTClient.initialize(
config: config,
user: user, completion: { _ in
}
)
// Should success and fullfill
expectation.fulfill()
} catch {}
}

do {
try BKTClient.destroy()
} catch {
// Should catch error, because we didn't on the main thread
expectation.fulfill()
}

DispatchQueue.main.sync {
do {
try BKTClient.destroy()
// Should success and fullfill
expectation.fulfill()
} catch {}
}
}

wait(for: [expectation], timeout: 1)
}

func testCurrentUser() {
let dataModule = MockDataModule(
userHolder: .init(user: .mock1)
Expand Down
252 changes: 155 additions & 97 deletions BucketeerTests/E2E/BucketeerE2ETests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,140 +25,198 @@ final class BucketeerE2ETests: XCTestCase {
try await super.tearDown()

try await BKTClient.shared.flush()
BKTClient.destroy()
try BKTClient.destroy()
UserDefaults.standard.removeObject(forKey: "bucketeer_user_evaluations_id")
try FileManager.default.removeItem(at: .database)
}

func testStringVariation() {
let client = BKTClient.shared
XCTAssertEqual(client.stringVariation(featureId: FEATURE_ID_STRING, defaultValue: ""), "value-1")
do {
let client = try BKTClient.shared
XCTAssertEqual(client.stringVariation(featureId: FEATURE_ID_STRING, defaultValue: ""), "value-1")
} catch {
XCTFail(error.localizedDescription)
}
}

func testStringVariationDetail() {
let client = BKTClient.shared
let actual = client.evaluationDetails(featureId: FEATURE_ID_STRING)

assertEvaluation(actual: actual, expected: .init(
id: "feature-ios-e2e-string:3:bucketeer-ios-user-id-1",
featureId: FEATURE_ID_STRING,
featureVersion: 3,
variationId: "349ed945-d2f9-4d04-8e83-82344cffd1ec",
variationName: "variation 1",
variationValue: "value-1",
reason: .default
))
do {
let client = try BKTClient.shared
let actual = client.evaluationDetails(featureId: FEATURE_ID_STRING)

assertEvaluation(actual: actual, expected: .init(
id: "feature-ios-e2e-string:3:bucketeer-ios-user-id-1",
featureId: FEATURE_ID_STRING,
featureVersion: 3,
variationId: "349ed945-d2f9-4d04-8e83-82344cffd1ec",
variationName: "variation 1",
variationValue: "value-1",
reason: .default
))
} catch {
XCTFail(error.localizedDescription)
}
}

func testIntVariation() {
let client = BKTClient.shared
XCTAssertEqual(client.intVariation(featureId: FEATURE_ID_INT, defaultValue: 0), 10)
do {
let client = try BKTClient.shared
XCTAssertEqual(client.intVariation(featureId: FEATURE_ID_INT, defaultValue: 0), 10)
} catch {
XCTFail(error.localizedDescription)
}
}

func testIntVariationDetail() {
let client = BKTClient.shared
let actual = client.evaluationDetails(featureId: FEATURE_ID_INT)

assertEvaluation(actual: actual, expected: .init(
id: "feature-ios-e2e-integer:3:bucketeer-ios-user-id-1",
featureId: FEATURE_ID_INT,
featureVersion: 3,
variationId: "9c5fd2d2-d587-4ba2-8de2-0fc9454d564e",
variationName: "variation 10",
variationValue: "10",
reason: .default
))
do {
let client = try BKTClient.shared
let actual = client.evaluationDetails(featureId: FEATURE_ID_INT)

assertEvaluation(actual: actual, expected: .init(
id: "feature-ios-e2e-integer:3:bucketeer-ios-user-id-1",
featureId: FEATURE_ID_INT,
featureVersion: 3,
variationId: "9c5fd2d2-d587-4ba2-8de2-0fc9454d564e",
variationName: "variation 10",
variationValue: "10",
reason: .default
))
} catch {
XCTFail(error.localizedDescription)
}
}

func testDoubleVariation() {
let client = BKTClient.shared
XCTAssertEqual(client.doubleVariation(featureId: FEATURE_ID_DOUBLE, defaultValue: 0.1), 2.1)
do {
let client = try BKTClient.shared
XCTAssertEqual(client.doubleVariation(featureId: FEATURE_ID_DOUBLE, defaultValue: 0.1), 2.1)
} catch {
XCTFail(error.localizedDescription)
}
}

func testDoubleVariationDetail() async throws {
let client = BKTClient.shared
let actual = client.evaluationDetails(featureId: FEATURE_ID_DOUBLE)

assertEvaluation(actual: actual, expected: .init(
id: "feature-ios-e2e-double:3:bucketeer-ios-user-id-1",
featureId: FEATURE_ID_DOUBLE,
featureVersion: 3,
variationId: "38078d8f-c6eb-4b93-9d58-c3e57010983f",
variationName: "variation 2.1",
variationValue: "2.1",
reason: .default
))
do {
let client = try BKTClient.shared
let actual = client.evaluationDetails(featureId: FEATURE_ID_DOUBLE)

assertEvaluation(actual: actual, expected: .init(
id: "feature-ios-e2e-double:3:bucketeer-ios-user-id-1",
featureId: FEATURE_ID_DOUBLE,
featureVersion: 3,
variationId: "38078d8f-c6eb-4b93-9d58-c3e57010983f",
variationName: "variation 2.1",
variationValue: "2.1",
reason: .default
))
} catch {
XCTFail(error.localizedDescription)
}
}

func testBoolVariation() {
let client = BKTClient.shared
XCTAssertEqual(client.boolVariation(featureId: FEATURE_ID_BOOLEAN, defaultValue: false), true)
do {
let client = try BKTClient.shared
let actual = client.evaluationDetails(featureId: FEATURE_ID_BOOLEAN)

assertEvaluation(actual: actual, expected: .init(
id: "feature-ios-e2e-bool:3:bucketeer-ios-user-id-1",
featureId: FEATURE_ID_BOOLEAN,
featureVersion: 3,
variationId: "4f9e0f88-e053-42a9-93e1-95d407f67021",
variationName: "variation true",
variationValue: "true",
reason: .default
))
} catch {
XCTFail(error.localizedDescription)
}
}

func testBoolVariationDetail() {
let client = BKTClient.shared
let actual = client.evaluationDetails(featureId: FEATURE_ID_BOOLEAN)

assertEvaluation(actual: actual, expected: .init(
id: "feature-ios-e2e-bool:3:bucketeer-ios-user-id-1",
featureId: FEATURE_ID_BOOLEAN,
featureVersion: 3,
variationId: "4f9e0f88-e053-42a9-93e1-95d407f67021",
variationName: "variation true",
variationValue: "true",
reason: .default
))
do {
let client = try BKTClient.shared
let actual = client.evaluationDetails(featureId: FEATURE_ID_BOOLEAN)

assertEvaluation(actual: actual, expected: .init(
id: "feature-ios-e2e-bool:3:bucketeer-ios-user-id-1",
featureId: FEATURE_ID_BOOLEAN,
featureVersion: 3,
variationId: "4f9e0f88-e053-42a9-93e1-95d407f67021",
variationName: "variation true",
variationValue: "true",
reason: .default
))
} catch {
XCTFail(error.localizedDescription)
}
}

func testJSONVariation() {
let client = BKTClient.shared
let json = client.jsonVariation(featureId: FEATURE_ID_JSON, defaultValue: [:])
XCTAssertEqual(json as? [String: String], ["key": "value-1"])
do {
let client = try BKTClient.shared
let json = client.jsonVariation(featureId: FEATURE_ID_JSON, defaultValue: [:])
XCTAssertEqual(json as? [String: String], ["key": "value-1"])
} catch {
XCTFail(error.localizedDescription)
}
}

func testJSONVariationDetail() {
let client = BKTClient.shared
let actual = client.evaluationDetails(featureId: FEATURE_ID_JSON)

assertEvaluation(actual: actual, expected: .init(
id: "feature-ios-e2e-json:3:bucketeer-ios-user-id-1",
featureId: FEATURE_ID_JSON,
featureVersion: 3,
variationId: "06f5be6b-0c79-431f-a057-822babd9d3eb",
variationName: "variation 1",
variationValue: "{ \"key\": \"value-1\" }",
reason: .default
))
do {
let client = try BKTClient.shared
let actual = client.evaluationDetails(featureId: FEATURE_ID_JSON)

assertEvaluation(actual: actual, expected: .init(
id: "feature-ios-e2e-json:3:bucketeer-ios-user-id-1",
featureId: FEATURE_ID_JSON,
featureVersion: 3,
variationId: "06f5be6b-0c79-431f-a057-822babd9d3eb",
variationName: "variation 1",
variationValue: "{ \"key\": \"value-1\" }",
reason: .default
))
} catch {
XCTFail(error.localizedDescription)
}
}

func testEvaluationUpdateFlow() async throws {
let client = BKTClient.shared
XCTAssertEqual(client.stringVariation(featureId: FEATURE_ID_STRING, defaultValue: ""), "value-1")

client.updateUserAttributes(attributes: ["app_version": "0.0.1"])

try await client.fetchEvaluations(timeoutMillis: nil)
XCTAssertEqual(client.stringVariation(featureId: FEATURE_ID_STRING, defaultValue: ""), "value-2")

let details = client.evaluationDetails(featureId: FEATURE_ID_STRING)
assertEvaluation(actual: details, expected: .init(
id: "feature-ios-e2e-string:3:bucketeer-ios-user-id-1",
featureId: FEATURE_ID_STRING,
featureVersion: 3,
variationId: "b4931643-e82f-4079-bd3c-aed02852cdd6",
variationName: "variation 2",
variationValue: "value-2",
reason: .rule
))
do {
let client = try BKTClient.shared
XCTAssertEqual(client.stringVariation(featureId: FEATURE_ID_STRING, defaultValue: ""), "value-1")

client.updateUserAttributes(attributes: ["app_version": "0.0.1"])

try await client.fetchEvaluations(timeoutMillis: nil)
XCTAssertEqual(client.stringVariation(featureId: FEATURE_ID_STRING, defaultValue: ""), "value-2")

let details = client.evaluationDetails(featureId: FEATURE_ID_STRING)
assertEvaluation(actual: details, expected: .init(
id: "feature-ios-e2e-string:3:bucketeer-ios-user-id-1",
featureId: FEATURE_ID_STRING,
featureVersion: 3,
variationId: "b4931643-e82f-4079-bd3c-aed02852cdd6",
variationName: "variation 2",
variationValue: "value-2",
reason: .rule
))
} catch {
XCTFail(error.localizedDescription)
}
}

func testTrack() async throws {
let client = BKTClient.shared
client.assert(expectedEventCount: 2)
client.track(goalId: GOAL_ID, value: GOAL_VALUE)
try await Task.sleep(nanoseconds: 1_000_000)
client.assert(expectedEventCount: 3)
try await client.flush()
client.assert(expectedEventCount: 0)
do {
let client = try BKTClient.shared
client.assert(expectedEventCount: 2)
client.track(goalId: GOAL_ID, value: GOAL_VALUE)
try await Task.sleep(nanoseconds: 1_000_000)
client.assert(expectedEventCount: 3)
try await client.flush()
client.assert(expectedEventCount: 0)
} catch {
XCTFail(error.localizedDescription)
}
}
}
Loading

0 comments on commit 881de7f

Please sign in to comment.