Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: remove fatalError from BKTClient.shared #18

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