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

realm / realm-core / 1892

05 Dec 2023 07:16PM UTC coverage: 91.692% (+0.02%) from 91.674%
1892

push

Evergreen

web-flow
Merge pull request #7161 from realm/tg/in-place-client-reset

Rewrite the local changesets in-place for client reset recovery

92346 of 169330 branches covered (0.0%)

835 of 865 new or added lines in 17 files covered. (96.53%)

103 existing lines in 16 files now uncovered.

231991 of 253012 relevant lines covered (91.69%)

6443355.21 hits per line

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

98.46
/test/object-store/sync/client_reset.cpp
1
////////////////////////////////////////////////////////////////////////////
2
//
3
// Copyright 2021 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 <collection_fixtures.hpp>
20
#include <util/event_loop.hpp>
21
#include <util/index_helpers.hpp>
22
#include <util/test_file.hpp>
23
#include <util/test_utils.hpp>
24
#include <util/sync/baas_admin_api.hpp>
25
#include <util/sync/sync_test_utils.hpp>
26

27
#include <realm/object-store/thread_safe_reference.hpp>
28
#include <realm/object-store/util/scheduler.hpp>
29
#include <realm/object-store/impl/object_accessor_impl.hpp>
30
#include <realm/object-store/impl/realm_coordinator.hpp>
31
#include <realm/object-store/property.hpp>
32
#include <realm/object-store/sync/app.hpp>
33
#include <realm/object-store/sync/app_credentials.hpp>
34
#include <realm/object-store/sync/async_open_task.hpp>
35
#include <realm/object-store/sync/sync_session.hpp>
36

37
#include <realm/sync/noinst/client_reset.hpp>
38
#include <realm/sync/noinst/client_reset_operation.hpp>
39
#include <realm/sync/noinst/client_history_impl.hpp>
40
#include <realm/sync/network/websocket.hpp>
41

42
#include <realm/util/flat_map.hpp>
43
#include <realm/util/overload.hpp>
44

45
#include <catch2/catch_all.hpp>
46

47
#include <external/mpark/variant.hpp>
48

49
#include <algorithm>
50
#include <iostream>
51

52
struct ThreadSafeSyncError {
53
    void operator=(const realm::SyncError& e)
54
    {
26✔
55
        std::lock_guard<std::mutex> lock(m_mutex);
26✔
56
        m_error = e;
26✔
57
    }
26✔
58
    operator bool() const
59
    {
3,503✔
60
        std::lock_guard<std::mutex> lock(m_mutex);
3,503✔
61
        return bool(m_error);
3,503✔
62
    }
3,503✔
63
    realm::util::Optional<realm::SyncError> value() const
64
    {
24✔
65
        std::lock_guard<std::mutex> lock(m_mutex);
24✔
66
        return m_error;
24✔
67
    }
24✔
68

69
private:
70
    mutable std::mutex m_mutex;
71
    realm::util::Optional<realm::SyncError> m_error;
72
};
73

74
namespace Catch {
75
template <>
76
struct StringMaker<ThreadSafeSyncError> {
77
    static std::string convert(const ThreadSafeSyncError& err)
78
    {
×
79
        auto value = err.value();
×
80
        if (!value) {
×
81
            return "No SyncError";
×
82
        }
×
83
        return realm::util::format("SyncError(%1), is_fatal: %2, with message: '%3'", value->status.code_string(),
×
84
                                   value->is_fatal, value->status.reason());
×
85
    }
×
86
};
87
} // namespace Catch
88

89
using namespace realm;
90

91
namespace realm {
92
class TestHelper {
93
public:
94
    static DBRef& get_db(SharedRealm const& shared_realm)
95
    {
96
        return Realm::Internal::get_db(*shared_realm);
97
    }
98
};
99
} // namespace realm
100

101
namespace {
102
TableRef get_table(Realm& realm, StringData object_type)
103
{
18,500✔
104
    return ObjectStore::table_for_object_type(realm.read_group(), object_type);
18,500✔
105
}
18,500✔
106
} // anonymous namespace
107

108
#if REALM_ENABLE_AUTH_TESTS
109

110
namespace cf = realm::collection_fixtures;
111
using reset_utils::create_object;
112

113
TEST_CASE("sync: large reset with recovery is restartable", "[sync][pbs][client reset][baas]") {
2✔
114
    const reset_utils::Partition partition{"realm_id", random_string(20)};
2✔
115
    Property partition_prop = {partition.property_name, PropertyType::String | PropertyType::Nullable};
2✔
116
    Schema schema{
2✔
117
        {"object",
2✔
118
         {
2✔
119
             {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
120
             {"value", PropertyType::String},
2✔
121
             partition_prop,
2✔
122
         }},
2✔
123
    };
2✔
124

1✔
125
    auto server_app_config = minimal_app_config("client_reset_tests", schema);
2✔
126
    server_app_config.partition_key = partition_prop;
2✔
127
    TestAppSession test_app_session(create_app(server_app_config));
2✔
128
    auto app = test_app_session.app();
2✔
129

1✔
130
    create_user_and_log_in(app);
2✔
131
    SyncTestFile realm_config(app->current_user(), partition.value, schema);
2✔
132
    realm_config.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
133
    realm_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError err) {
1✔
134
        if (err.status == ErrorCodes::ConnectionClosed) {
×
135
            return;
×
136
        }
×
137

138
        if (err.server_requests_action == sync::ProtocolErrorInfo::Action::Warning ||
×
139
            err.server_requests_action == sync::ProtocolErrorInfo::Action::Transient) {
×
140
            return;
×
141
        }
×
142

143
        FAIL(util::format("got error from server: %1", err.status));
×
144
    };
×
145

1✔
146
    auto realm = Realm::get_shared_realm(realm_config);
2✔
147
    std::vector<ObjectId> expected_obj_ids;
2✔
148
    {
2✔
149
        auto obj_id = ObjectId::gen();
2✔
150
        expected_obj_ids.push_back(obj_id);
2✔
151
        realm->begin_transaction();
2✔
152
        CppContext c(realm);
2✔
153
        Object::create(c, realm, "object",
2✔
154
                       std::any(AnyDict{{"_id", obj_id},
2✔
155
                                        {"value", std::string{"hello world"}},
2✔
156
                                        {partition.property_name, partition.value}}));
2✔
157
        realm->commit_transaction();
2✔
158
        wait_for_upload(*realm);
2✔
159
        reset_utils::wait_for_object_to_persist_to_atlas(app->current_user(), test_app_session.app_session(),
2✔
160
                                                         "object", {{"_id", obj_id}});
2✔
161
        realm->sync_session()->pause();
2✔
162
    }
2✔
163

1✔
164
    reset_utils::trigger_client_reset(test_app_session.app_session(), realm);
2✔
165
    {
2✔
166
        SyncTestFile realm_config(app->current_user(), partition.value, schema);
2✔
167
        auto second_realm = Realm::get_shared_realm(realm_config);
2✔
168

1✔
169
        second_realm->begin_transaction();
2✔
170
        CppContext c(second_realm);
2✔
171
        for (size_t i = 0; i < 100; ++i) {
202✔
172
            auto obj_id = ObjectId::gen();
200✔
173
            expected_obj_ids.push_back(obj_id);
200✔
174
            Object::create(c, second_realm, "object",
200✔
175
                           std::any(AnyDict{{"_id", obj_id},
200✔
176
                                            {"value", random_string(1024 * 128)},
200✔
177
                                            {partition.property_name, partition.value}}));
200✔
178
        }
200✔
179
        second_realm->commit_transaction();
2✔
180

1✔
181
        wait_for_upload(*second_realm);
2✔
182
    }
2✔
183

1✔
184
    realm->sync_session()->resume();
2✔
185
    timed_wait_for([&] {
25,035✔
186
        return util::File::exists(_impl::client_reset::get_fresh_path_for(realm_config.path));
25,035✔
187
    });
25,035✔
188
    realm->sync_session()->pause();
2✔
189
    realm->sync_session()->resume();
2✔
190
    wait_for_upload(*realm);
2✔
191
    wait_for_download(*realm);
2✔
192

1✔
193
    realm->refresh();
2✔
194
    auto table = realm->read_group().get_table("class_object");
2✔
195
    REQUIRE(table->size() == expected_obj_ids.size());
2!
196
    std::vector<ObjectId> found_object_ids;
2✔
197
    for (const auto& obj : *table) {
202✔
198
        found_object_ids.push_back(obj.get_primary_key().get_object_id());
202✔
199
    }
202✔
200

1✔
201
    std::stable_sort(expected_obj_ids.begin(), expected_obj_ids.end());
2✔
202
    std::stable_sort(found_object_ids.begin(), found_object_ids.end());
2✔
203
    REQUIRE(expected_obj_ids == found_object_ids);
2!
204
}
2✔
205

206
TEST_CASE("sync: pending client resets are cleared when downloads are complete", "[sync][pbs][client reset][baas]") {
2✔
207
    const reset_utils::Partition partition{"realm_id", random_string(20)};
2✔
208
    Property partition_prop = {partition.property_name, PropertyType::String | PropertyType::Nullable};
2✔
209
    Schema schema{
2✔
210
        {"object",
2✔
211
         {
2✔
212
             {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
213
             {"value", PropertyType::Int},
2✔
214
             partition_prop,
2✔
215
         }},
2✔
216
    };
2✔
217

1✔
218
    auto server_app_config = minimal_app_config("client_reset_tests", schema);
2✔
219
    server_app_config.partition_key = partition_prop;
2✔
220
    TestAppSession test_app_session(create_app(server_app_config));
2✔
221
    auto app = test_app_session.app();
2✔
222

1✔
223
    create_user_and_log_in(app);
2✔
224
    SyncTestFile realm_config(app->current_user(), partition.value, schema);
2✔
225
    realm_config.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
226
    realm_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError err) {
1✔
227
        if (err.server_requests_action == sync::ProtocolErrorInfo::Action::Warning ||
×
228
            err.server_requests_action == sync::ProtocolErrorInfo::Action::Transient) {
×
229
            return;
×
230
        }
×
231

232
        FAIL(util::format("got error from server: %1", err.status));
×
233
    };
×
234

1✔
235
    auto realm = Realm::get_shared_realm(realm_config);
2✔
236
    auto obj_id = ObjectId::gen();
2✔
237
    {
2✔
238
        realm->begin_transaction();
2✔
239
        CppContext c(realm);
2✔
240
        Object::create(
2✔
241
            c, realm, "object",
2✔
242
            std::any(AnyDict{{"_id", obj_id}, {"value", int64_t(5)}, {partition.property_name, partition.value}}));
2✔
243
        realm->commit_transaction();
2✔
244
        wait_for_upload(*realm);
2✔
245
    }
2✔
246
    wait_for_download(*realm, std::chrono::minutes(10));
2✔
247

1✔
248
    reset_utils::trigger_client_reset(test_app_session.app_session(), realm);
2✔
249

1✔
250
    wait_for_download(*realm, std::chrono::minutes(10));
2✔
251

1✔
252
    reset_utils::trigger_client_reset(test_app_session.app_session(), realm);
2✔
253

1✔
254
    wait_for_download(*realm, std::chrono::minutes(10));
2✔
255
}
2✔
256

257
TEST_CASE("sync: client reset", "[sync][pbs][client reset][baas]") {
84✔
258
    if (!util::EventLoop::has_implementation())
84✔
259
        return;
×
260

42✔
261
    const reset_utils::Partition partition{"realm_id", random_string(20)};
84✔
262
    Property partition_prop = {partition.property_name, PropertyType::String | PropertyType::Nullable};
84✔
263
    Schema schema{
84✔
264
        {"object",
84✔
265
         {
84✔
266
             {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
84✔
267
             {"value", PropertyType::Int},
84✔
268
             partition_prop,
84✔
269
         }},
84✔
270
        {"link target",
84✔
271
         {
84✔
272
             {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
84✔
273
             {"value", PropertyType::Int},
84✔
274
             partition_prop,
84✔
275
         }},
84✔
276
        {"pk link target",
84✔
277
         {
84✔
278
             {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
84✔
279
             {"value", PropertyType::Int},
84✔
280
             partition_prop,
84✔
281
         }},
84✔
282
        {"link origin",
84✔
283
         {
84✔
284
             {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
84✔
285
             {"link", PropertyType::Object | PropertyType::Nullable, "link target"},
84✔
286
             {"pk link", PropertyType::Object | PropertyType::Nullable, "pk link target"},
84✔
287
             {"list", PropertyType::Object | PropertyType::Array, "link target"},
84✔
288
             {"pk list", PropertyType::Object | PropertyType::Array, "pk link target"},
84✔
289
             partition_prop,
84✔
290
         }},
84✔
291
    };
84✔
292
    auto server_app_config = minimal_app_config("client_reset_tests", schema);
84✔
293
    server_app_config.partition_key = partition_prop;
84✔
294
    TestAppSession test_app_session(create_app(server_app_config));
84✔
295
    auto app = test_app_session.app();
84✔
296
    auto get_valid_config = [&]() -> SyncTestFile {
172✔
297
        create_user_and_log_in(app);
172✔
298
        return SyncTestFile(app->current_user(), partition.value, schema);
172✔
299
    };
172✔
300
    SyncTestFile local_config = get_valid_config();
84✔
301
    SyncTestFile remote_config = get_valid_config();
84✔
302
    auto make_reset = [&](Realm::Config config_local,
84✔
303
                          Realm::Config config_remote) -> std::unique_ptr<reset_utils::TestClientReset> {
108✔
304
        return reset_utils::make_baas_client_reset(config_local, config_remote, test_app_session);
108✔
305
    };
108✔
306

42✔
307
    // this is just for ease of debugging
42✔
308
    local_config.path = local_config.path + ".local";
84✔
309
    remote_config.path = remote_config.path + ".remote";
84✔
310

42✔
311
// TODO: remote-baas: This test fails consistently with Windows remote baas server - to be fixed in RCORE-1674
42✔
312
// This may be due to the realm file at `orig_path` not being deleted on Windows since it is still in use.
42✔
313
#ifndef _WIN32
84✔
314
    SECTION("a client reset in manual mode can be handled") {
84✔
315
        std::string orig_path, recovery_path;
2✔
316
        local_config.sync_config->client_resync_mode = ClientResyncMode::Manual;
2✔
317
        ThreadSafeSyncError err;
2✔
318
        local_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) {
2✔
319
            REQUIRE(error.is_client_reset_requested());
2!
320
            REQUIRE(error.user_info.size() >= 2);
2!
321
            auto orig_path_it = error.user_info.find(SyncError::c_original_file_path_key);
2✔
322
            auto recovery_path_it = error.user_info.find(SyncError::c_recovery_file_path_key);
2✔
323
            REQUIRE(orig_path_it != error.user_info.end());
2!
324
            REQUIRE(recovery_path_it != error.user_info.end());
2!
325
            orig_path = orig_path_it->second;
2✔
326
            recovery_path = recovery_path_it->second;
2✔
327
            REQUIRE(util::File::exists(orig_path));
2!
328
            REQUIRE(!util::File::exists(recovery_path));
2!
329
            bool did_reset_files = test_app_session.app()->sync_manager()->immediately_run_file_actions(orig_path);
2✔
330
            REQUIRE(did_reset_files);
2!
331
            REQUIRE(!util::File::exists(orig_path));
2!
332
            REQUIRE(util::File::exists(recovery_path));
2!
333
            err = error;
2✔
334
        };
2✔
335

1✔
336
        make_reset(local_config, remote_config)
2✔
337
            ->on_post_reset([&](SharedRealm) {
2✔
338
                util::EventLoop::main().run_until([&] {
3,448✔
339
                    return bool(err);
3,448✔
340
                });
3,448✔
341
            })
2✔
342
            ->run();
2✔
343

1✔
344
        REQUIRE(err);
2!
345
        SyncError error = *err.value();
2✔
346
        REQUIRE(error.is_client_reset_requested());
2!
347
        REQUIRE(!util::File::exists(orig_path));
2!
348
        REQUIRE(util::File::exists(recovery_path));
2!
349
        local_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError err) {
2✔
350
            CAPTURE(err.status);
×
351
            CAPTURE(local_config.path);
×
352
            FAIL("Error handler should not have been called");
×
353
        };
×
354
        auto post_reset_realm = Realm::get_shared_realm(local_config);
2✔
355
        wait_for_download(*post_reset_realm); // this should now succeed without any sync errors
2✔
356
        REQUIRE(util::File::exists(orig_path));
2!
357
    }
2✔
358
#endif
84✔
359

42✔
360
    local_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError err) {
84✔
361
        CAPTURE(err.status);
×
362
        CAPTURE(local_config.path);
×
363
        FAIL("Error handler should not have been called");
×
364
    };
×
365

42✔
366
    local_config.cache = false;
84✔
367
    local_config.automatic_change_notifications = false;
84✔
368
    const std::string fresh_path = realm::_impl::client_reset::get_fresh_path_for(local_config.path);
84✔
369
    size_t before_callback_invocations = 0;
84✔
370
    size_t after_callback_invocations = 0;
84✔
371
    std::mutex mtx;
84✔
372
    local_config.sync_config->notify_before_client_reset = [&](SharedRealm before) {
73✔
373
        std::lock_guard<std::mutex> lock(mtx);
62✔
374
        ++before_callback_invocations;
62✔
375
        REQUIRE(before);
62!
376
        REQUIRE(before->is_frozen());
62!
377
        REQUIRE(before->read_group().get_table("class_object"));
62!
378
        REQUIRE(before->config().path == local_config.path);
62!
379
        REQUIRE_FALSE(before->schema().empty());
62!
380
        REQUIRE(before->schema_version() != ObjectStore::NotVersioned);
62!
381
        REQUIRE(util::File::exists(local_config.path));
62!
382
    };
62✔
383
    local_config.sync_config->notify_after_client_reset = [&](SharedRealm before, ThreadSafeReference after_ref,
84✔
384
                                                              bool) {
67✔
385
        std::lock_guard<std::mutex> lock(mtx);
50✔
386
        SharedRealm after = Realm::get_shared_realm(std::move(after_ref), util::Scheduler::make_default());
50✔
387
        ++after_callback_invocations;
50✔
388
        REQUIRE(before);
50!
389
        REQUIRE(before->is_frozen());
50!
390
        REQUIRE(before->read_group().get_table("class_object"));
50!
391
        REQUIRE(before->config().path == local_config.path);
50!
392
        REQUIRE(after);
50!
393
        REQUIRE(!after->is_frozen());
50!
394
        REQUIRE(after->read_group().get_table("class_object"));
50!
395
        REQUIRE(after->config().path == local_config.path);
50!
396
        REQUIRE(after->current_transaction_version() > before->current_transaction_version());
50!
397
    };
50✔
398
    auto get_key_for_object_with_value = [&](TableRef table, int64_t value) -> ObjKey {
56✔
399
        REQUIRE(table);
28!
400
        auto target = std::find_if(table->begin(), table->end(), [&](auto& it) -> bool {
50✔
401
            return it.template get<Int>("value") == value;
50✔
402
        });
50✔
403
        if (target == table->end()) {
28✔
404
            return {};
×
405
        }
×
406
        return target->get_key();
28✔
407
    };
28✔
408

42✔
409
    Results results;
84✔
410
    Object object;
84✔
411
    CollectionChangeSet object_changes, results_changes;
84✔
412
    NotificationToken object_token, results_token;
84✔
413
    auto setup_listeners = [&](SharedRealm realm) {
53✔
414
        results = Results(realm, ObjectStore::table_for_object_type(realm->read_group(), "object"))
22✔
415
                      .sort({{{"value", true}}});
22✔
416
        if (results.size() >= 1) {
22✔
417
            REQUIRE(results.get<Obj>(0).get<Int>("value") == 4);
18!
418

9✔
419
            auto obj = results.get<Obj>(0);
18✔
420
            REQUIRE(obj.get<Int>("value") == 4);
18!
421
            object = Object(realm, obj);
18✔
422
            object_token = object.add_notification_callback([&](CollectionChangeSet changes) {
32✔
423
                object_changes = std::move(changes);
32✔
424
            });
32✔
425
        }
18✔
426
        results_token = results.add_notification_callback([&](CollectionChangeSet changes) {
42✔
427
            results_changes = std::move(changes);
42✔
428
        });
42✔
429
    };
22✔
430

42✔
431
    SECTION("recovery") {
84✔
432
        local_config.sync_config->client_resync_mode = ClientResyncMode::Recover;
14✔
433
        std::unique_ptr<reset_utils::TestClientReset> test_reset = make_reset(local_config, remote_config);
14✔
434
        SECTION("modify an existing object") {
14✔
435
            test_reset
2✔
436
                ->on_post_local_changes([&](SharedRealm realm) {
2✔
437
                    setup_listeners(realm);
2✔
438
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
439
                    CHECK(results.size() == 1);
2!
440
                    CHECK(results.get<Obj>(0).get<Int>("value") == 4);
2!
441
                })
2✔
442
                ->on_post_reset([&](SharedRealm realm) {
2✔
443
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
444

1✔
445
                    CHECK(before_callback_invocations == 1);
2!
446
                    CHECK(after_callback_invocations == 1);
2!
447
                    CHECK(results.size() == 1);
2!
448
                    CHECK(results.get<Obj>(0).get<Int>("value") == 4);
2!
449
                    CHECK(object.get_obj().get<Int>("value") == 4);
2!
450
                    REQUIRE_INDICES(results_changes.modifications);
2!
451
                    REQUIRE_INDICES(results_changes.insertions);
2!
452
                    REQUIRE_INDICES(results_changes.deletions);
2!
453
                    REQUIRE_INDICES(object_changes.modifications);
2!
454
                    REQUIRE_INDICES(object_changes.insertions);
2!
455
                    REQUIRE_INDICES(object_changes.deletions);
2!
456
                    // make sure that the reset operation has cleaned up after itself
1✔
457
                    REQUIRE(util::File::exists(local_config.path));
2!
458
                    REQUIRE_FALSE(util::File::exists(fresh_path));
2!
459
                })
2✔
460
                ->run();
2✔
461
        }
2✔
462
        SECTION("modify a deleted object") {
14✔
463
            ObjectId pk = ObjectId::gen();
2✔
464
            test_reset
2✔
465
                ->setup([&](SharedRealm realm) {
2✔
466
                    auto table = get_table(*realm, "object");
2✔
467
                    REQUIRE(table);
2!
468
                    auto obj = create_object(*realm, "object", {pk}, partition);
2✔
469
                    auto col = obj.get_table()->get_column_key("value");
2✔
470
                    obj.set(col, 100);
2✔
471
                })
2✔
472
                ->make_local_changes([&](SharedRealm realm) {
2✔
473
                    auto table = get_table(*realm, "object");
2✔
474
                    REQUIRE(table);
2!
475
                    REQUIRE(table->size() == 2);
2!
476
                    ObjKey key = table->get_objkey_from_primary_key(pk);
2✔
477
                    REQUIRE(key);
2!
478
                    Obj obj = table->get_object(key);
2✔
479
                    obj.set("value", 200);
2✔
480
                })
2✔
481
                ->make_remote_changes([&](SharedRealm remote) {
2✔
482
                    auto table = get_table(*remote, "object");
2✔
483
                    REQUIRE(table);
2!
484
                    REQUIRE(table->size() == 2);
2!
485
                    ObjKey key = table->get_objkey_from_primary_key(pk);
2✔
486
                    REQUIRE(key);
2!
487
                    table->remove_object(key);
2✔
488
                })
2✔
489
                ->on_post_local_changes([&](SharedRealm realm) {
2✔
490
                    setup_listeners(realm);
2✔
491
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
492
                    CHECK(results.size() == 2);
2!
493
                    CHECK(results.get<Obj>(0).get<Int>("value") == 4);
2!
494
                    CHECK(results.get<Obj>(1).get<Int>("value") == 200);
2!
495
                })
2✔
496
                ->on_post_reset([&](SharedRealm realm) {
2✔
497
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
498
                    CHECK(before_callback_invocations == 1);
2!
499
                    CHECK(after_callback_invocations == 1);
2!
500
                    CHECK(results.size() == 1);
2!
501
                    CHECK(results.get<Obj>(0).get<Int>("value") == 4);
2!
502
                    CHECK(object.get_obj().get<Int>("value") == 4);
2!
503
                    REQUIRE_INDICES(results_changes.modifications);
2!
504
                    REQUIRE_INDICES(results_changes.insertions);
2!
505
                    REQUIRE_INDICES(results_changes.deletions, 1); // the deletion "wins"
2!
506
                    REQUIRE_INDICES(object_changes.modifications);
2!
507
                    REQUIRE_INDICES(object_changes.insertions);
2!
508
                    REQUIRE_INDICES(object_changes.deletions);
2!
509
                    // make sure that the reset operation has cleaned up after itself
1✔
510
                    REQUIRE(util::File::exists(local_config.path));
2!
511
                    REQUIRE_FALSE(util::File::exists(fresh_path));
2!
512
                })
2✔
513
                ->run();
2✔
514
        }
2✔
515
        SECTION("insert") {
14✔
516
            int64_t new_value = 42;
2✔
517
            test_reset
2✔
518
                ->make_local_changes([&](SharedRealm realm) {
2✔
519
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
520
                    auto table = get_table(*realm, "object");
2✔
521
                    REQUIRE(table);
2!
522
                    REQUIRE(table->size() == 1);
2!
523
                    ObjectId different_pk = ObjectId::gen();
2✔
524
                    auto obj = create_object(*realm, "object", {different_pk}, partition);
2✔
525
                    auto col = obj.get_table()->get_column_key("value");
2✔
526
                    obj.set(col, new_value);
2✔
527
                })
2✔
528
                ->on_post_local_changes([&](SharedRealm realm) {
2✔
529
                    setup_listeners(realm);
2✔
530
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
531
                    CHECK(results.size() == 2);
2!
532
                })
2✔
533
                ->on_post_reset([&](SharedRealm realm) {
2✔
534
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
535
                    CHECK(before_callback_invocations == 1);
2!
536
                    CHECK(after_callback_invocations == 1);
2!
537
                    CHECK(results.size() == 2);
2!
538
                    CHECK(results.get<Obj>(0).get<Int>("value") == 4);
2!
539
                    CHECK(results.get<Obj>(1).get<Int>("value") == new_value);
2!
540
                    CHECK(object.get_obj().get<Int>("value") == 4);
2!
541
                    REQUIRE_INDICES(results_changes.modifications);
2!
542
                    REQUIRE_INDICES(results_changes.insertions);
2!
543
                    REQUIRE_INDICES(results_changes.deletions);
2!
544
                    REQUIRE_INDICES(object_changes.modifications);
2!
545
                    REQUIRE_INDICES(object_changes.insertions);
2!
546
                    REQUIRE_INDICES(object_changes.deletions);
2!
547
                    // make sure that the reset operation has cleaned up after itself
1✔
548
                    REQUIRE(util::File::exists(local_config.path));
2!
549
                    REQUIRE_FALSE(util::File::exists(fresh_path));
2!
550
                })
2✔
551
                ->run();
2✔
552
        }
2✔
553

7✔
554
        SECTION("delete") {
14✔
555
            test_reset
2✔
556
                ->make_local_changes([&](SharedRealm local) {
2✔
557
                    auto table = get_table(*local, "object");
2✔
558
                    REQUIRE(table);
2!
559
                    REQUIRE(table->size() == 1);
2!
560
                    table->clear();
2✔
561
                    REQUIRE(table->size() == 0);
2!
562
                })
2✔
563
                ->on_post_local_changes([&](SharedRealm realm) {
2✔
564
                    setup_listeners(realm);
2✔
565
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
566
                    CHECK(results.size() == 0);
2!
567
                })
2✔
568
                ->on_post_reset([&](SharedRealm realm) {
2✔
569
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
570
                    CHECK(results.size() == 0);
2!
571
                    CHECK(!object.is_valid());
2!
572
                    REQUIRE_INDICES(results_changes.modifications);
2!
573
                    REQUIRE_INDICES(results_changes.insertions);
2!
574
                    REQUIRE_INDICES(results_changes.deletions);
2!
575
                })
2✔
576
                ->run();
2✔
577
        }
2✔
578

7✔
579
        SECTION("Simultaneous compatible schema changes are allowed") {
14✔
580
            const std::string new_table_name = "same new table name";
2✔
581
            const std::string existing_table_name = "preexisting table name";
2✔
582
            const std::string locally_added_table_name = "locally added table";
2✔
583
            const std::string remotely_added_table_name = "remotely added table";
2✔
584
            const Property pk_id = {"_id", PropertyType::ObjectId | PropertyType::Nullable,
2✔
585
                                    Property::IsPrimary{true}};
2✔
586
            const Property shared_added_property = {"added identical property",
2✔
587
                                                    PropertyType::UUID | PropertyType::Nullable};
2✔
588
            const Property locally_added_property = {"locally added property", PropertyType::ObjectId};
2✔
589
            const Property remotely_added_property = {"remotely added property",
2✔
590
                                                      PropertyType::Float | PropertyType::Nullable};
2✔
591
            ObjectId pk1 = ObjectId::gen();
2✔
592
            ObjectId pk2 = ObjectId::gen();
2✔
593
            auto verify_changes = [&](SharedRealm realm) {
4✔
594
                REQUIRE_NOTHROW(advance_and_notify(*realm));
4✔
595
                std::vector<std::string> tables_to_check = {existing_table_name, new_table_name,
4✔
596
                                                            locally_added_table_name, remotely_added_table_name};
4✔
597
                for (auto& table_name : tables_to_check) {
16✔
598
                    CAPTURE(table_name);
16✔
599
                    auto table = get_table(*realm, table_name);
16✔
600
                    REQUIRE(table);
16!
601
                    REQUIRE(table->get_column_key(shared_added_property.name));
16!
602
                    REQUIRE(table->get_column_key(locally_added_property.name));
16!
603
                    REQUIRE(table->get_column_key(remotely_added_property.name));
16!
604
                    auto sorted_results = table->get_sorted_view(table->get_column_key(pk_id.name));
16✔
605
                    REQUIRE(sorted_results.size() == 2);
16!
606
                    REQUIRE(sorted_results.get_object(0).get_primary_key().get_object_id() == pk1);
16!
607
                    REQUIRE(sorted_results.get_object(1).get_primary_key().get_object_id() == pk2);
16!
608
                }
16✔
609
            };
4✔
610
            make_reset(local_config, remote_config)
2✔
611
                ->set_development_mode(true)
2✔
612
                ->setup([&](SharedRealm before) {
2✔
613
                    before->update_schema(
2✔
614
                        {
2✔
615
                            {existing_table_name,
2✔
616
                             {
2✔
617
                                 pk_id,
2✔
618
                                 partition_prop,
2✔
619
                             }},
2✔
620
                        },
2✔
621
                        0, nullptr, nullptr, true);
2✔
622
                })
2✔
623
                ->make_local_changes([&](SharedRealm local) {
2✔
624
                    local->update_schema(
2✔
625
                        {
2✔
626
                            {new_table_name,
2✔
627
                             {
2✔
628
                                 pk_id,
2✔
629
                                 partition_prop,
2✔
630
                                 locally_added_property,
2✔
631
                                 shared_added_property,
2✔
632
                             }},
2✔
633
                            {existing_table_name,
2✔
634
                             {
2✔
635
                                 pk_id,
2✔
636
                                 partition_prop,
2✔
637
                                 locally_added_property,
2✔
638
                                 shared_added_property,
2✔
639
                             }},
2✔
640
                            {locally_added_table_name,
2✔
641
                             {
2✔
642
                                 pk_id,
2✔
643
                                 partition_prop,
2✔
644
                                 locally_added_property,
2✔
645
                                 shared_added_property,
2✔
646
                                 remotely_added_property,
2✔
647
                             }},
2✔
648
                        },
2✔
649
                        0, nullptr, nullptr, true);
2✔
650

1✔
651
                    create_object(*local, new_table_name, {pk1}, partition);
2✔
652
                    create_object(*local, existing_table_name, {pk1}, partition);
2✔
653
                    create_object(*local, locally_added_table_name, {pk1}, partition);
2✔
654
                    create_object(*local, locally_added_table_name, {pk2}, partition);
2✔
655
                })
2✔
656
                ->make_remote_changes([&](SharedRealm remote) {
2✔
657
                    remote->update_schema(
2✔
658
                        {
2✔
659
                            {new_table_name,
2✔
660
                             {
2✔
661
                                 pk_id,
2✔
662
                                 partition_prop,
2✔
663
                                 remotely_added_property,
2✔
664
                                 shared_added_property,
2✔
665
                             }},
2✔
666
                            {existing_table_name,
2✔
667
                             {
2✔
668
                                 pk_id,
2✔
669
                                 partition_prop,
2✔
670
                                 remotely_added_property,
2✔
671
                                 shared_added_property,
2✔
672
                             }},
2✔
673
                            {remotely_added_table_name,
2✔
674
                             {
2✔
675
                                 pk_id,
2✔
676
                                 partition_prop,
2✔
677
                                 remotely_added_property,
2✔
678
                                 locally_added_property,
2✔
679
                                 shared_added_property,
2✔
680
                             }},
2✔
681
                        },
2✔
682
                        0, nullptr, nullptr, true);
2✔
683

1✔
684
                    create_object(*remote, new_table_name, {pk2}, partition);
2✔
685
                    create_object(*remote, existing_table_name, {pk2}, partition);
2✔
686
                    create_object(*remote, remotely_added_table_name, {pk1}, partition);
2✔
687
                    create_object(*remote, remotely_added_table_name, {pk2}, partition);
2✔
688
                })
2✔
689
                ->on_post_reset([&](SharedRealm local) {
2✔
690
                    verify_changes(local);
2✔
691
                })
2✔
692
                ->run();
2✔
693
            auto remote = Realm::get_shared_realm(remote_config);
2✔
694
            wait_for_upload(*remote);
2✔
695
            wait_for_download(*remote);
2✔
696
            verify_changes(remote);
2✔
697
            REQUIRE(before_callback_invocations == 1);
2!
698
            REQUIRE(after_callback_invocations == 1);
2!
699
        }
2✔
700

7✔
701
        SECTION("incompatible property changes are rejected") {
14✔
702
            const Property pk_id = {"_id", PropertyType::ObjectId | PropertyType::Nullable,
2✔
703
                                    Property::IsPrimary{true}};
2✔
704
            const std::string table_name = "new table";
2✔
705
            const std::string prop_name = "new_property";
2✔
706
            ThreadSafeSyncError err;
2✔
707
            local_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) {
2✔
708
                err = error;
2✔
709
            };
2✔
710
            make_reset(local_config, remote_config)
2✔
711
                ->set_development_mode(true)
2✔
712
                ->make_local_changes([&](SharedRealm local) {
2✔
713
                    local->update_schema(
2✔
714
                        {
2✔
715
                            {table_name,
2✔
716
                             {
2✔
717
                                 pk_id,
2✔
718
                                 partition_prop,
2✔
719
                                 {prop_name, PropertyType::Float},
2✔
720
                             }},
2✔
721
                        },
2✔
722
                        0, nullptr, nullptr, true);
2✔
723
                })
2✔
724
                ->make_remote_changes([&](SharedRealm remote) {
2✔
725
                    remote->update_schema(
2✔
726
                        {
2✔
727
                            {table_name,
2✔
728
                             {
2✔
729
                                 pk_id,
2✔
730
                                 partition_prop,
2✔
731
                                 {prop_name, PropertyType::Int},
2✔
732
                             }},
2✔
733
                        },
2✔
734
                        0, nullptr, nullptr, true);
2✔
735
                })
2✔
736
                ->on_post_reset([&](SharedRealm realm) {
2✔
737
                    util::EventLoop::main().run_until([&] {
3✔
738
                        return bool(err);
3✔
739
                    });
3✔
740
                    REQUIRE_NOTHROW(realm->refresh());
2✔
741
                })
2✔
742
                ->run();
2✔
743
            REQUIRE(err);
2!
744
            REQUIRE(err.value()->is_client_reset_requested());
2!
745
            REQUIRE(before_callback_invocations == 1);
2!
746
            REQUIRE(after_callback_invocations == 0);
2!
747
        }
2✔
748

7✔
749
        SECTION("add remotely deleted object to list") {
14✔
750
            test_reset
2✔
751
                ->setup([&](SharedRealm realm) {
2✔
752
                    ObjKey k1 =
2✔
753
                        create_object(*realm, "link target", ObjectId::gen(), partition).set("value", 1).get_key();
2✔
754
                    ObjKey k2 =
2✔
755
                        create_object(*realm, "link target", ObjectId::gen(), partition).set("value", 2).get_key();
2✔
756
                    ObjKey k3 =
2✔
757
                        create_object(*realm, "link target", ObjectId::gen(), partition).set("value", 3).get_key();
2✔
758
                    Obj o = create_object(*realm, "link origin", ObjectId::gen(), partition);
2✔
759
                    auto list = o.get_linklist("list");
2✔
760
                    list.add(k1);
2✔
761
                    list.add(k2);
2✔
762
                    list.add(k3);
2✔
763
                    // 1, 2, 3
1✔
764
                })
2✔
765
                ->make_local_changes([&](SharedRealm local) {
2✔
766
                    auto key1 = get_key_for_object_with_value(get_table(*local, "link target"), 1);
2✔
767
                    auto key2 = get_key_for_object_with_value(get_table(*local, "link target"), 2);
2✔
768
                    auto key3 = get_key_for_object_with_value(get_table(*local, "link target"), 3);
2✔
769
                    auto table = get_table(*local, "link origin");
2✔
770
                    auto list = table->begin()->get_linklist("list");
2✔
771
                    REQUIRE(list.size() == 3);
2!
772
                    list.insert(1, key2);
2✔
773
                    list.add(key2);
2✔
774
                    list.add(key3); // common suffix of key3
2✔
775
                    // 1, 2, 2, 3, 2, 3
1✔
776
                    // this set operation triggers the list copy because the index becomes ambiguious
1✔
777
                    list.set(0, key1);
2✔
778
                })
2✔
779
                ->make_remote_changes([&](SharedRealm remote) {
2✔
780
                    auto table = get_table(*remote, "link target");
2✔
781
                    auto key = get_key_for_object_with_value(table, 2);
2✔
782
                    REQUIRE(key);
2!
783
                    table->remove_object(key);
2✔
784
                })
2✔
785
                ->on_post_reset([&](SharedRealm realm) {
2✔
786
                    REQUIRE_NOTHROW(realm->refresh());
2✔
787
                    auto table = get_table(*realm, "link origin");
2✔
788
                    auto target_table = get_table(*realm, "link target");
2✔
789
                    REQUIRE(table->size() == 1);
2!
790
                    REQUIRE(target_table->size() == 2);
2!
791
                    REQUIRE(get_key_for_object_with_value(target_table, 1));
2!
792
                    REQUIRE(get_key_for_object_with_value(target_table, 3));
2!
793
                    auto list = table->begin()->get_linklist("list");
2✔
794
                    REQUIRE(list.size() == 3); // 1, 3, 3
2!
795
                    REQUIRE(list.get_object(0).get<Int>("value") == 1);
2!
796
                    REQUIRE(list.get_object(1).get<Int>("value") == 3);
2!
797
                    REQUIRE(list.get_object(2).get<Int>("value") == 3);
2!
798
                })
2✔
799
                ->run();
2✔
800
        }
2✔
801
    } // end recovery section
14✔
802

42✔
803
    SECTION("discard local") {
84✔
804
        local_config.sync_config->client_resync_mode = ClientResyncMode::DiscardLocal;
48✔
805
        std::unique_ptr<reset_utils::TestClientReset> test_reset = make_reset(local_config, remote_config);
48✔
806

24✔
807
        SECTION("modify") {
48✔
808
            test_reset
2✔
809
                ->on_post_local_changes([&](SharedRealm realm) {
2✔
810
                    setup_listeners(realm);
2✔
811
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
812
                    CHECK(results.size() == 1);
2!
813
                    CHECK(results.get<Obj>(0).get<Int>("value") == 4);
2!
814
                })
2✔
815
                ->on_post_reset([&](SharedRealm realm) {
2✔
816
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
817

1✔
818
                    CHECK(before_callback_invocations == 1);
2!
819
                    CHECK(after_callback_invocations == 1);
2!
820
                    CHECK(results.size() == 1);
2!
821
                    CHECK(results.get<Obj>(0).get<Int>("value") == 6);
2!
822
                    CHECK(object.get_obj().get<Int>("value") == 6);
2!
823
                    REQUIRE_INDICES(results_changes.modifications, 0);
2!
824
                    REQUIRE_INDICES(results_changes.insertions);
2!
825
                    REQUIRE_INDICES(results_changes.deletions);
2!
826
                    REQUIRE_INDICES(object_changes.modifications, 0);
2!
827
                    REQUIRE_INDICES(object_changes.insertions);
2!
828
                    REQUIRE_INDICES(object_changes.deletions);
2!
829
                    // make sure that the reset operation has cleaned up after itself
1✔
830
                    REQUIRE(util::File::exists(local_config.path));
2!
831
                    REQUIRE_FALSE(util::File::exists(fresh_path));
2!
832
                })
2✔
833
                ->run();
2✔
834

1✔
835
            SECTION("a Realm can be reset twice") {
2✔
836
                // keep the Realm to reset (config) the same, but change out the remote (config2)
1✔
837
                // to a new path because otherwise it will be reset as well which we don't want
1✔
838
                SyncTestFile config3 = get_valid_config();
2✔
839
                ObjectId to_continue_reset = test_reset->get_pk_of_object_driving_reset();
2✔
840
                test_reset = make_reset(local_config, config3);
2✔
841
                test_reset->set_pk_of_object_driving_reset(to_continue_reset);
2✔
842
                test_reset
2✔
843
                    ->setup([&](SharedRealm realm) {
2✔
844
                        // after a reset we already start with a value of 6
1✔
845
                        TableRef table = get_table(*realm, "object");
2✔
846
                        REQUIRE(table->size() == 1);
2!
847
                        REQUIRE(table->begin()->get<Int>("value") == 6);
2!
848
                        REQUIRE_NOTHROW(advance_and_notify(*object.get_realm()));
2✔
849
                        CHECK(object.get_obj().get<Int>("value") == 6);
2!
850
                        object_changes = {};
2✔
851
                        results_changes = {};
2✔
852
                    })
2✔
853
                    ->on_post_local_changes([&](SharedRealm) {
2✔
854
                        // advance the object's realm because the one passed here is different
1✔
855
                        REQUIRE_NOTHROW(advance_and_notify(*object.get_realm()));
2✔
856
                        // 6 -> 4
1✔
857
                        CHECK(results.size() == 1);
2!
858
                        CHECK(results.get<Obj>(0).get<Int>("value") == 4);
2!
859
                        CHECK(object.get_obj().get<Int>("value") == 4);
2!
860
                        REQUIRE_INDICES(results_changes.modifications, 0);
2!
861
                        REQUIRE_INDICES(results_changes.insertions);
2!
862
                        REQUIRE_INDICES(results_changes.deletions);
2!
863
                        REQUIRE_INDICES(object_changes.modifications, 0);
2!
864
                        REQUIRE_INDICES(object_changes.insertions);
2!
865
                        REQUIRE_INDICES(object_changes.deletions);
2!
866
                        object_changes = {};
2✔
867
                        results_changes = {};
2✔
868
                    })
2✔
869
                    ->on_post_reset([&](SharedRealm) {
2✔
870
                        REQUIRE_NOTHROW(advance_and_notify(*object.get_realm()));
2✔
871
                        CHECK(before_callback_invocations == 2);
2!
872
                        CHECK(after_callback_invocations == 2);
2!
873
                        // 4 -> 6
1✔
874
                        CHECK(results.size() == 1);
2!
875
                        CHECK(results.get<Obj>(0).get<Int>("value") == 6);
2!
876
                        CHECK(object.get_obj().get<Int>("value") == 6);
2!
877
                        REQUIRE_INDICES(results_changes.modifications, 0);
2!
878
                        REQUIRE_INDICES(results_changes.insertions);
2!
879
                        REQUIRE_INDICES(results_changes.deletions);
2!
880
                        REQUIRE_INDICES(object_changes.modifications, 0);
2!
881
                        REQUIRE_INDICES(object_changes.insertions);
2!
882
                        REQUIRE_INDICES(object_changes.deletions);
2!
883
                    })
2✔
884
                    ->run();
2✔
885
            }
2✔
886
        }
2✔
887

24✔
888
        SECTION("can be reset without notifiers") {
48✔
889
            local_config.sync_config->notify_before_client_reset = nullptr;
2✔
890
            local_config.sync_config->notify_after_client_reset = nullptr;
2✔
891
            make_reset(local_config, remote_config)->run();
2✔
892
            REQUIRE(before_callback_invocations == 0);
2!
893
            REQUIRE(after_callback_invocations == 0);
2!
894
        }
2✔
895

24✔
896
        SECTION("callbacks are seeded with Realm instances even if the coordinator dies") {
48✔
897
            auto client_reset_harness = make_reset(local_config, remote_config);
2✔
898
            client_reset_harness->disable_wait_for_reset_completion();
2✔
899
            std::shared_ptr<SyncSession> session;
2✔
900
            client_reset_harness
2✔
901
                ->on_post_local_changes([&](SharedRealm local) {
2✔
902
                    // retain a reference so the sync session completes, even though the Realm is cleaned up
1✔
903
                    session = local->sync_session();
2✔
904
                })
2✔
905
                ->run();
2✔
906
            auto local_coordinator = realm::_impl::RealmCoordinator::get_existing_coordinator(local_config.path);
2✔
907
            REQUIRE(!local_coordinator);
2!
908
            REQUIRE(before_callback_invocations == 0);
2!
909
            REQUIRE(after_callback_invocations == 0);
2!
910
            timed_sleeping_wait_for(
2✔
911
                [&]() -> bool {
138✔
912
                    std::lock_guard<std::mutex> lock(mtx);
138✔
913
                    return after_callback_invocations > 0;
138✔
914
                },
138✔
915
                std::chrono::seconds(60));
2✔
916
            // this test also relies on the test config above to verify the Realm instances in the callbacks
1✔
917
            REQUIRE(before_callback_invocations == 1);
2!
918
            REQUIRE(after_callback_invocations == 1);
2!
919
        }
2✔
920

24✔
921
        SECTION("notifiers work if the session instance changes") {
48✔
922
            // run this test with ASAN to check for use after free
1✔
923
            size_t before_callback_invocations_2 = 0;
2✔
924
            size_t after_callback_invocations_2 = 0;
2✔
925
            std::shared_ptr<SyncSession> session;
2✔
926
            std::unique_ptr<SyncConfig> config_copy;
2✔
927
            {
2✔
928
                SyncTestFile temp_config = get_valid_config();
2✔
929
                temp_config.persist();
2✔
930
                temp_config.sync_config->client_resync_mode = ClientResyncMode::DiscardLocal;
2✔
931
                config_copy = std::make_unique<SyncConfig>(*temp_config.sync_config);
2✔
932
                config_copy->notify_before_client_reset = [&](SharedRealm before_realm) {
1✔
933
                    std::lock_guard<std::mutex> lock(mtx);
×
934
                    REQUIRE(before_realm);
×
935
                    REQUIRE(before_realm->schema_version() != ObjectStore::NotVersioned);
×
936
                    ++before_callback_invocations_2;
×
937
                };
×
938
                config_copy->notify_after_client_reset = [&](SharedRealm, ThreadSafeReference, bool) {
1✔
939
                    std::lock_guard<std::mutex> lock(mtx);
×
940
                    ++after_callback_invocations_2;
×
941
                };
×
942

1✔
943
                temp_config.sync_config->notify_before_client_reset = [&](SharedRealm before_realm) {
2✔
944
                    std::lock_guard<std::mutex> lock(mtx);
2✔
945
                    ++before_callback_invocations;
2✔
946
                    REQUIRE(session);
2!
947
                    REQUIRE(config_copy);
2!
948
                    REQUIRE(before_realm);
2!
949
                    REQUIRE(before_realm->schema_version() != ObjectStore::NotVersioned);
2!
950
                    session->update_configuration(*config_copy);
2✔
951
                };
2✔
952

1✔
953
                auto realm = Realm::get_shared_realm(temp_config);
2✔
954
                wait_for_upload(*realm);
2✔
955

1✔
956
                session = test_app_session.app()->sync_manager()->get_existing_session(temp_config.path);
2✔
957
                REQUIRE(session);
2!
958
            }
2✔
959
            sync::SessionErrorInfo synthetic(Status{ErrorCodes::SyncClientResetRequired, "A fake client reset error"},
2✔
960
                                             sync::IsFatal{true});
2✔
961
            synthetic.server_requests_action = sync::ProtocolErrorInfo::Action::ClientReset;
2✔
962
            SyncSession::OnlyForTesting::handle_error(*session, std::move(synthetic));
2✔
963

1✔
964
            session->revive_if_needed();
2✔
965
            timed_sleeping_wait_for(
2✔
966
                [&]() -> bool {
76✔
967
                    std::lock_guard<std::mutex> lock(mtx);
76✔
968
                    return before_callback_invocations > 0;
76✔
969
                },
76✔
970
                std::chrono::seconds(120));
2✔
971
            millisleep(500); // just make some space for the after callback to be attempted
2✔
972
            REQUIRE(before_callback_invocations == 1);
2!
973
            REQUIRE(after_callback_invocations == 0);
2!
974
            REQUIRE(before_callback_invocations_2 == 0);
2!
975
            REQUIRE(after_callback_invocations_2 == 0);
2!
976
        }
2✔
977

24✔
978
        SECTION("an interrupted reset can recover on the next session") {
48✔
979
            struct SessionInterruption : public std::runtime_error {
2✔
980
                using std::runtime_error::runtime_error;
2✔
981
            };
2✔
982
            try {
2✔
983
                test_reset
2✔
984
                    ->on_post_local_changes([&](SharedRealm) {
2✔
985
                        throw SessionInterruption("fake interruption during reset");
2✔
986
                    })
2✔
987
                    ->run();
2✔
988
            }
2✔
989
            catch (const SessionInterruption&) {
2✔
990
                REQUIRE(before_callback_invocations == 0);
2!
991
                REQUIRE(after_callback_invocations == 0);
2!
992
                test_reset.reset();
2✔
993
                auto realm = Realm::get_shared_realm(local_config);
2✔
994
                timed_sleeping_wait_for(
2✔
995
                    [&]() -> bool {
126✔
996
                        std::lock_guard<std::mutex> lock(mtx);
126✔
997
                        realm->begin_transaction();
126✔
998
                        TableRef table = get_table(*realm, "object");
126✔
999
                        REQUIRE(table);
126!
1000
                        REQUIRE(table->size() == 1);
126!
1001
                        auto col = table->get_column_key("value");
126✔
1002
                        int64_t value = table->begin()->get<Int>(col);
126✔
1003
                        realm->cancel_transaction();
126✔
1004
                        return value == 6;
126✔
1005
                    },
126✔
1006
                    std::chrono::seconds(20));
2✔
1007
            }
2✔
1008
            auto session = test_app_session.app()->sync_manager()->get_existing_session(local_config.path);
2✔
1009
            if (session) {
2✔
1010
                session->shutdown_and_wait();
2✔
1011
            }
2✔
1012
            {
2✔
1013
                std::lock_guard<std::mutex> lock(mtx);
2✔
1014
                REQUIRE(before_callback_invocations == 1);
2!
1015
                REQUIRE(after_callback_invocations == 1);
2!
1016
            }
2✔
1017
        }
2✔
1018

24✔
1019
        SECTION("an interrupted reset can recover on the next session restart") {
48✔
1020
            test_reset->disable_wait_for_reset_completion();
2✔
1021
            SharedRealm realm;
2✔
1022
            test_reset
2✔
1023
                ->on_post_local_changes([&](SharedRealm local) {
2✔
1024
                    // retain a reference of the realm.
1✔
1025
                    realm = local;
2✔
1026
                })
2✔
1027
                ->run();
2✔
1028

1✔
1029
            timed_wait_for([&] {
1,942✔
1030
                return util::File::exists(_impl::client_reset::get_fresh_path_for(local_config.path));
1,942✔
1031
            });
1,942✔
1032

1✔
1033
            // Restart the session before the client reset finishes.
1✔
1034
            realm->sync_session()->restart_session();
2✔
1035

1✔
1036
            REQUIRE(!wait_for_upload(*realm));
2!
1037
            REQUIRE(!wait_for_download(*realm));
2!
1038
            realm->refresh();
2✔
1039

1✔
1040
            auto table = realm->read_group().get_table("class_object");
2✔
1041
            REQUIRE(table->size() == 1);
2!
1042
            auto col = table->get_column_key("value");
2✔
1043
            int64_t value = table->begin()->get<Int>(col);
2✔
1044
            REQUIRE(value == 6);
2!
1045

1✔
1046
            {
2✔
1047
                std::lock_guard<std::mutex> lock(mtx);
2✔
1048
                REQUIRE(before_callback_invocations == 1);
2!
1049
                REQUIRE(after_callback_invocations == 1);
2!
1050
            }
2✔
1051
        }
2✔
1052

24✔
1053
        SECTION("invalid files at the fresh copy path are cleaned up") {
48✔
1054
            ThreadSafeSyncError err;
2✔
1055
            local_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) {
1✔
1056
                err = error;
×
1057
            };
×
1058
            std::string fresh_path = realm::_impl::client_reset::get_fresh_path_for(local_config.path);
2✔
1059
            util::File f(fresh_path, util::File::Mode::mode_Write);
2✔
1060
            f.write("a non empty file");
2✔
1061
            f.sync();
2✔
1062
            f.close();
2✔
1063

1✔
1064
            make_reset(local_config, remote_config)->run();
2✔
1065
            REQUIRE(!err);
2!
1066
            REQUIRE(before_callback_invocations == 1);
2!
1067
            REQUIRE(after_callback_invocations == 1);
2!
1068
        }
2✔
1069

24✔
1070
        SECTION("failing to download a fresh copy results in an error") {
48✔
1071
            ThreadSafeSyncError err;
2✔
1072
            local_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) {
2✔
1073
                err = error;
2✔
1074
            };
2✔
1075
            std::string fresh_path = realm::_impl::client_reset::get_fresh_path_for(local_config.path);
2✔
1076
            // create a non-empty directory that we'll fail to delete
1✔
1077
            util::make_dir(fresh_path);
2✔
1078
            util::File(util::File::resolve("file", fresh_path), util::File::mode_Write);
2✔
1079

1✔
1080
            REQUIRE(!err);
2!
1081
            make_reset(local_config, remote_config)
2✔
1082
                ->on_post_reset([&](SharedRealm) {
2✔
1083
                    util::EventLoop::main().run_until([&] {
3✔
1084
                        return bool(err);
3✔
1085
                    });
3✔
1086
                })
2✔
1087
                ->run();
2✔
1088
            REQUIRE(err);
2!
1089
            REQUIRE(err.value()->is_client_reset_requested());
2!
1090
        }
2✔
1091

24✔
1092
        SECTION("should honor encryption key for downloaded Realm") {
48✔
1093
            local_config.encryption_key.resize(64, 'a');
2✔
1094

1✔
1095
            make_reset(local_config, remote_config)
2✔
1096
                ->on_post_reset([&](SharedRealm realm) {
2✔
1097
                    realm->close();
2✔
1098
                    SharedRealm r_after;
2✔
1099
                    REQUIRE_NOTHROW(r_after = Realm::get_shared_realm(local_config));
2✔
1100
                    CHECK(ObjectStore::table_for_object_type(r_after->read_group(), "object")
2!
1101
                              ->begin()
2✔
1102
                              ->get<Int>("value") == 6);
2✔
1103
                })
2✔
1104
                ->run();
2✔
1105
        }
2✔
1106

24✔
1107
        SECTION("delete and insert new") {
48✔
1108
            constexpr int64_t new_value = 42;
2✔
1109
            test_reset
2✔
1110
                ->make_remote_changes([&](SharedRealm remote) {
2✔
1111
                    auto table = get_table(*remote, "object");
2✔
1112
                    REQUIRE(table);
2!
1113
                    REQUIRE(table->size() == 1);
2!
1114
                    ObjectId different_pk = ObjectId::gen();
2✔
1115
                    table->clear();
2✔
1116
                    auto obj = create_object(*remote, "object", {different_pk}, partition);
2✔
1117
                    auto col = obj.get_table()->get_column_key("value");
2✔
1118
                    obj.set(col, new_value);
2✔
1119
                })
2✔
1120
                ->on_post_local_changes([&](SharedRealm realm) {
2✔
1121
                    setup_listeners(realm);
2✔
1122
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
1123
                    CHECK(results.size() == 1);
2!
1124
                    CHECK(results.get<Obj>(0).get<Int>("value") == 4);
2!
1125
                })
2✔
1126
                ->on_post_reset([&](SharedRealm realm) {
2✔
1127
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
1128
                    CHECK(results.size() == 1);
2!
1129
                    CHECK(results.get<Obj>(0).get<Int>("value") == new_value);
2!
1130
                    CHECK(!object.is_valid());
2!
1131
                    REQUIRE_INDICES(results_changes.modifications);
2!
1132
                    REQUIRE_INDICES(results_changes.insertions, 0);
2!
1133
                    REQUIRE_INDICES(results_changes.deletions, 0);
2!
1134
                    REQUIRE_INDICES(object_changes.modifications);
2!
1135
                    REQUIRE_INDICES(object_changes.insertions);
2!
1136
                    REQUIRE_INDICES(object_changes.deletions, 0);
2!
1137
                })
2✔
1138
                ->run();
2✔
1139
        }
2✔
1140

24✔
1141
        SECTION("delete and insert same pk is reported as modification") {
48✔
1142
            constexpr int64_t new_value = 42;
2✔
1143
            test_reset
2✔
1144
                ->make_remote_changes([&](SharedRealm remote) {
2✔
1145
                    auto table = get_table(*remote, "object");
2✔
1146
                    REQUIRE(table);
2!
1147
                    REQUIRE(table->size() == 1);
2!
1148
                    Mixed orig_pk = table->begin()->get_primary_key();
2✔
1149
                    table->clear();
2✔
1150
                    auto obj = create_object(*remote, "object", {orig_pk.get_object_id()}, partition);
2✔
1151
                    REQUIRE(obj.get_primary_key() == orig_pk);
2!
1152
                    auto col = obj.get_table()->get_column_key("value");
2✔
1153
                    obj.set(col, new_value);
2✔
1154
                })
2✔
1155
                ->on_post_local_changes([&](SharedRealm realm) {
2✔
1156
                    setup_listeners(realm);
2✔
1157
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
1158
                    CHECK(results.size() == 1);
2!
1159
                    CHECK(results.get<Obj>(0).get<Int>("value") == 4);
2!
1160
                })
2✔
1161
                ->on_post_reset([&](SharedRealm realm) {
2✔
1162
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
1163
                    CHECK(results.size() == 1);
2!
1164
                    CHECK(results.get<Obj>(0).get<Int>("value") == new_value);
2!
1165
                    CHECK(object.is_valid());
2!
1166
                    CHECK(object.get_obj().get<Int>("value") == new_value);
2!
1167
                    REQUIRE_INDICES(results_changes.modifications, 0);
2!
1168
                    REQUIRE_INDICES(results_changes.insertions);
2!
1169
                    REQUIRE_INDICES(results_changes.deletions);
2!
1170
                    REQUIRE_INDICES(object_changes.modifications, 0);
2!
1171
                    REQUIRE_INDICES(object_changes.insertions);
2!
1172
                    REQUIRE_INDICES(object_changes.deletions);
2!
1173
                })
2✔
1174
                ->run();
2✔
1175
        }
2✔
1176

24✔
1177
        SECTION("insert in discarded transaction is deleted") {
48✔
1178
            constexpr int64_t new_value = 42;
2✔
1179
            test_reset
2✔
1180
                ->make_local_changes([&](SharedRealm local) {
2✔
1181
                    auto table = get_table(*local, "object");
2✔
1182
                    REQUIRE(table);
2!
1183
                    REQUIRE(table->size() == 1);
2!
1184
                    auto obj = create_object(*local, "object", util::none, partition);
2✔
1185
                    auto col = obj.get_table()->get_column_key("value");
2✔
1186
                    REQUIRE(table->size() == 2);
2!
1187
                    obj.set(col, new_value);
2✔
1188
                })
2✔
1189
                ->on_post_local_changes([&](SharedRealm realm) {
2✔
1190
                    setup_listeners(realm);
2✔
1191
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
1192
                    CHECK(results.size() == 2);
2!
1193
                })
2✔
1194
                ->on_post_reset([&](SharedRealm realm) {
2✔
1195
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
1196
                    CHECK(results.size() == 1);
2!
1197
                    CHECK(results.get<Obj>(0).get<Int>("value") == 6);
2!
1198
                    CHECK(object.is_valid());
2!
1199
                    CHECK(object.get_obj().get<Int>("value") == 6);
2!
1200
                    REQUIRE_INDICES(results_changes.modifications, 0);
2!
1201
                    REQUIRE_INDICES(results_changes.insertions);
2!
1202
                    REQUIRE_INDICES(results_changes.deletions, 1);
2!
1203
                    REQUIRE_INDICES(object_changes.modifications, 0);
2!
1204
                    REQUIRE_INDICES(object_changes.insertions);
2!
1205
                    REQUIRE_INDICES(object_changes.deletions);
2!
1206
                })
2✔
1207
                ->run();
2✔
1208
        }
2✔
1209

24✔
1210
        SECTION("delete in discarded transaction is recovered") {
48✔
1211
            test_reset
2✔
1212
                ->make_local_changes([&](SharedRealm local) {
2✔
1213
                    auto table = get_table(*local, "object");
2✔
1214
                    REQUIRE(table);
2!
1215
                    REQUIRE(table->size() == 1);
2!
1216
                    table->clear();
2✔
1217
                    REQUIRE(table->size() == 0);
2!
1218
                })
2✔
1219
                ->on_post_local_changes([&](SharedRealm realm) {
2✔
1220
                    setup_listeners(realm);
2✔
1221
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
1222
                    CHECK(results.size() == 0);
2!
1223
                })
2✔
1224
                ->on_post_reset([&](SharedRealm realm) {
2✔
1225
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
1226
                    CHECK(results.size() == 1);
2!
1227
                    CHECK(results.get<Obj>(0).get<Int>("value") == 6);
2!
1228
                    CHECK(!object.is_valid());
2!
1229
                    REQUIRE_INDICES(results_changes.modifications);
2!
1230
                    REQUIRE_INDICES(results_changes.insertions, 0);
2!
1231
                    REQUIRE_INDICES(results_changes.deletions);
2!
1232
                })
2✔
1233
                ->run();
2✔
1234
        }
2✔
1235

24✔
1236
        SECTION("extra local table creates a client reset error") {
48✔
1237
            ThreadSafeSyncError err;
2✔
1238
            local_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) {
2✔
1239
                err = error;
2✔
1240
            };
2✔
1241
            make_reset(local_config, remote_config)
2✔
1242
                ->set_development_mode(true)
2✔
1243
                ->make_local_changes([&](SharedRealm local) {
2✔
1244
                    local->update_schema(
2✔
1245
                        {
2✔
1246
                            {"object2",
2✔
1247
                             {
2✔
1248
                                 {"_id", PropertyType::ObjectId | PropertyType::Nullable, Property::IsPrimary{true}},
2✔
1249
                                 {"realm_id", PropertyType::String | PropertyType::Nullable},
2✔
1250
                             }},
2✔
1251
                        },
2✔
1252
                        0, nullptr, nullptr, true);
2✔
1253
                    create_object(*local, "object2", ObjectId::gen(), partition);
2✔
1254
                    create_object(*local, "object2", ObjectId::gen(), partition);
2✔
1255
                })
2✔
1256
                ->on_post_reset([&](SharedRealm realm) {
2✔
1257
                    util::EventLoop::main().run_until([&] {
3✔
1258
                        return bool(err);
3✔
1259
                    });
3✔
1260
                    REQUIRE_NOTHROW(realm->refresh());
2✔
1261
                })
2✔
1262
                ->run();
2✔
1263
            REQUIRE(err);
2!
1264
            REQUIRE(err.value()->is_client_reset_requested());
2!
1265
            REQUIRE(before_callback_invocations == 1);
2!
1266
            REQUIRE(after_callback_invocations == 0);
2!
1267
        }
2✔
1268

24✔
1269
        SECTION("extra local column creates a client reset error") {
48✔
1270
            ThreadSafeSyncError err;
2✔
1271
            local_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) {
2✔
1272
                err = error;
2✔
1273
            };
2✔
1274
            make_reset(local_config, remote_config)
2✔
1275
                ->set_development_mode(true)
2✔
1276
                ->make_local_changes([](SharedRealm local) {
2✔
1277
                    local->update_schema(
2✔
1278
                        {
2✔
1279
                            {"object",
2✔
1280
                             {
2✔
1281
                                 {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
1282
                                 {"value2", PropertyType::Int},
2✔
1283
                                 {"array", PropertyType::Int | PropertyType::Array},
2✔
1284
                                 {"link", PropertyType::Object | PropertyType::Nullable, "object"},
2✔
1285
                                 {"realm_id", PropertyType::String | PropertyType::Nullable},
2✔
1286
                             }},
2✔
1287
                        },
2✔
1288
                        0, nullptr, nullptr, true);
2✔
1289
                    auto table = ObjectStore::table_for_object_type(local->read_group(), "object");
2✔
1290
                    table->begin()->set(table->get_column_key("value2"), 123);
2✔
1291
                })
2✔
1292
                ->on_post_reset([&](SharedRealm realm) {
2✔
1293
                    util::EventLoop::main().run_until([&] {
3✔
1294
                        return bool(err);
3✔
1295
                    });
3✔
1296
                    REQUIRE_NOTHROW(realm->refresh());
2✔
1297
                })
2✔
1298
                ->run();
2✔
1299

1✔
1300
            REQUIRE(err);
2!
1301
            REQUIRE(err.value()->is_client_reset_requested());
2!
1302
            REQUIRE(before_callback_invocations == 1);
2!
1303
            REQUIRE(after_callback_invocations == 0);
2!
1304
        }
2✔
1305

24✔
1306
        SECTION("compatible schema changes in both remote and local transactions") {
48✔
1307
            test_reset->set_development_mode(true)
2✔
1308
                ->make_local_changes([](SharedRealm local) {
2✔
1309
                    local->update_schema(
2✔
1310
                        {
2✔
1311
                            {"object",
2✔
1312
                             {
2✔
1313
                                 {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
1314
                                 {"value2", PropertyType::Int},
2✔
1315
                                 {"realm_id", PropertyType::String | PropertyType::Nullable},
2✔
1316
                             }},
2✔
1317
                            {"object2",
2✔
1318
                             {
2✔
1319
                                 {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
1320
                                 {"link", PropertyType::Object | PropertyType::Nullable, "object"},
2✔
1321
                                 {"realm_id", PropertyType::String | PropertyType::Nullable},
2✔
1322
                             }},
2✔
1323
                        },
2✔
1324
                        0, nullptr, nullptr, true);
2✔
1325
                })
2✔
1326
                ->make_remote_changes([](SharedRealm remote) {
2✔
1327
                    remote->update_schema(
2✔
1328
                        {
2✔
1329
                            {"object",
2✔
1330
                             {
2✔
1331
                                 {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
1332
                                 {"value2", PropertyType::Int},
2✔
1333
                                 {"realm_id", PropertyType::String | PropertyType::Nullable},
2✔
1334
                             }},
2✔
1335
                            {"object2",
2✔
1336
                             {
2✔
1337
                                 {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
1338
                                 {"link", PropertyType::Object | PropertyType::Nullable, "object"},
2✔
1339
                                 {"realm_id", PropertyType::String | PropertyType::Nullable},
2✔
1340
                             }},
2✔
1341
                        },
2✔
1342
                        0, nullptr, nullptr, true);
2✔
1343
                })
2✔
1344
                ->on_post_reset([](SharedRealm realm) {
2✔
1345
                    REQUIRE_NOTHROW(realm->refresh());
2✔
1346
                    auto table = ObjectStore::table_for_object_type(realm->read_group(), "object2");
2✔
1347
                    REQUIRE(table->get_column_count() == 3);
2!
1348
                    REQUIRE(bool(table->get_column_key("link")));
2!
1349
                })
2✔
1350
                ->run();
2✔
1351
        }
2✔
1352

24✔
1353
        SECTION("incompatible schema changes in remote and local transactions") {
48✔
1354
            ThreadSafeSyncError err;
2✔
1355
            local_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) {
2✔
1356
                err = error;
2✔
1357
            };
2✔
1358
            make_reset(local_config, remote_config)
2✔
1359
                ->set_development_mode(true)
2✔
1360
                ->make_local_changes([](SharedRealm local) {
2✔
1361
                    local->update_schema(
2✔
1362
                        {
2✔
1363
                            {"object",
2✔
1364
                             {
2✔
1365
                                 {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
1366
                                 {"value2", PropertyType::Float},
2✔
1367
                                 {"realm_id", PropertyType::String | PropertyType::Nullable},
2✔
1368
                             }},
2✔
1369
                        },
2✔
1370
                        0, nullptr, nullptr, true);
2✔
1371
                })
2✔
1372
                ->make_remote_changes([](SharedRealm remote) {
2✔
1373
                    remote->update_schema(
2✔
1374
                        {
2✔
1375
                            {"object",
2✔
1376
                             {
2✔
1377
                                 {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
1378
                                 {"value2", PropertyType::Int},
2✔
1379
                                 {"realm_id", PropertyType::String | PropertyType::Nullable},
2✔
1380
                             }},
2✔
1381
                        },
2✔
1382
                        0, nullptr, nullptr, true);
2✔
1383
                })
2✔
1384
                ->on_post_reset([&](SharedRealm realm) {
2✔
1385
                    util::EventLoop::main().run_until([&] {
3✔
1386
                        return bool(err);
3✔
1387
                    });
3✔
1388
                    REQUIRE_NOTHROW(realm->refresh());
2✔
1389
                })
2✔
1390
                ->run();
2✔
1391
            REQUIRE(err);
2!
1392
            REQUIRE(err.value()->is_client_reset_requested());
2!
1393
        }
2✔
1394

24✔
1395
        SECTION("primary key type cannot be changed") {
48✔
1396
            ThreadSafeSyncError err;
2✔
1397
            local_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) {
2✔
1398
                err = error;
2✔
1399
            };
2✔
1400

1✔
1401
            make_reset(local_config, remote_config)
2✔
1402
                ->set_development_mode(true)
2✔
1403
                ->make_local_changes([](SharedRealm local) {
2✔
1404
                    local->update_schema(
2✔
1405
                        {
2✔
1406
                            {"new table",
2✔
1407
                             {
2✔
1408
                                 {"_id", PropertyType::Int, Property::IsPrimary{true}},
2✔
1409
                                 {"realm_id", PropertyType::String | PropertyType::Nullable},
2✔
1410
                             }},
2✔
1411
                        },
2✔
1412
                        0, nullptr, nullptr, true);
2✔
1413
                })
2✔
1414
                ->make_remote_changes([](SharedRealm remote) {
2✔
1415
                    remote->update_schema(
2✔
1416
                        {
2✔
1417
                            {"new table",
2✔
1418
                             {
2✔
1419
                                 {"_id", PropertyType::String, Property::IsPrimary{true}},
2✔
1420
                                 {"realm_id", PropertyType::String | PropertyType::Nullable},
2✔
1421
                             }},
2✔
1422
                        },
2✔
1423
                        0, nullptr, nullptr, true);
2✔
1424
                })
2✔
1425
                ->on_post_reset([&](SharedRealm realm) {
2✔
1426
                    util::EventLoop::main().run_until([&] {
3✔
1427
                        return bool(err);
3✔
1428
                    });
3✔
1429
                    REQUIRE_NOTHROW(realm->refresh());
2✔
1430
                })
2✔
1431
                ->run();
2✔
1432
            REQUIRE(err);
2!
1433
            REQUIRE(err.value()->is_client_reset_requested());
2!
1434
        }
2✔
1435

24✔
1436
        SECTION("list operations") {
48✔
1437
            ObjKey k0, k1, k2;
6✔
1438
            test_reset->setup([&](SharedRealm realm) {
6✔
1439
                k0 = create_object(*realm, "link target", ObjectId::gen(), partition).set("value", 1).get_key();
6✔
1440
                k1 = create_object(*realm, "link target", ObjectId::gen(), partition).set("value", 2).get_key();
6✔
1441
                k2 = create_object(*realm, "link target", ObjectId::gen(), partition).set("value", 3).get_key();
6✔
1442
                Obj o = create_object(*realm, "link origin", ObjectId::gen(), partition);
6✔
1443
                auto list = o.get_linklist(o.get_table()->get_column_key("list"));
6✔
1444
                list.add(k0);
6✔
1445
                list.add(k1);
6✔
1446
                list.add(k2);
6✔
1447
            });
6✔
1448
            auto check_links = [&](auto& realm) {
6✔
1449
                auto table = get_table(*realm, "link origin");
6✔
1450
                REQUIRE(table->size() == 1);
6!
1451
                auto list = table->begin()->get_linklist(table->get_column_key("list"));
6✔
1452
                REQUIRE(list.size() == 3);
6!
1453
                REQUIRE(list.get_object(0).template get<Int>("value") == 1);
6!
1454
                REQUIRE(list.get_object(1).template get<Int>("value") == 2);
6!
1455
                REQUIRE(list.get_object(2).template get<Int>("value") == 3);
6!
1456
            };
6✔
1457

3✔
1458
            SECTION("list insertions in local transaction") {
6✔
1459
                test_reset
2✔
1460
                    ->make_local_changes([&](SharedRealm local) {
2✔
1461
                        auto table = get_table(*local, "link origin");
2✔
1462
                        auto list = table->begin()->get_linklist(table->get_column_key("list"));
2✔
1463
                        list.add(k0);
2✔
1464
                        list.insert(0, k2);
2✔
1465
                        list.insert(0, k1);
2✔
1466
                    })
2✔
1467
                    ->on_post_reset([&](SharedRealm realm) {
2✔
1468
                        REQUIRE_NOTHROW(realm->refresh());
2✔
1469
                        check_links(realm);
2✔
1470
                    })
2✔
1471
                    ->run();
2✔
1472
            }
2✔
1473

3✔
1474
            SECTION("list deletions in local transaction") {
6✔
1475
                test_reset
2✔
1476
                    ->make_local_changes([&](SharedRealm local) {
2✔
1477
                        auto table = get_table(*local, "link origin");
2✔
1478
                        auto list = table->begin()->get_linklist(table->get_column_key("list"));
2✔
1479
                        list.remove(1);
2✔
1480
                    })
2✔
1481
                    ->on_post_reset([&](SharedRealm realm) {
2✔
1482
                        REQUIRE_NOTHROW(realm->refresh());
2✔
1483
                        check_links(realm);
2✔
1484
                    })
2✔
1485
                    ->run();
2✔
1486
            }
2✔
1487

3✔
1488
            SECTION("list clear in local transaction") {
6✔
1489
                test_reset
2✔
1490
                    ->make_local_changes([&](SharedRealm local) {
2✔
1491
                        auto table = get_table(*local, "link origin");
2✔
1492
                        auto list = table->begin()->get_linklist(table->get_column_key("list"));
2✔
1493
                        list.clear();
2✔
1494
                    })
2✔
1495
                    ->on_post_reset([&](SharedRealm realm) {
2✔
1496
                        REQUIRE_NOTHROW(realm->refresh());
2✔
1497
                        check_links(realm);
2✔
1498
                    })
2✔
1499
                    ->run();
2✔
1500
            }
2✔
1501
        }
6✔
1502

24✔
1503
        SECTION("conflicting primary key creations") {
48✔
1504
            ObjectId id1 = ObjectId::gen();
2✔
1505
            ObjectId id2 = ObjectId::gen();
2✔
1506
            ObjectId id3 = ObjectId::gen();
2✔
1507
            ObjectId id4 = ObjectId::gen();
2✔
1508
            test_reset
2✔
1509
                ->make_local_changes([&](SharedRealm local) {
2✔
1510
                    auto table = get_table(*local, "object");
2✔
1511
                    table->clear();
2✔
1512
                    create_object(*local, "object", {id1}, partition).set("value", 4);
2✔
1513
                    create_object(*local, "object", {id2}, partition).set("value", 5);
2✔
1514
                    create_object(*local, "object", {id3}, partition).set("value", 6);
2✔
1515
                })
2✔
1516
                ->make_remote_changes([&](SharedRealm remote) {
2✔
1517
                    auto table = get_table(*remote, "object");
2✔
1518
                    table->clear();
2✔
1519
                    create_object(*remote, "object", {id1}, partition).set("value", 4);
2✔
1520
                    create_object(*remote, "object", {id2}, partition).set("value", 7);
2✔
1521
                    create_object(*remote, "object", {id4}, partition).set("value", 8);
2✔
1522
                })
2✔
1523
                ->on_post_local_changes([&](SharedRealm realm) {
2✔
1524
                    setup_listeners(realm);
2✔
1525
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
1526
                    CHECK(results.size() == 3);
2!
1527
                    CHECK(results.get<Obj>(0).get<Int>("value") == 4);
2!
1528
                })
2✔
1529
                ->on_post_reset([&](SharedRealm realm) {
2✔
1530
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
1531
                    CHECK(results.size() == 3);
2!
1532
                    // here we rely on results being sorted by "value"
1✔
1533
                    CHECK(results.get<Obj>(0).get<ObjectId>("_id") == id1);
2!
1534
                    CHECK(results.get<Obj>(0).get<Int>("value") == 4);
2!
1535
                    CHECK(results.get<Obj>(1).get<ObjectId>("_id") == id2);
2!
1536
                    CHECK(results.get<Obj>(1).get<Int>("value") == 7);
2!
1537
                    CHECK(results.get<Obj>(2).get<ObjectId>("_id") == id4);
2!
1538
                    CHECK(results.get<Obj>(2).get<Int>("value") == 8);
2!
1539
                    CHECK(object.is_valid());
2!
1540
                    REQUIRE_INDICES(results_changes.modifications, 1);
2!
1541
                    REQUIRE_INDICES(results_changes.insertions, 2);
2!
1542
                    REQUIRE_INDICES(results_changes.deletions, 2);
2!
1543
                    REQUIRE_INDICES(object_changes.modifications);
2!
1544
                    REQUIRE_INDICES(object_changes.insertions);
2!
1545
                    REQUIRE_INDICES(object_changes.deletions);
2!
1546
                })
2✔
1547
                ->run();
2✔
1548
        }
2✔
1549

24✔
1550
        SECTION("link to remotely deleted object") {
48✔
1551
            test_reset
2✔
1552
                ->setup([&](SharedRealm realm) {
2✔
1553
                    auto k0 =
2✔
1554
                        create_object(*realm, "link target", ObjectId::gen(), partition).set("value", 1).get_key();
2✔
1555
                    create_object(*realm, "link target", ObjectId::gen(), partition).set("value", 2);
2✔
1556
                    create_object(*realm, "link target", ObjectId::gen(), partition).set("value", 3);
2✔
1557

1✔
1558
                    Obj o = create_object(*realm, "link origin", ObjectId::gen(), partition);
2✔
1559
                    o.set("link", k0);
2✔
1560
                })
2✔
1561
                ->make_local_changes([&](SharedRealm local) {
2✔
1562
                    auto target_table = get_table(*local, "link target");
2✔
1563
                    auto key_of_second_target = get_key_for_object_with_value(target_table, 2);
2✔
1564
                    REQUIRE(key_of_second_target);
2!
1565
                    auto table = get_table(*local, "link origin");
2✔
1566
                    table->begin()->set("link", key_of_second_target);
2✔
1567
                })
2✔
1568
                ->make_remote_changes([&](SharedRealm remote) {
2✔
1569
                    auto table = get_table(*remote, "link target");
2✔
1570
                    auto key_of_second_target = get_key_for_object_with_value(table, 2);
2✔
1571
                    table->remove_object(key_of_second_target);
2✔
1572
                })
2✔
1573
                ->on_post_reset([&](SharedRealm realm) {
2✔
1574
                    REQUIRE_NOTHROW(realm->refresh());
2✔
1575
                    auto origin = get_table(*realm, "link origin");
2✔
1576
                    auto target = get_table(*realm, "link target");
2✔
1577
                    REQUIRE(origin->size() == 1);
2!
1578
                    REQUIRE(target->size() == 2);
2!
1579
                    REQUIRE(get_key_for_object_with_value(target, 1));
2!
1580
                    REQUIRE(get_key_for_object_with_value(target, 3));
2!
1581
                    auto key = origin->begin()->get<ObjKey>("link");
2✔
1582
                    auto obj = target->get_object(key);
2✔
1583
                    REQUIRE(obj.get<Int>("value") == 1);
2!
1584
                })
2✔
1585
                ->run();
2✔
1586
        }
2✔
1587

24✔
1588
        SECTION("add remotely deleted object to list") {
48✔
1589
            ObjKey k0, k1, k2;
2✔
1590
            test_reset
2✔
1591
                ->setup([&](SharedRealm realm) {
2✔
1592
                    k0 = create_object(*realm, "link target", ObjectId::gen(), partition).set("value", 1).get_key();
2✔
1593
                    k1 = create_object(*realm, "link target", ObjectId::gen(), partition).set("value", 2).get_key();
2✔
1594
                    k2 = create_object(*realm, "link target", ObjectId::gen(), partition).set("value", 3).get_key();
2✔
1595
                    Obj o = create_object(*realm, "link origin", ObjectId::gen(), partition);
2✔
1596
                    o.get_linklist("list").add(k0);
2✔
1597
                })
2✔
1598
                ->make_local_changes([&](SharedRealm local) {
2✔
1599
                    auto key = get_key_for_object_with_value(get_table(*local, "link target"), 2);
2✔
1600
                    auto table = get_table(*local, "link origin");
2✔
1601
                    auto list = table->begin()->get_linklist("list");
2✔
1602
                    list.add(key);
2✔
1603
                })
2✔
1604
                ->make_remote_changes([&](SharedRealm remote) {
2✔
1605
                    auto table = get_table(*remote, "link target");
2✔
1606
                    auto key = get_key_for_object_with_value(table, 2);
2✔
1607
                    REQUIRE(key);
2!
1608
                    table->remove_object(key);
2✔
1609
                })
2✔
1610
                ->on_post_reset([&](SharedRealm realm) {
2✔
1611
                    REQUIRE_NOTHROW(realm->refresh());
2✔
1612
                    auto table = get_table(*realm, "link origin");
2✔
1613
                    auto target_table = get_table(*realm, "link target");
2✔
1614
                    REQUIRE(table->size() == 1);
2!
1615
                    REQUIRE(target_table->size() == 2);
2!
1616
                    REQUIRE(get_key_for_object_with_value(target_table, 1));
2!
1617
                    REQUIRE(get_key_for_object_with_value(target_table, 3));
2!
1618
                    auto list = table->begin()->get_linklist("list");
2✔
1619
                    REQUIRE(list.size() == 1);
2!
1620
                    REQUIRE(list.get_object(0).get<Int>("value") == 1);
2!
1621
                })
2✔
1622
                ->run();
2✔
1623
        }
2✔
1624
    } // end discard local section
48✔
1625

42✔
1626
    SECTION("cycle detection") {
84✔
1627
        auto has_reset_cycle_flag = [](SharedRealm realm) -> util::Optional<_impl::client_reset::PendingReset> {
12✔
1628
            auto db = TestHelper::get_db(realm);
8✔
1629
            auto rt = db->start_read();
8✔
1630
            return _impl::client_reset::has_pending_reset(*rt);
8✔
1631
        };
8✔
1632
        ThreadSafeSyncError err;
16✔
1633
        local_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) {
13✔
1634
            err = error;
10✔
1635
        };
10✔
1636
        auto make_fake_previous_reset = [&local_config](ClientResyncMode type) {
14✔
1637
            local_config.sync_config->notify_before_client_reset = [previous_type = type](SharedRealm realm) {
12✔
1638
                auto db = TestHelper::get_db(realm);
12✔
1639
                auto wt = db->start_write();
12✔
1640
                _impl::client_reset::track_reset(*wt, previous_type);
12✔
1641
                wt->commit();
12✔
1642
            };
12✔
1643
        };
12✔
1644
        SECTION("a normal reset adds and removes a cycle detection flag") {
16✔
1645
            local_config.sync_config->client_resync_mode = ClientResyncMode::RecoverOrDiscard;
2✔
1646
            local_config.sync_config->notify_before_client_reset = [&](SharedRealm realm) {
2✔
1647
                REQUIRE_FALSE(has_reset_cycle_flag(realm));
2!
1648
                std::lock_guard lock(mtx);
2✔
1649
                ++before_callback_invocations;
2✔
1650
            };
2✔
1651
            local_config.sync_config->notify_after_client_reset = [&](SharedRealm, ThreadSafeReference realm_ref,
2✔
1652
                                                                      bool did_recover) {
2✔
1653
                SharedRealm realm = Realm::get_shared_realm(std::move(realm_ref), util::Scheduler::make_default());
2✔
1654
                auto flag = has_reset_cycle_flag(realm);
2✔
1655
                REQUIRE(bool(flag));
2!
1656
                REQUIRE(flag->type == ClientResyncMode::Recover);
2!
1657
                REQUIRE(did_recover);
2!
1658
                std::lock_guard lock(mtx);
2✔
1659
                ++after_callback_invocations;
2✔
1660
            };
2✔
1661
            make_reset(local_config, remote_config)
2✔
1662
                ->on_post_local_changes([&](SharedRealm realm) {
2✔
1663
                    REQUIRE_FALSE(has_reset_cycle_flag(realm));
2!
1664
                })
2✔
1665
                ->run();
2✔
1666
            REQUIRE(!err);
2!
1667
            REQUIRE(before_callback_invocations == 1);
2!
1668
            REQUIRE(after_callback_invocations == 1);
2!
1669
        }
2✔
1670

8✔
1671
        SECTION("a failed reset leaves a cycle detection flag") {
16✔
1672
            local_config.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
1673
            make_reset(local_config, remote_config)
2✔
1674
                ->make_local_changes([](SharedRealm realm) {
2✔
1675
                    auto table = realm->read_group().get_table("class_object");
2✔
1676
                    table->remove_column(table->add_column(type_Int, "new col"));
2✔
1677
                })
2✔
1678
                ->run();
2✔
1679
            local_config.sync_config.reset();
2✔
1680
            local_config.force_sync_history = true;
2✔
1681
            auto realm = Realm::get_shared_realm(local_config);
2✔
1682
            auto flag = has_reset_cycle_flag(realm);
2✔
1683
            REQUIRE(flag);
2!
1684
            CHECK(flag->type == ClientResyncMode::Recover);
2!
1685
        }
2✔
1686

8✔
1687
        SECTION("In DiscardLocal mode: a previous failed discard reset is detected and generates an error") {
16✔
1688
            local_config.sync_config->client_resync_mode = ClientResyncMode::DiscardLocal;
2✔
1689
            make_fake_previous_reset(ClientResyncMode::DiscardLocal);
2✔
1690
            make_reset(local_config, remote_config)->run();
2✔
1691
            timed_sleeping_wait_for([&]() -> bool {
2✔
1692
                return !!err;
2✔
1693
            });
2✔
1694
            REQUIRE(err.value()->is_client_reset_requested());
2!
1695
        }
2✔
1696
        SECTION("In Recover mode: a previous failed recover reset is detected and generates an error") {
16✔
1697
            local_config.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
1698
            make_fake_previous_reset(ClientResyncMode::Recover);
2✔
1699
            make_reset(local_config, remote_config)->run();
2✔
1700
            timed_sleeping_wait_for([&]() -> bool {
2✔
1701
                return !!err;
2✔
1702
            });
2✔
1703
            REQUIRE(err.value()->is_client_reset_requested());
2!
1704
        }
2✔
1705
        SECTION("In Recover mode: a previous failed discard reset is detected and generates an error") {
16✔
1706
            local_config.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
1707
            make_fake_previous_reset(ClientResyncMode::DiscardLocal);
2✔
1708
            make_reset(local_config, remote_config)->run();
2✔
1709
            timed_sleeping_wait_for([&]() -> bool {
2✔
1710
                return !!err;
2✔
1711
            });
2✔
1712
            REQUIRE(err.value()->is_client_reset_requested());
2!
1713
        }
2✔
1714
        SECTION("In RecoverOrDiscard mode: a previous failed discard reset is detected and generates an error") {
16✔
1715
            local_config.sync_config->client_resync_mode = ClientResyncMode::RecoverOrDiscard;
2✔
1716
            make_fake_previous_reset(ClientResyncMode::DiscardLocal);
2✔
1717
            make_reset(local_config, remote_config)->run();
2✔
1718
            timed_sleeping_wait_for([&]() -> bool {
2✔
1719
                return !!err;
2✔
1720
            });
2✔
1721
            REQUIRE(err.value()->is_client_reset_requested());
2!
1722
        }
2✔
1723
        const ObjectId added_pk = ObjectId::gen();
16✔
1724
        auto has_added_object = [&](SharedRealm realm) -> bool {
12✔
1725
            REQUIRE_NOTHROW(realm->refresh());
8✔
1726
            auto table = get_table(*realm, "object");
8✔
1727
            REQUIRE(table);
8!
1728
            ObjKey key = table->find_primary_key(added_pk);
8✔
1729
            return !!key;
8✔
1730
        };
8✔
1731
        SECTION(
16✔
1732
            "In RecoverOrDiscard mode: a previous failed recovery is detected and triggers a DiscardLocal reset") {
9✔
1733
            local_config.sync_config->client_resync_mode = ClientResyncMode::RecoverOrDiscard;
2✔
1734
            make_fake_previous_reset(ClientResyncMode::Recover);
2✔
1735
            local_config.sync_config->notify_after_client_reset = [&](SharedRealm before,
2✔
1736
                                                                      ThreadSafeReference after_ref,
2✔
1737
                                                                      bool did_recover) {
2✔
1738
                SharedRealm after = Realm::get_shared_realm(std::move(after_ref), util::Scheduler::make_default());
2✔
1739

1✔
1740
                REQUIRE(!did_recover);
2!
1741
                REQUIRE(has_added_object(before));
2!
1742
                REQUIRE(!has_added_object(after)); // discarded insert due to fallback to DiscardLocal mode
2!
1743
                std::lock_guard<std::mutex> lock(mtx);
2✔
1744
                ++after_callback_invocations;
2✔
1745
            };
2✔
1746
            make_reset(local_config, remote_config)
2✔
1747
                ->make_local_changes([&](SharedRealm realm) {
2✔
1748
                    auto table = get_table(*realm, "object");
2✔
1749
                    REQUIRE(table);
2!
1750
                    create_object(*realm, "object", {added_pk}, partition);
2✔
1751
                })
2✔
1752
                ->run();
2✔
1753
            timed_sleeping_wait_for(
2✔
1754
                [&]() -> bool {
2✔
1755
                    std::lock_guard<std::mutex> lock(mtx);
2✔
1756
                    return after_callback_invocations > 0 || err;
2!
1757
                },
2✔
1758
                std::chrono::seconds(120));
2✔
1759
            REQUIRE(!err);
2!
1760
        }
2✔
1761
        SECTION("In DiscardLocal mode: a previous failed recovery does not cause an error") {
16✔
1762
            local_config.sync_config->client_resync_mode = ClientResyncMode::DiscardLocal;
2✔
1763
            make_fake_previous_reset(ClientResyncMode::Recover);
2✔
1764
            local_config.sync_config->notify_after_client_reset = [&](SharedRealm before,
2✔
1765
                                                                      ThreadSafeReference after_ref,
2✔
1766
                                                                      bool did_recover) {
2✔
1767
                SharedRealm after = Realm::get_shared_realm(std::move(after_ref), util::Scheduler::make_default());
2✔
1768

1✔
1769
                REQUIRE(!did_recover);
2!
1770
                REQUIRE(has_added_object(before));
2!
1771
                REQUIRE(!has_added_object(after)); // not recovered
2!
1772
                std::lock_guard<std::mutex> lock(mtx);
2✔
1773
                ++after_callback_invocations;
2✔
1774
            };
2✔
1775
            make_reset(local_config, remote_config)
2✔
1776
                ->make_local_changes([&](SharedRealm realm) {
2✔
1777
                    auto table = get_table(*realm, "object");
2✔
1778
                    REQUIRE(table);
2!
1779
                    create_object(*realm, "object", {added_pk}, partition);
2✔
1780
                })
2✔
1781
                ->run();
2✔
1782
            timed_sleeping_wait_for(
2✔
1783
                [&]() -> bool {
2✔
1784
                    std::lock_guard<std::mutex> lock(mtx);
2✔
1785
                    return after_callback_invocations > 0 || err;
2!
1786
                },
2✔
1787
                std::chrono::seconds(120));
2✔
1788
            REQUIRE(!err);
2!
1789
        }
2✔
1790
    } // end cycle detection
16✔
1791
    SECTION("The server can prohibit recovery") {
84✔
1792
        const realm::AppSession& app_session = test_app_session.app_session();
4✔
1793
        auto sync_service = app_session.admin_api.get_sync_service(app_session.server_app_id);
4✔
1794
        auto sync_config = app_session.admin_api.get_config(app_session.server_app_id, sync_service);
4✔
1795
        REQUIRE(!sync_config.recovery_is_disabled);
4!
1796
        constexpr bool recovery_is_disabled = true;
4✔
1797
        app_session.admin_api.set_disable_recovery_to(app_session.server_app_id, sync_service.id, sync_config,
4✔
1798
                                                      recovery_is_disabled);
4✔
1799
        sync_config = app_session.admin_api.get_config(app_session.server_app_id, sync_service);
4✔
1800
        REQUIRE(sync_config.recovery_is_disabled);
4!
1801

2✔
1802
        SECTION("In Recover mode, a manual client reset is triggered") {
4✔
1803
            local_config.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
1804
            ThreadSafeSyncError err;
2✔
1805
            local_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) {
2✔
1806
                err = error;
2✔
1807
            };
2✔
1808
            make_reset(local_config, remote_config)
2✔
1809
                ->on_post_reset([&](SharedRealm) {
2✔
1810
                    util::EventLoop::main().run_until([&] {
3✔
1811
                        return bool(err);
3✔
1812
                    });
3✔
1813
                })
2✔
1814
                ->run();
2✔
1815
            REQUIRE(err);
2!
1816
            SyncError error = *err.value();
2✔
1817
            REQUIRE(error.is_client_reset_requested());
2!
1818
            REQUIRE(error.user_info.size() >= 2);
2!
1819
            REQUIRE(error.user_info.count(SyncError::c_original_file_path_key) == 1);
2!
1820
            REQUIRE(error.user_info.count(SyncError::c_recovery_file_path_key) == 1);
2!
1821
        }
2✔
1822
        SECTION("In RecoverOrDiscard mode, DiscardLocal is selected") {
4✔
1823
            local_config.sync_config->client_resync_mode = ClientResyncMode::RecoverOrDiscard;
2✔
1824
            constexpr int64_t new_value = 123456;
2✔
1825
            make_reset(local_config, remote_config)
2✔
1826
                ->make_local_changes([&](SharedRealm local) {
2✔
1827
                    auto table = get_table(*local, "object");
2✔
1828
                    REQUIRE(table);
2!
1829
                    REQUIRE(table->size() == 1);
2!
1830
                    auto obj = create_object(*local, "object", ObjectId::gen(), partition);
2✔
1831
                    auto col = obj.get_table()->get_column_key("value");
2✔
1832
                    REQUIRE(table->size() == 2);
2!
1833
                    obj.set(col, new_value);
2✔
1834
                })
2✔
1835
                ->on_post_local_changes([&](SharedRealm realm) {
2✔
1836
                    setup_listeners(realm);
2✔
1837
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
1838
                    CHECK(results.size() == 2);
2!
1839
                })
2✔
1840
                ->on_post_reset([&](SharedRealm realm) {
2✔
1841
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
1842
                    CHECK(results.size() == 1); // insert was discarded
2!
1843
                    CHECK(results.get<Obj>(0).get<Int>("value") == 6);
2!
1844
                    CHECK(object.is_valid());
2!
1845
                    CHECK(object.get_obj().get<Int>("value") == 6);
2!
1846
                })
2✔
1847
                ->run();
2✔
1848
        }
2✔
1849
    } // end: The server can prohibit recovery
4✔
1850
}
84✔
1851

1852
TEST_CASE("sync: Client reset during async open", "[sync][pbs][client reset][baas]") {
2✔
1853
    const reset_utils::Partition partition{"realm_id", random_string(20)};
2✔
1854
    Property partition_prop = {partition.property_name, PropertyType::String | PropertyType::Nullable};
2✔
1855
    Schema schema{
2✔
1856
        {"object",
2✔
1857
         {
2✔
1858
             {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
1859
             {"value", PropertyType::String},
2✔
1860
             partition_prop,
2✔
1861
         }},
2✔
1862
    };
2✔
1863

1✔
1864
    auto server_app_config = minimal_app_config("client_reset_tests", schema);
2✔
1865
    server_app_config.partition_key = partition_prop;
2✔
1866
    TestAppSession test_app_session(create_app(server_app_config));
2✔
1867
    auto app = test_app_session.app();
2✔
1868

1✔
1869
    create_user_and_log_in(app);
2✔
1870
    SyncTestFile realm_config(app->current_user(), partition.value, std::nullopt,
2✔
1871
                              [](std::shared_ptr<SyncSession>, SyncError) { /*noop*/ });
1✔
1872
    realm_config.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
1873

1✔
1874
    bool client_reset_triggered = false;
2✔
1875
    realm_config.sync_config->on_sync_client_event_hook = [&](std::weak_ptr<SyncSession> weak_sess,
2✔
1876
                                                              const SyncClientHookData& event_data) mutable {
12✔
1877
        auto sess = weak_sess.lock();
12✔
1878
        if (!sess) {
12✔
NEW
1879
            return SyncClientHookAction::NoAction;
×
NEW
1880
        }
×
1881
        if (sess->path() != realm_config.path) {
12✔
1882
            return SyncClientHookAction::NoAction;
4✔
1883
        }
4✔
1884

4✔
1885
        if (event_data.event != SyncClientHookEvent::DownloadMessageReceived) {
8✔
1886
            return SyncClientHookAction::NoAction;
4✔
1887
        }
4✔
1888

2✔
1889
        if (client_reset_triggered) {
4✔
1890
            return SyncClientHookAction::NoAction;
2✔
1891
        }
2✔
1892
        client_reset_triggered = true;
2✔
1893
        reset_utils::trigger_client_reset(test_app_session.app_session(), *sess);
2✔
1894
        return SyncClientHookAction::SuspendWithRetryableError;
2✔
1895
    };
2✔
1896

1✔
1897
    // Expected behaviour is that the frozen realm passed in the callback should have no
1✔
1898
    // schema initialized if a client reset happens during an async open and the realm has never been opened before.
1✔
1899
    // SDK's should handle any edge cases which require the use of a schema i.e
1✔
1900
    // calling set_schema_subset(...)
1✔
1901
    auto before_callback_called = util::make_promise_future<void>();
2✔
1902
    realm_config.sync_config->notify_before_client_reset = [&](std::shared_ptr<Realm> realm) {
2✔
1903
        CHECK(realm->schema_version() == ObjectStore::NotVersioned);
2!
1904
        before_callback_called.promise.emplace_value();
2✔
1905
    };
2✔
1906

1✔
1907
    auto after_callback_called = util::make_promise_future<void>();
2✔
1908
    realm_config.sync_config->notify_after_client_reset = [&](std::shared_ptr<Realm> realm, ThreadSafeReference,
2✔
1909
                                                              bool) {
2✔
1910
        CHECK(realm->schema_version() == ObjectStore::NotVersioned);
2!
1911
        after_callback_called.promise.emplace_value();
2✔
1912
    };
2✔
1913

1✔
1914
    auto realm_task = Realm::get_synchronized_realm(realm_config);
2✔
1915
    auto realm_pf = util::make_promise_future<SharedRealm>();
2✔
1916
    realm_task->start([&](ThreadSafeReference ref, std::exception_ptr ex) {
2✔
1917
        try {
2✔
1918
            if (ex) {
2✔
1919
                std::rethrow_exception(ex);
×
1920
            }
×
1921
            auto realm = Realm::get_shared_realm(std::move(ref));
2✔
1922
            realm_pf.promise.emplace_value(std::move(realm));
2✔
1923
        }
2✔
1924
        catch (...) {
1✔
NEW
1925
            realm_pf.promise.set_error(exception_to_status());
×
UNCOV
1926
        }
×
1927
    });
2✔
1928
    auto realm = realm_pf.future.get();
2✔
1929
    before_callback_called.future.get();
2✔
1930
    after_callback_called.future.get();
2✔
1931
}
2✔
1932

1933
#endif // REALM_ENABLE_AUTH_TESTS
1934

1935
namespace cf = realm::collection_fixtures;
1936
TEMPLATE_TEST_CASE("client reset types", "[sync][pbs][client reset]", cf::MixedVal, cf::Int, cf::Bool, cf::Float,
1937
                   cf::Double, cf::String, cf::Binary, cf::Date, cf::OID, cf::Decimal, cf::UUID,
1938
                   cf::BoxedOptional<cf::Int>, cf::BoxedOptional<cf::Bool>, cf::BoxedOptional<cf::Float>,
1939
                   cf::BoxedOptional<cf::Double>, cf::BoxedOptional<cf::OID>, cf::BoxedOptional<cf::UUID>,
1940
                   cf::UnboxedOptional<cf::String>, cf::UnboxedOptional<cf::Binary>, cf::UnboxedOptional<cf::Date>,
1941
                   cf::UnboxedOptional<cf::Decimal>)
1942
{
2,256✔
1943
    auto values = TestType::values();
2,256✔
1944
    using T = typename TestType::Type;
2,256✔
1945

1,128✔
1946
    if (!util::EventLoop::has_implementation())
2,256✔
1947
        return;
×
1948

1,128✔
1949
    TestSyncManager init_sync_manager;
2,256✔
1950
    SyncTestFile config(init_sync_manager.app(), "default");
2,256✔
1951
    config.cache = false;
2,256✔
1952
    config.automatic_change_notifications = false;
2,256✔
1953
    ClientResyncMode test_mode = GENERATE(ClientResyncMode::DiscardLocal, ClientResyncMode::Recover);
2,256✔
1954
    CAPTURE(test_mode);
2,256✔
1955
    config.sync_config->client_resync_mode = test_mode;
2,256✔
1956
    config.schema = Schema{
2,256✔
1957
        {"object",
2,256✔
1958
         {
2,256✔
1959
             {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2,256✔
1960
             {"value", PropertyType::Int},
2,256✔
1961
         }},
2,256✔
1962
        {"test type",
2,256✔
1963
         {{"_id", PropertyType::Int, Property::IsPrimary{true}},
2,256✔
1964
          {"value", TestType::property_type},
2,256✔
1965
          {"list", PropertyType::Array | TestType::property_type},
2,256✔
1966
          {"dictionary", PropertyType::Dictionary | TestType::property_type},
2,256✔
1967
          {"set", PropertyType::Set | TestType::property_type}}},
2,256✔
1968
    };
2,256✔
1969

1,128✔
1970
    SyncTestFile config2(init_sync_manager.app(), "default");
2,256✔
1971
    config2.schema = config.schema;
2,256✔
1972

1,128✔
1973
    Results results;
2,256✔
1974
    Object object;
2,256✔
1975
    CollectionChangeSet object_changes, results_changes;
2,256✔
1976
    NotificationToken object_token, results_token;
2,256✔
1977
    auto setup_listeners = [&](SharedRealm realm) {
2,256✔
1978
        results = Results(realm, ObjectStore::table_for_object_type(realm->read_group(), "test type"))
2,256✔
1979
                      .sort({{{"_id", true}}});
2,256✔
1980
        if (results.size() >= 1) {
2,256✔
1981
            auto obj = *ObjectStore::table_for_object_type(realm->read_group(), "test type")->begin();
2,256✔
1982
            object = Object(realm, obj);
2,256✔
1983
            object_token = object.add_notification_callback([&](CollectionChangeSet changes) {
3,510✔
1984
                object_changes = std::move(changes);
3,510✔
1985
            });
3,510✔
1986
        }
2,256✔
1987
        results_token = results.add_notification_callback([&](CollectionChangeSet changes) {
3,510✔
1988
            results_changes = std::move(changes);
3,510✔
1989
        });
3,510✔
1990
    };
2,256✔
1991

1,128✔
1992
    auto check_list = [&](Obj obj, std::vector<T>& expected) {
3,024✔
1993
        ColKey col = obj.get_table()->get_column_key("list");
3,024✔
1994
        auto actual = obj.get_list_values<T>(col);
3,024✔
1995
        REQUIRE(actual == expected);
3,024!
1996
    };
3,024✔
1997

1,128✔
1998
    auto check_dictionary = [&](Obj obj, std::map<std::string, Mixed>& expected) {
2,136✔
1999
        ColKey col = obj.get_table()->get_column_key("dictionary");
2,016✔
2000
        Dictionary dict = obj.get_dictionary(col);
2,016✔
2001
        REQUIRE(dict.size() == expected.size());
2,016!
2002
        for (auto& pair : expected) {
3,612✔
2003
            auto it = dict.find(pair.first);
3,612✔
2004
            REQUIRE(it != dict.end());
3,612!
2005
            REQUIRE((*it).second == pair.second);
3,612!
2006
        }
3,612✔
2007
    };
2,016✔
2008

1,128✔
2009
    auto check_set = [&](Obj obj, std::set<Mixed>& expected) {
2,688✔
2010
        ColKey col = obj.get_table()->get_column_key("set");
2,688✔
2011
        SetBasePtr set = obj.get_setbase_ptr(col);
2,688✔
2012
        REQUIRE(set->size() == expected.size());
2,688!
2013
        for (auto& value : expected) {
3,024✔
2014
            auto ndx = set->find_any(value);
3,024✔
2015
            CAPTURE(value);
3,024✔
2016
            REQUIRE(ndx != realm::not_found);
3,024!
2017
        }
3,024✔
2018
    };
2,688✔
2019

1,128✔
2020
    std::unique_ptr<reset_utils::TestClientReset> test_reset =
2,256✔
2021
        reset_utils::make_fake_local_client_reset(config, config2);
2,256✔
2022

1,128✔
2023
    SECTION("property") {
2,256✔
2024
        REQUIRE(values.size() >= 2);
324!
2025
        REQUIRE(values[0] != values[1]);
324!
2026
        int64_t pk_val = 0;
324✔
2027
        T initial_value = values[0];
324✔
2028

162✔
2029
        auto set_value = [](SharedRealm realm, T value) {
648✔
2030
            auto table = get_table(*realm, "test type");
648✔
2031
            REQUIRE(table);
648!
2032
            REQUIRE(table->size() == 1);
648!
2033
            ColKey col = table->get_column_key("value");
648✔
2034
            table->begin()->set<T>(col, value);
648✔
2035
        };
648✔
2036
        auto check_value = [](Obj obj, T value) {
1,296✔
2037
            ColKey col = obj.get_table()->get_column_key("value");
1,296✔
2038
            REQUIRE(obj.get<T>(col) == value);
1,296!
2039
        };
1,296✔
2040

162✔
2041
        test_reset->setup([&pk_val, &initial_value](SharedRealm realm) {
648✔
2042
            auto table = get_table(*realm, "test type");
648✔
2043
            REQUIRE(table);
648!
2044
            auto obj = table->create_object_with_primary_key(pk_val);
648✔
2045
            ColKey col = table->get_column_key("value");
648✔
2046
            obj.set<T>(col, initial_value);
648✔
2047
        });
648✔
2048

162✔
2049
        auto reset_property = [&](T local_state, T remote_state) {
324✔
2050
            test_reset
324✔
2051
                ->make_local_changes([&](SharedRealm local_realm) {
324✔
2052
                    set_value(local_realm, local_state);
324✔
2053
                })
324✔
2054
                ->make_remote_changes([&](SharedRealm remote_realm) {
324✔
2055
                    set_value(remote_realm, remote_state);
324✔
2056
                })
324✔
2057
                ->on_post_local_changes([&](SharedRealm realm) {
324✔
2058
                    setup_listeners(realm);
324✔
2059
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
324✔
2060
                    CHECK(results.size() == 1);
324!
2061
                    CHECK(results.get<Obj>(0).get<Int>("_id") == pk_val);
324!
2062
                    CHECK(object.is_valid());
324!
2063
                    check_value(results.get<Obj>(0), local_state);
324✔
2064
                    check_value(object.get_obj(), local_state);
324✔
2065
                })
324✔
2066
                ->on_post_reset([&](SharedRealm realm) {
324✔
2067
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
324✔
2068

162✔
2069
                    CHECK(results.size() == 1);
324!
2070
                    CHECK(object.is_valid());
324!
2071
                    T expected_state = (test_mode == ClientResyncMode::DiscardLocal) ? remote_state : local_state;
324✔
2072
                    check_value(results.get<Obj>(0), expected_state);
324✔
2073
                    check_value(object.get_obj(), expected_state);
324✔
2074
                    if (local_state == expected_state) {
324✔
2075
                        REQUIRE_INDICES(results_changes.modifications);
162!
2076
                        REQUIRE_INDICES(object_changes.modifications);
162!
2077
                    }
162✔
2078
                    else {
162✔
2079
                        REQUIRE_INDICES(results_changes.modifications, 0);
162!
2080
                        REQUIRE_INDICES(object_changes.modifications, 0);
162!
2081
                    }
162✔
2082
                    REQUIRE_INDICES(results_changes.insertions);
324!
2083
                    REQUIRE_INDICES(results_changes.deletions);
324!
2084
                    REQUIRE_INDICES(object_changes.insertions);
324!
2085
                    REQUIRE_INDICES(object_changes.deletions);
324!
2086
                })
324✔
2087
                ->run();
324✔
2088
        };
324✔
2089

162✔
2090
        SECTION("modify") {
324✔
2091
            reset_property(values[0], values[1]);
84✔
2092
        }
84✔
2093
        SECTION("modify opposite") {
324✔
2094
            reset_property(values[1], values[0]);
84✔
2095
        }
84✔
2096
        // verify whatever other test values are provided (type bool only has two)
162✔
2097
        for (size_t i = 2; i < values.size(); ++i) {
1,928✔
2098
            SECTION(util::format("modify to value: %1", i)) {
1,604✔
2099
                reset_property(values[0], values[i]);
156✔
2100
            }
156✔
2101
        }
1,604✔
2102
    }
324✔
2103

1,128✔
2104
    SECTION("lists") {
2,256✔
2105
        REQUIRE(values.size() >= 2);
756!
2106
        REQUIRE(values[0] != values[1]);
756!
2107
        int64_t pk_val = 0;
756✔
2108
        // MSVC doesn't seem to automatically capture a templated variable so
378✔
2109
        // the following lambda is explicit about it's captures
378✔
2110
        T initial_list_value = values[0];
756✔
2111
        test_reset->setup([&pk_val, &initial_list_value](SharedRealm realm) {
1,512✔
2112
            auto table = get_table(*realm, "test type");
1,512✔
2113
            REQUIRE(table);
1,512!
2114
            auto obj = table->create_object_with_primary_key(pk_val);
1,512✔
2115
            ColKey col = table->get_column_key("list");
1,512✔
2116
            obj.template set_list_values<T>(col, {initial_list_value});
1,512✔
2117
        });
1,512✔
2118

378✔
2119
        auto reset_list = [&](std::vector<T>&& local_state, std::vector<T>&& remote_state) {
756✔
2120
            test_reset
756✔
2121
                ->make_local_changes([&](SharedRealm local_realm) {
756✔
2122
                    auto table = get_table(*local_realm, "test type");
756✔
2123
                    REQUIRE(table);
756!
2124
                    REQUIRE(table->size() == 1);
756!
2125
                    ColKey col = table->get_column_key("list");
756✔
2126
                    table->begin()->template set_list_values<T>(col, local_state);
756✔
2127
                })
756✔
2128
                ->make_remote_changes([&](SharedRealm remote_realm) {
756✔
2129
                    auto table = get_table(*remote_realm, "test type");
756✔
2130
                    REQUIRE(table);
756!
2131
                    REQUIRE(table->size() == 1);
756!
2132
                    ColKey col = table->get_column_key("list");
756✔
2133
                    table->begin()->template set_list_values<T>(col, remote_state);
756✔
2134
                })
756✔
2135
                ->on_post_local_changes([&](SharedRealm realm) {
756✔
2136
                    setup_listeners(realm);
756✔
2137
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
756✔
2138
                    CHECK(results.size() == 1);
756!
2139
                    CHECK(results.get<Obj>(0).get<Int>("_id") == pk_val);
756!
2140
                    CHECK(object.is_valid());
756!
2141
                    check_list(results.get<Obj>(0), local_state);
756✔
2142
                    check_list(object.get_obj(), local_state);
756✔
2143
                })
756✔
2144
                ->on_post_reset([&](SharedRealm realm) {
756✔
2145
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
756✔
2146

378✔
2147
                    CHECK(results.size() == 1);
756!
2148
                    CHECK(object.is_valid());
756!
2149
                    std::vector<T>& expected_state = remote_state;
756✔
2150
                    if (test_mode == ClientResyncMode::Recover) {
756✔
2151
                        expected_state = local_state;
378✔
2152
                    }
378✔
2153
                    check_list(results.get<Obj>(0), expected_state);
756✔
2154
                    check_list(object.get_obj(), expected_state);
756✔
2155
                    if (local_state == expected_state) {
756✔
2156
                        REQUIRE_INDICES(results_changes.modifications);
462!
2157
                        REQUIRE_INDICES(object_changes.modifications);
462!
2158
                    }
462✔
2159
                    else {
294✔
2160
                        REQUIRE_INDICES(results_changes.modifications, 0);
294!
2161
                        REQUIRE_INDICES(object_changes.modifications, 0);
294!
2162
                    }
294✔
2163
                    REQUIRE_INDICES(results_changes.insertions);
756!
2164
                    REQUIRE_INDICES(results_changes.deletions);
756!
2165
                    REQUIRE_INDICES(object_changes.insertions);
756!
2166
                    REQUIRE_INDICES(object_changes.deletions);
756!
2167
                })
756✔
2168
                ->run();
756✔
2169
        };
756✔
2170

378✔
2171
        SECTION("modify") {
756✔
2172
            reset_list({values[0]}, {values[1]});
84✔
2173
        }
84✔
2174
        SECTION("modify opposite") {
756✔
2175
            reset_list({values[1]}, {values[0]});
84✔
2176
        }
84✔
2177
        SECTION("empty remote") {
756✔
2178
            reset_list({values[1], values[0], values[1]}, {});
84✔
2179
        }
84✔
2180
        SECTION("empty local") {
756✔
2181
            reset_list({}, {values[0], values[1]});
84✔
2182
        }
84✔
2183
        SECTION("empty both") {
756✔
2184
            reset_list({}, {});
84✔
2185
        }
84✔
2186
        SECTION("equal suffix") {
756✔
2187
            reset_list({values[0], values[0], values[1]}, {values[0], values[1]});
84✔
2188
        }
84✔
2189
        SECTION("equal prefix") {
756✔
2190
            reset_list({values[0]}, {values[0], values[1], values[1]});
84✔
2191
        }
84✔
2192
        SECTION("equal lists") {
756✔
2193
            reset_list({values[0]}, {values[0]});
84✔
2194
        }
84✔
2195
        SECTION("equal middle") {
756✔
2196
            reset_list({values[0], values[1], values[0]}, {values[1], values[1], values[1]});
84✔
2197
        }
84✔
2198
    }
756✔
2199

1,128✔
2200
    SECTION("dictionary") {
2,256✔
2201
        REQUIRE(values.size() >= 2);
504!
2202
        REQUIRE(values[0] != values[1]);
504!
2203
        int64_t pk_val = 0;
504✔
2204
        std::string dict_key = "hello";
504✔
2205
        test_reset->setup([&](SharedRealm realm) {
1,008✔
2206
            auto table = get_table(*realm, "test type");
1,008✔
2207
            REQUIRE(table);
1,008!
2208
            auto obj = table->create_object_with_primary_key(pk_val);
1,008✔
2209
            ColKey col = table->get_column_key("dictionary");
1,008✔
2210
            Dictionary dict = obj.get_dictionary(col);
1,008✔
2211
            dict.insert(dict_key, Mixed{values[0]});
1,008✔
2212
        });
1,008✔
2213

252✔
2214
        auto reset_dictionary = [&](std::map<std::string, Mixed>&& local_state,
504✔
2215
                                    std::map<std::string, Mixed>&& remote_state) {
504✔
2216
            test_reset
504✔
2217
                ->make_local_changes([&](SharedRealm local_realm) {
504✔
2218
                    auto table = get_table(*local_realm, "test type");
504✔
2219
                    REQUIRE(table);
504!
2220
                    REQUIRE(table->size() == 1);
504!
2221
                    ColKey col = table->get_column_key("dictionary");
504✔
2222
                    Dictionary dict = table->begin()->get_dictionary(col);
504✔
2223
                    for (auto& pair : local_state) {
756✔
2224
                        dict.insert(pair.first, pair.second);
756✔
2225
                    }
756✔
2226
                    for (auto it = dict.begin(); it != dict.end();) {
1,428✔
2227
                        auto found = std::any_of(local_state.begin(), local_state.end(), [&](auto pair) {
2,016✔
2228
                            return Mixed{pair.first} == (*it).first && Mixed{pair.second} == (*it).second;
2,016✔
2229
                        });
2,016✔
2230
                        if (!found) {
924✔
2231
                            it = dict.erase(it);
168✔
2232
                        }
168✔
2233
                        else {
756✔
2234
                            ++it;
756✔
2235
                        }
756✔
2236
                    }
924✔
2237
                })
504✔
2238
                ->make_remote_changes([&](SharedRealm remote_realm) {
504✔
2239
                    auto table = get_table(*remote_realm, "test type");
504✔
2240
                    REQUIRE(table);
504!
2241
                    REQUIRE(table->size() == 1);
504!
2242
                    ColKey col = table->get_column_key("dictionary");
504✔
2243
                    Dictionary dict = table->begin()->get_dictionary(col);
504✔
2244
                    for (auto& pair : remote_state) {
1,008✔
2245
                        dict.insert(pair.first, pair.second);
1,008✔
2246
                    }
1,008✔
2247
                    for (auto it = dict.begin(); it != dict.end();) {
1,680✔
2248
                        auto found = std::any_of(remote_state.begin(), remote_state.end(), [&](auto pair) {
2,772✔
2249
                            return Mixed{pair.first} == (*it).first && Mixed{pair.second} == (*it).second;
2,772✔
2250
                        });
2,772✔
2251
                        if (!found) {
1,176✔
2252
                            it = dict.erase(it);
168✔
2253
                        }
168✔
2254
                        else {
1,008✔
2255
                            ++it;
1,008✔
2256
                        }
1,008✔
2257
                    }
1,176✔
2258
                })
504✔
2259
                ->on_post_local_changes([&](SharedRealm realm) {
504✔
2260
                    setup_listeners(realm);
504✔
2261
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
504✔
2262
                    CHECK(results.size() == 1);
504!
2263
                    CHECK(results.get<Obj>(0).get<Int>("_id") == pk_val);
504!
2264
                    CHECK(object.is_valid());
504!
2265
                    check_dictionary(results.get<Obj>(0), local_state);
504✔
2266
                    check_dictionary(object.get_obj(), local_state);
504✔
2267
                })
504✔
2268
                ->on_post_reset([&](SharedRealm realm) {
504✔
2269
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
504✔
2270
                    CHECK(results.size() == 1);
504!
2271
                    CHECK(object.is_valid());
504!
2272

252✔
2273
                    auto& expected_state = remote_state;
504✔
2274
                    if (test_mode == ClientResyncMode::Recover) {
504✔
2275
                        for (auto it : local_state) {
378✔
2276
                            expected_state[it.first] = it.second;
378✔
2277
                        }
378✔
2278
                        if (local_state.find(dict_key) == local_state.end()) {
252✔
2279
                            expected_state.erase(dict_key); // explict erasure of initial state occurred
84✔
2280
                        }
84✔
2281
                    }
252✔
2282
                    check_dictionary(results.get<Obj>(0), expected_state);
504✔
2283
                    check_dictionary(object.get_obj(), expected_state);
504✔
2284
                    if (local_state == expected_state) {
504✔
2285
                        REQUIRE_INDICES(results_changes.modifications);
168!
2286
                        REQUIRE_INDICES(object_changes.modifications);
168!
2287
                    }
168✔
2288
                    else {
336✔
2289
                        REQUIRE_INDICES(results_changes.modifications, 0);
336!
2290
                        REQUIRE_INDICES(object_changes.modifications, 0);
336!
2291
                    }
336✔
2292
                    REQUIRE_INDICES(results_changes.insertions);
504!
2293
                    REQUIRE_INDICES(results_changes.deletions);
504!
2294
                    REQUIRE_INDICES(object_changes.insertions);
504!
2295
                    REQUIRE_INDICES(object_changes.deletions);
504!
2296
                })
504✔
2297
                ->run();
504✔
2298
        };
504✔
2299

252✔
2300
        SECTION("modify") {
504✔
2301
            reset_dictionary({{dict_key, Mixed{values[0]}}}, {{dict_key, Mixed{values[1]}}});
84✔
2302
        }
84✔
2303
        SECTION("modify opposite") {
504✔
2304
            reset_dictionary({{dict_key, Mixed{values[1]}}}, {{dict_key, Mixed{values[0]}}});
84✔
2305
        }
84✔
2306
        SECTION("modify complex") {
504✔
2307
            std::map<std::string, Mixed> local;
84✔
2308
            local.emplace(std::make_pair("adam", Mixed(values[0])));
84✔
2309
            local.emplace(std::make_pair("bernie", Mixed(values[0])));
84✔
2310
            local.emplace(std::make_pair("david", Mixed(values[0])));
84✔
2311
            local.emplace(std::make_pair("eric", Mixed(values[0])));
84✔
2312
            local.emplace(std::make_pair("frank", Mixed(values[1])));
84✔
2313
            std::map<std::string, Mixed> remote;
84✔
2314
            remote.emplace(std::make_pair("adam", Mixed(values[0])));
84✔
2315
            remote.emplace(std::make_pair("bernie", Mixed(values[1])));
84✔
2316
            remote.emplace(std::make_pair("carl", Mixed(values[0])));
84✔
2317
            remote.emplace(std::make_pair("david", Mixed(values[1])));
84✔
2318
            remote.emplace(std::make_pair("frank", Mixed(values[0])));
84✔
2319
            reset_dictionary(std::move(local), std::move(remote));
84✔
2320
        }
84✔
2321
        SECTION("empty remote") {
504✔
2322
            reset_dictionary({{dict_key, Mixed{values[1]}}}, {});
84✔
2323
        }
84✔
2324
        SECTION("empty local") {
504✔
2325
            reset_dictionary({}, {{dict_key, Mixed{values[1]}}});
84✔
2326
        }
84✔
2327
        SECTION("extra values on remote") {
504✔
2328
            reset_dictionary({{dict_key, Mixed{values[0]}}}, {{dict_key, Mixed{values[0]}},
84✔
2329
                                                              {"world", Mixed{values[1]}},
84✔
2330
                                                              {"foo", Mixed{values[1]}},
84✔
2331
                                                              {"aaa", Mixed{values[0]}}});
84✔
2332
        }
84✔
2333
    }
504✔
2334

1,128✔
2335
    SECTION("set") {
2,256✔
2336
        int64_t pk_val = 0;
672✔
2337

336✔
2338
        auto reset_set = [&](std::set<Mixed> local_state, std::set<Mixed> remote_state) {
672✔
2339
            test_reset
672✔
2340
                ->make_local_changes([&](SharedRealm local_realm) {
672✔
2341
                    auto table = get_table(*local_realm, "test type");
672✔
2342
                    REQUIRE(table);
672!
2343
                    ColKey col = table->get_column_key("set");
672✔
2344
                    SetBasePtr set = table->begin()->get_setbase_ptr(col);
672✔
2345
                    for (size_t i = set->size(); i > 0; --i) {
1,344✔
2346
                        Mixed si = set->get_any(i - 1);
672✔
2347
                        if (local_state.find(si) == local_state.end()) {
672✔
2348
                            set->erase_any(si);
252✔
2349
                        }
252✔
2350
                    }
672✔
2351
                    for (auto e : local_state) {
756✔
2352
                        set->insert_any(e);
756✔
2353
                    }
756✔
2354
                })
672✔
2355
                ->make_remote_changes([&](SharedRealm remote_realm) {
672✔
2356
                    auto table = get_table(*remote_realm, "test type");
672✔
2357
                    REQUIRE(table);
672!
2358
                    ColKey col = table->get_column_key("set");
672✔
2359
                    SetBasePtr set = table->begin()->get_setbase_ptr(col);
672✔
2360
                    for (size_t i = set->size(); i > 0; --i) {
1,344✔
2361
                        Mixed si = set->get_any(i - 1);
672✔
2362
                        if (remote_state.find(si) == remote_state.end()) {
672✔
2363
                            set->erase_any(si);
336✔
2364
                        }
336✔
2365
                    }
672✔
2366
                    for (auto e : remote_state) {
756✔
2367
                        set->insert_any(e);
756✔
2368
                    }
756✔
2369
                })
672✔
2370
                ->on_post_local_changes([&](SharedRealm realm) {
672✔
2371
                    setup_listeners(realm);
672✔
2372
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
672✔
2373
                    CHECK(results.size() == 1);
672!
2374
                    CHECK(results.get<Obj>(0).get<Int>("_id") == pk_val);
672!
2375
                    CHECK(object.is_valid());
672!
2376
                    check_set(results.get<Obj>(0), local_state);
672✔
2377
                    check_set(object.get_obj(), local_state);
672✔
2378
                })
672✔
2379
                ->on_post_reset([&](SharedRealm realm) {
672✔
2380
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
672✔
2381
                    CHECK(results.size() == 1);
672!
2382
                    CHECK(object.is_valid());
672!
2383
                    std::set<Mixed>& expected = remote_state;
672✔
2384
                    if (test_mode == ClientResyncMode::Recover) {
672✔
2385
                        bool do_erase_initial = remote_state.find(Mixed{values[0]}) == remote_state.end() ||
336✔
2386
                                                local_state.find(Mixed{values[0]}) == local_state.end();
252✔
2387
                        for (auto& e : local_state) {
378✔
2388
                            expected.insert(e);
378✔
2389
                        }
378✔
2390
                        if (do_erase_initial) {
336✔
2391
                            expected.erase(Mixed{values[0]}); // explicit erase of initial element occurred
252✔
2392
                        }
252✔
2393
                    }
336✔
2394
                    check_set(results.get<Obj>(0), expected);
672✔
2395
                    check_set(object.get_obj(), expected);
672✔
2396
                    if (local_state == expected) {
672✔
2397
                        REQUIRE_INDICES(results_changes.modifications);
210!
2398
                        REQUIRE_INDICES(object_changes.modifications);
210!
2399
                    }
210✔
2400
                    else {
462✔
2401
                        REQUIRE_INDICES(results_changes.modifications, 0);
462!
2402
                        REQUIRE_INDICES(object_changes.modifications, 0);
462!
2403
                    }
462✔
2404
                    REQUIRE_INDICES(results_changes.insertions);
672!
2405
                    REQUIRE_INDICES(results_changes.deletions);
672!
2406
                    REQUIRE_INDICES(object_changes.insertions);
672!
2407
                    REQUIRE_INDICES(object_changes.deletions);
672!
2408
                })
672✔
2409
                ->run();
672✔
2410
        };
672✔
2411

336✔
2412
        REQUIRE(values.size() >= 2);
672!
2413
        REQUIRE(values[0] != values[1]);
672!
2414
        test_reset->setup([&](SharedRealm realm) {
1,344✔
2415
            auto table = get_table(*realm, "test type");
1,344✔
2416
            REQUIRE(table);
1,344!
2417
            auto obj = table->create_object_with_primary_key(pk_val);
1,344✔
2418
            ColKey col = table->get_column_key("set");
1,344✔
2419
            SetBasePtr set = obj.get_setbase_ptr(col);
1,344✔
2420
            set->insert_any(Mixed{values[0]});
1,344✔
2421
        });
1,344✔
2422

336✔
2423
        SECTION("modify") {
672✔
2424
            reset_set({Mixed{values[0]}}, {Mixed{values[1]}});
84✔
2425
        }
84✔
2426
        SECTION("modify opposite") {
672✔
2427
            reset_set({Mixed{values[1]}}, {Mixed{values[0]}});
84✔
2428
        }
84✔
2429
        SECTION("empty remote") {
672✔
2430
            reset_set({Mixed{values[1]}, Mixed{values[0]}}, {});
84✔
2431
        }
84✔
2432
        SECTION("empty local") {
672✔
2433
            reset_set({}, {Mixed{values[0]}, Mixed{values[1]}});
84✔
2434
        }
84✔
2435
        SECTION("empty both") {
672✔
2436
            reset_set({}, {});
84✔
2437
        }
84✔
2438
        SECTION("equal suffix") {
672✔
2439
            reset_set({Mixed{values[0]}, Mixed{values[1]}}, {Mixed{values[1]}});
84✔
2440
        }
84✔
2441
        SECTION("equal prefix") {
672✔
2442
            reset_set({Mixed{values[0]}}, {Mixed{values[1]}, Mixed{values[0]}});
84✔
2443
        }
84✔
2444
        SECTION("equal lists") {
672✔
2445
            reset_set({Mixed{values[0]}, Mixed{values[1]}}, {Mixed{values[0]}, Mixed{values[1]}});
84✔
2446
        }
84✔
2447
    }
672✔
2448
}
2,256✔
2449

2450
namespace test_instructions {
2451

2452
struct Add {
2453
    Add(util::Optional<int64_t> key)
2454
        : pk(key)
2455
    {
1,348✔
2456
    }
1,348✔
2457
    util::Optional<int64_t> pk;
2458
};
2459

2460
struct Remove {
2461
    Remove(util::Optional<int64_t> key)
2462
        : pk(key)
2463
    {
824✔
2464
    }
824✔
2465
    util::Optional<int64_t> pk;
2466
};
2467

2468
struct Clear {};
2469

2470
struct RemoveObject {
2471
    RemoveObject(std::string_view name, util::Optional<int64_t> key)
2472
        : pk(key)
2473
        , class_name(name)
2474
    {
468✔
2475
    }
468✔
2476
    util::Optional<int64_t> pk;
2477
    std::string_view class_name;
2478
};
2479

2480
struct CreateObject {
2481
    CreateObject(std::string_view name, util::Optional<int64_t> key)
2482
        : pk(key)
2483
        , class_name(name)
2484
    {
100✔
2485
    }
100✔
2486
    util::Optional<int64_t> pk;
2487
    std::string_view class_name;
2488
};
2489

2490
struct Move {
2491
    Move(size_t from_ndx, size_t to_ndx)
2492
        : from(from_ndx)
2493
        , to(to_ndx)
2494
    {
96✔
2495
    }
96✔
2496
    size_t from;
2497
    size_t to;
2498
};
2499

2500
struct Insert {
2501
    Insert(size_t index, util::Optional<int64_t> key)
2502
        : ndx(index)
2503
        , pk(key)
2504
    {
32✔
2505
    }
32✔
2506
    size_t ndx;
2507
    util::Optional<int64_t> pk;
2508
};
2509

2510
struct CollectionOperation {
2511
    CollectionOperation(Add op)
2512
        : m_op(op)
2513
    {
1,348✔
2514
    }
1,348✔
2515
    CollectionOperation(Remove op)
2516
        : m_op(op)
2517
    {
824✔
2518
    }
824✔
2519
    CollectionOperation(RemoveObject op)
2520
        : m_op(op)
2521
    {
468✔
2522
    }
468✔
2523
    CollectionOperation(CreateObject op)
2524
        : m_op(op)
2525
    {
100✔
2526
    }
100✔
2527
    CollectionOperation(Clear op)
2528
        : m_op(op)
2529
    {
204✔
2530
    }
204✔
2531
    CollectionOperation(Move op)
2532
        : m_op(op)
2533
    {
96✔
2534
    }
96✔
2535
    CollectionOperation(Insert op)
2536
        : m_op(op)
2537
    {
32✔
2538
    }
32✔
2539
    void apply(collection_fixtures::LinkedCollectionBase* collection, Obj src_obj, TableRef dst_table)
2540
    {
3,072✔
2541
        auto get_table = [&](std::string_view name) -> TableRef {
1,820✔
2542
            Group* group = dst_table->get_parent_group();
568✔
2543
            Group::TableNameBuffer buffer;
568✔
2544
            TableRef table = group->get_table(Group::class_name_to_table_name(name, buffer));
568✔
2545
            REALM_ASSERT(table);
568✔
2546
            return table;
568✔
2547
        };
568✔
2548
        mpark::visit(
3,072✔
2549
            util::overload{
3,072✔
2550
                [&](Add add_link) {
2,210✔
2551
                    Mixed pk_to_add = add_link.pk ? Mixed{add_link.pk} : Mixed{};
1,336✔
2552
                    ObjKey dst_key = dst_table->find_primary_key(pk_to_add);
1,348✔
2553
                    REALM_ASSERT(dst_key);
1,348✔
2554
                    collection->add_link(src_obj, ObjLink{dst_table->get_key(), dst_key});
1,348✔
2555
                },
1,348✔
2556
                [&](Remove remove_link) {
1,948✔
2557
                    Mixed pk_to_remove = remove_link.pk ? Mixed{remove_link.pk} : Mixed{};
824✔
2558
                    ObjKey dst_key = dst_table->find_primary_key(pk_to_remove);
824✔
2559
                    REALM_ASSERT(dst_key);
824✔
2560
                    bool did_remove = collection->remove_link(src_obj, ObjLink{dst_table->get_key(), dst_key});
824✔
2561
                    REALM_ASSERT(did_remove);
824✔
2562
                },
824✔
2563
                [&](RemoveObject remove_object) {
1,770✔
2564
                    TableRef table = get_table(remove_object.class_name);
468✔
2565
                    ObjKey dst_key = table->find_primary_key(Mixed{remove_object.pk});
468✔
2566
                    REALM_ASSERT(dst_key);
468✔
2567
                    table->remove_object(dst_key);
468✔
2568
                },
468✔
2569
                [&](CreateObject create_object) {
1,586✔
2570
                    TableRef table = get_table(create_object.class_name);
100✔
2571
                    table->create_object_with_primary_key(Mixed{create_object.pk});
100✔
2572
                },
100✔
2573
                [&](Clear) {
1,638✔
2574
                    collection->clear_collection(src_obj);
204✔
2575
                },
204✔
2576
                [&](Insert insert) {
1,552✔
2577
                    Mixed pk_to_add = insert.pk ? Mixed{insert.pk} : Mixed{};
32✔
2578
                    ObjKey dst_key = dst_table->find_primary_key(pk_to_add);
32✔
2579
                    REALM_ASSERT(dst_key);
32✔
2580
                    collection->insert(src_obj, insert.ndx, ObjLink{dst_table->get_key(), dst_key});
32✔
2581
                },
32✔
2582
                [&](Move move) {
1,584✔
2583
                    collection->move(src_obj, move.from, move.to);
96✔
2584
                }},
96✔
2585
            m_op);
3,072✔
2586
    }
3,072✔
2587

2588
private:
2589
    mpark::variant<Add, Remove, Clear, RemoveObject, CreateObject, Move, Insert> m_op;
2590
};
2591

2592
} // namespace test_instructions
2593

2594
TEMPLATE_TEST_CASE("client reset collections of links", "[sync][pbs][client reset][links][collections]",
2595
                   cf::ListOfObjects, cf::ListOfMixedLinks, cf::SetOfObjects, cf::SetOfMixedLinks,
2596
                   cf::DictionaryOfObjects, cf::DictionaryOfMixedLinks)
2597
{
1,048✔
2598
    if (!util::EventLoop::has_implementation())
1,048✔
2599
        return;
×
2600

524✔
2601
    using namespace test_instructions;
1,048✔
2602
    const std::string valid_pk_name = "_id";
1,048✔
2603
    const auto partition = random_string(100);
1,048✔
2604
    const std::string collection_prop_name = "collection";
1,048✔
2605
    TestType test_type(collection_prop_name, "dest");
1,048✔
2606
    constexpr bool test_type_is_array = realm::is_any_v<TestType, cf::ListOfObjects, cf::ListOfMixedLinks>;
1,048✔
2607
    constexpr bool test_type_is_set = realm::is_any_v<TestType, cf::SetOfObjects, cf::SetOfMixedLinks>;
1,048✔
2608
    Schema schema = {
1,048✔
2609
        {"source",
1,048✔
2610
         {{valid_pk_name, PropertyType::Int | PropertyType::Nullable, true},
1,048✔
2611
          {"realm_id", PropertyType::String | PropertyType::Nullable},
1,048✔
2612
          test_type.property()}},
1,048✔
2613
        {"dest",
1,048✔
2614
         {
1,048✔
2615
             {valid_pk_name, PropertyType::Int | PropertyType::Nullable, true},
1,048✔
2616
             {"realm_id", PropertyType::String | PropertyType::Nullable},
1,048✔
2617
         }},
1,048✔
2618
        {"object",
1,048✔
2619
         {
1,048✔
2620
             {valid_pk_name, PropertyType::ObjectId, Property::IsPrimary{true}},
1,048✔
2621
             {"value", PropertyType::Int},
1,048✔
2622
             {"realm_id", PropertyType::String | PropertyType::Nullable},
1,048✔
2623
         }},
1,048✔
2624
    };
1,048✔
2625

524✔
2626
    TestSyncManager init_sync_manager;
1,048✔
2627
    SyncTestFile config(init_sync_manager.app(), "default");
1,048✔
2628
    config.cache = false;
1,048✔
2629
    config.automatic_change_notifications = false;
1,048✔
2630
    config.schema = schema;
1,048✔
2631
    ClientResyncMode test_mode = GENERATE(ClientResyncMode::DiscardLocal, ClientResyncMode::Recover);
1,048✔
2632
    CAPTURE(test_mode);
1,048✔
2633
    config.sync_config->client_resync_mode = test_mode;
1,048✔
2634

524✔
2635
    SyncTestFile config2(init_sync_manager.app(), "default");
1,048✔
2636
    config2.schema = schema;
1,048✔
2637

524✔
2638
    std::unique_ptr<reset_utils::TestClientReset> test_reset =
1,048✔
2639
        reset_utils::make_fake_local_client_reset(config, config2);
1,048✔
2640

524✔
2641
    CppContext c;
1,048✔
2642
    auto create_one_source_object = [&](realm::SharedRealm r, int64_t val, std::vector<ObjLink> links = {}) {
2,096✔
2643
        auto object = Object::create(
2,096✔
2644
            c, r, "source",
2,096✔
2645
            std::any(realm::AnyDict{{valid_pk_name, std::any(val)}, {"realm_id", std::string(partition)}}),
2,096✔
2646
            CreatePolicy::ForceCreate);
2,096✔
2647

1,048✔
2648
        for (auto link : links) {
6,144✔
2649
            test_type.add_link(object.get_obj(), link);
6,144✔
2650
        }
6,144✔
2651
    };
2,096✔
2652

524✔
2653
    auto create_one_dest_object = [&](realm::SharedRealm r, util::Optional<int64_t> val) -> ObjLink {
10,288✔
2654
        std::any v;
10,288✔
2655
        if (val) {
10,288✔
2656
            v = std::any(*val);
10,240✔
2657
        }
10,240✔
2658
        auto obj = Object::create(
10,288✔
2659
            c, r, "dest",
10,288✔
2660
            std::any(realm::AnyDict{{valid_pk_name, std::move(v)}, {"realm_id", std::string(partition)}}),
10,288✔
2661
            CreatePolicy::ForceCreate);
10,288✔
2662
        return ObjLink{obj.get_obj().get_table()->get_key(), obj.get_obj().get_key()};
10,288✔
2663
    };
10,288✔
2664

524✔
2665
    auto require_links_to_match_ids = [&](std::vector<Obj>& links, std::vector<util::Optional<int64_t>>& expected,
1,048✔
2666
                                          bool sorted) {
1,018✔
2667
        std::vector<util::Optional<int64_t>> actual;
988✔
2668
        for (auto obj : links) {
2,396✔
2669
            if (obj.is_null(valid_pk_name)) {
2,396✔
2670
                actual.push_back(util::none);
12✔
2671
            }
12✔
2672
            else {
2,384✔
2673
                actual.push_back(obj.get<Int>(valid_pk_name));
2,384✔
2674
            }
2,384✔
2675
        }
2,396✔
2676
        if (sorted) {
988✔
2677
            std::sort(actual.begin(), actual.end());
592✔
2678
        }
592✔
2679
        REQUIRE(actual == expected);
988!
2680
    };
988✔
2681

524✔
2682
    constexpr int64_t source_pk = 0;
1,048✔
2683
    constexpr util::Optional<int64_t> dest_pk_1 = 1;
1,048✔
2684
    constexpr util::Optional<int64_t> dest_pk_2 = 2;
1,048✔
2685
    constexpr util::Optional<int64_t> dest_pk_3 = 3;
1,048✔
2686
    constexpr util::Optional<int64_t> dest_pk_4 = 4;
1,048✔
2687
    constexpr util::Optional<int64_t> dest_pk_5 = 5;
1,048✔
2688

524✔
2689
    Results results;
1,048✔
2690
    Object object;
1,048✔
2691
    CollectionChangeSet object_changes, results_changes;
1,048✔
2692
    NotificationToken object_token, results_token;
1,048✔
2693
    auto setup_listeners = [&](SharedRealm realm) {
1,018✔
2694
        TableRef source_table = get_table(*realm, "source");
988✔
2695
        ColKey id_col = source_table->get_column_key("_id");
988✔
2696
        results = Results(realm, source_table->where().equal(id_col, source_pk));
988✔
2697
        if (auto obj = results.first()) {
988✔
2698
            object = Object(realm, *obj);
988✔
2699
            object_token = object.add_notification_callback([&](CollectionChangeSet changes) {
1,712✔
2700
                object_changes = std::move(changes);
1,712✔
2701
            });
1,712✔
2702
        }
988✔
2703
        results_token = results.add_notification_callback([&](CollectionChangeSet changes) {
1,712✔
2704
            results_changes = std::move(changes);
1,712✔
2705
        });
1,712✔
2706
    };
988✔
2707

524✔
2708
    auto get_source_object = [&](SharedRealm realm) -> Obj {
5,048✔
2709
        TableRef src_table = get_table(*realm, "source");
5,048✔
2710
        return src_table->try_get_object(src_table->find_primary_key(Mixed{source_pk}));
5,048✔
2711
    };
5,048✔
2712
    auto apply_instructions = [&](SharedRealm realm, std::vector<CollectionOperation>& instructions) {
2,096✔
2713
        TableRef dst_table = get_table(*realm, "dest");
2,096✔
2714
        for (auto& instruction : instructions) {
3,072✔
2715
            Obj src_obj = get_source_object(realm);
3,072✔
2716
            instruction.apply(&test_type, src_obj, dst_table);
3,072✔
2717
        }
3,072✔
2718
    };
2,096✔
2719

524✔
2720
    auto reset_collection =
1,048✔
2721
        [&](std::vector<CollectionOperation>&& local_ops, std::vector<CollectionOperation>&& remote_ops,
1,048✔
2722
            std::vector<util::Optional<int64_t>>&& expected_recovered_state, size_t num_expected_nulls = 0) {
1,018✔
2723
            std::vector<util::Optional<int64_t>> remote_pks;
988✔
2724
            std::vector<util::Optional<int64_t>> local_pks;
988✔
2725
            test_reset
988✔
2726
                ->make_local_changes([&](SharedRealm local_realm) {
988✔
2727
                    apply_instructions(local_realm, local_ops);
988✔
2728
                    Obj source_obj = get_source_object(local_realm);
988✔
2729
                    if (source_obj) {
988✔
2730
                        auto local_links = test_type.get_links(source_obj);
988✔
2731
                        std::transform(local_links.begin(), local_links.end(), std::back_inserter(local_pks),
988✔
2732
                                       [](auto obj) -> util::Optional<int64_t> {
2,724✔
2733
                                           Mixed pk = obj.get_primary_key();
2,724✔
2734
                                           return pk.is_null() ? util::none : util::make_optional(pk.get_int());
2,712✔
2735
                                       });
2,724✔
2736
                    }
988✔
2737
                })
988✔
2738
                ->make_remote_changes([&](SharedRealm remote_realm) {
988✔
2739
                    apply_instructions(remote_realm, remote_ops);
988✔
2740
                    Obj source_obj = get_source_object(remote_realm);
988✔
2741
                    if (source_obj) {
988✔
2742
                        auto remote_links = test_type.get_links(source_obj);
976✔
2743
                        std::transform(remote_links.begin(), remote_links.end(), std::back_inserter(remote_pks),
976✔
2744
                                       [](auto obj) -> util::Optional<int64_t> {
2,424✔
2745
                                           Mixed pk = obj.get_primary_key();
2,424✔
2746
                                           return pk.is_null() ? util::none : util::make_optional(pk.get_int());
2,424✔
2747
                                       });
2,424✔
2748
                    }
976✔
2749
                })
988✔
2750
                ->on_post_local_changes([&](SharedRealm realm) {
988✔
2751
                    setup_listeners(realm);
988✔
2752
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
988✔
2753
                    CHECK(results.size() == 1);
988!
2754
                })
988✔
2755
                ->on_post_reset([&](SharedRealm realm) {
988✔
2756
                    object_changes = {};
988✔
2757
                    results_changes = {};
988✔
2758
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
988✔
2759
                    CHECK(results.size() == 1);
988!
2760
                    CHECK(object.is_valid());
988!
2761
                    Obj origin = results.get(0);
988✔
2762
                    auto linked_objects = test_type.get_links(origin);
988✔
2763
                    std::vector<util::Optional<int64_t>>& expected_links = remote_pks;
988✔
2764
                    const size_t actual_size = test_type.size_of_collection(origin);
988✔
2765
                    if (test_mode == ClientResyncMode::Recover) {
988✔
2766
                        expected_links = expected_recovered_state;
500✔
2767
                        size_t expected_size = expected_links.size();
500✔
2768
                        if (!test_type.will_erase_removed_object_links()) {
500✔
2769
                            // dictionary size will remain the same because the key is preserved with a null value
74✔
2770
                            expected_size += num_expected_nulls;
148✔
2771
                        }
148✔
2772
                        CHECK(actual_size == expected_size);
500!
2773
                        if (actual_size != expected_size) {
500✔
2774
                            std::vector<Obj> links = test_type.get_links(origin);
×
2775
                            std::cout << "actual {";
×
2776
                            for (auto link : links) {
×
2777
                                std::cout << link.get_primary_key() << ", ";
×
2778
                            }
×
2779
                            std::cout << "}\n";
×
2780
                        }
×
2781
                    }
500✔
2782
                    if (!test_type_is_array) {
988✔
2783
                        // order should not matter except for lists
296✔
2784
                        std::sort(local_pks.begin(), local_pks.end());
592✔
2785
                        std::sort(expected_links.begin(), expected_links.end());
592✔
2786
                    }
592✔
2787
                    require_links_to_match_ids(linked_objects, expected_links, !test_type_is_array);
988✔
2788
                    if (local_pks != expected_links) {
988✔
2789
                        REQUIRE_INDICES(results_changes.modifications, 0);
724!
2790
                        REQUIRE_INDICES(object_changes.modifications, 0);
724!
2791
                    }
724✔
2792
                    else {
264✔
2793
                        REQUIRE_INDICES(results_changes.modifications);
264!
2794
                        REQUIRE_INDICES(object_changes.modifications);
264!
2795
                    }
264✔
2796
                    REQUIRE_INDICES(results_changes.insertions);
988!
2797
                    REQUIRE_INDICES(results_changes.deletions);
988!
2798
                    REQUIRE_INDICES(object_changes.insertions);
988!
2799
                    REQUIRE_INDICES(object_changes.deletions);
988!
2800
                })
988✔
2801
                ->run();
988✔
2802
        };
988✔
2803

524✔
2804
    auto reset_collection_removing_source_object = [&](std::vector<CollectionOperation>&& local_ops,
1,048✔
2805
                                                       std::vector<CollectionOperation>&& remote_ops) {
554✔
2806
        test_reset
60✔
2807
            ->make_local_changes([&](SharedRealm local_realm) {
60✔
2808
                apply_instructions(local_realm, local_ops);
60✔
2809
            })
60✔
2810
            ->make_remote_changes([&](SharedRealm remote_realm) {
60✔
2811
                apply_instructions(remote_realm, remote_ops);
60✔
2812
            })
60✔
2813
            ->on_post_reset([&](SharedRealm realm) {
60✔
2814
                REQUIRE_NOTHROW(advance_and_notify(*realm));
60✔
2815
                TableRef table = realm->read_group().get_table("class_source");
60✔
2816
                REQUIRE(!table->find_primary_key(Mixed{source_pk}));
60!
2817
            })
60✔
2818
            ->run();
60✔
2819
    };
60✔
2820
    auto populate_initial_state = [&](SharedRealm realm) {
2,048✔
2821
        test_type.reset_test_state();
2,048✔
2822
        // add a container collection with three valid links
1,024✔
2823
        ObjLink dest1 = create_one_dest_object(realm, dest_pk_1);
2,048✔
2824
        ObjLink dest2 = create_one_dest_object(realm, dest_pk_2);
2,048✔
2825
        ObjLink dest3 = create_one_dest_object(realm, dest_pk_3);
2,048✔
2826
        create_one_dest_object(realm, dest_pk_4);
2,048✔
2827
        create_one_dest_object(realm, dest_pk_5);
2,048✔
2828
        create_one_source_object(realm, source_pk, {dest1, dest2, dest3});
2,048✔
2829
    };
2,048✔
2830

524✔
2831
    test_reset->setup([&](SharedRealm realm) {
1,760✔
2832
        populate_initial_state(realm);
1,760✔
2833
    });
1,760✔
2834

524✔
2835
    SECTION("no changes") {
1,048✔
2836
        reset_collection({}, {}, {dest_pk_1, dest_pk_2, dest_pk_3});
24✔
2837
    }
24✔
2838
    SECTION("remote removes all") {
1,048✔
2839
        reset_collection({}, {{Remove{dest_pk_3}}, {Remove{dest_pk_2}}, {Remove{dest_pk_1}}}, {});
24✔
2840
    }
24✔
2841
    SECTION("local removes all") { // local client state wins
1,048✔
2842
        reset_collection({{Remove{dest_pk_3}}, {Remove{dest_pk_2}}, {Remove{dest_pk_1}}}, {}, {});
24✔
2843
    }
24✔
2844
    SECTION("both remove all links") { // local client state wins
1,048✔
2845
        reset_collection({{Remove{dest_pk_3}}, {Remove{dest_pk_2}}, {Remove{dest_pk_1}}},
24✔
2846
                         {{Remove{dest_pk_3}}, {Remove{dest_pk_2}}, {Remove{dest_pk_1}}}, {});
24✔
2847
    }
24✔
2848
    SECTION("local removes first link") { // local client state wins
1,048✔
2849
        reset_collection({{Remove{dest_pk_1}}}, {}, {dest_pk_2, dest_pk_3});
24✔
2850
    }
24✔
2851
    SECTION("local removes middle link") { // local client state wins
1,048✔
2852
        reset_collection({{Remove{dest_pk_2}}}, {}, {dest_pk_1, dest_pk_3});
24✔
2853
    }
24✔
2854
    SECTION("local removes last link") { // local client state wins
1,048✔
2855
        reset_collection({{Remove{dest_pk_3}}}, {}, {dest_pk_1, dest_pk_2});
24✔
2856
    }
24✔
2857
    SECTION("remote removes first link") {
1,048✔
2858
        reset_collection({}, {{Remove{dest_pk_1}}}, {dest_pk_2, dest_pk_3});
24✔
2859
    }
24✔
2860
    SECTION("remote removes middle link") {
1,048✔
2861
        reset_collection({}, {{Remove{dest_pk_2}}}, {dest_pk_1, dest_pk_3});
24✔
2862
    }
24✔
2863
    SECTION("remote removes last link") {
1,048✔
2864
        reset_collection({}, {{Remove{dest_pk_3}}}, {dest_pk_1, dest_pk_2});
24✔
2865
    }
24✔
2866
    SECTION("local adds a link with a null pk value") {
1,048✔
2867
        test_reset->setup([&](SharedRealm realm) {
48✔
2868
            test_type.reset_test_state();
48✔
2869
            create_one_dest_object(realm, util::none);
48✔
2870
            create_one_source_object(realm, source_pk, {});
48✔
2871
        });
48✔
2872
        reset_collection({Add{util::none}}, {}, {util::none});
24✔
2873
    }
24✔
2874
    SECTION("removal of different links") {
1,048✔
2875
        std::vector<util::Optional<int64_t>> expected = {dest_pk_2};
24✔
2876
        if constexpr (test_type_is_array) {
24✔
2877
            expected = {dest_pk_2, dest_pk_3}; // local client state wins
8✔
2878
        }
8✔
2879
        reset_collection({Remove{dest_pk_1}}, {Remove{dest_pk_3}}, std::move(expected));
24✔
2880
    }
24✔
2881
    SECTION("local addition") {
1,048✔
2882
        reset_collection({Add{dest_pk_4}}, {}, {dest_pk_1, dest_pk_2, dest_pk_3, dest_pk_4});
24✔
2883
    }
24✔
2884
    SECTION("remote addition") {
1,048✔
2885
        reset_collection({}, {Add{dest_pk_4}}, {dest_pk_1, dest_pk_2, dest_pk_3, dest_pk_4});
24✔
2886
    }
24✔
2887
    SECTION("both addition of different items") {
1,048✔
2888
        reset_collection({Add{dest_pk_4}}, {Add{dest_pk_5}, Remove{dest_pk_5}, Add{dest_pk_5}},
24✔
2889
                         {dest_pk_1, dest_pk_2, dest_pk_3, dest_pk_4, dest_pk_5});
24✔
2890
    }
24✔
2891
    SECTION("both addition of same items") {
1,048✔
2892
        std::vector<util::Optional<int64_t>> expected = {dest_pk_1, dest_pk_2, dest_pk_3, dest_pk_4};
24✔
2893
        if constexpr (test_type_is_array) {
24✔
2894
            expected = {dest_pk_1, dest_pk_2, dest_pk_3, dest_pk_4, dest_pk_4};
8✔
2895
        }
8✔
2896
        // dictionary has added the new link to the same key on both sides
12✔
2897
        reset_collection({Add{dest_pk_4}}, {Add{dest_pk_4}}, std::move(expected));
24✔
2898
    }
24✔
2899
    SECTION("local add/delete, remote add/delete/add different") {
1,048✔
2900
        reset_collection({Add{dest_pk_4}, Remove{dest_pk_4}}, {Add{dest_pk_5}, Remove{dest_pk_5}, Add{dest_pk_5}},
24✔
2901
                         {dest_pk_1, dest_pk_2, dest_pk_3, dest_pk_5});
24✔
2902
    }
24✔
2903
    SECTION("remote add/delete, local add") {
1,048✔
2904
        reset_collection({Add{dest_pk_4}}, {Add{dest_pk_5}, Remove{dest_pk_5}},
24✔
2905
                         {dest_pk_1, dest_pk_2, dest_pk_3, dest_pk_4});
24✔
2906
    }
24✔
2907
    SECTION("local remove, remote add") {
1,048✔
2908
        std::vector<util::Optional<int64_t>> expected = {dest_pk_1, dest_pk_3, dest_pk_4, dest_pk_5};
24✔
2909
        if constexpr (test_type_is_array) {
24✔
2910
            expected = {dest_pk_1, dest_pk_3}; // local client state wins
8✔
2911
        }
8✔
2912
        reset_collection({Remove{dest_pk_2}}, {Add{dest_pk_4}, Add{dest_pk_5}}, std::move(expected));
24✔
2913
    }
24✔
2914
    SECTION("local adds link to remotely deleted object") {
1,048✔
2915
        reset_collection({Add{dest_pk_4}}, {RemoveObject{"dest", dest_pk_4}}, {dest_pk_1, dest_pk_2, dest_pk_3}, 1);
24✔
2916
    }
24✔
2917
    SECTION("local clear") {
1,048✔
2918
        reset_collection({Clear{}}, {}, {});
24✔
2919
    }
24✔
2920
    SECTION("remote clear") {
1,048✔
2921
        reset_collection({}, {Clear{}}, {});
24✔
2922
    }
24✔
2923
    SECTION("both clear") {
1,048✔
2924
        reset_collection({Clear{}}, {Clear{}}, {});
24✔
2925
    }
24✔
2926
    SECTION("both clear and add") {
1,048✔
2927
        reset_collection({Clear{}, Add{dest_pk_1}}, {Clear{}, Add{dest_pk_2}}, {dest_pk_1});
24✔
2928
    }
24✔
2929
    SECTION("both clear and add/remove/add/add") {
1,048✔
2930
        reset_collection({Clear{}, Add{dest_pk_1}, Remove{dest_pk_1}, Add{dest_pk_2}, Add{dest_pk_3}},
24✔
2931
                         {Clear{}, Add{dest_pk_1}, Remove{dest_pk_1}, Add{dest_pk_2}, Add{dest_pk_3}},
24✔
2932
                         {dest_pk_2, dest_pk_3});
24✔
2933
    }
24✔
2934
    SECTION("local add to remotely deleted object") {
1,048✔
2935
        reset_collection({Add{dest_pk_4}}, {Add{dest_pk_4}, RemoveObject{"dest", dest_pk_4}},
24✔
2936
                         {dest_pk_1, dest_pk_2, dest_pk_3}, 1);
24✔
2937
    }
24✔
2938
    SECTION("remote adds link to locally deleted object with link") {
1,048✔
2939
        reset_collection({Add{dest_pk_4}, RemoveObject{"dest", dest_pk_4}}, {Add{dest_pk_4}, Add{dest_pk_5}},
24✔
2940
                         {dest_pk_1, dest_pk_2, dest_pk_3, dest_pk_5}, 1);
24✔
2941
    }
24✔
2942
    SECTION("remote adds link to locally deleted object without link") {
1,048✔
2943
        reset_collection({RemoveObject{"dest", dest_pk_4}}, {Add{dest_pk_4}, Add{dest_pk_5}},
24✔
2944
                         {dest_pk_1, dest_pk_2, dest_pk_3, dest_pk_5}, 1);
24✔
2945
    }
24✔
2946
    SECTION("local adds two links to objects which are both removed by the remote") {
1,048✔
2947
        reset_collection({Add{dest_pk_4}, Add{dest_pk_5}, CreateObject("dest", 6), Add{6}},
24✔
2948
                         {RemoveObject("dest", dest_pk_4), RemoveObject("dest", dest_pk_5)},
24✔
2949
                         {dest_pk_1, dest_pk_2, dest_pk_3, 6}, 2);
24✔
2950
    }
24✔
2951
    SECTION("local removes two objects which were linked to by remote") {
1,048✔
2952
        reset_collection(
24✔
2953
            {RemoveObject("dest", dest_pk_1), RemoveObject("dest", dest_pk_2), CreateObject("dest", 6), Add{6}}, {},
24✔
2954
            {dest_pk_3, 6}, 2);
24✔
2955
    }
24✔
2956
    SECTION("local has unresolved links") {
1,048✔
2957
        test_reset->setup([&](SharedRealm realm) {
288✔
2958
            populate_initial_state(realm);
288✔
2959

144✔
2960
            auto invalidate_object = [&](SharedRealm realm, std::string_view table_name, Mixed pk) {
288✔
2961
                TableRef table = get_table(*realm, table_name);
288✔
2962
                Obj obj = table->get_object_with_primary_key(pk);
288✔
2963
                REALM_ASSERT(obj.is_valid());
288✔
2964
                if (realm->config().path == config.path) {
288✔
2965
                    // the local realm does an invalidation
72✔
2966
                    table->invalidate_object(obj.get_key());
144✔
2967
                }
144✔
2968
                else {
144✔
2969
                    // the remote realm has deleted it
72✔
2970
                    table->remove_object(obj.get_key());
144✔
2971
                }
144✔
2972
            };
288✔
2973

144✔
2974
            invalidate_object(realm, "dest", dest_pk_1);
288✔
2975
        });
288✔
2976

72✔
2977
        SECTION("remote adds a link") {
144✔
2978
            reset_collection({}, {Add{dest_pk_4}}, {dest_pk_2, dest_pk_3, dest_pk_4}, 1);
24✔
2979
        }
24✔
2980
        SECTION("remote removes a link") {
144✔
2981
            reset_collection({}, {Remove{dest_pk_2}}, {dest_pk_3}, 1);
24✔
2982
        }
24✔
2983
        SECTION("remote deletes a dest object that local links to") {
144✔
2984
            reset_collection({Add{dest_pk_4}}, {RemoveObject{"dest", dest_pk_4}}, {dest_pk_2, dest_pk_3}, 2);
24✔
2985
        }
24✔
2986
        SECTION("remote deletes a different dest object") {
144✔
2987
            reset_collection({Add{dest_pk_4}}, {RemoveObject{"dest", dest_pk_2}}, {dest_pk_3, dest_pk_4}, 2);
24✔
2988
        }
24✔
2989
        SECTION("local adds two new links and remote deletes a different dest object") {
144✔
2990
            reset_collection({Add{dest_pk_4}, Add{dest_pk_5}}, {RemoveObject{"dest", dest_pk_2}},
24✔
2991
                             {dest_pk_3, dest_pk_4, dest_pk_5}, 2);
24✔
2992
        }
24✔
2993
        SECTION("remote deletes an object, then removes and adds to the list") {
144✔
2994
            reset_collection({}, {RemoveObject{"dest", dest_pk_2}, Remove{dest_pk_3}, Add{dest_pk_4}}, {dest_pk_4},
24✔
2995
                             2);
24✔
2996
        }
24✔
2997
    }
144✔
2998

524✔
2999
    if (test_mode == ClientResyncMode::Recover) {
1,048✔
3000
        SECTION("local adds a list item and removes source object, remote modifies list") {
548✔
3001
            reset_collection_removing_source_object({Add{dest_pk_4}, RemoveObject{"source", source_pk}},
12✔
3002
                                                    {Add{dest_pk_5}});
12✔
3003
        }
12✔
3004
        SECTION("local erases list item then removes source object, remote modifies list") {
548✔
3005
            reset_collection_removing_source_object({Remove{dest_pk_1}, RemoveObject{"source", source_pk}},
12✔
3006
                                                    {Add{dest_pk_5}});
12✔
3007
        }
12✔
3008
        SECTION("remote removes source object, recover local modifications") {
548✔
3009
            reset_collection_removing_source_object({Add{dest_pk_4}, Clear{}}, {RemoveObject{"source", source_pk}});
12✔
3010
        }
12✔
3011
        SECTION("remote removes source object, local attempts to ccpy over list state") {
548✔
3012
            reset_collection_removing_source_object({Remove{dest_pk_1}}, {RemoveObject{"source", source_pk}});
12✔
3013
        }
12✔
3014
        SECTION("remote removes source object, local adds it back and modifies it") {
548✔
3015
            reset_collection({Add{dest_pk_4}, RemoveObject{"source", source_pk}, CreateObject{"source", source_pk},
12✔
3016
                              Add{dest_pk_1}},
12✔
3017
                             {RemoveObject{"source", source_pk}}, {dest_pk_1});
12✔
3018
        }
12✔
3019
    }
548✔
3020
    else if (test_mode == ClientResyncMode::DiscardLocal) {
500✔
3021
        SECTION("remote removes source object") {
500✔
3022
            reset_collection_removing_source_object({Add{dest_pk_4}}, {RemoveObject{"source", source_pk}});
12✔
3023
        }
12✔
3024
    }
500✔
3025
    if constexpr (test_type_is_array) {
1,048✔
3026
        SECTION("local moves on non-added elements causes a diff which overrides server changes") {
524✔
3027
            reset_collection({Move{0, 1}, Add{dest_pk_5}}, {Add{dest_pk_4}},
8✔
3028
                             {dest_pk_2, dest_pk_1, dest_pk_3, dest_pk_5});
8✔
3029
        }
8✔
3030
        SECTION("local moves on non-added elements with server dest obj removal") {
524✔
3031
            reset_collection(
8✔
3032
                {Move{0, 1}, Add{dest_pk_5}}, {Add{dest_pk_4}, RemoveObject("dest", dest_pk_1)},
8✔
3033
                {dest_pk_2, dest_pk_3,
8✔
3034
                 dest_pk_5}); // copy over local list, but without the dest_pk_1 link because that object was deleted
8✔
3035
        }
8✔
3036
        SECTION("local moves on non-added elements with all server dest objs removed") {
524✔
3037
            reset_collection({Move{0, 1}, Add{dest_pk_5}},
8✔
3038
                             {Add{dest_pk_4}, RemoveObject("dest", dest_pk_1), RemoveObject("dest", dest_pk_2),
8✔
3039
                              RemoveObject("dest", dest_pk_3), RemoveObject("dest", dest_pk_5)},
8✔
3040
                             {}); // copy over local list, but all links have been removed
8✔
3041
        }
8✔
3042
        SECTION("local moves on non-added elements when server creates a new object and adds it to the list") {
524✔
3043
            reset_collection({Move{0, 1}, Add{dest_pk_5}}, {CreateObject("dest", 6), Add{6}},
8✔
3044
                             {dest_pk_2, dest_pk_1, dest_pk_3, dest_pk_5});
8✔
3045
        }
8✔
3046
        SECTION("local moves on locally-added elements when server removes the object that the new links point to") {
524✔
3047
            reset_collection({Add{dest_pk_5}, Add{dest_pk_5}, Move{4, 3}},
8✔
3048
                             {Add{dest_pk_4}, RemoveObject("dest", dest_pk_5)},
8✔
3049
                             {dest_pk_1, dest_pk_2, dest_pk_3}); // local overwrite, but without pk_5
8✔
3050
        }
8✔
3051
        SECTION("local insert and delete can be recovered even if a local link was deleted by remote") {
524✔
3052
            // start  : 1, 2, 3
4✔
3053
            // local  : 1, 2, 3, 5, 6, 1
4✔
3054
            // remote : 4, 1, 2, 3 {remove obj 5}
4✔
3055
            // result : 1, 2, 3, 6, 1
4✔
3056
            reset_collection({CreateObject("dest", 6), Add{dest_pk_5}, Add{6}, Insert{4, dest_pk_4},
8✔
3057
                              Remove{dest_pk_4}, Add{dest_pk_1}},
8✔
3058
                             {Insert{0, dest_pk_4}, RemoveObject("dest", dest_pk_5)},
8✔
3059
                             {dest_pk_4, dest_pk_1, dest_pk_2, dest_pk_3, 6, dest_pk_1});
8✔
3060
        }
8✔
3061
        SECTION("both add link to object which has been deleted by other side") {
524✔
3062
            // start  : 1, 2, 3
4✔
3063
            // local  : 1, 1, 2, 3, 5, {remove object 4}
4✔
3064
            // remote : 1, 2, 3, 3, 4, {remove obj 5}
4✔
3065
            // result : 1, 1, 2, 3, 3
4✔
3066
            reset_collection({Add{dest_pk_5}, Insert{0, dest_pk_1}, RemoveObject("dest", dest_pk_4)},
8✔
3067
                             {Add{dest_pk_4}, Insert{3, dest_pk_3}, RemoveObject("dest", dest_pk_5)},
8✔
3068
                             {dest_pk_1, dest_pk_1, dest_pk_2, dest_pk_3, dest_pk_3});
8✔
3069
        }
8✔
3070

316✔
3071
        SECTION("local moves on added elements can be merged with remote moves") {
524✔
3072
            reset_collection({Add{dest_pk_4}, Add{dest_pk_5}, Move{3, 4}}, {Move{0, 1}},
8✔
3073
                             {dest_pk_2, dest_pk_1, dest_pk_3, dest_pk_5, dest_pk_4});
8✔
3074
        }
8✔
3075
        SECTION("local moves on added elements can be merged with remote additions") {
524✔
3076
            reset_collection({Add{dest_pk_4}, Add{dest_pk_5}, Move{3, 4}}, {Add{dest_pk_1}, Add{dest_pk_2}},
8✔
3077
                             {dest_pk_1, dest_pk_2, dest_pk_3, dest_pk_5, dest_pk_4, dest_pk_1, dest_pk_2});
8✔
3078
        }
8✔
3079
        SECTION("local moves on added elements can be merged with remote deletions") {
524✔
3080
            reset_collection({Add{dest_pk_4}, Add{dest_pk_5}, Move{3, 4}}, {Remove{dest_pk_1}, Remove{dest_pk_2}},
8✔
3081
                             {dest_pk_3, dest_pk_5, dest_pk_4});
8✔
3082
        }
8✔
3083
        SECTION("local move (down) on added elements can be merged with remote deletions") {
524✔
3084
            reset_collection({Add{dest_pk_4}, Add{dest_pk_5}, Move{4, 3}}, {Remove{dest_pk_1}, Remove{dest_pk_2}},
8✔
3085
                             {dest_pk_3, dest_pk_5, dest_pk_4});
8✔
3086
        }
8✔
3087
        SECTION("local move with delete on added elements can be merged with remote deletions") {
524✔
3088
            reset_collection({Add{dest_pk_4}, Add{dest_pk_5}, Move{3, 4}, Remove{dest_pk_5}},
8✔
3089
                             {Remove{dest_pk_1}, Remove{dest_pk_2}}, {dest_pk_3, dest_pk_4});
8✔
3090
        }
8✔
3091
        SECTION("local move (down) with delete on added elements can be merged with remote deletions") {
524✔
3092
            reset_collection({Add{dest_pk_4}, Add{dest_pk_5}, Move{4, 3}, Remove{dest_pk_5}},
8✔
3093
                             {Remove{dest_pk_1}, Remove{dest_pk_2}}, {dest_pk_3, dest_pk_4});
8✔
3094
        }
8✔
3095
    }
416✔
3096
    else if constexpr (test_type_is_set) {
632✔
3097
        SECTION("remote adds two links to objects which are both removed by local") {
320✔
3098
            reset_collection({RemoveObject("dest", dest_pk_4), RemoveObject("dest", dest_pk_5),
8✔
3099
                              CreateObject("dest", 6), Add{6}, Remove{dest_pk_1}},
8✔
3100
                             {Remove{dest_pk_2}, Add{dest_pk_4}, Add{dest_pk_5}, CreateObject("dest", 6), Add{6},
8✔
3101
                              CreateObject("dest", 7), Add{7}, RemoveObject("dest", dest_pk_5)},
8✔
3102
                             {dest_pk_3, 6, 7});
8✔
3103
        }
8✔
3104
    }
320✔
3105
}
1,048✔
3106

3107
template <typename T>
3108
void set_embedded_list(const std::vector<T>& array_values, LnkLst& list)
3109
{
3,688✔
3110
    for (size_t i = 0; i < array_values.size(); ++i) {
5,080✔
3111
        Obj link;
1,392✔
3112
        if (i >= list.size()) {
1,392✔
3113
            link = list.create_and_insert_linked_object(list.size());
1,076✔
3114
        }
1,076✔
3115
        else {
316✔
3116
            link = list.get_object(i);
316✔
3117
        }
316✔
3118
        array_values[i].assign_to(link);
1,392✔
3119
    }
1,392✔
3120
    if (list.size() > array_values.size()) {
3,688✔
3121
        if (array_values.size() == 0) {
12!
3122
            list.clear();
8✔
3123
        }
8✔
3124
        else {
4✔
3125
            list.remove(array_values.size(), list.size());
4✔
3126
        }
4✔
3127
    }
12✔
3128
}
3,688✔
3129

3130
template <typename T>
3131
void combine_array_values(std::vector<T>& from, const std::vector<T>& to)
3132
{
96✔
3133
    auto it = from.begin();
96✔
3134
    for (auto val : to) {
300✔
3135
        it = ++from.insert(it, val);
300✔
3136
    }
300✔
3137
}
96✔
3138

3139
TEST_CASE("client reset with embedded object", "[sync][pbs][client reset][embedded objects]") {
172✔
3140
    if (!util::EventLoop::has_implementation())
172✔
3141
        return;
×
3142

86✔
3143
    TestSyncManager init_sync_manager;
172✔
3144
    SyncTestFile config(init_sync_manager.app(), "default");
172✔
3145
    config.cache = false;
172✔
3146
    config.automatic_change_notifications = false;
172✔
3147
    ClientResyncMode test_mode = GENERATE(ClientResyncMode::DiscardLocal, ClientResyncMode::Recover);
172✔
3148
    CAPTURE(test_mode);
172✔
3149
    config.sync_config->client_resync_mode = test_mode;
172✔
3150

86✔
3151
    ObjectSchema shared_class = {"object",
172✔
3152
                                 {
172✔
3153
                                     {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
172✔
3154
                                     {"value", PropertyType::Int},
172✔
3155
                                 }};
172✔
3156

86✔
3157
    config.schema = Schema{
172✔
3158
        shared_class,
172✔
3159
        {"TopLevel",
172✔
3160
         {
172✔
3161
             {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
172✔
3162
             {"array_of_objs", PropertyType::Object | PropertyType::Array, "EmbeddedObject"},
172✔
3163
             {"embedded_obj", PropertyType::Object | PropertyType::Nullable, "EmbeddedObject"},
172✔
3164
             {"embedded_dict", PropertyType::Object | PropertyType::Dictionary | PropertyType::Nullable,
172✔
3165
              "EmbeddedObject"},
172✔
3166
         }},
172✔
3167
        {"EmbeddedObject",
172✔
3168
         ObjectSchema::ObjectType::Embedded,
172✔
3169
         {
172✔
3170
             {"array", PropertyType::Int | PropertyType::Array},
172✔
3171
             {"name", PropertyType::String | PropertyType::Nullable},
172✔
3172
             {"link_to_embedded_object2", PropertyType::Object | PropertyType::Nullable, "EmbeddedObject2"},
172✔
3173
             {"array_of_seconds", PropertyType::Object | PropertyType::Array, "EmbeddedObject2"},
172✔
3174
             {"int_value", PropertyType::Int},
172✔
3175
         }},
172✔
3176
        {"EmbeddedObject2",
172✔
3177
         ObjectSchema::ObjectType::Embedded,
172✔
3178
         {
172✔
3179
             {"notes", PropertyType::String | PropertyType::Dictionary | PropertyType::Nullable},
172✔
3180
             {"set_of_ids", PropertyType::Set | PropertyType::ObjectId | PropertyType::Nullable},
172✔
3181
             {"date", PropertyType::Date},
172✔
3182
             {"top_level_link", PropertyType::Object | PropertyType::Nullable, "TopLevel"},
172✔
3183
         }},
172✔
3184
    };
172✔
3185
    struct SecondLevelEmbeddedContent {
172✔
3186
        using DictType = util::FlatMap<std::string, std::string>;
172✔
3187
        DictType dict_values = DictType::container_type{{"key A", random_string(10)}, {"key B", random_string(10)}};
172✔
3188
        std::set<ObjectId> set_of_objects = {ObjectId::gen(), ObjectId::gen()};
172✔
3189
        Timestamp datetime = Timestamp{random_int(), 0};
172✔
3190
        util::Optional<Mixed> pk_of_linked_object;
172✔
3191
        void apply_recovery_from(const SecondLevelEmbeddedContent& other)
172✔
3192
        {
108✔
3193
            datetime = other.datetime;
44✔
3194
            pk_of_linked_object = other.pk_of_linked_object;
44✔
3195
            for (auto it : other.dict_values) {
88✔
3196
                dict_values[it.first] = it.second;
88✔
3197
            }
88✔
3198
            for (auto oid : other.set_of_objects) {
88✔
3199
                set_of_objects.insert(oid);
88✔
3200
            }
88✔
3201
        }
44✔
3202
        void test(const SecondLevelEmbeddedContent& other) const
172✔
3203
        {
1,084✔
3204
            REQUIRE(datetime == other.datetime);
1,084!
3205
            REQUIRE(pk_of_linked_object == other.pk_of_linked_object);
1,084!
3206
            REQUIRE(set_of_objects == other.set_of_objects);
1,084!
3207
            REQUIRE(dict_values.size() == other.dict_values.size());
1,084!
3208
            for (auto kv : dict_values) {
2,164✔
3209
                INFO("dict_value: (" << kv.first << ", " << kv.second << ")");
2,164✔
3210
                auto it = other.dict_values.find(kv.first);
2,164✔
3211
                REQUIRE(it != other.dict_values.end());
2,164!
3212
                REQUIRE(it->second == kv.second);
2,164!
3213
            }
2,164✔
3214
        }
1,084✔
3215
        static SecondLevelEmbeddedContent get_from(Obj second)
172✔
3216
        {
1,450✔
3217
            REALM_ASSERT(second.is_valid());
1,450✔
3218
            SecondLevelEmbeddedContent content{};
1,450✔
3219
            content.datetime = second.get<Timestamp>("date");
1,450✔
3220
            ColKey top_link_col = second.get_table()->get_column_key("top_level_link");
1,450✔
3221
            ObjKey actual_link = second.get<ObjKey>(top_link_col);
1,450✔
3222
            if (actual_link) {
1,450✔
3223
                TableRef top_table = second.get_table()->get_opposite_table(top_link_col);
6✔
3224
                Obj actual_top_obj = top_table->get_object(actual_link);
6✔
3225
                content.pk_of_linked_object = Mixed{actual_top_obj.get_primary_key()};
6✔
3226
            }
6✔
3227
            Dictionary dict = second.get_dictionary("notes");
1,450✔
3228
            content.dict_values.clear();
1,450✔
3229
            for (auto it : dict) {
2,896✔
3230
                content.dict_values.insert({it.first.get_string(), it.second.get_string()});
2,896✔
3231
            }
2,896✔
3232
            Set<ObjectId> set = second.get_set<ObjectId>("set_of_ids");
1,450✔
3233
            content.set_of_objects.clear();
1,450✔
3234
            for (auto oid : set) {
2,900✔
3235
                content.set_of_objects.insert(oid);
2,900✔
3236
            }
2,900✔
3237
            return content;
1,450✔
3238
        }
1,450✔
3239
        void assign_to(Obj second) const
172✔
3240
        {
3,296✔
3241
            if (second.get<Timestamp>("date") != datetime) {
3,296✔
3242
                second.set("date", datetime);
2,572✔
3243
            }
2,572✔
3244
            ColKey top_link_col = second.get_table()->get_column_key("top_level_link");
3,296✔
3245
            if (pk_of_linked_object) {
3,296✔
3246
                TableRef top_table = second.get_table()->get_opposite_table(top_link_col);
8✔
3247
                ObjKey top_link = top_table->find_primary_key(*(pk_of_linked_object));
8✔
3248
                second.set(top_link_col, top_link);
8✔
3249
            }
8✔
3250
            else {
3,288✔
3251
                if (!second.is_null(top_link_col)) {
3,288✔
3252
                    second.set_null(top_link_col);
×
3253
                }
×
3254
            }
3,288✔
3255
            Dictionary dict = second.get_dictionary("notes");
3,296✔
3256
            for (auto it = dict.begin(); it != dict.end(); ++it) {
4,860✔
3257
                if (std::find_if(dict_values.begin(), dict_values.end(), [&](auto& pair) {
2,340✔
3258
                        return pair.first == (*it).first.get_string();
2,340✔
3259
                    }) == dict_values.end()) {
1,176✔
3260
                    dict.erase(it);
12✔
3261
                }
12✔
3262
            }
1,564✔
3263
            for (auto& it : dict_values) {
6,580✔
3264
                auto existing = dict.find(it.first);
6,580✔
3265
                if (existing == dict.end() || (*existing).second.get_string() != it.second) {
6,580✔
3266
                    dict.insert(it.first, it.second);
5,120✔
3267
                }
5,120✔
3268
            }
6,580✔
3269
            Set<ObjectId> set = second.get_set<ObjectId>("set_of_ids");
3,296✔
3270
            if (set_of_objects.empty()) {
3,296✔
3271
                set.clear();
12✔
3272
            }
12✔
3273
            else {
3,284✔
3274
                std::vector<size_t> indices, to_remove;
3,284✔
3275
                set.sort(indices);
3,284✔
3276
                for (size_t ndx : indices) {
2,418✔
3277
                    if (set_of_objects.count(set.get(ndx)) == 0) {
1,552✔
3278
                        to_remove.push_back(ndx);
104✔
3279
                    }
104✔
3280
                }
1,552✔
3281
                std::sort(to_remove.rbegin(), to_remove.rend());
3,284✔
3282
                for (auto ndx : to_remove) {
1,694✔
3283
                    set.erase(set.get(ndx));
104✔
3284
                }
104✔
3285
                for (auto oid : set_of_objects) {
6,568✔
3286
                    if (set.find(oid) == realm::npos) {
6,568✔
3287
                        set.insert(oid);
5,120✔
3288
                    }
5,120✔
3289
                }
6,568✔
3290
            }
3,284✔
3291
        }
3,296✔
3292
    };
172✔
3293

86✔
3294
    struct EmbeddedContent {
172✔
3295
        std::string name = random_string(10);
172✔
3296
        int64_t int_value = random_int();
172✔
3297
        std::vector<Int> array_vals = {random_int(), random_int(), random_int()};
172✔
3298
        util::Optional<SecondLevelEmbeddedContent> second_level = SecondLevelEmbeddedContent();
172✔
3299
        std::vector<SecondLevelEmbeddedContent> array_of_seconds = {};
172✔
3300
        void apply_recovery_from(const EmbeddedContent& other)
172✔
3301
        {
110✔
3302
            name = other.name;
48✔
3303
            int_value = other.int_value;
48✔
3304
            combine_array_values(array_vals, other.array_vals);
48✔
3305
            if (second_level && other.second_level) {
48✔
3306
                second_level->apply_recovery_from(*other.second_level);
44✔
3307
            }
44✔
3308
            else {
4✔
3309
                second_level = other.second_level;
4✔
3310
            }
4✔
3311
        }
48✔
3312
        void test(const EmbeddedContent& other) const
172✔
3313
        {
1,056✔
3314
            INFO("Checking EmbeddedContent" << name);
1,056✔
3315
            REQUIRE(name == other.name);
1,056!
3316
            REQUIRE(int_value == other.int_value);
1,056!
3317
            REQUIRE(array_vals == other.array_vals);
1,056!
3318
            REQUIRE(array_of_seconds.size() == other.array_of_seconds.size());
1,056!
3319
            for (size_t i = 0; i < array_of_seconds.size(); ++i) {
1,084✔
3320
                array_of_seconds[i].test(other.array_of_seconds[i]);
28✔
3321
            }
28✔
3322
            if (!second_level) {
1,056✔
3323
                REQUIRE(!other.second_level);
2!
3324
            }
2✔
3325
            else {
1,054✔
3326
                REQUIRE(!!other.second_level);
1,054!
3327
                second_level->test(*other.second_level);
1,054✔
3328
            }
1,054✔
3329
        }
1,056✔
3330
        static util::Optional<EmbeddedContent> get_from(Obj embedded)
172✔
3331
        {
1,450✔
3332
            util::Optional<EmbeddedContent> value;
1,450✔
3333
            if (embedded.is_valid()) {
1,450✔
3334
                value = EmbeddedContent{};
1,410✔
3335
                value->name = embedded.get_any("name").get<StringData>();
1,410✔
3336
                value->int_value = embedded.get_any("int_value").get<Int>();
1,410✔
3337
                ColKey list_col = embedded.get_table()->get_column_key("array");
1,410✔
3338
                value->array_vals = embedded.get_list_values<Int>(list_col);
1,410✔
3339

705✔
3340
                ColKey link2_col = embedded.get_table()->get_column_key("link_to_embedded_object2");
1,410✔
3341
                Obj second = embedded.get_linked_object(link2_col);
1,410✔
3342
                value->second_level = util::none;
1,410✔
3343
                if (second.is_valid()) {
1,410✔
3344
                    value->second_level = SecondLevelEmbeddedContent::get_from(second);
1,408✔
3345
                }
1,408✔
3346
                auto list = embedded.get_linklist("array_of_seconds");
1,410✔
3347
                for (size_t i = 0; i < list.size(); ++i) {
1,452✔
3348
                    value->array_of_seconds.push_back(SecondLevelEmbeddedContent::get_from(list.get_object(i)));
42✔
3349
                }
42✔
3350
            }
1,410✔
3351
            return value;
1,450✔
3352
        }
1,450✔
3353
        void assign_to(Obj embedded) const
172✔
3354
        {
3,236✔
3355
            if (embedded.get<StringData>("name") != name) {
3,236✔
3356
                embedded.set<StringData>("name", name);
2,532✔
3357
            }
2,532✔
3358
            if (embedded.get<Int>("int_value") != int_value) {
3,236✔
3359
                embedded.set<Int>("int_value", int_value);
2,504✔
3360
            }
2,504✔
3361
            ColKey list_col = embedded.get_table()->get_column_key("array");
3,236✔
3362
            if (embedded.get_list_values<Int>(list_col) != array_vals) {
3,236✔
3363
                embedded.set_list_values<Int>(list_col, array_vals);
2,532✔
3364
            }
2,532✔
3365
            ColKey link2_col = embedded.get_table()->get_column_key("link_to_embedded_object2");
3,236✔
3366
            if (second_level) {
3,236✔
3367
                Obj second = embedded.get_linked_object(link2_col);
3,232✔
3368
                if (!second) {
3,232✔
3369
                    second = embedded.create_and_set_linked_object(link2_col);
2,456✔
3370
                }
2,456✔
3371
                second_level->assign_to(second);
3,232✔
3372
            }
3,232✔
3373
            else {
4✔
3374
                embedded.set_null(link2_col);
4✔
3375
            }
4✔
3376
            auto list = embedded.get_linklist("array_of_seconds");
3,236✔
3377
            set_embedded_list(array_of_seconds, list);
3,236✔
3378
        }
3,236✔
3379
    };
172✔
3380
    struct TopLevelContent {
172✔
3381
        util::Optional<EmbeddedContent> link_value = EmbeddedContent();
172✔
3382
        std::vector<EmbeddedContent> array_values{3};
172✔
3383
        using DictType = util::FlatMap<std::string, util::Optional<EmbeddedContent>>;
172✔
3384
        DictType dict_values = DictType::container_type{
172✔
3385
            {"foo", EmbeddedContent()},
172✔
3386
            {"bar", EmbeddedContent()},
172✔
3387
            {"baz", EmbeddedContent()},
172✔
3388
        };
172✔
3389
        void apply_recovery_from(const TopLevelContent& other)
172✔
3390
        {
110✔
3391
            combine_array_values(array_values, other.array_values);
48✔
3392
            for (auto it : other.dict_values) {
168✔
3393
                dict_values[it.first] = it.second;
168✔
3394
            }
168✔
3395
            if (link_value && other.link_value) {
48✔
3396
                link_value->apply_recovery_from(*other.link_value);
32✔
3397
            }
32✔
3398
            else if (link_value) {
16✔
3399
                link_value = other.link_value;
4✔
3400
            }
4✔
3401
            // assuming starting from an initial value, if the link_value is null, then it was intentionally deleted.
24✔
3402
        }
48✔
3403
        void test(const TopLevelContent& other) const
172✔
3404
        {
158✔
3405
            if (link_value) {
144✔
3406
                INFO("checking TopLevelContent.link_value");
130✔
3407
                REQUIRE(!!other.link_value);
130!
3408
                link_value->test(*other.link_value);
130✔
3409
            }
130✔
3410
            else {
14✔
3411
                REQUIRE(!other.link_value);
14!
3412
            }
14✔
3413
            REQUIRE(array_values.size() == other.array_values.size());
144!
3414
            for (size_t i = 0; i < array_values.size(); ++i) {
616✔
3415
                INFO("checking array_values: " << i);
472✔
3416
                array_values[i].test(other.array_values[i]);
472✔
3417
            }
472✔
3418
            REQUIRE(dict_values.size() == other.dict_values.size());
144!
3419
            for (auto it : dict_values) {
460✔
3420
                INFO("checking dict_values: " << it.first);
460✔
3421
                auto found = other.dict_values.find(it.first);
460✔
3422
                REQUIRE(found != other.dict_values.end());
460!
3423
                if (it.second) {
460✔
3424
                    REQUIRE(!!found->second);
434!
3425
                    it.second->test(*found->second);
434✔
3426
                }
434✔
3427
                else {
26✔
3428
                    REQUIRE(!found->second);
26!
3429
                }
26✔
3430
            }
460✔
3431
        }
144✔
3432
        static TopLevelContent get_from(Obj obj)
172✔
3433
        {
204✔
3434
            TopLevelContent content;
204✔
3435
            Obj embedded_link = obj.get_linked_object("embedded_obj");
204✔
3436
            content.link_value = EmbeddedContent::get_from(embedded_link);
204✔
3437
            auto list = obj.get_linklist("array_of_objs");
204✔
3438
            content.array_values.clear();
204✔
3439

102✔
3440
            for (size_t i = 0; i < list.size(); ++i) {
788✔
3441
                Obj link = list.get_object(i);
584✔
3442
                content.array_values.push_back(*EmbeddedContent::get_from(link));
584✔
3443
            }
584✔
3444
            auto dict = obj.get_dictionary("embedded_dict");
204✔
3445
            content.dict_values.clear();
204✔
3446
            for (auto it : dict) {
636✔
3447
                Obj link = dict.get_object(it.first.get_string());
636✔
3448
                content.dict_values.insert({it.first.get_string(), EmbeddedContent::get_from(link)});
636✔
3449
            }
636✔
3450
            return content;
204✔
3451
        }
204✔
3452
        void assign_to(Obj obj) const
172✔
3453
        {
452✔
3454
            ColKey link_col = obj.get_table()->get_column_key("embedded_obj");
452✔
3455
            if (!link_value) {
452✔
3456
                obj.set_null(link_col);
16✔
3457
            }
16✔
3458
            else {
436✔
3459
                Obj embedded_link = obj.get_linked_object(link_col);
436✔
3460
                if (!embedded_link) {
436✔
3461
                    embedded_link = obj.create_and_set_linked_object(link_col);
240✔
3462
                }
240✔
3463
                link_value->assign_to(embedded_link);
436✔
3464
            }
436✔
3465
            auto list = obj.get_linklist("array_of_objs");
452✔
3466
            set_embedded_list(array_values, list);
452✔
3467
            auto dict = obj.get_dictionary("embedded_dict");
452✔
3468
            for (auto it = dict.begin(); it != dict.end();) {
800✔
3469
                if (dict_values.find((*it).first.get_string()) == dict_values.end()) {
348✔
3470
                    it = dict.erase(it);
16✔
3471
                }
16✔
3472
                else {
332✔
3473
                    ++it;
332✔
3474
                }
332✔
3475
            }
348✔
3476
            for (auto it : dict_values) {
1,412✔
3477
                if (it.second) {
1,412✔
3478
                    auto embedded = dict.get_object(it.first);
1,368✔
3479
                    if (!embedded) {
1,368✔
3480
                        embedded = dict.create_and_insert_linked_object(it.first);
1,040✔
3481
                    }
1,040✔
3482
                    it.second->assign_to(embedded);
1,368✔
3483
                }
1,368✔
3484
                else {
44✔
3485
                    dict.insert(it.first, Mixed{});
44✔
3486
                }
44✔
3487
            }
1,412✔
3488
        }
452✔
3489
    };
172✔
3490

86✔
3491
    SyncTestFile config2(init_sync_manager.app(), "default");
172✔
3492
    config2.schema = config.schema;
172✔
3493

86✔
3494
    std::unique_ptr<reset_utils::TestClientReset> test_reset =
172✔
3495
        reset_utils::make_fake_local_client_reset(config, config2);
172✔
3496

86✔
3497
    auto get_top_object = [](SharedRealm realm) {
472✔
3498
        advance_and_notify(*realm);
472✔
3499
        TableRef table = get_table(*realm, "TopLevel");
472✔
3500
        REQUIRE(table->size() == 1);
472!
3501
        Obj obj = *table->begin();
472✔
3502
        return obj;
472✔
3503
    };
472✔
3504

86✔
3505
    using StateList = std::vector<TopLevelContent>;
172✔
3506
    auto reset_embedded_object = [&](StateList local_content, StateList remote_content,
172✔
3507
                                     TopLevelContent expected_recovered) {
138✔
3508
        test_reset
104✔
3509
            ->make_local_changes([&](SharedRealm local_realm) {
104✔
3510
                Obj obj = get_top_object(local_realm);
104✔
3511
                for (auto& s : local_content) {
108✔
3512
                    s.assign_to(obj);
108✔
3513
                }
108✔
3514
            })
104✔
3515
            ->make_remote_changes([&](SharedRealm remote_realm) {
104✔
3516
                Obj obj = get_top_object(remote_realm);
104✔
3517
                for (auto& s : remote_content) {
104✔
3518
                    s.assign_to(obj);
104✔
3519
                }
104✔
3520
            })
104✔
3521
            ->on_post_reset([&](SharedRealm local_realm) {
104✔
3522
                Obj obj = get_top_object(local_realm);
104✔
3523
                TopLevelContent actual = TopLevelContent::get_from(obj);
104✔
3524
                if (test_mode == ClientResyncMode::Recover) {
104✔
3525
                    actual.test(expected_recovered);
52✔
3526
                }
52✔
3527
                else if (test_mode == ClientResyncMode::DiscardLocal) {
52✔
3528
                    REQUIRE(remote_content.size() > 0);
52!
3529
                    actual.test(remote_content.back());
52✔
3530
                }
52✔
3531
                else {
×
3532
                    REALM_UNREACHABLE();
3533
                }
×
3534
            })
104✔
3535
            ->run();
104✔
3536
    };
104✔
3537

86✔
3538
    ObjectId pk_val = ObjectId::gen();
172✔
3539
    test_reset->setup([&pk_val](SharedRealm realm) {
134✔
3540
        auto table = get_table(*realm, "TopLevel");
96✔
3541
        REQUIRE(table);
96!
3542
        auto obj = table->create_object_with_primary_key(pk_val);
96✔
3543
        Obj embedded_link = obj.create_and_set_linked_object(table->get_column_key("embedded_obj"));
96✔
3544
        embedded_link.set<String>("name", "initial name");
96✔
3545
    });
96✔
3546

86✔
3547
    SECTION("identical changes") {
172✔
3548
        TopLevelContent state;
4✔
3549
        TopLevelContent expected_recovered = state;
4✔
3550
        expected_recovered.apply_recovery_from(state);
4✔
3551
        reset_embedded_object({state}, {state}, expected_recovered);
4✔
3552
    }
4✔
3553
    SECTION("modify every embedded property") {
172✔
3554
        TopLevelContent local, remote;
4✔
3555
        TopLevelContent expected_recovered = remote;
4✔
3556
        expected_recovered.apply_recovery_from(local);
4✔
3557
        reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3558
    }
4✔
3559
    SECTION("remote nullifies embedded links") {
172✔
3560
        TopLevelContent local;
4✔
3561
        TopLevelContent remote = local;
4✔
3562
        remote.link_value.reset();
4✔
3563
        for (auto& val : remote.dict_values) {
12✔
3564
            val.second.reset();
12✔
3565
        }
12✔
3566
        remote.array_values.clear();
4✔
3567
        TopLevelContent expected_recovered = remote;
4✔
3568
        expected_recovered.apply_recovery_from(local);
4✔
3569
        reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3570
    }
4✔
3571
    SECTION("local nullifies embedded links") {
172✔
3572
        TopLevelContent local;
4✔
3573
        TopLevelContent remote = local;
4✔
3574
        local.link_value.reset();
4✔
3575
        for (auto& val : local.dict_values) {
12✔
3576
            val.second.reset();
12✔
3577
        }
12✔
3578
        local.array_values.clear();
4✔
3579
        TopLevelContent expected_recovered = remote;
4✔
3580
        expected_recovered.apply_recovery_from(local);
4✔
3581
        reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3582
    }
4✔
3583
    SECTION("remote adds embedded objects") {
172✔
3584
        TopLevelContent local;
4✔
3585
        TopLevelContent remote = local;
4✔
3586
        remote.dict_values["new key1"] = {EmbeddedContent{}};
4✔
3587
        remote.dict_values["new key2"] = {EmbeddedContent{}};
4✔
3588
        remote.dict_values["new key3"] = {};
4✔
3589
        remote.array_values.push_back({EmbeddedContent{}});
4✔
3590
        remote.array_values.push_back({});
4✔
3591
        remote.array_values.push_back({EmbeddedContent{}});
4✔
3592
        TopLevelContent expected_recovered = remote;
4✔
3593
        expected_recovered.apply_recovery_from(local);
4✔
3594
        reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3595
    }
4✔
3596
    SECTION("local adds some embedded objects") {
172✔
3597
        TopLevelContent local;
4✔
3598
        TopLevelContent remote = local;
4✔
3599
        local.dict_values["new key1"] = {EmbeddedContent{}};
4✔
3600
        local.dict_values["new key2"] = {EmbeddedContent{}};
4✔
3601
        local.dict_values["new key3"] = {};
4✔
3602
        local.array_values.push_back({EmbeddedContent{}});
4✔
3603
        local.array_values.push_back({});
4✔
3604
        local.array_values.push_back({EmbeddedContent{}});
4✔
3605
        TopLevelContent expected_recovered = remote;
4✔
3606
        expected_recovered.apply_recovery_from(local);
4✔
3607
        reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3608
    }
4✔
3609
    SECTION("both add conflicting embedded objects") {
172✔
3610
        TopLevelContent local;
4✔
3611
        TopLevelContent remote = local;
4✔
3612
        local.dict_values["new key1"] = {EmbeddedContent{}};
4✔
3613
        local.dict_values["new key2"] = {EmbeddedContent{}};
4✔
3614
        local.dict_values["new key3"] = {};
4✔
3615
        local.array_values.push_back({EmbeddedContent{}});
4✔
3616
        local.array_values.push_back({});
4✔
3617
        local.array_values.push_back({EmbeddedContent{}});
4✔
3618
        remote.dict_values["new key1"] = {EmbeddedContent{}};
4✔
3619
        remote.dict_values["new key2"] = {EmbeddedContent{}};
4✔
3620
        remote.dict_values["new key3"] = {};
4✔
3621
        remote.array_values.push_back({EmbeddedContent{}});
4✔
3622
        remote.array_values.push_back({});
4✔
3623
        remote.array_values.push_back({EmbeddedContent{}});
4✔
3624
        TopLevelContent expected_recovered = remote;
4✔
3625
        expected_recovered.apply_recovery_from(local);
4✔
3626
        reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3627
    }
4✔
3628
    SECTION("local modifies an embedded object which is removed by the remote") {
172✔
3629
        TopLevelContent local, remote;
4✔
3630
        local.link_value->name = "modified value";
4✔
3631
        remote.link_value = util::none;
4✔
3632
        TopLevelContent expected_recovered = remote;
4✔
3633
        expected_recovered.apply_recovery_from(local);
4✔
3634
        reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3635
    }
4✔
3636
    SECTION("local modifies a deep embedded object which is removed by the remote") {
172✔
3637
        TopLevelContent local, remote;
4✔
3638
        local.link_value->second_level->datetime = Timestamp{1, 1};
4✔
3639
        remote.link_value = util::none;
4✔
3640
        TopLevelContent expected_recovered = remote;
4✔
3641
        expected_recovered.apply_recovery_from(local);
4✔
3642
        reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3643
    }
4✔
3644
    SECTION("local modifies a deep embedded object which is removed at the second level by the remote") {
172✔
3645
        TopLevelContent local, remote;
4✔
3646
        local.link_value->second_level->datetime = Timestamp{1, 1};
4✔
3647
        remote.link_value->second_level = util::none;
4✔
3648
        TopLevelContent expected_recovered = remote;
4✔
3649
        expected_recovered.apply_recovery_from(local);
4✔
3650
        reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3651
    }
4✔
3652
    SECTION("with shared initial state") {
172✔
3653
        TopLevelContent initial;
116✔
3654
        test_reset->setup([&](SharedRealm realm) {
232✔
3655
            auto table = get_table(*realm, "TopLevel");
232✔
3656
            REQUIRE(table);
232!
3657
            auto obj = table->create_object_with_primary_key(pk_val);
232✔
3658
            initial.assign_to(obj);
232✔
3659
        });
232✔
3660
        TopLevelContent local = initial;
116✔
3661
        TopLevelContent remote = initial;
116✔
3662

58✔
3663
        SECTION("local modifications to an embedded object through a dictionary which is removed by the remote are "
116✔
3664
                "ignored") {
60✔
3665
            local.dict_values["foo"]->name = "modified";
4✔
3666
            local.dict_values["foo"]->second_level->datetime = Timestamp{1, 1};
4✔
3667
            local.dict_values["foo"]->array_vals.push_back(random_int());
4✔
3668
            local.dict_values["foo"]->array_vals.erase(local.dict_values["foo"]->array_vals.begin());
4✔
3669
            local.dict_values["foo"]->second_level->dict_values.erase(
4✔
3670
                local.dict_values["foo"]->second_level->dict_values.begin());
4✔
3671
            local.dict_values["foo"]->second_level->set_of_objects.clear();
4✔
3672
            remote.dict_values["foo"] = util::none;
4✔
3673
            TopLevelContent expected_recovered = remote;
4✔
3674
            reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3675
        }
4✔
3676
        SECTION("local modifications to an embedded object through a linklist element which is removed by the remote "
116✔
3677
                "triggers a list copy") {
60✔
3678
            local.array_values.begin()->name = "modified";
4✔
3679
            local.array_values.begin()->second_level->datetime = Timestamp{1, 1};
4✔
3680
            local.array_values.begin()->array_vals.push_back(random_int());
4✔
3681
            local.array_values.begin()->array_vals.erase(local.array_values.begin()->array_vals.begin());
4✔
3682
            local.array_values.begin()->second_level->dict_values.erase(
4✔
3683
                local.array_values.begin()->second_level->dict_values.begin());
4✔
3684
            local.array_values.begin()->second_level->set_of_objects.clear();
4✔
3685
            remote.array_values.erase(remote.array_values.begin());
4✔
3686
            TopLevelContent expected_recovered = local;
4✔
3687
            reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3688
        }
4✔
3689
        SECTION("local ArraySet to an embedded object through a deep link->linklist element which is removed by the "
116✔
3690
                "remote triggers a list copy") {
60✔
3691
            local.link_value->array_vals[0] = 12345;
4✔
3692
            remote.link_value->array_vals.erase(remote.link_value->array_vals.begin());
4✔
3693
            TopLevelContent expected_recovered = local;
4✔
3694
            reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3695
        }
4✔
3696
        SECTION("local ArrayErase to an embedded object through a deep link->linklist element which is removed by "
116✔
3697
                "the remote triggers a list copy") {
60✔
3698
            local.link_value->array_vals.erase(local.link_value->array_vals.begin());
4✔
3699
            remote.link_value->array_vals.clear();
4✔
3700
            TopLevelContent expected_recovered = local;
4✔
3701
            reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3702
        }
4✔
3703
        SECTION("local modifications to an embedded object through a linklist cleared by the remote triggers a list "
116✔
3704
                "copy") {
60✔
3705
            local.array_values.begin()->name = "modified";
4✔
3706
            local.array_values.begin()->second_level->datetime = Timestamp{1, 1};
4✔
3707
            local.array_values.begin()->array_vals.push_back(random_int());
4✔
3708
            local.array_values.begin()->array_vals.erase(local.array_values.begin()->array_vals.begin());
4✔
3709
            local.array_values.begin()->second_level->dict_values.erase(
4✔
3710
                local.array_values.begin()->second_level->dict_values.begin());
4✔
3711
            local.array_values.begin()->second_level->set_of_objects.clear();
4✔
3712
            remote.array_values.clear();
4✔
3713
            TopLevelContent expected_recovered = local;
4✔
3714
            reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3715
        }
4✔
3716
        SECTION("moving preexisting list items triggers a list copy") {
116✔
3717
            test_reset
4✔
3718
                ->make_local_changes([&](SharedRealm local_realm) {
4✔
3719
                    Obj obj = get_top_object(local_realm);
4✔
3720
                    auto list = obj.get_linklist("array_of_objs");
4✔
3721
                    REQUIRE(list.size() == 3);
4!
3722
                    list.move(0, 1);
4✔
3723
                    list.move(1, 2);
4✔
3724
                    list.move(1, 0);
4✔
3725
                })
4✔
3726
                ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
3727
                    Obj obj = get_top_object(remote_realm);
4✔
3728
                    auto list = obj.get_linklist("array_of_objs");
4✔
3729
                    list.remove(0, list.size()); // any change here is lost
4✔
3730
                    remote = TopLevelContent::get_from(obj);
4✔
3731
                })
4✔
3732
                ->on_post_reset([&](SharedRealm local_realm) {
4✔
3733
                    Obj obj = get_top_object(local_realm);
4✔
3734
                    TopLevelContent actual = TopLevelContent::get_from(obj);
4✔
3735
                    if (test_mode == ClientResyncMode::Recover) {
4✔
3736
                        TopLevelContent expected_recovered = local;
2✔
3737
                        std::iter_swap(expected_recovered.array_values.begin(),
2✔
3738
                                       expected_recovered.array_values.begin() + 1);
2✔
3739
                        std::iter_swap(expected_recovered.array_values.begin() + 1,
2✔
3740
                                       expected_recovered.array_values.begin() + 2);
2✔
3741
                        std::iter_swap(expected_recovered.array_values.begin() + 1,
2✔
3742
                                       expected_recovered.array_values.begin());
2✔
3743
                        actual.test(expected_recovered);
2✔
3744
                    }
2✔
3745
                    else {
2✔
3746
                        actual.test(remote);
2✔
3747
                    }
2✔
3748
                })
4✔
3749
                ->run();
4✔
3750
        }
4✔
3751
        SECTION("inserting new embedded objects into a list which has indices modified by the remote are recovered") {
116✔
3752
            EmbeddedContent new_element1, new_element2;
4✔
3753
            local.array_values.insert(local.array_values.end(), new_element1);
4✔
3754
            local.array_values.insert(local.array_values.begin(), new_element2);
4✔
3755
            remote.array_values.erase(remote.array_values.begin());
4✔
3756
            remote.array_values.erase(remote.array_values.begin());
4✔
3757
            test_reset
4✔
3758
                ->make_local_changes([&](SharedRealm local) {
4✔
3759
                    Obj obj = get_top_object(local);
4✔
3760
                    auto list = obj.get_linklist("array_of_objs");
4✔
3761
                    auto embedded = list.create_and_insert_linked_object(3);
4✔
3762
                    new_element1.assign_to(embedded);
4✔
3763
                    embedded = list.create_and_insert_linked_object(0);
4✔
3764
                    new_element2.assign_to(embedded);
4✔
3765
                })
4✔
3766
                ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
3767
                    Obj obj = get_top_object(remote_realm);
4✔
3768
                    auto list = obj.get_linklist("array_of_objs");
4✔
3769
                    list.remove(0, list.size() - 1);
4✔
3770
                    remote = TopLevelContent::get_from(obj);
4✔
3771
                })
4✔
3772
                ->on_post_reset([&](SharedRealm local_realm) {
4✔
3773
                    Obj obj = get_top_object(local_realm);
4✔
3774
                    TopLevelContent actual = TopLevelContent::get_from(obj);
4✔
3775
                    if (test_mode == ClientResyncMode::Recover) {
4✔
3776
                        TopLevelContent expected_recovered = remote;
2✔
3777
                        expected_recovered.array_values.insert(expected_recovered.array_values.end(), new_element1);
2✔
3778
                        expected_recovered.array_values.insert(expected_recovered.array_values.begin(), new_element2);
2✔
3779
                        actual.test(expected_recovered);
2✔
3780
                    }
2✔
3781
                    else {
2✔
3782
                        actual.test(remote);
2✔
3783
                    }
2✔
3784
                })
4✔
3785
                ->run();
4✔
3786
        }
4✔
3787
        SECTION("local list clear removes remotely inserted objects") {
116✔
3788
            EmbeddedContent new_element_local, new_element_remote;
4✔
3789
            local.array_values.clear();
4✔
3790
            TopLevelContent local2 = local;
4✔
3791
            local2.array_values.push_back(new_element_local);
4✔
3792
            remote.array_values.erase(remote.array_values.begin());
4✔
3793
            remote.array_values.push_back(new_element_remote); // lost via local.clear()
4✔
3794
            TopLevelContent expected_recovered = local2;
4✔
3795
            reset_embedded_object({local, local2}, {remote}, expected_recovered);
4✔
3796
        }
4✔
3797
        SECTION("local modification of a dictionary value which is removed by the remote") {
116✔
3798
            local.dict_values["foo"] = EmbeddedContent{};
4✔
3799
            remote.dict_values.erase("foo");
4✔
3800
            TopLevelContent expected_recovered = remote;
4✔
3801
            reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3802
        }
4✔
3803
        SECTION("local delete of a dictionary value which is removed by the remote") {
116✔
3804
            local.dict_values.erase("foo");
4✔
3805
            remote.dict_values.erase("foo");
4✔
3806
            TopLevelContent expected_recovered = remote;
4✔
3807
            reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3808
        }
4✔
3809
        SECTION("local delete of a dictionary value which is modified by the remote") {
116✔
3810
            local.dict_values.erase("foo");
4✔
3811
            remote.dict_values["foo"] = EmbeddedContent{};
4✔
3812
            TopLevelContent expected_recovered = local;
4✔
3813
            reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3814
        }
4✔
3815
        SECTION("both modify a dictionary value") {
116✔
3816
            EmbeddedContent new_local, new_remote;
4✔
3817
            local.dict_values["foo"] = new_local;
4✔
3818
            remote.dict_values["foo"] = new_remote;
4✔
3819
            TopLevelContent expected_recovered = remote;
4✔
3820
            expected_recovered.dict_values["foo"]->apply_recovery_from(*local.dict_values["foo"]);
4✔
3821
            // a verbatim list copy is triggered by modifications to items which were not just inserted
2✔
3822
            expected_recovered.dict_values["foo"]->array_vals = local.dict_values["foo"]->array_vals;
4✔
3823
            reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3824
        }
4✔
3825
        SECTION("both add the same dictionary key") {
116✔
3826
            auto key = GENERATE("new key", "", "\0");
12✔
3827
            EmbeddedContent new_local, new_remote;
12✔
3828
            local.dict_values[key] = new_local;
12✔
3829
            remote.dict_values[key] = new_remote;
12✔
3830
            TopLevelContent expected_recovered = remote;
12✔
3831
            expected_recovered.dict_values[key]->apply_recovery_from(*local.dict_values[key]);
12✔
3832
            // a verbatim list copy is triggered by modifications to items which were not just inserted
6✔
3833
            expected_recovered.dict_values[key]->array_vals = local.dict_values[key]->array_vals;
12✔
3834
            expected_recovered.dict_values[key]->second_level = local.dict_values[key]->second_level;
12✔
3835
            reset_embedded_object({local}, {remote}, expected_recovered);
12✔
3836
        }
12✔
3837
        SECTION("deep modifications to inserted and swapped list items are recovered") {
116✔
3838
            EmbeddedContent local_added_at_begin, local_added_at_end, local_added_before_end, remote_added;
4✔
3839
            size_t list_end = initial.array_values.size();
4✔
3840
            test_reset
4✔
3841
                ->make_local_changes([&](SharedRealm local) {
4✔
3842
                    Obj obj = get_top_object(local);
4✔
3843
                    auto list = obj.get_linklist("array_of_objs");
4✔
3844
                    auto embedded = list.create_and_insert_linked_object(0);
4✔
3845
                    local_added_at_begin.assign_to(embedded);
4✔
3846
                    embedded = list.create_and_insert_linked_object(list_end - 1);
4✔
3847
                    // this item is needed here so that move does not trigger a copy of the list
2✔
3848
                    local_added_before_end.assign_to(embedded);
4✔
3849
                    embedded = list.create_and_insert_linked_object(list_end);
4✔
3850
                    local_added_at_end.assign_to(embedded);
4✔
3851
                    local->commit_transaction();
4✔
3852
                    local->begin_transaction();
4✔
3853
                    // generates two move instructions, move(0, list_end), move(list_end - 1, 0)
2✔
3854
                    list.swap(0, list_end);
4✔
3855
                    local->commit_transaction();
4✔
3856
                    local->begin_transaction();
4✔
3857
                    local_added_at_end.name = "should be at begin now";
4✔
3858
                    local_added_at_begin.name = "should be at end now";
4✔
3859
                    local_added_at_end.assign_to(list.get_object(0));
4✔
3860
                    local_added_at_begin.assign_to(list.get_object(list_end));
4✔
3861
                })
4✔
3862
                ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
3863
                    Obj obj = get_top_object(remote_realm);
4✔
3864
                    auto list = obj.get_linklist("array_of_objs");
4✔
3865
                    list.remove(0, list.size()); // individual ArrayErase instructions, not a clear.
4✔
3866
                    remote_added.name = "remote added at zero, should end up in the middle of the list";
4✔
3867
                    remote_added.assign_to(list.create_and_insert_linked_object(0));
4✔
3868
                    remote = TopLevelContent::get_from(obj);
4✔
3869
                })
4✔
3870
                ->on_post_reset([&](SharedRealm local_realm) {
4✔
3871
                    Obj obj = get_top_object(local_realm);
4✔
3872
                    if (test_mode == ClientResyncMode::Recover) {
4✔
3873
                        auto list = obj.get_linklist("array_of_objs");
2✔
3874
                        REQUIRE(list.size() == 4);
2!
3875
                        EmbeddedContent embedded_0 = *EmbeddedContent::get_from(list.get_object(0));
2✔
3876
                        EmbeddedContent embedded_1 = *EmbeddedContent::get_from(list.get_object(1));
2✔
3877
                        EmbeddedContent embedded_2 = *EmbeddedContent::get_from(list.get_object(2));
2✔
3878
                        EmbeddedContent embedded_3 = *EmbeddedContent::get_from(list.get_object(3));
2✔
3879
                        embedded_0.test(local_added_at_end); // local added at end, moved to 0
2✔
3880
                        embedded_1.test(remote_added); // remote added at 0, bumped to 1 by recovered insert at 0
2✔
3881
                        embedded_2.test(local_added_before_end); // local added at 2, not moved
2✔
3882
                        embedded_3.test(local_added_at_begin);   // local added at 0, moved to end
2✔
3883
                    }
2✔
3884
                    else {
2✔
3885
                        TopLevelContent actual = TopLevelContent::get_from(obj);
2✔
3886
                        actual.test(remote);
2✔
3887
                    }
2✔
3888
                })
4✔
3889
                ->run();
4✔
3890
        }
4✔
3891
        SECTION("deep modifications to inserted and moved list items are recovered") {
116✔
3892
            EmbeddedContent local_added_at_begin, local_added_at_end, remote_added;
4✔
3893
            test_reset
4✔
3894
                ->make_local_changes([&](SharedRealm local_realm) {
4✔
3895
                    Obj obj = get_top_object(local_realm);
4✔
3896
                    auto list = obj.get_linklist("array_of_objs");
4✔
3897
                    auto embedded = list.create_and_insert_linked_object(0);
4✔
3898
                    local_added_at_begin.assign_to(embedded);
4✔
3899
                    embedded = list.create_and_insert_linked_object(list.size());
4✔
3900
                    local_added_at_end.assign_to(embedded);
4✔
3901
                    local_realm->commit_transaction();
4✔
3902
                    advance_and_notify(*local_realm);
4✔
3903
                    local_realm->begin_transaction();
4✔
3904
                    list.move(list.size() - 1, 0);
4✔
3905
                    local_realm->commit_transaction();
4✔
3906
                    advance_and_notify(*local_realm);
4✔
3907
                    local_realm->begin_transaction();
4✔
3908
                    local_added_at_end.name = "added at end, moved to 0";
4✔
3909
                    local_added_at_begin.name = "added at 0, bumped to 1";
4✔
3910
                    local_added_at_end.assign_to(list.get_object(0));
4✔
3911
                    local_added_at_begin.assign_to(list.get_object(1));
4✔
3912
                })
4✔
3913
                ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
3914
                    Obj obj = get_top_object(remote_realm);
4✔
3915
                    auto list = obj.get_linklist("array_of_objs");
4✔
3916
                    list.remove(0, list.size()); // individual ArrayErase instructions, not a clear.
4✔
3917
                    remote_added.name = "remote added at zero, should end up at the end of the list";
4✔
3918
                    remote_added.assign_to(list.create_and_insert_linked_object(0));
4✔
3919
                    remote = TopLevelContent::get_from(obj);
4✔
3920
                })
4✔
3921
                ->on_post_reset([&](SharedRealm local_realm) {
4✔
3922
                    Obj obj = get_top_object(local_realm);
4✔
3923
                    if (test_mode == ClientResyncMode::Recover) {
4✔
3924
                        auto list = obj.get_linklist("array_of_objs");
2✔
3925
                        REQUIRE(list.size() == 3);
2!
3926
                        EmbeddedContent embedded_0 = *EmbeddedContent::get_from(list.get_object(0));
2✔
3927
                        EmbeddedContent embedded_1 = *EmbeddedContent::get_from(list.get_object(1));
2✔
3928
                        EmbeddedContent embedded_2 = *EmbeddedContent::get_from(list.get_object(2));
2✔
3929
                        embedded_0.test(local_added_at_end);   // local added at end, moved to 0
2✔
3930
                        embedded_1.test(local_added_at_begin); // local added at begin, bumped up by move
2✔
3931
                        embedded_2.test(
2✔
3932
                            remote_added); // remote added at 0, bumped to 2 by recovered insert at 0 and move to 0
2✔
3933
                    }
2✔
3934
                    else {
2✔
3935
                        TopLevelContent actual = TopLevelContent::get_from(obj);
2✔
3936
                        actual.test(remote);
2✔
3937
                    }
2✔
3938
                })
4✔
3939
                ->run();
4✔
3940
        }
4✔
3941
        SECTION("removing an added list item does not trigger a list copy") {
116✔
3942
            EmbeddedContent local_added_and_removed, local_added;
4✔
3943
            test_reset
4✔
3944
                ->make_local_changes([&](SharedRealm local_realm) {
4✔
3945
                    Obj obj = get_top_object(local_realm);
4✔
3946
                    auto list = obj.get_linklist("array_of_objs");
4✔
3947
                    auto embedded = list.create_and_insert_linked_object(0);
4✔
3948
                    local_added_and_removed.assign_to(embedded);
4✔
3949
                    embedded = list.create_and_insert_linked_object(1);
4✔
3950
                    local_added.assign_to(embedded);
4✔
3951
                    local_realm->commit_transaction();
4✔
3952
                    local_realm->begin_transaction();
4✔
3953
                    list.remove(0);
4✔
3954
                })
4✔
3955
                ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
3956
                    Obj obj = get_top_object(remote_realm);
4✔
3957
                    auto list = obj.get_linklist("array_of_objs");
4✔
3958
                    list.remove(0, list.size()); // individual ArrayErase instructions, not a clear.
4✔
3959
                    remote = TopLevelContent::get_from(obj);
4✔
3960
                })
4✔
3961
                ->on_post_reset([&](SharedRealm local_realm) {
4✔
3962
                    Obj obj = get_top_object(local_realm);
4✔
3963
                    TopLevelContent actual = TopLevelContent::get_from(obj);
4✔
3964
                    if (test_mode == ClientResyncMode::Recover) {
4✔
3965
                        TopLevelContent expected_recovered = remote;
2✔
3966
                        expected_recovered.array_values.insert(expected_recovered.array_values.begin(), local_added);
2✔
3967
                        actual.test(expected_recovered);
2✔
3968
                    }
2✔
3969
                    else {
2✔
3970
                        actual.test(remote);
2✔
3971
                    }
2✔
3972
                })
4✔
3973
                ->run();
4✔
3974
        }
4✔
3975
        SECTION("removing a preexisting list item triggers a list copy") {
116✔
3976
            EmbeddedContent remote_updated_item_0, local_added;
4✔
3977
            test_reset
4✔
3978
                ->make_local_changes([&](SharedRealm local_realm) {
4✔
3979
                    Obj obj = get_top_object(local_realm);
4✔
3980
                    auto list = obj.get_linklist("array_of_objs");
4✔
3981
                    list.remove(0);
4✔
3982
                    list.remove(0);
4✔
3983
                    auto embedded = list.create_and_insert_linked_object(1);
4✔
3984
                    local_added.assign_to(embedded);
4✔
3985
                    local = TopLevelContent::get_from(obj);
4✔
3986
                })
4✔
3987
                ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
3988
                    // any change made to the list here is overwritten by the list copy
2✔
3989
                    Obj obj = get_top_object(remote_realm);
4✔
3990
                    auto list = obj.get_linklist("array_of_objs");
4✔
3991
                    list.remove(1, list.size()); // individual ArrayErase instructions, not a clear.
4✔
3992
                    remote_updated_item_0.assign_to(list.get_object(0));
4✔
3993
                    remote = TopLevelContent::get_from(obj);
4✔
3994
                })
4✔
3995
                ->on_post_reset([&](SharedRealm local_realm) {
4✔
3996
                    Obj obj = get_top_object(local_realm);
4✔
3997
                    TopLevelContent actual = TopLevelContent::get_from(obj);
4✔
3998
                    if (test_mode == ClientResyncMode::Recover) {
4✔
3999
                        actual.test(local);
2✔
4000
                    }
2✔
4001
                    else {
2✔
4002
                        actual.test(remote);
2✔
4003
                    }
2✔
4004
                })
4✔
4005
                ->run();
4✔
4006
        }
4✔
4007
        SECTION("adding and removing a list item when the remote removes the base object has no effect") {
116✔
4008
            EmbeddedContent local_added_at_begin;
4✔
4009
            test_reset
4✔
4010
                ->make_local_changes([&](SharedRealm local_realm) {
4✔
4011
                    Obj obj = get_top_object(local_realm);
4✔
4012
                    auto list = obj.get_linklist("array_of_objs");
4✔
4013
                    auto embedded = list.create_and_insert_linked_object(0);
4✔
4014
                    local_added_at_begin.assign_to(embedded);
4✔
4015
                    local_realm->commit_transaction();
4✔
4016
                    local_realm->begin_transaction();
4✔
4017
                    list.remove(0);
4✔
4018
                })
4✔
4019
                ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
4020
                    // any change made to the list here is overwritten by the list copy
2✔
4021
                    Obj obj = get_top_object(remote_realm);
4✔
4022
                    obj.remove();
4✔
4023
                })
4✔
4024
                ->on_post_reset([&](SharedRealm local_realm) {
4✔
4025
                    advance_and_notify(*local_realm);
4✔
4026
                    TableRef table = get_table(*local_realm, "TopLevel");
4✔
4027
                    REQUIRE(table->size() == 0);
4!
4028
                })
4✔
4029
                ->run();
4✔
4030
        }
4✔
4031
        SECTION("removing a preexisting list item when the remote removes the base object has no effect") {
116✔
4032
            test_reset
4✔
4033
                ->make_local_changes([&](SharedRealm local_realm) {
4✔
4034
                    Obj obj = get_top_object(local_realm);
4✔
4035
                    auto list = obj.get_linklist("array_of_objs");
4✔
4036
                    list.remove(0);
4✔
4037
                })
4✔
4038
                ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
4039
                    // any change made to the list here is overwritten by the list copy
2✔
4040
                    Obj obj = get_top_object(remote_realm);
4✔
4041
                    obj.remove();
4✔
4042
                })
4✔
4043
                ->on_post_reset([&](SharedRealm local_realm) {
4✔
4044
                    advance_and_notify(*local_realm);
4✔
4045
                    TableRef table = get_table(*local_realm, "TopLevel");
4✔
4046
                    REQUIRE(table->size() == 0);
4!
4047
                })
4✔
4048
                ->run();
4✔
4049
        }
4✔
4050
        SECTION("modifications to an embedded object are ignored when the base object is removed") {
116✔
4051
            EmbeddedContent local_modifications;
4✔
4052
            test_reset
4✔
4053
                ->make_local_changes([&](SharedRealm local_realm) {
4✔
4054
                    Obj obj = get_top_object(local_realm);
4✔
4055
                    auto list = obj.get_linklist("array_of_objs");
4✔
4056
                    local_modifications.assign_to(list.get_object(0));
4✔
4057
                })
4✔
4058
                ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
4059
                    // any change made to the list here is overwritten by the list copy
2✔
4060
                    Obj obj = get_top_object(remote_realm);
4✔
4061
                    obj.remove();
4✔
4062
                })
4✔
4063
                ->on_post_reset([&](SharedRealm local_realm) {
4✔
4064
                    advance_and_notify(*local_realm);
4✔
4065
                    TableRef table = get_table(*local_realm, "TopLevel");
4✔
4066
                    REQUIRE(table->size() == 0);
4!
4067
                })
4✔
4068
                ->run();
4✔
4069
        }
4✔
4070
        SECTION("changes made through two layers of embedded lists can be recovered") {
116✔
4071
            EmbeddedContent local_added_at_0, local_added_at_1, remote_added;
4✔
4072
            local_added_at_0.name = "added at 0, moved to 1";
4✔
4073
            local_added_at_0.array_of_seconds = {{}, {}};
4✔
4074
            local_added_at_1.name = "added at 1, bumped to 0";
4✔
4075
            local_added_at_1.array_of_seconds = {{}, {}, {}};
4✔
4076
            remote_added.array_of_seconds = {{}, {}};
4✔
4077
            SecondLevelEmbeddedContent modified, inserted;
4✔
4078
            test_reset
4✔
4079
                ->make_local_changes([&](SharedRealm local_realm) {
4✔
4080
                    Obj obj = get_top_object(local_realm);
4✔
4081
                    auto list = obj.get_linklist("array_of_objs");
4✔
4082
                    auto embedded = list.create_and_insert_linked_object(0);
4✔
4083
                    local_added_at_0.assign_to(embedded);
4✔
4084
                    embedded = list.create_and_insert_linked_object(1);
4✔
4085
                    local_added_at_1.assign_to(embedded);
4✔
4086
                    local_realm->commit_transaction();
4✔
4087
                    local_realm->begin_transaction();
4✔
4088
                    auto list_of_seconds = embedded.get_linklist("array_of_seconds");
4✔
4089
                    list_of_seconds.move(0, 1);
4✔
4090
                    std::iter_swap(local_added_at_1.array_of_seconds.begin(),
4✔
4091
                                   local_added_at_1.array_of_seconds.begin() + 1);
4✔
4092
                    local_realm->commit_transaction();
4✔
4093
                    local_realm->begin_transaction();
4✔
4094
                    list.move(0, 1);
4✔
4095
                    local_realm->commit_transaction();
4✔
4096
                    local_realm->begin_transaction();
4✔
4097
                    modified.assign_to(list_of_seconds.get_object(0));
4✔
4098
                    auto new_second = list_of_seconds.create_and_insert_linked_object(0);
4✔
4099
                    inserted.assign_to(new_second);
4✔
4100
                    local_added_at_1.array_of_seconds[0] = modified;
4✔
4101
                    local_added_at_1.array_of_seconds.insert(local_added_at_1.array_of_seconds.begin(), inserted);
4✔
4102
                })
4✔
4103
                ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
4104
                    Obj obj = get_top_object(remote_realm);
4✔
4105
                    auto list = obj.get_linklist("array_of_objs");
4✔
4106
                    list.remove(0, list.size()); // individual ArrayErase instructions, not a clear.
4✔
4107
                    remote_added.name = "remote added at zero, should end up at the end of the list";
4✔
4108
                    remote_added.assign_to(list.create_and_insert_linked_object(0));
4✔
4109
                    remote = TopLevelContent::get_from(obj);
4✔
4110
                })
4✔
4111
                ->on_post_reset([&](SharedRealm local_realm) {
4✔
4112
                    Obj obj = get_top_object(local_realm);
4✔
4113
                    if (test_mode == ClientResyncMode::Recover) {
4✔
4114
                        auto list = obj.get_linklist("array_of_objs");
2✔
4115
                        REQUIRE(list.size() == 3);
2!
4116
                        EmbeddedContent embedded_0 = *EmbeddedContent::get_from(list.get_object(0));
2✔
4117
                        EmbeddedContent embedded_1 = *EmbeddedContent::get_from(list.get_object(1));
2✔
4118
                        EmbeddedContent embedded_2 = *EmbeddedContent::get_from(list.get_object(2));
2✔
4119
                        embedded_0.test(local_added_at_1); // local added at end, moved to 0
2✔
4120
                        embedded_1.test(local_added_at_0); // local added at begin, bumped up by move
2✔
4121
                        embedded_2.test(remote_added);     // remote added at 0, bumped to 2 by recovered
2✔
4122
                    }
2✔
4123
                    else {
2✔
4124
                        TopLevelContent actual = TopLevelContent::get_from(obj);
2✔
4125
                        actual.test(remote);
2✔
4126
                    }
2✔
4127
                })
4✔
4128
                ->run();
4✔
4129
        }
4✔
4130
        SECTION("insertions to a preexisting object through two layers of embedded lists triggers a list copy") {
116✔
4131
            SecondLevelEmbeddedContent local_added, remote_added;
4✔
4132
            test_reset
4✔
4133
                ->make_local_changes([&](SharedRealm local_realm) {
4✔
4134
                    Obj obj = get_top_object(local_realm);
4✔
4135
                    auto list = obj.get_linklist("array_of_objs");
4✔
4136
                    local_added.assign_to(
4✔
4137
                        list.get_object(0).get_linklist("array_of_seconds").create_and_insert_linked_object(0));
4✔
4138
                })
4✔
4139
                ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
4140
                    Obj obj = get_top_object(remote_realm);
4✔
4141
                    auto list = obj.get_linklist("array_of_objs");
4✔
4142
                    remote_added.assign_to(
4✔
4143
                        list.get_object(0).get_linklist("array_of_seconds").create_and_insert_linked_object(0));
4✔
4144
                    list.move(0, 1);
4✔
4145
                    remote = TopLevelContent::get_from(obj);
4✔
4146
                })
4✔
4147
                ->on_post_reset([&](SharedRealm local_realm) {
4✔
4148
                    Obj obj = get_top_object(local_realm);
4✔
4149
                    if (test_mode == ClientResyncMode::Recover) {
4✔
4150
                        auto list = obj.get_linklist("array_of_objs");
2✔
4151
                        REQUIRE(list.size() == 3);
2!
4152
                        EmbeddedContent embedded_0 = *EmbeddedContent::get_from(list.get_object(0));
2✔
4153
                        EmbeddedContent embedded_1 = *EmbeddedContent::get_from(list.get_object(1));
2✔
4154
                        EmbeddedContent embedded_2 = *EmbeddedContent::get_from(list.get_object(2));
2✔
4155
                        REQUIRE(embedded_0.array_of_seconds.size() == 1);
2!
4156
                        embedded_0.array_of_seconds[0].test(local_added);
2✔
4157
                        REQUIRE(embedded_1.array_of_seconds.size() ==
2!
4158
                                0); // remote changes overwritten by local list copy
2✔
4159
                        REQUIRE(embedded_2.array_of_seconds.size() == 0);
2!
4160
                    }
2✔
4161
                    else {
2✔
4162
                        TopLevelContent actual = TopLevelContent::get_from(obj);
2✔
4163
                        actual.test(remote);
2✔
4164
                    }
2✔
4165
                })
4✔
4166
                ->run();
4✔
4167
        }
4✔
4168

58✔
4169
        SECTION("modifications to a preexisting object through two layers of embedded lists triggers a list copy") {
116✔
4170
            SecondLevelEmbeddedContent preexisting_item, local_modified, remote_added;
4✔
4171
            initial.array_values[0].array_of_seconds.push_back(preexisting_item);
4✔
4172
            const size_t initial_item_pos = initial.array_values[0].array_of_seconds.size() - 1;
4✔
4173
            local = initial;
4✔
4174
            remote = initial;
4✔
4175
            local.array_values[0].array_of_seconds[initial_item_pos] = local_modified;
4✔
4176
            remote.array_values[0].array_of_seconds.push_back(remote_added); // overwritten by local!
4✔
4177
            TopLevelContent expected_recovered = local;
4✔
4178
            reset_embedded_object({local}, {remote}, expected_recovered);
4✔
4179
        }
4✔
4180

58✔
4181
        SECTION("add int") {
116✔
4182
            auto add_to_dict_item = [&](SharedRealm realm, std::string key, int64_t addition) {
20✔
4183
                Obj obj = get_top_object(realm);
20✔
4184
                auto dict = obj.get_dictionary("embedded_dict");
20✔
4185
                auto embedded = dict.get_object(key);
20✔
4186
                REQUIRE(!!embedded);
20!
4187
                embedded.add_int("int_value", addition);
20✔
4188
                return TopLevelContent::get_from(obj);
20✔
4189
            };
20✔
4190
            TopLevelContent expected_recovered;
16✔
4191
            const std::string existing_key = "foo";
16✔
4192

8✔
4193
            test_reset->on_post_reset([&](SharedRealm local_realm) {
14✔
4194
                Obj obj = get_top_object(local_realm);
12✔
4195
                TopLevelContent actual = TopLevelContent::get_from(obj);
12✔
4196
                actual.test(test_mode == ClientResyncMode::Recover ? expected_recovered : initial);
9✔
4197
            });
12✔
4198
            int64_t initial_value = initial.dict_values[existing_key]->int_value;
16✔
4199
            std::mt19937_64 engine(std::random_device{}());
16✔
4200
            std::uniform_int_distribution<int64_t> rng(-10'000'000'000, 10'000'000'000);
16✔
4201

8✔
4202
            int64_t addition = rng(engine);
16✔
4203
            SECTION("local add_int to an existing dictionary item") {
16✔
4204
                INFO("adding " << initial_value << " with " << addition);
4✔
4205
                expected_recovered = initial;
4✔
4206
                expected_recovered.dict_values[existing_key]->int_value += addition;
4✔
4207
                test_reset
4✔
4208
                    ->make_local_changes([&](SharedRealm local) {
4✔
4209
                        add_to_dict_item(local, existing_key, addition);
4✔
4210
                    })
4✔
4211
                    ->run();
4✔
4212
            }
4✔
4213
            SECTION("local and remote both create the same dictionary item and add to it") {
16✔
4214
                int64_t remote_addition = rng(engine);
4✔
4215
                INFO("adding " << initial_value << " with local " << addition << " and remote " << remote_addition);
4✔
4216
                expected_recovered = initial;
4✔
4217
                expected_recovered.dict_values[existing_key]->int_value += (addition + remote_addition);
4✔
4218
                test_reset
4✔
4219
                    ->make_local_changes([&](SharedRealm local) {
4✔
4220
                        add_to_dict_item(local, existing_key, addition);
4✔
4221
                    })
4✔
4222
                    ->make_remote_changes([&](SharedRealm remote) {
4✔
4223
                        initial = add_to_dict_item(remote, existing_key, remote_addition);
4✔
4224
                    })
4✔
4225
                    ->run();
4✔
4226
            }
4✔
4227
            SECTION("local add_int on a dictionary item which the remote removed is ignored") {
16✔
4228
                INFO("adding " << initial_value << " with " << addition);
4✔
4229
                test_reset
4✔
4230
                    ->make_local_changes([&](SharedRealm local) {
4✔
4231
                        add_to_dict_item(local, existing_key, addition);
4✔
4232
                    })
4✔
4233
                    ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
4234
                        Obj obj = get_top_object(remote_realm);
4✔
4235
                        auto dict = obj.get_dictionary("embedded_dict");
4✔
4236
                        dict.erase(Mixed{existing_key});
4✔
4237
                        initial = TopLevelContent::get_from(obj);
4✔
4238
                        expected_recovered = initial;
4✔
4239
                    })
4✔
4240
                    ->run();
4✔
4241
            }
4✔
4242
            SECTION("local add_int on a dictionary item when the entire root object is removed by the remote removed "
16✔
4243
                    "is ignored") {
10✔
4244
                INFO("adding " << initial_value << " with " << addition);
4✔
4245
                test_reset
4✔
4246
                    ->make_local_changes([&](SharedRealm local) {
4✔
4247
                        add_to_dict_item(local, existing_key, addition);
4✔
4248
                    })
4✔
4249
                    ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
4250
                        Obj obj = get_top_object(remote_realm);
4✔
4251
                        TableRef table = obj.get_table();
4✔
4252
                        obj.remove();
4✔
4253
                        REQUIRE(table->size() == 0);
4!
4254
                    })
4✔
4255
                    ->on_post_reset([&](SharedRealm local_realm) {
4✔
4256
                        advance_and_notify(*local_realm);
4✔
4257
                        TableRef table = get_table(*local_realm, "TopLevel");
4✔
4258
                        REQUIRE(table->size() == 0);
4!
4259
                    })
4✔
4260
                    ->run();
4✔
4261
            }
4✔
4262
        }
16✔
4263
    }
116✔
4264
    SECTION("remote adds a top level link cycle") {
172✔
4265
        TopLevelContent local;
4✔
4266
        TopLevelContent remote = local;
4✔
4267
        remote.link_value->second_level->pk_of_linked_object = Mixed{pk_val};
4✔
4268
        TopLevelContent expected_recovered = remote;
4✔
4269
        expected_recovered.apply_recovery_from(local);
4✔
4270
        // the remote change exists because no local instruction set the value to anything (default)
2✔
4271
        expected_recovered.link_value->second_level->pk_of_linked_object = Mixed{pk_val};
4✔
4272
        reset_embedded_object({local}, {remote}, expected_recovered);
4✔
4273
    }
4✔
4274
    SECTION("local adds a top level link cycle") {
172✔
4275
        TopLevelContent local;
4✔
4276
        TopLevelContent remote = local;
4✔
4277
        local.link_value->second_level->pk_of_linked_object = Mixed{pk_val};
4✔
4278
        TopLevelContent expected_recovered = remote;
4✔
4279
        expected_recovered.apply_recovery_from(local);
4✔
4280
        reset_embedded_object({local}, {remote}, expected_recovered);
4✔
4281
    }
4✔
4282
    SECTION("server adds embedded object classes") {
172✔
4283
        SyncTestFile config2(init_sync_manager.app(), "default");
4✔
4284
        config2.schema = config.schema;
4✔
4285
        config.schema = Schema{shared_class};
4✔
4286
        test_reset = reset_utils::make_fake_local_client_reset(config, config2);
4✔
4287
        TopLevelContent remote_content;
4✔
4288

2✔
4289
        test_reset
4✔
4290
            ->make_remote_changes([&](SharedRealm remote) {
4✔
4291
                advance_and_notify(*remote);
4✔
4292
                TableRef table = get_table(*remote, "TopLevel");
4✔
4293
                auto obj = table->create_object_with_primary_key(pk_val);
4✔
4294
                REQUIRE(table->size() == 1);
4!
4295
                remote_content.assign_to(obj);
4✔
4296
            })
4✔
4297
            ->on_post_reset([&](SharedRealm local) {
4✔
4298
                advance_and_notify(*local);
4✔
4299
                TableRef table = get_table(*local, "TopLevel");
4✔
4300
                REQUIRE(table->size() == 1);
4!
4301
                Obj obj = *table->begin();
4✔
4302
                TopLevelContent actual = TopLevelContent::get_from(obj);
4✔
4303
                actual.test(remote_content);
4✔
4304
            })
4✔
4305
            ->run();
4✔
4306
    }
4✔
4307
    SECTION("client adds embedded object classes") {
172✔
4308
        SyncTestFile config2(init_sync_manager.app(), "default");
4✔
4309
        config2.schema = Schema{shared_class};
4✔
4310
        test_reset = reset_utils::make_fake_local_client_reset(config, config2);
4✔
4311
        TopLevelContent local_content;
4✔
4312
        test_reset->make_local_changes([&](SharedRealm local) {
4✔
4313
            TableRef table = get_table(*local, "TopLevel");
4✔
4314
            auto obj = table->create_object_with_primary_key(pk_val);
4✔
4315
            REQUIRE(table->size() == 1);
4!
4316
            local_content.assign_to(obj);
4✔
4317
        });
4✔
4318
        if (test_mode == ClientResyncMode::DiscardLocal) {
4✔
4319
            REQUIRE_THROWS_WITH(test_reset->run(), "Client reset cannot recover when classes have been removed: "
2✔
4320
                                                   "{EmbeddedObject, EmbeddedObject2, TopLevel}");
2✔
4321
        }
2✔
4322
        else {
2✔
4323
            // In recovery mode, AddTable should succeed if the server is in dev mode, and fail
1✔
4324
            // if the server is in production which in that case the changes will be rejected.
1✔
4325
            // Since this is a fake reset, it always succeeds here.
1✔
4326
            test_reset
2✔
4327
                ->on_post_reset([&](SharedRealm local) {
2✔
4328
                    TableRef table = get_table(*local, "TopLevel");
2✔
4329
                    REQUIRE(table->size() == 1);
2!
4330
                })
2✔
4331
                ->run();
2✔
4332
        }
2✔
4333
    }
4✔
4334
}
172✔
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

© 2026 Coveralls, Inc