Skip to content

Commit a00925f

Browse files
authored
FIRQueryTests.mm: improve the test that resumes a query with existence filter to actually validate the existence filter (#11209)
1 parent 2c81997 commit a00925f

File tree

9 files changed

+707
-2
lines changed

9 files changed

+707
-2
lines changed

Firestore/Example/Firestore.xcodeproj/project.pbxproj

Lines changed: 30 additions & 0 deletions
Large diffs are not rendered by default.

Firestore/Example/Tests/Integration/API/FIRQueryTests.mm

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
#import "Firestore/Example/Tests/Util/FSTHelpers.h"
2323
#import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h"
2424

25+
#include "Firestore/core/test/unit/testutil/testing_hooks_util.h"
26+
2527
namespace {
2628

2729
NSArray<NSString *> *SortedStringsNotIn(NSSet<NSString *> *set, NSSet<NSString *> *remove) {
@@ -1191,6 +1193,10 @@ - (void)testOrderByEquality {
11911193
}
11921194

11931195
- (void)testResumingAQueryShouldUseExistenceFilterToDetectDeletes {
1196+
// Set this test to stop when the first failure occurs because some test assertion failures make
1197+
// the rest of the test not applicable or will even crash.
1198+
[self setContinueAfterFailure:NO];
1199+
11941200
// Prepare the names and contents of the 100 documents to create.
11951201
NSMutableDictionary<NSString *, NSDictionary<NSString *, id> *> *testDocs =
11961202
[[NSMutableDictionary alloc] init];
@@ -1236,8 +1242,13 @@ - (void)testResumingAQueryShouldUseExistenceFilterToDetectDeletes {
12361242
[NSThread sleepForTimeInterval:10.0f];
12371243

12381244
// Resume the query and save the resulting snapshot for verification.
1239-
FIRQuerySnapshot *querySnapshot2 = [self readDocumentSetForRef:collRef
1240-
source:FIRFirestoreSourceDefault];
1245+
// Use some internal testing hooks to "capture" the existence filter mismatches to verify them.
1246+
FIRQuerySnapshot *querySnapshot2;
1247+
std::vector<firebase::firestore::util::TestingHooks::ExistenceFilterMismatchInfo>
1248+
existence_filter_mismatches =
1249+
firebase::firestore::testutil::CaptureExistenceFilterMismatches([&] {
1250+
querySnapshot2 = [self readDocumentSetForRef:collRef source:FIRFirestoreSourceDefault];
1251+
});
12411252

12421253
// Verify that the snapshot from the resumed query contains the expected documents; that is,
12431254
// that it contains the 50 documents that were _not_ deleted.
@@ -1272,6 +1283,21 @@ - (void)testResumingAQueryShouldUseExistenceFilterToDetectDeletes {
12721283
[missingDocumentIds componentsJoinedByString:@", "]);
12731284
}
12741285
}
1286+
1287+
// Skip the verification of the existence filter mismatch when testing against the Firestore
1288+
// emulator because the Firestore emulator fails to to send an existence filter at all.
1289+
// TODO(b/270731363): Enable the verification of the existence filter mismatch once the Firestore
1290+
// emulator is fixed to send an existence filter.
1291+
if ([FSTIntegrationTestCase isRunningAgainstEmulator]) {
1292+
return;
1293+
}
1294+
1295+
// Verify that Watch sent an existence filter with the correct counts when the query was resumed.
1296+
XCTAssertEqual(static_cast<int>(existence_filter_mismatches.size()), 1);
1297+
firebase::firestore::util::TestingHooks::ExistenceFilterMismatchInfo &info =
1298+
existence_filter_mismatches[0];
1299+
XCTAssertEqual(info.local_cache_count, 100);
1300+
XCTAssertEqual(info.existence_filter_count, 50);
12751301
}
12761302

12771303
@end

Firestore/core/src/remote/remote_event.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include <utility>
2020

2121
#include "Firestore/core/src/local/target_data.h"
22+
#include "Firestore/core/src/util/testing_hooks.h"
2223

2324
namespace firebase {
2425
namespace firestore {
@@ -34,6 +35,7 @@ using model::MutableDocument;
3435
using model::SnapshotVersion;
3536
using model::TargetId;
3637
using nanopb::ByteString;
38+
using util::TestingHooks;
3739

3840
// TargetChange
3941

@@ -239,6 +241,8 @@ void WatchChangeAggregator::HandleExistenceFilter(
239241
// snapshot with `isFromCache:true`.
240242
ResetTarget(target_id);
241243
pending_target_resets_.insert(target_id);
244+
TestingHooks::GetInstance().NotifyOnExistenceFilterMismatch(
245+
{current_size, expected_count});
242246
}
243247
}
244248
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Copyright 2023 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include "Firestore/core/src/util/testing_hooks.h"
18+
19+
#include <functional>
20+
#include <mutex> // NOLINT(build/c++11)
21+
#include <type_traits>
22+
#include <unordered_map>
23+
#include <utility>
24+
#include <vector>
25+
26+
#include "Firestore/core/src/util/no_destructor.h"
27+
28+
namespace firebase {
29+
namespace firestore {
30+
namespace util {
31+
32+
namespace {
33+
34+
/**
35+
* An implementation of `ListenerRegistration` whose `Remove()` method simply
36+
* invokes the function specified to the constructor. This allows easily
37+
* creating `ListenerRegistration` objects that call a lambda.
38+
*/
39+
class RemoveDelegatingListenerRegistration final
40+
: public api::ListenerRegistration {
41+
public:
42+
RemoveDelegatingListenerRegistration(std::function<void()> delegate)
43+
: delegate_(std::move(delegate)) {
44+
}
45+
46+
void Remove() override {
47+
delegate_();
48+
}
49+
50+
private:
51+
std::function<void()> delegate_;
52+
};
53+
54+
} // namespace
55+
56+
/** Returns the singleton instance of this class. */
57+
TestingHooks& TestingHooks::GetInstance() {
58+
static NoDestructor<TestingHooks> instance;
59+
return *instance;
60+
}
61+
62+
std::shared_ptr<api::ListenerRegistration>
63+
TestingHooks::OnExistenceFilterMismatch(
64+
ExistenceFilterMismatchCallback callback) {
65+
// Register the callback.
66+
std::unique_lock<std::mutex> lock(mutex_);
67+
const int id = next_id_++;
68+
existence_filter_mismatch_callbacks_.insert(
69+
{id,
70+
std::make_shared<ExistenceFilterMismatchCallback>(std::move(callback))});
71+
lock.unlock();
72+
73+
// NOTE: Capturing `this` in the lambda below is safe because the destructor
74+
// is deleted and, therefore, `this` can never be deleted. The static_assert
75+
// statements below verify this invariant.
76+
using this_type = std::remove_pointer<decltype(this)>::type;
77+
static_assert(std::is_same<this_type, TestingHooks>::value, "");
78+
static_assert(!std::is_destructible<this_type>::value, "");
79+
80+
// Create a ListenerRegistration that the caller can use to unregister the
81+
// callback.
82+
return std::make_shared<RemoveDelegatingListenerRegistration>([this, id]() {
83+
std::lock_guard<std::mutex> lock(mutex_);
84+
auto iter = existence_filter_mismatch_callbacks_.find(id);
85+
if (iter != existence_filter_mismatch_callbacks_.end()) {
86+
existence_filter_mismatch_callbacks_.erase(iter);
87+
}
88+
});
89+
}
90+
91+
void TestingHooks::NotifyOnExistenceFilterMismatch(
92+
const ExistenceFilterMismatchInfo& info) {
93+
std::unique_lock<std::mutex> lock(mutex_);
94+
95+
// Short-circuit to avoid any unnecessary work if there is nothing to do.
96+
if (existence_filter_mismatch_callbacks_.empty()) {
97+
return;
98+
}
99+
100+
// Copy the callbacks into a vector so that they can be invoked after
101+
// releasing the lock.
102+
std::vector<std::shared_ptr<ExistenceFilterMismatchCallback>> callbacks;
103+
for (auto&& entry : existence_filter_mismatch_callbacks_) {
104+
callbacks.push_back(entry.second);
105+
}
106+
107+
// Release the lock so that the callback invocations are done _without_
108+
// holding the lock. This avoids deadlock in the case that invocations are
109+
// re-entrant.
110+
lock.unlock();
111+
112+
for (std::shared_ptr<ExistenceFilterMismatchCallback> callback : callbacks) {
113+
callback->operator()(info);
114+
}
115+
}
116+
117+
} // namespace util
118+
} // namespace firestore
119+
} // namespace firebase
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* Copyright 2023 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#ifndef FIRESTORE_CORE_SRC_UTIL_TESTING_HOOKS_H_
18+
#define FIRESTORE_CORE_SRC_UTIL_TESTING_HOOKS_H_
19+
20+
#include <functional>
21+
#include <memory>
22+
#include <mutex> // NOLINT(build/c++11)
23+
#include <unordered_map>
24+
25+
#include "Firestore/core/src/api/listener_registration.h"
26+
#include "Firestore/core/src/util/no_destructor.h"
27+
28+
namespace firebase {
29+
namespace firestore {
30+
namespace util {
31+
32+
/**
33+
* Manages "testing hooks", hooks into the internals of the SDK to verify
34+
* internal state and events during integration tests. Do not use this class
35+
* except for testing purposes.
36+
*/
37+
class TestingHooks final {
38+
public:
39+
/** Returns the singleton instance of this class. */
40+
static TestingHooks& GetInstance();
41+
42+
/**
43+
* Information about an existence filter mismatch, as specified to callbacks
44+
* registered with `OnExistenceFilterMismatch()`.
45+
*/
46+
struct ExistenceFilterMismatchInfo {
47+
/** The number of documents that matched the query in the local cache. */
48+
int local_cache_count = -1;
49+
50+
/**
51+
* The number of documents that matched the query on the server, as
52+
* specified in the `ExistenceFilter` message's `count` field.
53+
*/
54+
int existence_filter_count = -1;
55+
};
56+
57+
using ExistenceFilterMismatchCallback =
58+
std::function<void(const TestingHooks::ExistenceFilterMismatchInfo&)>;
59+
60+
/**
61+
* Registers a callback to be invoked when an existence filter mismatch occurs
62+
* in the Watch listen stream.
63+
*
64+
* The relative order in which callbacks are notified is unspecified; do not
65+
* rely on any particular ordering. If a given callback is registered multiple
66+
* times then it will be notified multiple times, once per registration.
67+
*
68+
* The listener callbacks are performed synchronously in
69+
* `NotifyOnExistenceFilterMismatch()`; therefore, listeners should perform
70+
* their work as quickly as possible and return to avoid blocking any critical
71+
* work. In particular, the listener callbacks should *not* block or perform
72+
* long-running operations.
73+
*
74+
* The `ExistenceFilterMismatchInfo` reference specified to the callback is
75+
* only valid during the lifetime of the callback. Once the callback returns
76+
* then it must not use the given `ExistenceFilterMismatchInfo` reference
77+
* again.
78+
*
79+
* @param callback the callback to invoke upon existence filter mismatch.
80+
*
81+
* @return an object whose `Remove()` member function unregisters the given
82+
* callback; only the first invocation of `Remove()` does anything; all
83+
* subsequent invocations do nothing. Note that due to inherent race
84+
* conditions it is technically possible, although unlikely, that callbacks
85+
* could still occur _after_ unregistering.
86+
*/
87+
std::shared_ptr<api::ListenerRegistration> OnExistenceFilterMismatch(
88+
ExistenceFilterMismatchCallback callback);
89+
90+
/**
91+
* Invokes all currently-registered `OnExistenceFilterMismatch` callbacks
92+
* synchronously.
93+
* @param info Information about the existence filter mismatch.
94+
*/
95+
void NotifyOnExistenceFilterMismatch(const ExistenceFilterMismatchInfo& info);
96+
97+
private:
98+
TestingHooks() = default;
99+
100+
// Delete the destructor so that the singleton instance of this class can
101+
// never be deleted.
102+
~TestingHooks() = delete;
103+
104+
TestingHooks(const TestingHooks&) = delete;
105+
TestingHooks(TestingHooks&&) = delete;
106+
TestingHooks& operator=(const TestingHooks&) = delete;
107+
TestingHooks& operator=(TestingHooks&&) = delete;
108+
109+
friend class NoDestructor<TestingHooks>;
110+
111+
mutable std::mutex mutex_;
112+
int next_id_ = 0;
113+
std::unordered_map<int, std::shared_ptr<ExistenceFilterMismatchCallback>>
114+
existence_filter_mismatch_callbacks_;
115+
};
116+
117+
} // namespace util
118+
} // namespace firestore
119+
} // namespace firebase
120+
121+
#endif // FIRESTORE_CORE_SRC_UTIL_TESTING_HOOKS_H_

0 commit comments

Comments
 (0)