• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

realm / realm-core / 2469

03 Jul 2024 10:12PM UTC coverage: 90.958% (-0.03%) from 90.984%
2469

push

Evergreen

web-flow
RCORE-2185 Sync client should steal file ident of fresh realm when performing client reset (#7850)

* Initial changes to use the file ident from the fresh realm during client reset

* Fixed failing realm_sync_test tests

* Don't send UPLOAD Messages while downloading fresh realm

* Allow sending QUERY bootstrap for fresh download sessions

* Added SHARED_GROUP_FRESH_PATH to generate path for fresh realm

* Removed SHARED_GROUP_FRESH_PATH and used session_reason setting instead

* Some cleanup after tests passing

* Added test to verify no UPLOAD messages are sent during fresh realm download

* Use is_fresh_path to determine if hook event called by client reset fresh realm download session

* Fixed tsan failure around REQUIRE() within hook event callback in flx_migration test

* Updates from review and streamlined changes based on recommendations

* Reverted some test changes that are no longer needed

* Updated logic for when to perform a client reset diff

* Updated fresh realm download to update upload progress but not send upload messages

* Removed has_client_reset_config flag in favor of get_cliet_reset_config()

* Updats from the review - renamed m_allow_uploads to m_delay_uploads

* Updated assert

* Updated test to start with file ident, added comment about client reset and no file ident

* Updated comment for m_delay_uploads

102284 of 180462 branches covered (56.68%)

140 of 147 new or added lines in 10 files covered. (95.24%)

90 existing lines in 15 files now uncovered.

215145 of 236531 relevant lines covered (90.96%)

6144068.37 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.9
/test/object-store/sync/flx_migration.cpp
1
////////////////////////////////////////////////////////////////////////////
2
//
3
// Copyright 2023 Realm Inc.
4
//
5
// Licensed under the Apache License, Version 2.0 (the "License");
6
// you may not use this file except in compliance with the License.
7
// You may obtain a copy of the License at
8
//
9
// http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing, software
12
// distributed under the License is distributed on an "AS IS" BASIS,
13
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
// See the License for the specific language governing permissions and
15
// limitations under the License.
16
//
17
////////////////////////////////////////////////////////////////////////////
18

19
#include <util/crypt_key.hpp>
20
#include <util/sync/baas_admin_api.hpp>
21
#include <util/sync/flx_sync_harness.hpp>
22
#include <util/sync/sync_test_utils.hpp>
23

24
#include <realm/object-store/impl/object_accessor_impl.hpp>
25
#include <realm/object-store/impl/realm_coordinator.hpp>
26
#include <realm/object-store/thread_safe_reference.hpp>
27
#include <realm/object-store/sync/async_open_task.hpp>
28
#include <realm/object-store/util/scheduler.hpp>
29

30
#include <realm/sync/protocol.hpp>
31
#include <realm/sync/noinst/client_history_impl.hpp>
32
#include <realm/sync/noinst/client_reset_operation.hpp>
33

34
#include <realm/util/future.hpp>
35

36
#include <catch2/catch_all.hpp>
37

38
#include <chrono>
39

40
#if REALM_ENABLE_SYNC
41
#if REALM_ENABLE_AUTH_TESTS
42

43
using namespace realm;
44

45
enum MigrationMode { MigrateToFLX, RollbackToPBS };
46

47
static void trigger_server_migration(const AppSession& app_session, MigrationMode switch_mode,
48
                                     const std::shared_ptr<util::Logger>& logger)
49
{
30✔
50
    auto baas_sync_service = app_session.admin_api.get_sync_service(app_session.server_app_id);
30✔
51

52
    REQUIRE(app_session.admin_api.is_sync_enabled(app_session.server_app_id));
30!
53
    app_session.admin_api.migrate_to_flx(app_session.server_app_id, baas_sync_service.id,
30✔
54
                                         switch_mode == MigrateToFLX);
30✔
55

56
    // While the server migration is in progress, the server cannot be used - wait until the migration
57
    // is complete. migrated with be populated with the 'isMigrated' value from the complete response
58
    AdminAPISession::MigrationStatus status;
30✔
59
    std::string last_status;
30✔
60
    std::string op_stg = [switch_mode] {
30✔
61
        if (switch_mode == MigrateToFLX)
30✔
62
            return "PBS->FLX Server migration";
18✔
63
        else
12✔
64
            return "FLX->PBS Server rollback";
12✔
65
    }();
30✔
66
    const int duration = 600; // 10 minutes, for now, since it sometimes takes longer than 300 seconds
30✔
67
    try {
30✔
68
        timed_sleeping_wait_for(
30✔
69
            [&] {
744✔
70
                status = app_session.admin_api.get_migration_status(app_session.server_app_id);
744✔
71
                if (logger && last_status != status.statusMessage) {
744✔
72
                    last_status = status.statusMessage;
111✔
73
                    logger->debug("%1 status: %2", op_stg, last_status);
111✔
74
                }
111✔
75
                return status.complete;
744✔
76
            },
744✔
77
            // Query the migration status every 0.5 seconds for up to 90 seconds
78
            std::chrono::seconds(duration), std::chrono::milliseconds(500));
30✔
79
    }
30✔
80
    catch (const std::runtime_error&) {
30✔
81
        if (logger)
×
82
            logger->debug("%1 timed out after %2 seconds", op_stg, duration);
×
83
        REQUIRE(false);
×
84
    }
×
85
    if (logger) {
30✔
86
        logger->debug("%1 complete", op_stg);
30✔
87
    }
30✔
88
    REQUIRE((switch_mode == MigrateToFLX) == status.isMigrated);
30!
89
}
30✔
90

91
// Add a set of count number of Object objects to the realm
92
static std::vector<ObjectId> fill_test_data(SyncTestFile& config, std::optional<std::string> partition = std::nullopt,
93
                                            int start = 1, int count = 5)
94
{
14✔
95
    std::vector<ObjectId> ret;
14✔
96
    auto realm = Realm::get_shared_realm(config);
14✔
97
    realm->begin_transaction();
14✔
98
    CppContext c(realm);
14✔
99
    // Add some objects with the provided partition value
100
    for (int i = 0; i < count; i++, ++start) {
84✔
101
        auto id = ObjectId::gen();
70✔
102
        auto obj = Object::create(
70✔
103
            c, realm, "Object",
70✔
104
            std::any(AnyDict{{"_id", std::any(id)}, {"string_field", util::format("value-%1", start)}}));
70✔
105

106
        if (partition) {
70✔
107
            obj.set_column_value("realm_id", *partition);
60✔
108
        }
60✔
109
        ret.push_back(id);
70✔
110
    }
70✔
111
    realm->commit_transaction();
14✔
112
    return ret;
14✔
113
}
14✔
114

115

116
TEST_CASE("Test server migration and rollback", "[sync][flx][flx migration][baas]") {
2✔
117
    auto logger_ptr = util::Logger::get_default_logger();
2✔
118

119
    const std::string partition1 = "migration-test";
2✔
120
    const std::string partition2 = "another-value";
2✔
121
    const Schema mig_schema{
2✔
122
        ObjectSchema("Object", {{"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
123
                                {"string_field", PropertyType::String | PropertyType::Nullable},
2✔
124
                                {"realm_id", PropertyType::String | PropertyType::Nullable}}),
2✔
125
    };
2✔
126
    auto server_app_config = minimal_app_config("server_migrate_rollback", mig_schema);
2✔
127
    TestAppSession session(create_app(server_app_config));
2✔
128
    SyncTestFile config1(session.app()->current_user(), partition1, server_app_config.schema);
2✔
129
    SyncTestFile config2(session.app()->current_user(), partition2, server_app_config.schema);
2✔
130

131
    // Fill some objects
132
    auto objects1 = fill_test_data(config1, partition1);    // 5 objects starting at 1
2✔
133
    auto objects2 = fill_test_data(config2, partition2, 6); // 5 objects starting at 6
2✔
134

135
    auto check_data = [&](SharedRealm& realm, bool check_set1, bool check_set2) {
14✔
136
        auto table = realm->read_group().get_table("class_Object");
14✔
137
        auto partition_col = table->get_column_key("realm_id");
14✔
138
        auto string_col = table->get_column_key("string_field");
14✔
139

140
        size_t table_size = [check_set1, check_set2] {
14✔
141
            if (check_set1 && check_set2)
14✔
142
                return 10;
×
143
            if (check_set1 || check_set2)
14✔
144
                return 5;
12✔
145
            return 0;
2✔
146
        }();
14✔
147

148
        REQUIRE(table->size() == table_size);
14!
149
        REQUIRE(bool(table->find_first(partition_col, StringData(partition1))) == check_set1);
14!
150
        REQUIRE(bool(table->find_first(string_col, StringData("value-5"))) == check_set1);
14!
151
        REQUIRE(bool(table->find_first(partition_col, StringData(partition2))) == check_set2);
14!
152
        REQUIRE(bool(table->find_first(string_col, StringData("value-6"))) == check_set2);
14!
153
    };
14✔
154

155
    // Wait for the two partition sets to upload
156
    {
2✔
157
        auto realm1 = Realm::get_shared_realm(config1);
2✔
158

159
        REQUIRE(!wait_for_upload(*realm1));
2!
160
        REQUIRE(!wait_for_download(*realm1));
2!
161

162
        check_data(realm1, true, false);
2✔
163

164
        auto realm2 = Realm::get_shared_realm(config2);
2✔
165

166
        REQUIRE(!wait_for_upload(*realm2));
2!
167
        REQUIRE(!wait_for_download(*realm2));
2!
168

169
        check_data(realm2, false, true);
2✔
170
    }
2✔
171

172
    // Migrate to FLX
173
    trigger_server_migration(session.app_session(), MigrateToFLX, logger_ptr);
2✔
174

175
    {
2✔
176
        SyncTestFile flx_config(session.app()->current_user(), server_app_config.schema,
2✔
177
                                SyncConfig::FLXSyncEnabled{});
2✔
178

179
        auto flx_realm = Realm::get_shared_realm(flx_config);
2✔
180
        {
2✔
181
            auto subs = flx_realm->get_latest_subscription_set();
2✔
182
            subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
183

184
            REQUIRE(!wait_for_upload(*flx_realm));
2!
185
            REQUIRE(!wait_for_download(*flx_realm));
2!
186

187
            check_data(flx_realm, false, false);
2✔
188
        }
2✔
189

190
        {
2✔
191
            auto flx_table = flx_realm->read_group().get_table("class_Object");
2✔
192
            auto mut_subs = flx_realm->get_latest_subscription_set().make_mutable_copy();
2✔
193
            mut_subs.insert_or_assign(
2✔
194
                "flx_migrated_Objects_1",
2✔
195
                Query(flx_table).equal(flx_table->get_column_key("realm_id"), StringData{partition1}));
2✔
196
            auto subs = std::move(mut_subs).commit();
2✔
197
            subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
198

199
            REQUIRE(!wait_for_upload(*flx_realm));
2!
200
            REQUIRE(!wait_for_download(*flx_realm));
2!
201
            wait_for_advance(*flx_realm);
2✔
202

203
            check_data(flx_realm, true, false);
2✔
204
        }
2✔
205

206
        {
2✔
207
            auto flx_table = flx_realm->read_group().get_table("class_Object");
2✔
208
            auto mut_subs = flx_realm->get_latest_subscription_set().make_mutable_copy();
2✔
209
            mut_subs.clear();
2✔
210
            mut_subs.insert_or_assign(
2✔
211
                "flx_migrated_Objects_2",
2✔
212
                Query(flx_table).equal(flx_table->get_column_key("realm_id"), StringData{partition2}));
2✔
213
            auto subs = std::move(mut_subs).commit();
2✔
214
            subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
215

216
            REQUIRE(!wait_for_upload(*flx_realm));
2!
217
            REQUIRE(!wait_for_download(*flx_realm));
2!
218
            wait_for_advance(*flx_realm);
2✔
219

220
            check_data(flx_realm, false, true);
2✔
221
        }
2✔
222
    }
2✔
223

224
    // Roll back to PBS
225
    trigger_server_migration(session.app_session(), RollbackToPBS, logger_ptr);
2✔
226

227
    // Try to connect as FLX
228
    {
2✔
229
        SyncTestFile flx_config(session.app()->current_user(), server_app_config.schema,
2✔
230
                                SyncConfig::FLXSyncEnabled{});
2✔
231
        auto [err_promise, err_future] = util::make_promise_future<SyncError>();
2✔
232
        util::CopyablePromiseHolder promise(std::move(err_promise));
2✔
233
        flx_config.sync_config->error_handler =
2✔
234
            [&logger_ptr, error_promise = std::move(promise)](std::shared_ptr<SyncSession>, SyncError err) mutable {
2✔
235
                // This situation should return the switch_to_pbs error
236
                logger_ptr->error("Server rolled back - connect as FLX received error: %1", err.status);
2✔
237
                error_promise.get_promise().emplace_value(std::move(err));
2✔
238
            };
2✔
239
        auto flx_realm = Realm::get_shared_realm(flx_config);
2✔
240
        auto err = wait_for_future(std::move(err_future), std::chrono::seconds(30)).get();
2✔
241
        REQUIRE(err.status == ErrorCodes::WrongSyncType);
2!
242
        REQUIRE(err.server_requests_action == sync::ProtocolErrorInfo::Action::ApplicationBug);
2!
243
    }
2✔
244

245
    {
2✔
246
        SyncTestFile pbs_config(session.app()->current_user(), partition1, server_app_config.schema);
2✔
247
        auto pbs_realm = Realm::get_shared_realm(pbs_config);
2✔
248

249
        REQUIRE(!wait_for_upload(*pbs_realm));
2!
250
        REQUIRE(!wait_for_download(*pbs_realm));
2!
251

252
        check_data(pbs_realm, true, false);
2✔
253
    }
2✔
254
    {
2✔
255
        SyncTestFile pbs_config(session.app()->current_user(), partition2, server_app_config.schema);
2✔
256
        auto pbs_realm = Realm::get_shared_realm(pbs_config);
2✔
257

258
        REQUIRE(!wait_for_upload(*pbs_realm));
2!
259
        REQUIRE(!wait_for_download(*pbs_realm));
2!
260

261
        check_data(pbs_realm, false, true);
2✔
262
    }
2✔
263
}
2✔
264

265
TEST_CASE("Test client migration and rollback", "[sync][flx][flx migration][baas]") {
2✔
266
    auto logger_ptr = util::Logger::get_default_logger();
2✔
267

268
    const std::string partition = "migration-test";
2✔
269
    const Schema mig_schema{
2✔
270
        ObjectSchema("Object", {{"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
271
                                {"string_field", PropertyType::String | PropertyType::Nullable},
2✔
272
                                {"realm_id", PropertyType::String | PropertyType::Nullable}}),
2✔
273
    };
2✔
274
    auto server_app_config = minimal_app_config("server_migrate_rollback", mig_schema);
2✔
275
    TestAppSession session(create_app(server_app_config));
2✔
276
    SyncTestFile config(session.app()->current_user(), partition, server_app_config.schema);
2✔
277
    config.sync_config->client_resync_mode = ClientResyncMode::DiscardLocal;
2✔
278
    config.schema_version = 0;
2✔
279

280
    // Fill some objects
281
    auto objects = fill_test_data(config, partition); // 5 objects starting at 1
2✔
282

283
    // Wait to upload the data
284
    {
2✔
285
        auto realm = Realm::get_shared_realm(config);
2✔
286

287
        REQUIRE(!wait_for_upload(*realm));
2!
288
        REQUIRE(!wait_for_download(*realm));
2!
289

290
        auto table = realm->read_group().get_table("class_Object");
2✔
291
        REQUIRE(table->size() == 5);
2!
292
    }
2✔
293

294
    // Migrate to FLX
295
    trigger_server_migration(session.app_session(), MigrateToFLX, logger_ptr);
2✔
296

297
    {
2✔
298
        auto realm = Realm::get_shared_realm(config);
2✔
299

300
        REQUIRE(!wait_for_upload(*realm));
2!
301
        REQUIRE(!wait_for_download(*realm));
2!
302

303
        auto table = realm->read_group().get_table("class_Object");
2✔
304
        REQUIRE(table->size() == 5);
2!
305
    }
2✔
306

307
    // Roll back to PBS
308
    trigger_server_migration(session.app_session(), RollbackToPBS, logger_ptr);
2✔
309

310
    {
2✔
311
        auto realm = Realm::get_shared_realm(config);
2✔
312

313
        REQUIRE(!wait_for_upload(*realm));
2!
314
        REQUIRE(!wait_for_download(*realm));
2!
315

316
        auto table = realm->read_group().get_table("class_Object");
2✔
317
        REQUIRE(table->size() == 5);
2!
318
    }
2✔
319
}
2✔
320

321
TEST_CASE("Test client migration and rollback with recovery", "[sync][flx][flx migration][baas]") {
2✔
322
    auto logger_ptr = util::Logger::get_default_logger();
2✔
323
    enum TestState { idle, wait_for_merge, merge_complete, rollback_complete };
2✔
324
    TestingStateMachine<TestState> test_state(TestState::idle);
2✔
325

326
    const std::string partition = "migration-test";
2✔
327
    const Schema mig_schema{
2✔
328
        ObjectSchema("Object", {{"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
329
                                {"string_field", PropertyType::String | PropertyType::Nullable}}),
2✔
330
    };
2✔
331
    auto server_app_config = minimal_app_config("server_migrate_rollback", mig_schema);
2✔
332
    TestAppSession session(create_app(server_app_config));
2✔
333
    SyncTestFile config(session.app()->current_user(), partition, server_app_config.schema);
2✔
334
    config.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
335
    config.schema_version = 0;
2✔
336
    config.sync_config->on_sync_client_event_hook = [&](std::weak_ptr<SyncSession>, const SyncClientHookData& data) {
188✔
337
        test_state.transition_with([data](TestState cur_state) -> std::optional<TestState> {
188✔
338
            if (data.event == SyncClientHookEvent::ClientResetMergeComplete &&
188✔
339
                cur_state == TestState::wait_for_merge) {
188✔
340
                return TestState::merge_complete;
2✔
341
            }
2✔
342
            return std::nullopt;
186✔
343
        });
188✔
344
        if (test_state.get() == TestState::merge_complete) {
188✔
345
            // Wait for the FLX->PBS rollback to complete before continuing
346
            test_state.wait_for(TestState::rollback_complete, std::chrono::seconds(25));
2✔
347
        }
2✔
348
        return SyncClientHookAction::NoAction;
188✔
349
    };
188✔
350

351
    // Fill some objects
352
    auto objects = fill_test_data(config); // 5 objects starting at 1 with no partition value set
2✔
353
    // Primary key of the object to recover
354
    auto obj_id = ObjectId::gen();
2✔
355

356
    // Keep this realm around for after the revert to PBS
357
    auto outer_realm = Realm::get_shared_realm(config);
2✔
358
    REQUIRE(!wait_for_upload(*outer_realm));
2!
359
    REQUIRE(!wait_for_download(*outer_realm));
2!
360

361
    // Wait to upload the data
362
    {
2✔
363
        auto table = outer_realm->read_group().get_table("class_Object");
2✔
364
        REQUIRE(table->size() == 5);
2!
365

366
        // Pause the sync session and make a change.
367
        // This will be recovered when it is resumed after the migration.
368
        outer_realm->sync_session()->pause();
2✔
369
        outer_realm->begin_transaction();
2✔
370
        outer_realm->read_group()
2✔
371
            .get_table("class_Object")
2✔
372
            ->create_object_with_primary_key(obj_id)
2✔
373
            .set("string_field", "partition-set-during-sync-upload");
2✔
374
        outer_realm->commit_transaction();
2✔
375
    }
2✔
376

377
    // Migrate to FLX
378
    trigger_server_migration(session.app_session(), MigrateToFLX, logger_ptr);
2✔
379

380
    // Resume the session and verify the additional object was uploaded after the migration
381
    outer_realm->sync_session()->resume();
2✔
382
    REQUIRE(!wait_for_upload(*outer_realm));
2!
383
    REQUIRE(!wait_for_download(*outer_realm));
2!
384

385
    {
2✔
386
        auto sync_session = outer_realm->sync_session();
2✔
387
        REQUIRE(sync_session);
2!
388
        auto sub_store = sync_session->get_flx_subscription_store();
2✔
389
        REQUIRE(sub_store);
2!
390
        auto active_subs = sub_store->get_active();
2✔
391
        REQUIRE(active_subs.size() == 1);
2!
392
        REQUIRE(active_subs.find("flx_migrated_Object"));
2!
393

394
        auto table = outer_realm->read_group().get_table("class_Object");
2✔
395
        REQUIRE(table->size() == 6);
2!
396

397
        auto object_table = outer_realm->read_group().get_table("class_Object");
2✔
398
        auto pending_object = object_table->get_object_with_primary_key(obj_id);
2✔
399
        REQUIRE(pending_object.get<String>("string_field") == "partition-set-during-sync-upload");
2!
400
    }
2✔
401

402
    // Pause the sync session so a pending subscription and object can be created
403
    // before processing the rollback
404
    outer_realm->sync_session()->pause();
2✔
405
    util::Future<sync::SubscriptionSet::State> new_subs_future = [&] {
2✔
406
        auto sub_store = outer_realm->sync_session()->get_flx_subscription_store();
2✔
407
        auto mut_subs = sub_store->get_active().make_mutable_copy();
2✔
408

409
        auto object_table = outer_realm->read_group().get_table("class_Object");
2✔
410
        auto string_col_key = object_table->get_column_key("string_field");
2✔
411
        mut_subs.insert_or_assign("dummy_subs", Query(object_table).equal(string_col_key, StringData{"some-value"}));
2✔
412
        auto new_subs = mut_subs.commit();
2✔
413
        return new_subs.get_state_change_notification(sync::SubscriptionSet::State::Complete);
2✔
414
    }();
2✔
415

416
    // Add a local object while the session is paused. This will be recovered when connecting after the rollback.
417
    {
2✔
418
        outer_realm->begin_transaction();
2✔
419
        outer_realm->read_group()
2✔
420
            .get_table("class_Object")
2✔
421
            ->create_object_with_primary_key(ObjectId::gen())
2✔
422
            .set("string_field", "partition-set-by-pbs");
2✔
423
        outer_realm->commit_transaction();
2✔
424
    }
2✔
425

426
    // Wait for the object to be written to Atlas/MongoDB before rollback, otherwise it may be lost
427
    reset_utils::wait_for_object_to_persist_to_atlas(session.app()->current_user(), session.app_session(), "Object",
2✔
428
                                                     {{"_id", obj_id}});
2✔
429

430
    //  Roll back to PBS
431
    trigger_server_migration(session.app_session(), RollbackToPBS, logger_ptr);
2✔
432

433
    // Connect after rolling back to PBS
434
    outer_realm->sync_session()->resume();
2✔
435
    REQUIRE(!wait_for_upload(*outer_realm));
2!
436
    REQUIRE(!wait_for_download(*outer_realm));
2!
437

438
    {
2✔
439
        auto table = outer_realm->read_group().get_table("class_Object");
2✔
440
        REQUIRE(table->size() == 7);
2!
441

442
        // Verify the internal sync session subscription store has been cleared
443
        auto sync_session = outer_realm->sync_session();
2✔
444
        REQUIRE(sync_session);
2!
445
        auto sub_store = SyncSession::OnlyForTesting::get_subscription_store_base(*sync_session);
2✔
446
        REQUIRE(sub_store);
2!
447
        auto active_subs = sub_store->get_latest();
2✔
448
        REQUIRE(active_subs.size() == 0);
2!
449
        REQUIRE(active_subs.version() == 0);
2!
450

451
        auto result = wait_for_future(std::move(new_subs_future)).get_no_throw();
2✔
452
        REALM_ASSERT(result.is_ok());
2✔
453
        REALM_ASSERT(result.get_value() == sync::SubscriptionSet::State::Superseded);
2✔
454
    }
2✔
455

456
    test_state.transition_to(TestState::wait_for_merge);
2✔
457

458
    //  Migrate back to FLX - and keep the realm session open
459
    trigger_server_migration(session.app_session(), MigrateToFLX, logger_ptr);
2✔
460

461
    // Cancel any connect waits (since sync session is still active) and try to connect now
462
    outer_realm->sync_session()->handle_reconnect();
2✔
463

464
    // wait for the fresh realm to download and merge with the current local realm
465
    test_state.wait_for(TestState::merge_complete, std::chrono::seconds(180));
2✔
466

467
    // Verify data has been sync'ed and there is only 1 subscription for the Object table
468
    {
2✔
469
        auto table = outer_realm->read_group().get_table("class_Object");
2✔
470
        REQUIRE(table->size() == 7);
2!
471
        auto sync_session = outer_realm->sync_session();
2✔
472
        REQUIRE(sync_session);
2!
473
        auto sub_store = sync_session->get_flx_subscription_store();
2✔
474
        REQUIRE(sub_store);
2!
475
        auto active_subs = sub_store->get_active();
2✔
476
        REQUIRE(active_subs.size() == 1);
2!
477
        REQUIRE(active_subs.find("flx_migrated_Object"));
2!
478
    }
2✔
479

480
    // Roll back to PBS once again before the client reset is complete and keep the realm session open
481
    // NOTE: the realm session is blocked in the hook callback until the rollback is complete
482
    trigger_server_migration(session.app_session(), RollbackToPBS, logger_ptr);
2✔
483

484
    // Release the realm session; will reconnect and perform the rollback to PBS client reset
485
    test_state.transition_to(TestState::rollback_complete);
2✔
486

487
    // Cancel any connect waits (since sync session is still active) and try to connect now
488
    outer_realm->sync_session()->handle_reconnect();
2✔
489

490
    // During the rollback client reset, the previous migrate to flx client reset operation is still
491
    // tracked, but will be removed since the new rollback server requests action is incompatible.
492
    REQUIRE(!wait_for_upload(*outer_realm));
2!
493
    REQUIRE(!wait_for_download(*outer_realm));
2!
494

495
    {
2✔
496
        auto table = outer_realm->read_group().get_table("class_Object");
2✔
497
        REQUIRE(table->size() == 7);
2!
498
    }
2✔
499
}
2✔
500

501
TEST_CASE("An interrupted migration or rollback can recover on the next session",
502
          "[sync][flx][flx migration][baas]") {
2✔
503
    auto logger_ptr = util::Logger::get_default_logger();
2✔
504

505
    const std::string partition = "migration-test";
2✔
506
    const Schema mig_schema{
2✔
507
        ObjectSchema("Object", {{"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
508
                                {"string_field", PropertyType::String | PropertyType::Nullable},
2✔
509
                                {"realm_id", PropertyType::String | PropertyType::Nullable}}),
2✔
510
    };
2✔
511
    auto server_app_config = minimal_app_config("server_migrate_rollback", mig_schema);
2✔
512
    TestAppSession session(create_app(server_app_config));
2✔
513
    SyncTestFile config(session.app()->current_user(), partition, server_app_config.schema);
2✔
514
    config.sync_config->client_resync_mode = ClientResyncMode::DiscardLocal;
2✔
515
    config.schema_version = 0;
2✔
516

517
    // Fill some objects
518
    auto objects = fill_test_data(config, partition);
2✔
519

520
    // Wait to upload the data
521
    {
2✔
522
        auto realm = Realm::get_shared_realm(config);
2✔
523

524
        REQUIRE(!wait_for_upload(*realm));
2!
525
        REQUIRE(!wait_for_download(*realm));
2!
526

527
        auto table = realm->read_group().get_table("class_Object");
2✔
528
        CHECK(table->size() == 5);
2!
529
    }
2✔
530

531
    // Migrate to FLX
532
    trigger_server_migration(session.app_session(), MigrateToFLX, logger_ptr);
2✔
533
    enum class State { Initial, FirstError, InClientReset, Resumed, SecondError };
2✔
534
    TestingStateMachine<State> state(State::Initial);
2✔
535

536
    auto error_event_hook = [&config, &state](sync::ProtocolError error) {
4✔
537
        return [&config, &state, error](std::weak_ptr<SyncSession> weak_session,
4✔
538
                                        const SyncClientHookData& data) mutable {
92✔
539
            auto session = weak_session.lock();
92✔
540
            if (!session) {
92✔
NEW
541
                return SyncClientHookAction::NoAction;
×
NEW
542
            }
×
543

544
            if (data.event == SyncClientHookEvent::BindMessageSent &&
92✔
545
                session->path() == _impl::client_reset::get_fresh_path_for(config.path)) {
92✔
546
                bool wait_for_resume = false;
6✔
547
                state.transition_with([&](State cur_state) -> std::optional<State> {
6✔
548
                    if (cur_state == State::FirstError) {
6✔
549
                        wait_for_resume = true;
4✔
550
                        return State::InClientReset;
4✔
551
                    }
4✔
552
                    return std::nullopt;
2✔
553
                });
6✔
554
                if (wait_for_resume) {
6✔
555
                    state.wait_for(State::Resumed);
4✔
556
                }
4✔
557
            }
6✔
558

559
            if (data.event != SyncClientHookEvent::ErrorMessageReceived) {
92✔
560
                return SyncClientHookAction::NoAction;
84✔
561
            }
84✔
562
            if (session->path() != config.path) {
8✔
563
                return SyncClientHookAction::NoAction;
×
564
            }
×
565

566
            auto error_code = sync::ProtocolError(data.error_info->raw_error_code);
8✔
567
            if (error_code == sync::ProtocolError::initial_sync_not_completed) {
8✔
568
                return SyncClientHookAction::NoAction;
×
569
            }
×
570

571
            REQUIRE(error_code == error);
8!
572
            state.transition_with([&](State cur_state) -> std::optional<State> {
8✔
573
                switch (cur_state) {
8✔
574
                    case State::Initial:
4✔
575
                        return State::FirstError;
4✔
576
                    case State::Resumed:
4✔
577
                        return State::SecondError;
4✔
578
                    default:
✔
579
                        FAIL(util::format("Unxpected state %1", static_cast<int>(cur_state)));
×
580
                }
8✔
581
                return std::nullopt;
×
582
            });
8✔
583
            return SyncClientHookAction::NoAction;
8✔
584
        };
8✔
585
    };
4✔
586

587
    // Session is interrupted before the migration is completed.
588
    {
2✔
589
        config.sync_config->on_sync_client_event_hook = error_event_hook(sync::ProtocolError::migrate_to_flx);
2✔
590
        auto realm = Realm::get_shared_realm(config);
2✔
591

592
        state.wait_for(State::InClientReset);
2✔
593

594
        // Pause then resume the session. This triggers the server to send a new client reset request.
595
        realm->sync_session()->pause();
2✔
596
        realm->sync_session()->resume();
2✔
597
        state.transition_with([&](State cur_state) {
2✔
598
            REQUIRE(cur_state == State::InClientReset);
2!
599
            return State::Resumed;
2✔
600
        });
2✔
601

602
        state.wait_for(State::SecondError);
2✔
603
        REQUIRE(!wait_for_upload(*realm));
2!
604
        REQUIRE(!wait_for_download(*realm));
2!
605

606
        auto table = realm->read_group().get_table("class_Object");
2✔
607
        CHECK(table->size() == 5);
2!
608
    }
2✔
609

610
    state.transition_with([](State) {
2✔
611
        return State::Initial;
2✔
612
    });
2✔
613
    //  Roll back to PBS
614
    trigger_server_migration(session.app_session(), RollbackToPBS, logger_ptr);
2✔
615

616

617
    // Session is interrupted before the rollback is completed.
618
    {
2✔
619
        config.sync_config->on_sync_client_event_hook = error_event_hook(sync::ProtocolError::revert_to_pbs);
2✔
620
        auto realm = Realm::get_shared_realm(config);
2✔
621

622
        state.wait_for(State::InClientReset);
2✔
623

624
        // Pause then resume the session. This triggers the server to send a new client reset request.
625
        realm->sync_session()->pause();
2✔
626
        realm->sync_session()->resume();
2✔
627
        state.transition_with([&](State cur_state) {
2✔
628
            REQUIRE(cur_state == State::InClientReset);
2!
629
            return State::Resumed;
2✔
630
        });
2✔
631

632
        state.wait_for(State::SecondError);
2✔
633
        REQUIRE(!wait_for_upload(*realm));
2!
634
        REQUIRE(!wait_for_download(*realm));
2!
635

636
        auto table = realm->read_group().get_table("class_Object");
2✔
637
        CHECK(table->size() == 5);
2!
638
    }
2✔
639
}
2✔
640

641
TEST_CASE("Update to native FLX after migration", "[sync][flx][flx migration][baas]") {
2✔
642
    auto logger_ptr = util::Logger::get_default_logger();
2✔
643

644
    const std::string partition = "migration-test";
2✔
645
    const Schema mig_schema{
2✔
646
        ObjectSchema("Object", {{"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
647
                                {"string_field", PropertyType::String | PropertyType::Nullable},
2✔
648
                                {"realm_id", PropertyType::String | PropertyType::Nullable}}),
2✔
649
    };
2✔
650
    auto server_app_config = minimal_app_config("server_migrate_rollback", mig_schema);
2✔
651
    TestAppSession session(create_app(server_app_config));
2✔
652
    SyncTestFile config(session.app()->current_user(), partition, server_app_config.schema);
2✔
653
    config.sync_config->client_resync_mode = ClientResyncMode::DiscardLocal;
2✔
654
    config.schema_version = 0;
2✔
655

656
    // Fill some objects
657
    auto objects = fill_test_data(config, partition); // 5 objects starting at 1
2✔
658

659
    // Wait to upload the data
660
    {
2✔
661
        auto realm = Realm::get_shared_realm(config);
2✔
662

663
        REQUIRE(!wait_for_upload(*realm));
2!
664
        REQUIRE(!wait_for_download(*realm));
2!
665

666
        auto table = realm->read_group().get_table("class_Object");
2✔
667
        CHECK(table->size() == 5);
2!
668
    }
2✔
669

670
    // Migrate to FLX
671
    trigger_server_migration(session.app_session(), MigrateToFLX, logger_ptr);
2✔
672

673
    {
2✔
674
        auto realm = Realm::get_shared_realm(config);
2✔
675

676
        REQUIRE(!wait_for_upload(*realm));
2!
677
        REQUIRE(!wait_for_download(*realm));
2!
678

679
        auto table = realm->read_group().get_table("class_Object");
2✔
680
        CHECK(table->size() == 5);
2!
681

682
        // Close the sync session and make a change. This will be recovered by the migration.
683
        realm->sync_session()->force_close();
2✔
684
        realm->begin_transaction();
2✔
685
        realm->read_group()
2✔
686
            .get_table("class_Object")
2✔
687
            ->create_object_with_primary_key(ObjectId::gen())
2✔
688
            .set("string_field", "flx_migration_object");
2✔
689
        realm->commit_transaction();
2✔
690
    }
2✔
691

692
    // Update to native FLX
693
    {
2✔
694
        SyncTestFile flx_config(session.app()->current_user(), server_app_config.schema,
2✔
695
                                SyncConfig::FLXSyncEnabled{});
2✔
696
        flx_config.path = config.path;
2✔
697

698
        auto realm = Realm::get_shared_realm(flx_config);
2✔
699

700
        realm->begin_transaction();
2✔
701
        realm->read_group()
2✔
702
            .get_table("class_Object")
2✔
703
            ->create_object_with_primary_key(ObjectId::gen())
2✔
704
            .set("realm_id", partition)
2✔
705
            .set("string_field", "flx_native_object");
2✔
706
        ;
2✔
707
        realm->commit_transaction();
2✔
708

709
        REQUIRE(!wait_for_upload(*realm));
2!
710
        REQUIRE(!wait_for_download(*realm));
2!
711

712
        auto table = realm->read_group().get_table("class_Object");
2✔
713
        CHECK(table->size() == 7);
2!
714
    }
2✔
715

716
    // Open a new realm and check all data is sync'ed.
717
    {
2✔
718
        SyncTestFile flx_config(session.app()->current_user(), server_app_config.schema,
2✔
719
                                SyncConfig::FLXSyncEnabled{});
2✔
720

721
        auto flx_realm = Realm::get_shared_realm(flx_config);
2✔
722

723
        auto flx_table = flx_realm->read_group().get_table("class_Object");
2✔
724
        auto mut_subs = flx_realm->get_latest_subscription_set().make_mutable_copy();
2✔
725
        auto partition_col_key = flx_table->get_column_key("realm_id");
2✔
726
        mut_subs.insert_or_assign("flx_migrated_Object",
2✔
727
                                  Query(flx_table).equal(partition_col_key, StringData{partition}));
2✔
728
        auto subs = std::move(mut_subs).commit();
2✔
729
        subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
730

731
        flx_realm->refresh();
2✔
732
        auto table = flx_realm->read_group().get_table("class_Object");
2✔
733
        CHECK(table->size() == 7);
2!
734
    }
2✔
735

736
    //  Roll back to PBS
737
    trigger_server_migration(session.app_session(), RollbackToPBS, logger_ptr);
2✔
738

739
    // Connect again as native FLX: server replies with SwitchToPBS
740
    {
2✔
741
        SyncTestFile flx_config(session.app()->current_user(), server_app_config.schema,
2✔
742
                                SyncConfig::FLXSyncEnabled{});
2✔
743
        flx_config.path = config.path;
2✔
744

745
        auto [err_promise, err_future] = util::make_promise_future<SyncError>();
2✔
746
        util::CopyablePromiseHolder promise(std::move(err_promise));
2✔
747
        flx_config.sync_config->error_handler =
2✔
748
            [&logger_ptr, error_promise = std::move(promise)](std::shared_ptr<SyncSession>, SyncError err) mutable {
2✔
749
                // This situation should return the switch_to_pbs error
750
                logger_ptr->error("Server rolled back - connect as FLX received error: %1", err.status);
2✔
751
                error_promise.get_promise().emplace_value(std::move(err));
2✔
752
            };
2✔
753
        auto flx_realm = Realm::get_shared_realm(flx_config);
2✔
754
        auto err = wait_for_future(std::move(err_future), std::chrono::seconds(30)).get();
2✔
755
        REQUIRE(err.status == ErrorCodes::WrongSyncType);
2!
756
        REQUIRE(err.server_requests_action == sync::ProtocolErrorInfo::Action::ApplicationBug);
2!
757
    }
2✔
758
}
2✔
759

760
TEST_CASE("New table is synced after migration", "[sync][flx][flx migration][baas]") {
2✔
761
    auto logger_ptr = util::Logger::get_default_logger();
2✔
762

763
    const std::string partition = "migration-test";
2✔
764
    const auto obj1_schema = ObjectSchema("Object", {{"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
765
                                                     {"string_field", PropertyType::String | PropertyType::Nullable},
2✔
766
                                                     {"realm_id", PropertyType::String | PropertyType::Nullable}});
2✔
767
    const auto obj2_schema = ObjectSchema("Object2", {{"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
768
                                                      {"realm_id", PropertyType::String | PropertyType::Nullable}});
2✔
769
    const Schema mig_schema{obj1_schema};
2✔
770
    const Schema two_obj_schema{obj1_schema, obj2_schema};
2✔
771
    auto server_app_config = minimal_app_config("server_migrate_rollback", two_obj_schema);
2✔
772
    TestAppSession session(create_app(server_app_config));
2✔
773
    SyncTestFile config(session.app()->current_user(), partition, server_app_config.schema);
2✔
774
    config.sync_config->client_resync_mode = ClientResyncMode::DiscardLocal;
2✔
775
    config.schema_version = 0;
2✔
776

777
    // Fill some objects
778
    auto objects = fill_test_data(config, partition); // 5 objects starting at 1
2✔
779

780
    // Wait to upload the data
781
    {
2✔
782
        auto realm = Realm::get_shared_realm(config);
2✔
783

784
        REQUIRE(!wait_for_upload(*realm));
2!
785
        REQUIRE(!wait_for_download(*realm));
2!
786

787
        auto table = realm->read_group().get_table("class_Object");
2✔
788
        CHECK(table->size() == 5);
2!
789
    }
2✔
790

791
    // Migrate to FLX
792
    trigger_server_migration(session.app_session(), MigrateToFLX, logger_ptr);
2✔
793

794
    {
2✔
795
        auto realm = Realm::get_shared_realm(config);
2✔
796

797
        REQUIRE(!wait_for_upload(*realm));
2!
798
        REQUIRE(!wait_for_download(*realm));
2!
799

800
        auto table = realm->read_group().get_table("class_Object");
2✔
801
        CHECK(table->size() == 5);
2!
802
    }
2✔
803

804
    // Open a new realm with an additional table.
805
    {
2✔
806
        SyncTestFile flx_config(session.app()->current_user(), two_obj_schema, SyncConfig::FLXSyncEnabled{});
2✔
807

808
        auto flx_realm = Realm::get_shared_realm(flx_config);
2✔
809

810
        // Create a subscription for the new table.
811
        auto table = flx_realm->read_group().get_table("class_Object2");
2✔
812
        auto mut_subs = flx_realm->get_latest_subscription_set().make_mutable_copy();
2✔
813
        mut_subs.insert_or_assign(Query(table).equal(table->get_column_key("realm_id"), StringData{partition}));
2✔
814

815
        auto subs = std::move(mut_subs).commit();
2✔
816
        subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
817

818
        wait_for_upload(*flx_realm);
2✔
819

820
        // Create one object of the new table type.
821
        flx_realm->begin_transaction();
2✔
822
        flx_realm->read_group()
2✔
823
            .get_table("class_Object2")
2✔
824
            ->create_object_with_primary_key(ObjectId::gen())
2✔
825
            .set("realm_id", partition);
2✔
826
        flx_realm->commit_transaction();
2✔
827

828
        wait_for_upload(*flx_realm);
2✔
829
        wait_for_download(*flx_realm);
2✔
830
    }
2✔
831

832
    // Open the migrated realm and sync the new table and data.
833
    {
2✔
834
        auto realm = Realm::get_shared_realm(config);
2✔
835

836
        REQUIRE(!wait_for_upload(*realm));
2!
837
        REQUIRE(!wait_for_download(*realm));
2!
838

839
        auto table = realm->read_group().get_table("class_Object");
2✔
840
        CHECK(table->size() == 5);
2!
841
        auto table2 = realm->read_group().get_table("class_Object2");
2✔
842
        CHECK(table2->size() == 1);
2!
843
        auto sync_session = realm->sync_session();
2✔
844
        REQUIRE(sync_session);
2!
845
        auto sub_store = sync_session->get_flx_subscription_store();
2✔
846
        REQUIRE(sub_store);
2!
847
        auto active_subs = sub_store->get_active();
2✔
848
        REQUIRE(active_subs.size() == 2);
2!
849
        REQUIRE(active_subs.find("flx_migrated_Object2"));
2!
850
    }
2✔
851
}
2✔
852

853
// There is a sequence of events where we tried to open a frozen Realm with new
854
// object types in the schema and this fails schema validation causing client reset
855
// to fail.
856
//   - Add a new class to the schema, but use async open to initiate
857
//     sync without any schema
858
//   - Have the server send a client reset.
859
//   - The client tries to populate the notify_before callback with a frozen Realm using
860
//     the schema with the new class, but the class is not stored on disk yet.
861
// This hits the update_schema() check that makes sure that the frozen Realm's schema is
862
// a subset of the one found on disk. Since it is not, a schema exception is thrown
863
// which is eventually forwarded to the sync error handler and client reset fails.
864
TEST_CASE("Async open + client reset", "[sync][flx][flx migration][baas]") {
4✔
865
    auto logger_ptr = util::Logger::get_default_logger();
4✔
866

867
    const std::string partition = "async-open-migration-test";
4✔
868
    ObjectSchema shared_object("Object", {{"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
4✔
869
                                          {"string_field", PropertyType::String | PropertyType::Nullable},
4✔
870
                                          {"realm_id", PropertyType::String | PropertyType::Nullable}});
4✔
871
    const Schema mig_schema{shared_object};
4✔
872
    size_t num_before_reset_notifications = 0;
4✔
873
    size_t num_after_reset_notifications = 0;
4✔
874
    auto server_app_config = minimal_app_config("async_open_during_migration", mig_schema);
4✔
875
    server_app_config.dev_mode_enabled = true;
4✔
876
    std::optional<SyncTestFile> config; // destruct this after the sessions are torn down
4✔
877
    TestAppSession session(create_app(server_app_config));
4✔
878
    config.emplace(session.app()->current_user(), partition, server_app_config.schema);
4✔
879
    config->sync_config->client_resync_mode = ClientResyncMode::Recover;
4✔
880
    config->sync_config->notify_before_client_reset = [&](SharedRealm before) {
4✔
881
        logger_ptr->debug("notify_before_client_reset");
4✔
882
        REQUIRE(before);
4!
883
        REQUIRE(before->is_frozen());
4!
884
        auto table = before->read_group().get_table("class_Object");
4✔
885
        CHECK(table);
4!
886
        ++num_before_reset_notifications;
4✔
887
    };
4✔
888
    config->sync_config->notify_after_client_reset = [&](SharedRealm before, ThreadSafeReference after_ref,
4✔
889
                                                         bool did_recover) {
4✔
890
        logger_ptr->debug("notify_after_client_reset");
4✔
891
        CHECK(did_recover);
4!
892
        REQUIRE(before);
4!
893
        auto table_before = before->read_group().get_table("class_Object");
4✔
894
        CHECK(table_before);
4!
895
        SharedRealm after = Realm::get_shared_realm(std::move(after_ref), util::Scheduler::make_dummy());
4✔
896
        REQUIRE(after);
4!
897
        auto table_after = after->read_group().get_table("class_Object");
4✔
898
        REQUIRE(table_after);
4!
899
        ++num_after_reset_notifications;
4✔
900
    };
4✔
901
    config->schema_version = 0;
4✔
902

903
    ObjectSchema locally_added("LocallyAdded", {{"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
4✔
904
                                                {"string_field", PropertyType::String | PropertyType::Nullable},
4✔
905
                                                {"realm_id", PropertyType::String | PropertyType::Nullable}});
4✔
906

907
    SECTION("no initial state") {
4✔
908
        // Migrate to FLX
909
        trigger_server_migration(session.app_session(), MigrateToFLX, logger_ptr);
2✔
910
        shared_object.persisted_properties.push_back({"oid_field", PropertyType::ObjectId | PropertyType::Nullable});
2✔
911
        config->schema = {shared_object, locally_added};
2✔
912

913
        auto realm = successfully_async_open_realm(*config);
2✔
914

915
        auto table = realm->read_group().get_table("class_Object");
2✔
916
        REQUIRE(table->size() == 0);
2!
917
        REQUIRE(num_before_reset_notifications == 1);
2!
918
        REQUIRE(num_after_reset_notifications == 1);
2!
919

920
        auto locally_added_table = realm->read_group().get_table("class_LocallyAdded");
2✔
921
        REQUIRE(locally_added_table);
2!
922
        REQUIRE(locally_added_table->size() == 0);
2!
923
    }
2✔
924

925
    SECTION("initial state") {
4✔
926
        {
2✔
927
            config->schema = {shared_object};
2✔
928
            auto realm = Realm::get_shared_realm(*config);
2✔
929
            realm->begin_transaction();
2✔
930
            auto table = realm->read_group().get_table("class_Object");
2✔
931
            table->create_object_with_primary_key(ObjectId::gen());
2✔
932
            realm->commit_transaction();
2✔
933
            wait_for_upload(*realm);
2✔
934
        }
2✔
935
        trigger_server_migration(session.app_session(), MigrateToFLX, logger_ptr);
2✔
936
        {
2✔
937
            shared_object.persisted_properties.push_back(
2✔
938
                {"oid_field", PropertyType::ObjectId | PropertyType::Nullable});
2✔
939
            config->schema = {shared_object, locally_added};
2✔
940

941
            auto realm = successfully_async_open_realm(*config);
2✔
942

943
            auto table = realm->read_group().get_table("class_Object");
2✔
944
            REQUIRE(table->size() == 1);
2!
945
            REQUIRE(num_before_reset_notifications == 1);
2!
946
            REQUIRE(num_after_reset_notifications == 1);
2!
947

948
            auto locally_added_table = realm->read_group().get_table("class_LocallyAdded");
2✔
949
            REQUIRE(locally_added_table);
2!
950
            REQUIRE(locally_added_table->size() == 0);
2!
951
        }
2✔
952
    }
2✔
953
}
4✔
954

955
#endif // REALM_ENABLE_AUTH_TESTS
956
#endif // REALM_ENABLE_SYNC
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc