Skip to content
Open
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
295ee89
refactor ConnectivityMonitorApple to use NW_PATH_MONITOR
cherylEnkidu Apr 23, 2026
6e95d71
format
cherylEnkidu Apr 23, 2026
6a82fbd
add Network package dependency
cherylEnkidu Apr 23, 2026
2d6a1cb
add test
cherylEnkidu Apr 23, 2026
11a630d
add include file
cherylEnkidu Apr 23, 2026
20e84db
format
cherylEnkidu Apr 23, 2026
4309cfb
add test availability
cherylEnkidu Apr 23, 2026
4ab254d
add dummy callback
cherylEnkidu Apr 23, 2026
2f95e78
add test log
cherylEnkidu Apr 24, 2026
191630c
add debuglog
cherylEnkidu Apr 27, 2026
a5fab0d
format
cherylEnkidu Apr 28, 2026
25d5431
format
cherylEnkidu Apr 28, 2026
3bd3498
add shutdown protector
cherylEnkidu Apr 28, 2026
0ca806a
add shutdown protection to auth
cherylEnkidu Apr 28, 2026
dea868d
move the activity monitor to the main thread
cherylEnkidu Apr 28, 2026
d8b7386
add debug log
cherylEnkidu Apr 28, 2026
52fb772
upload xcode log
cherylEnkidu Apr 28, 2026
b9bfb5d
revert the changes in app check and auth to isolate the problem
cherylEnkidu Apr 29, 2026
8d579ac
revert the changes in grpc to isolate issue
cherylEnkidu Apr 29, 2026
5e815cd
add safeguard to connectivity monitor
cherylEnkidu Apr 29, 2026
434d435
Improve the code
cherylEnkidu May 1, 2026
3495380
require test to explicitly destroy
cherylEnkidu May 1, 2026
85cd6d1
Merge branch 'main' into cheryl/SCNetworkReachability
cherylEnkidu May 4, 2026
f7b4d53
remove debug print
cherylEnkidu May 4, 2026
d37a9f3
remove unwanted changes
cherylEnkidu May 4, 2026
5927991
add more tests
cherylEnkidu May 6, 2026
2253f47
add changelog
cherylEnkidu May 6, 2026
0862789
fix bug and format code
cherylEnkidu May 6, 2026
55dad88
add cmake network library dependency
cherylEnkidu May 6, 2026
5b44b78
make sure the internet connection failure would not lead to crash
cherylEnkidu May 7, 2026
811c9b6
add comments
cherylEnkidu May 7, 2026
419b433
improve format style
cherylEnkidu May 7, 2026
4493f86
remove the function which drain the monitor queue in destructor
cherylEnkidu May 8, 2026
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
6 changes: 3 additions & 3 deletions FirebaseFirestoreInternal.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,9 @@ Google Cloud Firestore is a NoSQL document database built for automatic scaling,
s.dependency 'leveldb-library', '~> 1.22'
s.dependency 'nanopb', '~> 3.30910.0'

s.ios.frameworks = 'SystemConfiguration', 'UIKit'
s.osx.frameworks = 'SystemConfiguration'
s.tvos.frameworks = 'SystemConfiguration', 'UIKit'
s.ios.frameworks = 'SystemConfiguration', 'UIKit', 'Network'
s.osx.frameworks = 'SystemConfiguration', 'Network'
s.tvos.frameworks = 'SystemConfiguration', 'UIKit', 'Network'

s.library = 'c++'
s.pod_target_xcconfig = {
Expand Down
1 change: 1 addition & 0 deletions Firestore/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
- [feature] Added search stage support for `languageCode`, `offset`, `limit`, and `retrievalDepth`.
- [feature] Added support for Pipeline expressions `arraySlice`, `arrayFilter`, `arrayTransform` and `arrayTransformWithIndex`. (#16001)
- [fixed] Add missing `noexcept` specifiers to move, hash, swap operations [#16117].
- [changed] Migrates the network connectivity monitoring implementation for Apple platforms from the legacy SCNetworkReachability API to the modern NWPathMonitor API.
Comment on lines 1 to +5
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: 80 chars wrapping and put new entries under new "Unreleased" header

Suggested change
- [changed] Migrates the network connectivity monitoring implementation for Apple platforms from the legacy SCNetworkReachability API to the modern NWPathMonitor API.
# Unreleased
- [changed] Migrates the network connectivity monitoring implementation for
Apple platforms from the legacy SCNetworkReachability API to the modern
NWPathMonitor API.
# 12.13.0
- [feature] Added search stage support for `languageCode`, `offset`, `limit`, and `retrievalDepth`.
- [feature] Added support for Pipeline expressions `arraySlice`, `arrayFilter`, `arrayTransform` and `arrayTransformWithIndex`. (#16001)
- [fixed] Add missing `noexcept` specifiers to move, hash, swap operations [#16117].


# 12.12.0
- [feature] Added support for the `parent` Pipeline expression. (#16010)
Expand Down
14 changes: 11 additions & 3 deletions Firestore/Example/Firestore.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@
3D22F56C0DE7C7256C75DC06 /* tree_sorted_map_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 549CCA4D20A36DBB00BCEB75 /* tree_sorted_map_test.cc */; };
3D5F7AA7BB68529F47BE4B12 /* PipelineApiTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59BF06E5A4988F9F949DD871 /* PipelineApiTests.swift */; };
3D6AC48D6197E6539BBBD28F /* thread_safe_memoizer_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 6E42FA109D363EA7F3387AAE /* thread_safe_memoizer_testing.cc */; };
3D87E28662811BE6997481C1 /* FSTConnectivityMonitorTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 29B718A7F88CFA0F1FBFA815 /* FSTConnectivityMonitorTests.mm */; };
3D9619906F09108E34FF0C95 /* FSTSmokeTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E07C202154EB00B64F25 /* FSTSmokeTests.mm */; };
3DBB48F077C97200F32B51A0 /* value_util_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 40F9D09063A07F710811A84F /* value_util_test.cc */; };
3DBBC644BE08B140BCC23BD5 /* string_apple_benchmark.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4C73C0CC6F62A90D8573F383 /* string_apple_benchmark.mm */; };
Expand Down Expand Up @@ -886,6 +887,7 @@
6C388B2D0967088758FF2425 /* leveldb_target_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = E76F0CDF28E5FA62D21DE648 /* leveldb_target_cache_test.cc */; };
6C415868AE347DC4A26588C3 /* Validation_BloomFilterTest_MD5_500_0001_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = D22D4C211AC32E4F8B4883DA /* Validation_BloomFilterTest_MD5_500_0001_bloom_filter_proto.json */; };
6C74C16D4B1B356CF4719E05 /* inequality_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = A410E38FA5C3EB5AECDB6F1C /* inequality_test.cc */; };
6C815C08D2EB3A249AD182B8 /* FSTConnectivityMonitorTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 29B718A7F88CFA0F1FBFA815 /* FSTConnectivityMonitorTests.mm */; };
6C92AD45A3619A18ECCA5B1F /* query_listener_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 7C3F995E040E9E9C5E8514BB /* query_listener_test.cc */; };
6C941147D9DB62E1A845CAB7 /* debug_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F6DBD8EDF0074DD0079ECCE6 /* debug_test.cc */; };
6D2FC59BAA15B54EF960D936 /* string_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = EEF23C7104A4D040C3A8CF9B /* string_test.cc */; };
Expand Down Expand Up @@ -962,6 +964,7 @@
75C6CECF607CA94F56260BAB /* memory_document_overlay_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 29D9C76922DAC6F710BC1EF4 /* memory_document_overlay_cache_test.cc */; };
75CC1D1F7F1093C2E09D9998 /* inequality_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = A410E38FA5C3EB5AECDB6F1C /* inequality_test.cc */; };
75D124966E727829A5F99249 /* FIRTypeTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E071202154D600B64F25 /* FIRTypeTests.mm */; };
76380F7BB28DEAE97FE87132 /* FSTConnectivityMonitorTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 29B718A7F88CFA0F1FBFA815 /* FSTConnectivityMonitorTests.mm */; };
76A5447D76F060E996555109 /* task_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 899FC22684B0F7BEEAE13527 /* task_test.cc */; };
76AD5862714F170251BDEACB /* Validation_BloomFilterTest_MD5_50000_0001_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = A5D9044B72061CAF284BC9E4 /* Validation_BloomFilterTest_MD5_50000_0001_bloom_filter_proto.json */; };
76C18D1BA96E4F5DF1BF7F4B /* Validation_BloomFilterTest_MD5_500_1_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = 8AB49283E544497A9C5A0E59 /* Validation_BloomFilterTest_MD5_500_1_membership_test_result.json */; };
Expand Down Expand Up @@ -1957,6 +1960,7 @@
28B45B2104E2DAFBBF86DBB7 /* logic_utils_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = logic_utils_test.cc; sourceTree = "<group>"; };
28F2FB3623C4D103FAC984DD /* Pods_Firestore_Tests_tvOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Tests_tvOS.framework; sourceTree = BUILT_PRODUCTS_DIR; };
2996F8E339AD187C2C5068DE /* utils.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = utils.h; path = pipeline/utils.h; sourceTree = "<group>"; };
29B718A7F88CFA0F1FBFA815 /* FSTConnectivityMonitorTests.mm */ = {isa = PBXFileReference; includeInIndex = 1; path = FSTConnectivityMonitorTests.mm; sourceTree = "<group>"; };
29D9C76922DAC6F710BC1EF4 /* memory_document_overlay_cache_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = memory_document_overlay_cache_test.cc; sourceTree = "<group>"; };
2A0CF41BA5AED6049B0BEB2C /* objc_type_traits_apple_test.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; path = objc_type_traits_apple_test.mm; sourceTree = "<group>"; };
2BE59C9C2992E1A580D02935 /* disjunctive_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; name = disjunctive_test.cc; path = pipeline/disjunctive_test.cc; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3359,6 +3363,7 @@
isa = PBXGroup;
children = (
DE51B1BC1F0D48AC0013853F /* API */,
29B718A7F88CFA0F1FBFA815 /* FSTConnectivityMonitorTests.mm */,
5492E07E202154EC00B64F25 /* FSTDatastoreTests.mm */,
5492E07C202154EB00B64F25 /* FSTSmokeTests.mm */,
5492E07B202154EB00B64F25 /* FSTTransactionTests.mm */,
Expand Down Expand Up @@ -4989,6 +4994,7 @@
EF3A654D2C66B9560041EE69 /* FIRVectorValueTests.mm in Sources */,
BC0C98A9201E8F98B9A176A9 /* FIRWriteBatchTests.mm in Sources */,
D550446303227FB1B381133C /* FSTAPIHelpers.mm in Sources */,
76380F7BB28DEAE97FE87132 /* FSTConnectivityMonitorTests.mm in Sources */,
A4ECA8335000CBDF94586C94 /* FSTDatastoreTests.mm in Sources */,
2E169CF1E9E499F054BB873A /* FSTEventAccumulator.mm in Sources */,
D34E3F7FC4DC5210E671EF4D /* FSTExceptionCatcher.m in Sources */,
Expand Down Expand Up @@ -5207,8 +5213,8 @@
25D74F38A5EE96CC653ABB49 /* thread_safe_memoizer_testing.cc in Sources */,
688AC36AA9D0677E910D5A37 /* thread_safe_memoizer_testing_test.cc in Sources */,
6300709ECDE8E0B5A8645F8D /* time_testing.cc in Sources */,
0CEE93636BA4852D3C5EC428 /* timestamp_test.cc in Sources */,
A405A976DB6444D3ED3FCAB2 /* timestamp_test.cc in Sources */,
0CEE93636BA4852D3C5EC428 /* timestamp_test.cc in Sources */,
95DCD082374F871A86EF905F /* to_string_apple_test.mm in Sources */,
9E656F4FE92E8BFB7F625283 /* to_string_test.cc in Sources */,
96D95E144C383459D4E26E47 /* token_test.cc in Sources */,
Expand Down Expand Up @@ -5273,6 +5279,7 @@
EF3A654B2C66B9560041EE69 /* FIRVectorValueTests.mm in Sources */,
8705C4856498F66E471A0997 /* FIRWriteBatchTests.mm in Sources */,
881E55152AB34465412F8542 /* FSTAPIHelpers.mm in Sources */,
6C815C08D2EB3A249AD182B8 /* FSTConnectivityMonitorTests.mm in Sources */,
4A64A339BCA77B9F875D1D8B /* FSTDatastoreTests.mm in Sources */,
1C7254742A9F6F7042C9D78E /* FSTEventAccumulator.mm in Sources */,
B510921E4CD441289F6B2B78 /* FSTExceptionCatcher.m in Sources */,
Expand Down Expand Up @@ -5491,8 +5498,8 @@
CF18D52A88F4F6F62C5495EF /* thread_safe_memoizer_testing.cc in Sources */,
A7669E72BCED7FBADA4B1314 /* thread_safe_memoizer_testing_test.cc in Sources */,
A25FF76DEF542E01A2DF3B0E /* time_testing.cc in Sources */,
1E42CD0F60EB22A5D0C86D1F /* timestamp_test.cc in Sources */,
BDDAB87A7D76562BCB5D0BF8 /* timestamp_test.cc in Sources */,
1E42CD0F60EB22A5D0C86D1F /* timestamp_test.cc in Sources */,
F9705E595FC3818F13F6375A /* to_string_apple_test.mm in Sources */,
3BAFCABA851AE1865D904323 /* to_string_test.cc in Sources */,
1B9E54F4C4280A713B825981 /* token_test.cc in Sources */,
Expand Down Expand Up @@ -5839,6 +5846,7 @@
EF3A65492C66B9560041EE69 /* FIRVectorValueTests.mm in Sources */,
5492E078202154D600B64F25 /* FIRWriteBatchTests.mm in Sources */,
D9EF7FC0E3F8646B272B427E /* FSTAPIHelpers.mm in Sources */,
3D87E28662811BE6997481C1 /* FSTConnectivityMonitorTests.mm in Sources */,
5492E082202154EC00B64F25 /* FSTDatastoreTests.mm in Sources */,
5492E041202143E700B64F25 /* FSTEventAccumulator.mm in Sources */,
D0CD302D79FF5CE4F418FF0E /* FSTExceptionCatcher.m in Sources */,
Expand Down Expand Up @@ -6057,8 +6065,8 @@
D928302820891CCCAD0437DD /* thread_safe_memoizer_testing.cc in Sources */,
C099AEC05D44976755BA32A2 /* thread_safe_memoizer_testing_test.cc in Sources */,
2D220B9ABFA36CD7AC43D0A7 /* time_testing.cc in Sources */,
D91D86B29B86A60C05879A48 /* timestamp_test.cc in Sources */,
06B8A653BC26CB2C96024993 /* timestamp_test.cc in Sources */,
D91D86B29B86A60C05879A48 /* timestamp_test.cc in Sources */,
60260A06871DCB1A5F3448D3 /* to_string_apple_test.mm in Sources */,
ECED3B60C5718B085AAB14FB /* to_string_test.cc in Sources */,
F0EA84FB66813F2BC164EF7C /* token_test.cc in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ @implementation FIRIndexingTests
// Clears persistence for each test method to have a clean start.
- (void)setUp {
[super setUp];
self.db = [self firestore];
XCTestExpectation *exp = [self expectationWithDescription:@"clear persistence"];
[self.db clearPersistenceWithCompletion:^(NSError *) {
[exp fulfill];
Expand Down
3 changes: 2 additions & 1 deletion Firestore/Example/Tests/Integration/API/FIRQueryTests.mm
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,8 @@ - (void)testWatchSurvivesNetworkDisconnect {
addSnapshotListenerWithIncludeMetadataChanges:YES
listener:^(FIRQuerySnapshot *snapshot, NSError *error) {
XCTAssertNil(error);
if (!snapshot.empty && !snapshot.metadata.fromCache) {
if (!snapshot.empty && !snapshot.metadata.fromCache &&
!snapshot.metadata.hasPendingWrites) {
[testExpectiation fulfill];
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix flaky testWatchSurvivesNetworkDisconnect under new connectivity timing.

The test used addSnapshotListenerWithIncludeMetadataChanges:YES and a filter of !empty && !fromCache, expecting the listener to fulfill the expectation exactly once. With metadata changes enabled, the listener fires twice in the recovery sequence: once when the snapshot is raised from the server with hasPendingWrites=true (write enqueued, awaiting ack), and again when hasPendingWrites flips to false after server ack. Both pass the existing filter, causing over-fulfill.

This is a latent bug in the test, not a regression from the connectivity monitor refactor. The connectivity changes in this PR shifted the timing of network state delivery in a way that exposed it.

Tighten the filter to also require !hasPendingWrites, which is the condition the test actually intends to wait for: the write has fully round-tripped to the server.

}
}];
Expand Down
223 changes: 223 additions & 0 deletions Firestore/Example/Tests/Integration/FSTConnectivityMonitorTests.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
/*
* 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: 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`, fails the
// test via XCTFail through the provided test case pointer.
std::unique_ptr<ConnectivityMonitorApple> MakeMonitorAndWaitForInitialStatus(
const std::shared_ptr<AsyncQueue>& worker_queue, NSTimeInterval timeout_seconds = 2.0) {
auto monitor = std::make_unique<ConnectivityMonitorApple>(worker_queue);

// NWPathMonitor delivers its first pathUpdateHandler asynchronously.
// Poll GetCurrentStatus() until it has a value or we time out.
NSDate* timeoutDate = [NSDate dateWithTimeIntervalSinceNow:timeout_seconds];
while (!monitor->GetCurrentStatus().has_value() && [timeoutDate timeIntervalSinceNow] > 0) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.05]];
}

if (!monitor->GetCurrentStatus().has_value()) {
XCTFail(@"Timed out waiting for initial status update from NWPathMonitor");

Check failure on line 58 in Firestore/Example/Tests/Integration/FSTConnectivityMonitorTests.mm

View workflow job for this annotation

GitHub Actions / xcodetest_nightly (iOS, FirestoreEnterprise)

testForegroundNotificationInvokesCallback, failed - Timed out waiting for initial status update from NWPathMonitor
}
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(); });
}

} // namespace

@interface FSTConnectivityMonitorTests : XCTestCase
@end

@implementation FSTConnectivityMonitorTests

#pragma mark - Lifecycle

- (void)testConstructAndDestruct {
auto worker_queue = MakeWorkerQueue("test_construct_destruct");
auto monitor = std::make_unique<ConnectivityMonitorApple>(worker_queue);
DestroyOnQueue(monitor, worker_queue);
// No assertions — this passes if construction and on-queue destruction
// do not crash and the destructor's HARD_ASSERT does 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 = std::make_unique<ConnectivityMonitorApple>(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 = std::make_unique<ConnectivityMonitorApple>(worker_queue);

auto status = monitor->GetCurrentStatus();
// 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);

auto status = monitor->GetCurrentStatus();
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);

std::atomic<int> invocation_count{0};
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);

// 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 (monitor->GetCurrentStatus().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};
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);

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);

[[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
Loading
Loading