Files

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