Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
test(RemoteConfig): keep late callbacks from crashing APITests
APITests and FakeConsoleTests read `self.config` inside fetch and
activate completion handlers, but APITestBase.tearDown() sets `config`
to nil. When a test times out, its activate completion can still run
later, unwrap nil, and crash the test process. On the visionOS CI jobs
this turned three expectation timeouts into a crash and a test-runner
restart.

Capture a local strong reference to `config` at the start of each test
and use it in the handlers, so a late handler runs against a valid
instance instead of crashing. Document the pattern on
APITestBase.config.
  • Loading branch information
paulb777 committed Oct 2, 2026
commit 3b24b0968497224e4450b40b7c0cc586e13af55d
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,17 @@
// Test New API.
// Contrast with testUnchangedActivateWillFlag in APITests.swift.
func testChangedActivateWillNotFlag() {
let config: RemoteConfig = self.config
let expectation = self.expectation(description: #function)
config.fetch { status, error in
if let error {
XCTFail("Fetch Error \(error)")
}
XCTAssertEqual(status, RemoteConfigFetchStatus.success)
self.config.activate { changed, error in
config.activate { changed, error in
XCTAssertNil(error)
XCTAssertTrue(changed)
XCTAssertEqual(self.config["Key1"].stringValue, "Value1")
XCTAssertEqual(config["Key1"].stringValue, "Value1")
expectation.fulfill()
}
}
Expand All @@ -50,10 +51,10 @@
XCTFail("Fetch Error \(error)")
}
XCTAssertEqual(status, RemoteConfigFetchStatus.success)
self.config.activate { changed, error in
config.activate { changed, error in
XCTAssertNil(error)
XCTAssert(changed)
XCTAssertEqual(self.config["Key1"].stringValue, "Value2")
XCTAssertEqual(config["Key1"].stringValue, "Value2")

Check failure on line 57 in FirebaseRemoteConfig/Tests/Swift/FakeConsole/FakeConsoleTests.swift

View workflow job for this annotation

GitHub Actions / spm_2 / spm (macos-15, visionOS)

testStrongTypingViaDecoderAlternateDecoderApi, XCTAssertEqual failed: ("Value1") is not equal to ("Value2")
expectation2.fulfill()
}
}
Expand All @@ -62,7 +63,7 @@

private func waitForExpectations() {
let kFIRStorageIntegrationTestTimeout = 10.0
waitForExpectations(timeout: kFIRStorageIntegrationTestTimeout,

Check failure on line 66 in FirebaseRemoteConfig/Tests/Swift/FakeConsole/FakeConsoleTests.swift

View workflow job for this annotation

GitHub Actions / spm_2 / spm (macos-15, visionOS)

testChangedActivateWillNotFlag, Asynchronous wait failed: Exceeded timeout of 10 seconds, with unfulfilled expectations: "testChangedActivateWillNotFlag()2".
handler: { error in
if let error {
print(error)
Expand Down
3 changes: 3 additions & 0 deletions FirebaseRemoteConfig/Tests/Swift/SwiftAPI/APITestBase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ class APITestBase: XCTestCase {
static var mockedFetch: Bool!
static var mockedRealtime: Bool!
var app: FirebaseApp!
/// Reset to `nil` in `tearDown()`. A completion handler can still run after its test times out,
/// so handlers should use a local reference captured at the start of the test instead of
/// reading `self.config`.
var config: RemoteConfig!
var console: RemoteConfigConsole!
var fakeConsole: FakeConsole!
Expand Down
46 changes: 27 additions & 19 deletions FirebaseRemoteConfig/Tests/Swift/SwiftAPI/APITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,45 +19,48 @@

class APITests: APITestBase {
func testFetchThenActivate() {
let config: RemoteConfig = self.config
let expectation = self.expectation(description: #function)
config.fetch { status, error in
if let error {
XCTFail("Fetch Error \(error)")
}
XCTAssertEqual(status, RemoteConfigFetchStatus.success)
self.config.activate { _, error in
config.activate { _, error in
XCTAssertNil(error)
XCTAssertEqual(self.config[Constants.key1].stringValue, Constants.value1)
XCTAssertEqual(config[Constants.key1].stringValue, Constants.value1)
expectation.fulfill()
}
}
waitForExpectations()
}

func testFetchWithExpirationThenActivate() {
let config: RemoteConfig = self.config
let expectation = self.expectation(description: #function)
config.fetch(withExpirationDuration: 0) { status, error in
if let error {
XCTFail("Fetch Error \(error)")
}
XCTAssertEqual(status, RemoteConfigFetchStatus.success)
self.config.activate { _, error in
config.activate { _, error in
XCTAssertNil(error)
XCTAssertEqual(self.config[Constants.key1].stringValue, Constants.value1)
XCTAssertEqual(config[Constants.key1].stringValue, Constants.value1)
expectation.fulfill()
}
}
waitForExpectations()
}

func testFetchAndActivate() {
let config: RemoteConfig = self.config
let expectation = self.expectation(description: #function)
config.fetchAndActivate { status, error in
XCTAssertEqual(status, .successFetchedFromRemote)
if let error {
XCTFail("Fetch and Activate Error \(error)")
}
XCTAssertEqual(self.config[Constants.key1].stringValue, Constants.value1)
XCTAssertEqual(config[Constants.key1].stringValue, Constants.value1)
expectation.fulfill()
}
waitForExpectations()
Expand All @@ -66,16 +69,17 @@
// Test New API.
// Contrast with testChangedActivateWillNotFlag in FakeConsole.swift.
func testUnchangedActivateWillFlag() {
let config: RemoteConfig = self.config
let expectation = self.expectation(description: #function)
config.fetch { status, error in
if let error {
XCTFail("Fetch Error \(error)")
}
XCTAssertEqual(status, RemoteConfigFetchStatus.success)
self.config.activate { changed, error in
config.activate { changed, error in
XCTAssertTrue(!APITests.useFakeConfig || changed)
XCTAssertNil(error)
XCTAssertEqual(self.config[Constants.key1].stringValue, Constants.value1)
XCTAssertEqual(config[Constants.key1].stringValue, Constants.value1)
expectation.fulfill()
}
}
Expand All @@ -86,10 +90,10 @@
XCTFail("Fetch Error \(error)")
}
XCTAssertEqual(status, RemoteConfigFetchStatus.success)
self.config.activate { changed, error in
config.activate { changed, error in
XCTAssertFalse(changed)
XCTAssertNil(error)
XCTAssertEqual(self.config[Constants.key1].stringValue, Constants.value1)
XCTAssertEqual(config[Constants.key1].stringValue, Constants.value1)
expectation2.fulfill()
}
}
Expand All @@ -99,6 +103,7 @@
func testFetchAndActivateUnchangedConfig() throws {
guard APITests.useFakeConfig == false else { return }

let config: RemoteConfig = self.config
let expectation = self.expectation(description: #function)

XCTAssertEqual(config.settings.minimumFetchInterval, 0)
Expand All @@ -108,7 +113,7 @@
group.enter()
serialQueue.async {
// Represents pre-fetch occurring sometime in past.
self.config.fetch { status, error in
config.fetch { status, error in
XCTAssertNil(error, "Fetch Error \(error!)")
XCTAssertEqual(status, .success)
group.leave()
Expand All @@ -119,7 +124,7 @@
group.wait()
group.enter()
// Represents a `fetchAndActivate` being made to pull latest changes from Remote Config.
self.config.fetchAndActivate { status, error in
config.fetchAndActivate { status, error in
XCTAssertNil(error, "Fetch & Activate Error \(error!)")
// Since no updates to remote config have occurred we use the `.successUsingPreFetchedData`.
// The behavior of the next test changed in Firebase 7.0.0.
Expand All @@ -129,8 +134,8 @@
// XCTAssertEqual(status, .successUsingPreFetchedData)
XCTAssertEqual(status, .successFetchedFromRemote)
// The `lastETagUpdateTime` should either be older or the same time as `lastFetchTime`.
if let lastFetchTime = try? XCTUnwrap(self.config.lastFetchTime) {
XCTAssertLessThanOrEqual(Double(self.config.settings.lastETagUpdateTime),
if let lastFetchTime = try? XCTUnwrap(config.lastFetchTime) {
XCTAssertLessThanOrEqual(Double(config.settings.lastETagUpdateTime),
Double(lastFetchTime.timeIntervalSince1970))
} else {
XCTFail("Could not unwrap lastFetchTime.")
Expand Down Expand Up @@ -184,11 +189,12 @@
func testFetchConfigThenUpdateConsoleThenFetchAgain() {
guard APITests.useFakeConfig == false else { return }

let config: RemoteConfig = self.config
let expectation = self.expectation(description: #function)

config.fetchAndActivate { status, error in
XCTAssertNil(error, "Fetch & Activate Error \(error!)")
XCTAssertEqual(self.config.configValue(forKey: Constants.jedi).stringValue, Constants.obiwan)
XCTAssertEqual(config.configValue(forKey: Constants.jedi).stringValue, Constants.obiwan)
expectation.fulfill()
}
waitForExpectations()
Expand All @@ -199,7 +205,7 @@
let expectation2 = self.expectation(description: #function + "2")
config.fetchAndActivate { status, error in
XCTAssertNil(error, "Fetch & Activate Error \(error!)")
XCTAssertEqual(self.config.configValue(forKey: Constants.jedi).stringValue, Constants.yoda)
XCTAssertEqual(config.configValue(forKey: Constants.jedi).stringValue, Constants.yoda)
expectation2.fulfill()
}
waitForExpectations()
Expand All @@ -208,13 +214,14 @@
func testFetchConfigThenAddValueOnConsoleThenFetchAgain() {
guard APITests.useFakeConfig == false else { return }

let config: RemoteConfig = self.config
// Ensure no Sith Lord has been written to Remote Config yet.
let expectation = self.expectation(description: #function)

config.fetchAndActivate { status, error in
XCTAssertNil(error, "Fetch & Activate Error \(error!)")

XCTAssertTrue(self.config.configValue(forKey: Constants.sith).dataValue.isEmpty)
XCTAssertTrue(config.configValue(forKey: Constants.sith).dataValue.isEmpty)

expectation.fulfill()
}
Expand All @@ -229,7 +236,7 @@
config.fetchAndActivate { status, error in
XCTAssertNil(error, "Fetch & Activate Error \(error!)")
XCTAssertEqual(
self.config.configValue(forKey: Constants.sith).stringValue,
config.configValue(forKey: Constants.sith).stringValue,
Constants.darthSidious
)
expectation2.fulfill()
Expand All @@ -240,11 +247,12 @@
func testFetchConfigThenDeleteValueOnConsoleThenFetchAgain() {
guard APITests.useFakeConfig == false else { return }

let config: RemoteConfig = self.config
let expectation = self.expectation(description: #function)

config.fetchAndActivate { status, error in
XCTAssertNil(error, "Fetch & Activate Error \(error!)")
XCTAssertEqual(self.config.configValue(forKey: Constants.jedi).stringValue, Constants.obiwan)
XCTAssertEqual(config.configValue(forKey: Constants.jedi).stringValue, Constants.obiwan)
expectation.fulfill()
}
waitForExpectations()
Expand All @@ -256,7 +264,7 @@
config.fetchAndActivate { status, error in
XCTAssertNil(error, "Fetch & Activate Error \(error!)")

XCTAssertTrue(self.config.configValue(forKey: Constants.jedi).dataValue.isEmpty,
XCTAssertTrue(config.configValue(forKey: Constants.jedi).dataValue.isEmpty,
"Remote config should have been deleted.")

expectation2.fulfill()
Expand All @@ -268,7 +276,7 @@

private func waitForExpectations() {
let kTestTimeout = 10.0
waitForExpectations(timeout: kTestTimeout,

Check failure on line 279 in FirebaseRemoteConfig/Tests/Swift/SwiftAPI/APITests.swift

View workflow job for this annotation

GitHub Actions / spm_2 / spm (macos-15, iOS)

testUnchangedActivateWillFlag, Asynchronous wait failed: Exceeded timeout of 10 seconds, with unfulfilled expectations: "testUnchangedActivateWillFlag()".

Check failure on line 279 in FirebaseRemoteConfig/Tests/Swift/SwiftAPI/APITests.swift

View workflow job for this annotation

GitHub Actions / spm_2 / spm (macos-15, iOS)

testFetchWithExpirationThenActivate, Asynchronous wait failed: Exceeded timeout of 10 seconds, with unfulfilled expectations: "testFetchWithExpirationThenActivate()".

Check failure on line 279 in FirebaseRemoteConfig/Tests/Swift/SwiftAPI/APITests.swift

View workflow job for this annotation

GitHub Actions / spm_2 / spm (macos-15, iOS)

testFetchThenActivate, Asynchronous wait failed: Exceeded timeout of 10 seconds, with unfulfilled expectations: "testFetchThenActivate()".

Check failure on line 279 in FirebaseRemoteConfig/Tests/Swift/SwiftAPI/APITests.swift

View workflow job for this annotation

GitHub Actions / spm_2 / spm (macos-15, iOS)

testFetchAndActivate, Asynchronous wait failed: Exceeded timeout of 10 seconds, with unfulfilled expectations: "testFetchAndActivate()".

Check failure on line 279 in FirebaseRemoteConfig/Tests/Swift/SwiftAPI/APITests.swift

View workflow job for this annotation

GitHub Actions / remoteconfig-integration / build

testFetchAndActivate, Asynchronous wait failed: Exceeded timeout of 10 seconds, with unfulfilled expectations: "testFetchAndActivate()".

Check failure on line 279 in FirebaseRemoteConfig/Tests/Swift/SwiftAPI/APITests.swift

View workflow job for this annotation

GitHub Actions / spm_2 / spm (macos-26, iOS)

testUnchangedActivateWillFlag, Asynchronous wait failed: Exceeded timeout of 10 seconds, with unfulfilled expectations: "testUnchangedActivateWillFlag()".

Check failure on line 279 in FirebaseRemoteConfig/Tests/Swift/SwiftAPI/APITests.swift

View workflow job for this annotation

GitHub Actions / spm_2 / spm (macos-26, iOS)

testFetchWithExpirationThenActivate, Asynchronous wait failed: Exceeded timeout of 10 seconds, with unfulfilled expectations: "testFetchWithExpirationThenActivate()".

Check failure on line 279 in FirebaseRemoteConfig/Tests/Swift/SwiftAPI/APITests.swift

View workflow job for this annotation

GitHub Actions / spm_2 / spm (macos-26, iOS)

testFetchThenActivate, Asynchronous wait failed: Exceeded timeout of 10 seconds, with unfulfilled expectations: "testFetchThenActivate()".

Check failure on line 279 in FirebaseRemoteConfig/Tests/Swift/SwiftAPI/APITests.swift

View workflow job for this annotation

GitHub Actions / spm_2 / spm (macos-15, visionOS)

testUnchangedActivateWillFlag, Asynchronous wait failed: Exceeded timeout of 10 seconds, with unfulfilled expectations: "testUnchangedActivateWillFlag()".

Check failure on line 279 in FirebaseRemoteConfig/Tests/Swift/SwiftAPI/APITests.swift

View workflow job for this annotation

GitHub Actions / spm_2 / spm (macos-15, visionOS)

testFetchWithExpirationThenActivate, Asynchronous wait failed: Exceeded timeout of 10 seconds, with unfulfilled expectations: "testFetchWithExpirationThenActivate()".

Check failure on line 279 in FirebaseRemoteConfig/Tests/Swift/SwiftAPI/APITests.swift

View workflow job for this annotation

GitHub Actions / spm_2 / spm (macos-15, visionOS)

testFetchThenActivate, Asynchronous wait failed: Exceeded timeout of 10 seconds, with unfulfilled expectations: "testFetchThenActivate()".

Check failure on line 279 in FirebaseRemoteConfig/Tests/Swift/SwiftAPI/APITests.swift

View workflow job for this annotation

GitHub Actions / spm_2 / spm (macos-15, visionOS)

testFetchAndActivate, Asynchronous wait failed: Exceeded timeout of 10 seconds, with unfulfilled expectations: "testFetchAndActivate()".

Check failure on line 279 in FirebaseRemoteConfig/Tests/Swift/SwiftAPI/APITests.swift

View workflow job for this annotation

GitHub Actions / spm_2 / spm (macos-15, visionOS)

testUnchangedActivateWillFlag, Asynchronous wait failed: Exceeded timeout of 10 seconds, with unfulfilled expectations: "testUnchangedActivateWillFlag()".

Check failure on line 279 in FirebaseRemoteConfig/Tests/Swift/SwiftAPI/APITests.swift

View workflow job for this annotation

GitHub Actions / spm_2 / spm (macos-15, visionOS)

testFetchWithExpirationThenActivate, Asynchronous wait failed: Exceeded timeout of 10 seconds, with unfulfilled expectations: "testFetchWithExpirationThenActivate()".

Check failure on line 279 in FirebaseRemoteConfig/Tests/Swift/SwiftAPI/APITests.swift

View workflow job for this annotation

GitHub Actions / spm_2 / spm (macos-15, visionOS)

testFetchThenActivate, Asynchronous wait failed: Exceeded timeout of 10 seconds, with unfulfilled expectations: "testFetchThenActivate()".

Check failure on line 279 in FirebaseRemoteConfig/Tests/Swift/SwiftAPI/APITests.swift

View workflow job for this annotation

GitHub Actions / spm_2 / spm (macos-15, visionOS)

testFetchAndActivate, Asynchronous wait failed: Exceeded timeout of 10 seconds, with unfulfilled expectations: "testFetchAndActivate()".
handler: { error in
if let error {
print(error)
Expand Down
Loading