mirror of
https://github.com/firebase/firebase-ios-sdk
synced 2026-11-10 22:45:59 +05:45
289 lines
11 KiB
Plaintext
289 lines
11 KiB
Plaintext
/*
|
|
* Copyright 2026 Google LLC
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#import <XCTest/XCTest.h>
|
|
|
|
#include <atomic>
|
|
#include <memory>
|
|
|
|
#include "Firestore/core/src/remote/connectivity_monitor_apple.h"
|
|
#include "Firestore/core/src/util/async_queue.h"
|
|
#include "Firestore/core/src/util/executor.h"
|
|
|
|
using firebase::firestore::remote::ConnectivityMonitor;
|
|
using firebase::firestore::remote::ConnectivityMonitorApple;
|
|
using firebase::firestore::util::AsyncQueue;
|
|
using firebase::firestore::util::Executor;
|
|
using NetworkStatus = ConnectivityMonitor::NetworkStatus;
|
|
|
|
namespace {
|
|
|
|
// Helper: build a fresh AsyncQueue for an isolated test.
|
|
std::shared_ptr<AsyncQueue> MakeWorkerQueue(const char* name) {
|
|
return AsyncQueue::Create(Executor::CreateSerial(name));
|
|
}
|
|
|
|
// Helper: construct a monitor on its AsyncQueue, satisfying the lifecycle
|
|
// invariant documented in connectivity_monitor_apple.h.
|
|
std::unique_ptr<ConnectivityMonitorApple> MakeMonitorOnQueue(
|
|
const std::shared_ptr<AsyncQueue>& worker_queue) {
|
|
std::unique_ptr<ConnectivityMonitorApple> monitor;
|
|
worker_queue->EnqueueBlocking([&monitor, &worker_queue]() {
|
|
monitor = std::make_unique<ConnectivityMonitorApple>(worker_queue);
|
|
});
|
|
return monitor;
|
|
}
|
|
|
|
// Helper: destroy a monitor on its AsyncQueue, satisfying the lifecycle
|
|
// invariant documented in connectivity_monitor_apple.h.
|
|
void DestroyOnQueue(std::unique_ptr<ConnectivityMonitorApple>& monitor,
|
|
const std::shared_ptr<AsyncQueue>& worker_queue) {
|
|
worker_queue->EnqueueBlocking([&monitor]() { monitor.reset(); });
|
|
}
|
|
|
|
// Helper: read GetCurrentStatus on the AsyncQueue, satisfying the threading
|
|
// invariant documented in connectivity_monitor_apple.h.
|
|
absl::optional<NetworkStatus> GetStatusOnQueue(
|
|
const std::unique_ptr<ConnectivityMonitorApple>& monitor,
|
|
const std::shared_ptr<AsyncQueue>& worker_queue) {
|
|
absl::optional<NetworkStatus> status;
|
|
worker_queue->EnqueueBlocking([&monitor, &status]() { status = monitor->GetCurrentStatus(); });
|
|
return status;
|
|
}
|
|
|
|
// Helper: build a monitor and wait until NWPathMonitor delivers its first
|
|
// status update. Returns a unique_ptr; caller must destroy it on the
|
|
// worker queue (see DestroyOnQueue).
|
|
//
|
|
// If the first update never arrives within `timeout_seconds`, returns nullptr.
|
|
//
|
|
// FSTConnectivityMonitorTests integration tests depend on the simulator
|
|
// having a working network. NWPathMonitor occasionally fails to deliver
|
|
// initial status updates in restricted CI environments, causing these
|
|
// tests to fail. Such failures should be retried; they do not indicate
|
|
// regressions.
|
|
std::unique_ptr<ConnectivityMonitorApple> MakeMonitorAndWaitForInitialStatus(
|
|
const std::shared_ptr<AsyncQueue>& worker_queue, NSTimeInterval timeout_seconds = 5.0) {
|
|
auto monitor = MakeMonitorOnQueue(worker_queue);
|
|
|
|
NSDate* deadline = [NSDate dateWithTimeIntervalSinceNow:timeout_seconds];
|
|
while (!GetStatusOnQueue(monitor, worker_queue).has_value()) {
|
|
if ([[NSDate date] compare:deadline] != NSOrderedAscending) {
|
|
break; // timed out
|
|
}
|
|
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
|
|
beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.05]];
|
|
}
|
|
|
|
if (!GetStatusOnQueue(monitor, worker_queue).has_value()) {
|
|
// Tear down on the worker queue before returning, so the unique_ptr
|
|
// we are about to drop does not violate the destruction invariant.
|
|
DestroyOnQueue(monitor, worker_queue);
|
|
return nullptr;
|
|
}
|
|
|
|
return monitor;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
@interface FSTConnectivityMonitorTests : XCTestCase
|
|
@end
|
|
|
|
@implementation FSTConnectivityMonitorTests
|
|
|
|
#pragma mark - Lifecycle
|
|
|
|
- (void)testConstructAndDestruct {
|
|
auto worker_queue = MakeWorkerQueue("test_construct_destruct");
|
|
auto monitor = MakeMonitorOnQueue(worker_queue);
|
|
DestroyOnQueue(monitor, worker_queue);
|
|
// No assertions — this passes if on-queue construction and on-queue
|
|
// destruction do not crash and the HARD_ASSERTs do not fire.
|
|
}
|
|
|
|
- (void)testRapidConstructDestruct {
|
|
// Stress: repeated construction/destruction should not leak resources
|
|
// (NWPathMonitor handles, dispatch queues, observer registrations) and
|
|
// should not crash if a pathUpdateHandler is in flight when destruction
|
|
// begins.
|
|
for (int i = 0; i < 20; ++i) {
|
|
auto worker_queue = MakeWorkerQueue("test_rapid");
|
|
auto monitor = MakeMonitorOnQueue(worker_queue);
|
|
DestroyOnQueue(monitor, worker_queue);
|
|
}
|
|
}
|
|
|
|
- (void)testGetCurrentStatusBeforeUpdate {
|
|
// Immediately after construction, GetCurrentStatus may or may not
|
|
// already have a value depending on how fast NWPathMonitor delivers
|
|
// its first update. The contract is: it does not crash, and the
|
|
// returned optional is always meaningful.
|
|
auto worker_queue = MakeWorkerQueue("test_get_status_early");
|
|
auto monitor = MakeMonitorOnQueue(worker_queue);
|
|
|
|
auto status = GetStatusOnQueue(monitor, worker_queue);
|
|
// No assertion on the value — either nullopt or a real status is OK.
|
|
// We are testing that the call itself is safe immediately after construct.
|
|
(void)status;
|
|
|
|
DestroyOnQueue(monitor, worker_queue);
|
|
}
|
|
|
|
- (void)testGetCurrentStatusAfterUpdate {
|
|
auto worker_queue = MakeWorkerQueue("test_get_status_after");
|
|
auto monitor = MakeMonitorAndWaitForInitialStatus(worker_queue);
|
|
if (!monitor) {
|
|
XCTFail(@"NWPathMonitor did not deliver initial status within timeout. "
|
|
@"This is typically caused by an unstable network in the test "
|
|
@"environment. Retry the test.");
|
|
return;
|
|
}
|
|
|
|
auto status = GetStatusOnQueue(monitor, worker_queue);
|
|
XCTAssertTrue(status.has_value(), @"After waiting for initial update, status must be set");
|
|
|
|
DestroyOnQueue(monitor, worker_queue);
|
|
}
|
|
|
|
- (void)testAddCallbackDoesNotCrash {
|
|
auto worker_queue = MakeWorkerQueue("test_add_callback");
|
|
auto monitor = MakeMonitorAndWaitForInitialStatus(worker_queue);
|
|
if (!monitor) {
|
|
XCTFail(@"NWPathMonitor did not deliver initial status within timeout. "
|
|
@"This is typically caused by an unstable network in the test "
|
|
@"environment. Retry the test.");
|
|
return;
|
|
}
|
|
|
|
std::atomic<int> invocation_count{0};
|
|
worker_queue->EnqueueBlocking([&monitor, &invocation_count]() {
|
|
monitor->AddCallback(
|
|
[&invocation_count](NetworkStatus /*status*/) { invocation_count.fetch_add(1); });
|
|
});
|
|
|
|
// We don't assert on invocation_count: status changes are not deterministic
|
|
// in a unit test environment. We only assert that AddCallback itself is safe
|
|
// and does not crash.
|
|
|
|
DestroyOnQueue(monitor, worker_queue);
|
|
}
|
|
|
|
#pragma mark - Foreground notification (iOS / tvOS / visionOS)
|
|
|
|
#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION
|
|
|
|
- (void)testForegroundNotificationInvokesCallback {
|
|
auto worker_queue = MakeWorkerQueue("test_foreground_invokes");
|
|
auto monitor = MakeMonitorAndWaitForInitialStatus(worker_queue);
|
|
if (!monitor) {
|
|
XCTFail(@"NWPathMonitor did not deliver initial status within timeout. "
|
|
@"This is typically caused by an unstable network in the test "
|
|
@"environment. Retry the test.");
|
|
return;
|
|
}
|
|
|
|
// Skip the test if the simulator has no network — the foreground handler
|
|
// intentionally short-circuits when status is Unavailable, so the
|
|
// assertion below would not be meaningful.
|
|
if (GetStatusOnQueue(monitor, worker_queue).value() == NetworkStatus::Unavailable) {
|
|
DestroyOnQueue(monitor, worker_queue);
|
|
XCTSkip(@"Skipping: simulator reports Unavailable; "
|
|
@"foreground handler short-circuits in this state");
|
|
return;
|
|
}
|
|
|
|
XCTestExpectation* callbackExpectation =
|
|
[self expectationWithDescription:@"Callback invoked after foreground"];
|
|
std::atomic<bool> callback_invoked{false};
|
|
worker_queue->EnqueueBlocking([&]() {
|
|
monitor->AddCallback([&](NetworkStatus /*status*/) {
|
|
if (!callback_invoked.exchange(true)) {
|
|
[callbackExpectation fulfill];
|
|
}
|
|
});
|
|
});
|
|
|
|
[[NSNotificationCenter defaultCenter]
|
|
postNotificationName:UIApplicationWillEnterForegroundNotification
|
|
object:nil];
|
|
|
|
[self waitForExpectations:@[ callbackExpectation ] timeout:5.0];
|
|
XCTAssertTrue(callback_invoked.load());
|
|
|
|
DestroyOnQueue(monitor, worker_queue);
|
|
}
|
|
|
|
- (void)testForegroundNotificationAfterDestructionDoesNotCrash {
|
|
// Verifies that the destructor properly removes the
|
|
// NSNotificationCenter observer. If it didn't, posting a notification
|
|
// after destruction would call a block holding a dangling `this`.
|
|
auto worker_queue = MakeWorkerQueue("test_foreground_after_destruct");
|
|
auto monitor = MakeMonitorAndWaitForInitialStatus(worker_queue);
|
|
if (!monitor) {
|
|
XCTFail(@"NWPathMonitor did not deliver initial status within timeout. "
|
|
@"This is typically caused by an unstable network in the test "
|
|
@"environment. Retry the test.");
|
|
return;
|
|
}
|
|
|
|
DestroyOnQueue(monitor, worker_queue);
|
|
|
|
// Post the notification several times after destruction. If the observer
|
|
// were not removed, this would dispatch into a destroyed object and crash.
|
|
for (int i = 0; i < 5; ++i) {
|
|
[[NSNotificationCenter defaultCenter]
|
|
postNotificationName:UIApplicationWillEnterForegroundNotification
|
|
object:nil];
|
|
}
|
|
|
|
// Give any (incorrectly) dispatched handler a chance to run and crash
|
|
// before we declare success.
|
|
XCTestExpectation* drained =
|
|
[self expectationWithDescription:@"Settle after post-destruct notifications"];
|
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)),
|
|
dispatch_get_main_queue(), ^{
|
|
[drained fulfill];
|
|
});
|
|
[self waitForExpectations:@[ drained ] timeout:2.0];
|
|
}
|
|
|
|
- (void)testForegroundNotificationWithoutCallbacksDoesNotCrash {
|
|
// The foreground handler iterates registered callbacks. With zero
|
|
// callbacks registered, it must still complete without crashing.
|
|
auto worker_queue = MakeWorkerQueue("test_foreground_no_callbacks");
|
|
auto monitor = MakeMonitorAndWaitForInitialStatus(worker_queue);
|
|
if (!monitor) {
|
|
XCTFail(@"NWPathMonitor did not deliver initial status within timeout. "
|
|
@"This is typically caused by an unstable network in the test "
|
|
@"environment. Retry the test.");
|
|
return;
|
|
}
|
|
|
|
[[NSNotificationCenter defaultCenter]
|
|
postNotificationName:UIApplicationWillEnterForegroundNotification
|
|
object:nil];
|
|
|
|
// Let the worker queue drain any work the foreground handler enqueued.
|
|
worker_queue->EnqueueBlocking([]() {});
|
|
|
|
DestroyOnQueue(monitor, worker_queue);
|
|
}
|
|
|
|
#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION
|
|
|
|
@end
|