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

realm / realm-core / 1860

23 Nov 2023 05:57PM UTC coverage: 91.692% (+0.03%) from 91.66%
1860

push

Evergreen

web-flow
Client reset with recovery fixes (#7112)

* Do not resurrect objects that have been deleted by

the server when copying local lists with links in them.

* test insert in list recovery

* fix sort/distinct on LnkSet with unresolved links

* lint

92432 of 169288 branches covered (0.0%)

303 of 317 new or added lines in 8 files covered. (95.58%)

35 existing lines in 7 files now uncovered.

231734 of 252732 relevant lines covered (91.69%)

6882359.69 hits per line

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

98.29
/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,462✔
60
        std::lock_guard<std::mutex> lock(m_mutex);
3,462✔
61
        return bool(m_error);
3,462✔
62
    }
3,462✔
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,439✔
104
    return ObjectStore::table_for_object_type(realm.read_group(), object_type);
18,439✔
105
}
18,439✔
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,157✔
186
        return util::File::exists(_impl::client_reset::get_fresh_path_for(realm_config.path));
25,157✔
187
    });
25,157✔
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,407✔
339
                    return bool(err);
3,407✔
340
                });
3,407✔
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✔
NEW
404
            return {};
×
NEW
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 {
142✔
912
                    std::lock_guard<std::mutex> lock(mtx);
142✔
913
                    return after_callback_invocations > 0;
142✔
914
                },
142✔
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 {
75✔
967
                    std::lock_guard<std::mutex> lock(mtx);
75✔
968
                    return before_callback_invocations > 0;
75✔
969
                },
75✔
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 {
85✔
996
                        std::lock_guard<std::mutex> lock(mtx);
85✔
997
                        realm->begin_transaction();
85✔
998
                        TableRef table = get_table(*realm, "object");
85✔
999
                        REQUIRE(table);
85!
1000
                        REQUIRE(table->size() == 1);
85!
1001
                        auto col = table->get_column_key("value");
85✔
1002
                        int64_t value = table->begin()->get<Int>(col);
85✔
1003
                        realm->cancel_transaction();
85✔
1004
                        return value == 6;
85✔
1005
                    },
85✔
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,588✔
1030
                return util::File::exists(_impl::client_reset::get_fresh_path_for(local_config.path));
1,588✔
1031
            });
1,588✔
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
    auto before_callback_called = util::make_promise_future<void>();
2✔
1870
    auto after_callback_called = util::make_promise_future<void>();
2✔
1871
    create_user_and_log_in(app);
2✔
1872
    SyncTestFile realm_config(app->current_user(), partition.value, std::nullopt,
2✔
1873
                              [](std::shared_ptr<SyncSession>, SyncError) { /*noop*/ });
1✔
1874
    realm_config.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
1875

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

2✔
1887
            if (event_data.event != SyncClientHookEvent::DownloadMessageReceived) {
4✔
1888
                return SyncClientHookAction::NoAction;
2✔
1889
            }
2✔
1890

1✔
1891
            if (client_reset_triggered) {
2✔
1892
                return SyncClientHookAction::NoAction;
×
1893
            }
×
1894
            client_reset_triggered = true;
2✔
1895
            reset_utils::trigger_client_reset(test_app_session.app_session());
2✔
1896
            return SyncClientHookAction::EarlyReturn;
2✔
1897
        };
2✔
1898

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

1✔
1910
    realm_config.sync_config->notify_after_client_reset =
2✔
1911
        [promise = util::CopyablePromiseHolder(std::move(after_callback_called.promise))](
2✔
1912
            std::shared_ptr<Realm> realm, ThreadSafeReference, bool) mutable {
2✔
1913
            CHECK(realm->schema_version() == ObjectStore::NotVersioned);
2!
1914
            promise.get_promise().emplace_value();
2✔
1915
        };
2✔
1916

1✔
1917
    auto realm_task = Realm::get_synchronized_realm(realm_config);
2✔
1918
    auto realm_pf = util::make_promise_future<SharedRealm>();
2✔
1919
    realm_task->start([promise_holder = util::CopyablePromiseHolder(std::move(realm_pf.promise))](
2✔
1920
                          ThreadSafeReference ref, std::exception_ptr ex) mutable {
2✔
1921
        auto promise = promise_holder.get_promise();
2✔
1922
        if (ex) {
2✔
1923
            try {
×
1924
                std::rethrow_exception(ex);
×
1925
            }
×
1926
            catch (...) {
×
1927
                promise.set_error(exception_to_status());
×
1928
            }
×
1929
            return;
×
1930
        }
2✔
1931
        auto realm = Realm::get_shared_realm(std::move(ref));
2✔
1932
        if (!realm) {
2✔
1933
            promise.set_error({ErrorCodes::RuntimeError, "could not get realm from threadsaferef"});
×
1934
        }
×
1935
        promise.emplace_value(std::move(realm));
2✔
1936
    });
2✔
1937
    auto realm = realm_pf.future.get();
2✔
1938
    before_callback_called.future.get();
2✔
1939
    after_callback_called.future.get();
2✔
1940
}
2✔
1941

1942
#endif // REALM_ENABLE_AUTH_TESTS
1943

1944
namespace cf = realm::collection_fixtures;
1945
TEMPLATE_TEST_CASE("client reset types", "[sync][pbs][client reset]", cf::MixedVal, cf::Int, cf::Bool, cf::Float,
1946
                   cf::Double, cf::String, cf::Binary, cf::Date, cf::OID, cf::Decimal, cf::UUID,
1947
                   cf::BoxedOptional<cf::Int>, cf::BoxedOptional<cf::Bool>, cf::BoxedOptional<cf::Float>,
1948
                   cf::BoxedOptional<cf::Double>, cf::BoxedOptional<cf::OID>, cf::BoxedOptional<cf::UUID>,
1949
                   cf::UnboxedOptional<cf::String>, cf::UnboxedOptional<cf::Binary>, cf::UnboxedOptional<cf::Date>,
1950
                   cf::UnboxedOptional<cf::Decimal>)
1951
{
2,256✔
1952
    auto values = TestType::values();
2,256✔
1953
    using T = typename TestType::Type;
2,256✔
1954

1,128✔
1955
    if (!util::EventLoop::has_implementation())
2,256✔
1956
        return;
×
1957

1,128✔
1958
    TestSyncManager init_sync_manager;
2,256✔
1959
    SyncTestFile config(init_sync_manager.app(), "default");
2,256✔
1960
    config.cache = false;
2,256✔
1961
    config.automatic_change_notifications = false;
2,256✔
1962
    ClientResyncMode test_mode = GENERATE(ClientResyncMode::DiscardLocal, ClientResyncMode::Recover);
2,256✔
1963
    CAPTURE(test_mode);
2,256✔
1964
    config.sync_config->client_resync_mode = test_mode;
2,256✔
1965
    config.schema = Schema{
2,256✔
1966
        {"object",
2,256✔
1967
         {
2,256✔
1968
             {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2,256✔
1969
             {"value", PropertyType::Int},
2,256✔
1970
         }},
2,256✔
1971
        {"test type",
2,256✔
1972
         {{"_id", PropertyType::Int, Property::IsPrimary{true}},
2,256✔
1973
          {"value", TestType::property_type},
2,256✔
1974
          {"list", PropertyType::Array | TestType::property_type},
2,256✔
1975
          {"dictionary", PropertyType::Dictionary | TestType::property_type},
2,256✔
1976
          {"set", PropertyType::Set | TestType::property_type}}},
2,256✔
1977
    };
2,256✔
1978

1,128✔
1979
    SyncTestFile config2(init_sync_manager.app(), "default");
2,256✔
1980
    config2.schema = config.schema;
2,256✔
1981

1,128✔
1982
    Results results;
2,256✔
1983
    Object object;
2,256✔
1984
    CollectionChangeSet object_changes, results_changes;
2,256✔
1985
    NotificationToken object_token, results_token;
2,256✔
1986
    auto setup_listeners = [&](SharedRealm realm) {
2,256✔
1987
        results = Results(realm, ObjectStore::table_for_object_type(realm->read_group(), "test type"))
2,256✔
1988
                      .sort({{{"_id", true}}});
2,256✔
1989
        if (results.size() >= 1) {
2,256✔
1990
            auto obj = *ObjectStore::table_for_object_type(realm->read_group(), "test type")->begin();
2,256✔
1991
            object = Object(realm, obj);
2,256✔
1992
            object_token = object.add_notification_callback([&](CollectionChangeSet changes) {
3,510✔
1993
                object_changes = std::move(changes);
3,510✔
1994
            });
3,510✔
1995
        }
2,256✔
1996
        results_token = results.add_notification_callback([&](CollectionChangeSet changes) {
3,510✔
1997
            results_changes = std::move(changes);
3,510✔
1998
        });
3,510✔
1999
    };
2,256✔
2000

1,128✔
2001
    auto check_list = [&](Obj obj, std::vector<T>& expected) {
3,024✔
2002
        ColKey col = obj.get_table()->get_column_key("list");
3,024✔
2003
        auto actual = obj.get_list_values<T>(col);
3,024✔
2004
        REQUIRE(actual == expected);
3,024!
2005
    };
3,024✔
2006

1,128✔
2007
    auto check_dictionary = [&](Obj obj, std::map<std::string, Mixed>& expected) {
2,136✔
2008
        ColKey col = obj.get_table()->get_column_key("dictionary");
2,016✔
2009
        Dictionary dict = obj.get_dictionary(col);
2,016✔
2010
        REQUIRE(dict.size() == expected.size());
2,016!
2011
        for (auto& pair : expected) {
3,612✔
2012
            auto it = dict.find(pair.first);
3,612✔
2013
            REQUIRE(it != dict.end());
3,612!
2014
            REQUIRE((*it).second == pair.second);
3,612!
2015
        }
3,612✔
2016
    };
2,016✔
2017

1,128✔
2018
    auto check_set = [&](Obj obj, std::set<Mixed>& expected) {
2,688✔
2019
        ColKey col = obj.get_table()->get_column_key("set");
2,688✔
2020
        SetBasePtr set = obj.get_setbase_ptr(col);
2,688✔
2021
        REQUIRE(set->size() == expected.size());
2,688!
2022
        for (auto& value : expected) {
3,024✔
2023
            auto ndx = set->find_any(value);
3,024✔
2024
            CAPTURE(value);
3,024✔
2025
            REQUIRE(ndx != realm::not_found);
3,024!
2026
        }
3,024✔
2027
    };
2,688✔
2028

1,128✔
2029
    std::unique_ptr<reset_utils::TestClientReset> test_reset =
2,256✔
2030
        reset_utils::make_fake_local_client_reset(config, config2);
2,256✔
2031

1,128✔
2032
    SECTION("property") {
2,256✔
2033
        REQUIRE(values.size() >= 2);
324!
2034
        REQUIRE(values[0] != values[1]);
324!
2035
        int64_t pk_val = 0;
324✔
2036
        T initial_value = values[0];
324✔
2037

162✔
2038
        auto set_value = [](SharedRealm realm, T value) {
648✔
2039
            auto table = get_table(*realm, "test type");
648✔
2040
            REQUIRE(table);
648!
2041
            REQUIRE(table->size() == 1);
648!
2042
            ColKey col = table->get_column_key("value");
648✔
2043
            table->begin()->set<T>(col, value);
648✔
2044
        };
648✔
2045
        auto check_value = [](Obj obj, T value) {
1,296✔
2046
            ColKey col = obj.get_table()->get_column_key("value");
1,296✔
2047
            REQUIRE(obj.get<T>(col) == value);
1,296!
2048
        };
1,296✔
2049

162✔
2050
        test_reset->setup([&pk_val, &initial_value](SharedRealm realm) {
648✔
2051
            auto table = get_table(*realm, "test type");
648✔
2052
            REQUIRE(table);
648!
2053
            auto obj = table->create_object_with_primary_key(pk_val);
648✔
2054
            ColKey col = table->get_column_key("value");
648✔
2055
            obj.set<T>(col, initial_value);
648✔
2056
        });
648✔
2057

162✔
2058
        auto reset_property = [&](T local_state, T remote_state) {
324✔
2059
            test_reset
324✔
2060
                ->make_local_changes([&](SharedRealm local_realm) {
324✔
2061
                    set_value(local_realm, local_state);
324✔
2062
                })
324✔
2063
                ->make_remote_changes([&](SharedRealm remote_realm) {
324✔
2064
                    set_value(remote_realm, remote_state);
324✔
2065
                })
324✔
2066
                ->on_post_local_changes([&](SharedRealm realm) {
324✔
2067
                    setup_listeners(realm);
324✔
2068
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
324✔
2069
                    CHECK(results.size() == 1);
324!
2070
                    CHECK(results.get<Obj>(0).get<Int>("_id") == pk_val);
324!
2071
                    CHECK(object.is_valid());
324!
2072
                    check_value(results.get<Obj>(0), local_state);
324✔
2073
                    check_value(object.get_obj(), local_state);
324✔
2074
                })
324✔
2075
                ->on_post_reset([&](SharedRealm realm) {
324✔
2076
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
324✔
2077

162✔
2078
                    CHECK(results.size() == 1);
324!
2079
                    CHECK(object.is_valid());
324!
2080
                    T expected_state = (test_mode == ClientResyncMode::DiscardLocal) ? remote_state : local_state;
324✔
2081
                    check_value(results.get<Obj>(0), expected_state);
324✔
2082
                    check_value(object.get_obj(), expected_state);
324✔
2083
                    if (local_state == expected_state) {
324✔
2084
                        REQUIRE_INDICES(results_changes.modifications);
162!
2085
                        REQUIRE_INDICES(object_changes.modifications);
162!
2086
                    }
162✔
2087
                    else {
162✔
2088
                        REQUIRE_INDICES(results_changes.modifications, 0);
162!
2089
                        REQUIRE_INDICES(object_changes.modifications, 0);
162!
2090
                    }
162✔
2091
                    REQUIRE_INDICES(results_changes.insertions);
324!
2092
                    REQUIRE_INDICES(results_changes.deletions);
324!
2093
                    REQUIRE_INDICES(object_changes.insertions);
324!
2094
                    REQUIRE_INDICES(object_changes.deletions);
324!
2095
                })
324✔
2096
                ->run();
324✔
2097
        };
324✔
2098

162✔
2099
        SECTION("modify") {
324✔
2100
            reset_property(values[0], values[1]);
84✔
2101
        }
84✔
2102
        SECTION("modify opposite") {
324✔
2103
            reset_property(values[1], values[0]);
84✔
2104
        }
84✔
2105
        // verify whatever other test values are provided (type bool only has two)
162✔
2106
        for (size_t i = 2; i < values.size(); ++i) {
1,928✔
2107
            SECTION(util::format("modify to value: %1", i)) {
1,604✔
2108
                reset_property(values[0], values[i]);
156✔
2109
            }
156✔
2110
        }
1,604✔
2111
    }
324✔
2112

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

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

378✔
2156
                    CHECK(results.size() == 1);
756!
2157
                    CHECK(object.is_valid());
756!
2158
                    std::vector<T>& expected_state = remote_state;
756✔
2159
                    if (test_mode == ClientResyncMode::Recover) {
756✔
2160
                        expected_state = local_state;
378✔
2161
                    }
378✔
2162
                    check_list(results.get<Obj>(0), expected_state);
756✔
2163
                    check_list(object.get_obj(), expected_state);
756✔
2164
                    if (local_state == expected_state) {
756✔
2165
                        REQUIRE_INDICES(results_changes.modifications);
462!
2166
                        REQUIRE_INDICES(object_changes.modifications);
462!
2167
                    }
462✔
2168
                    else {
294✔
2169
                        REQUIRE_INDICES(results_changes.modifications, 0);
294!
2170
                        REQUIRE_INDICES(object_changes.modifications, 0);
294!
2171
                    }
294✔
2172
                    REQUIRE_INDICES(results_changes.insertions);
756!
2173
                    REQUIRE_INDICES(results_changes.deletions);
756!
2174
                    REQUIRE_INDICES(object_changes.insertions);
756!
2175
                    REQUIRE_INDICES(object_changes.deletions);
756!
2176
                })
756✔
2177
                ->run();
756✔
2178
        };
756✔
2179

378✔
2180
        SECTION("modify") {
756✔
2181
            reset_list({values[0]}, {values[1]});
84✔
2182
        }
84✔
2183
        SECTION("modify opposite") {
756✔
2184
            reset_list({values[1]}, {values[0]});
84✔
2185
        }
84✔
2186
        SECTION("empty remote") {
756✔
2187
            reset_list({values[1], values[0], values[1]}, {});
84✔
2188
        }
84✔
2189
        SECTION("empty local") {
756✔
2190
            reset_list({}, {values[0], values[1]});
84✔
2191
        }
84✔
2192
        SECTION("empty both") {
756✔
2193
            reset_list({}, {});
84✔
2194
        }
84✔
2195
        SECTION("equal suffix") {
756✔
2196
            reset_list({values[0], values[0], values[1]}, {values[0], values[1]});
84✔
2197
        }
84✔
2198
        SECTION("equal prefix") {
756✔
2199
            reset_list({values[0]}, {values[0], values[1], values[1]});
84✔
2200
        }
84✔
2201
        SECTION("equal lists") {
756✔
2202
            reset_list({values[0]}, {values[0]});
84✔
2203
        }
84✔
2204
        SECTION("equal middle") {
756✔
2205
            reset_list({values[0], values[1], values[0]}, {values[1], values[1], values[1]});
84✔
2206
        }
84✔
2207
    }
756✔
2208

1,128✔
2209
    SECTION("dictionary") {
2,256✔
2210
        REQUIRE(values.size() >= 2);
504!
2211
        REQUIRE(values[0] != values[1]);
504!
2212
        int64_t pk_val = 0;
504✔
2213
        std::string dict_key = "hello";
504✔
2214
        test_reset->setup([&](SharedRealm realm) {
1,008✔
2215
            auto table = get_table(*realm, "test type");
1,008✔
2216
            REQUIRE(table);
1,008!
2217
            auto obj = table->create_object_with_primary_key(pk_val);
1,008✔
2218
            ColKey col = table->get_column_key("dictionary");
1,008✔
2219
            Dictionary dict = obj.get_dictionary(col);
1,008✔
2220
            dict.insert(dict_key, Mixed{values[0]});
1,008✔
2221
        });
1,008✔
2222

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

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

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

1,128✔
2344
    SECTION("set") {
2,256✔
2345
        int64_t pk_val = 0;
672✔
2346

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

336✔
2421
        REQUIRE(values.size() >= 2);
672!
2422
        REQUIRE(values[0] != values[1]);
672!
2423
        test_reset->setup([&](SharedRealm realm) {
1,344✔
2424
            auto table = get_table(*realm, "test type");
1,344✔
2425
            REQUIRE(table);
1,344!
2426
            auto obj = table->create_object_with_primary_key(pk_val);
1,344✔
2427
            ColKey col = table->get_column_key("set");
1,344✔
2428
            SetBasePtr set = obj.get_setbase_ptr(col);
1,344✔
2429
            set->insert_any(Mixed{values[0]});
1,344✔
2430
        });
1,344✔
2431

336✔
2432
        SECTION("modify") {
672✔
2433
            reset_set({Mixed{values[0]}}, {Mixed{values[1]}});
84✔
2434
        }
84✔
2435
        SECTION("modify opposite") {
672✔
2436
            reset_set({Mixed{values[1]}}, {Mixed{values[0]}});
84✔
2437
        }
84✔
2438
        SECTION("empty remote") {
672✔
2439
            reset_set({Mixed{values[1]}, Mixed{values[0]}}, {});
84✔
2440
        }
84✔
2441
        SECTION("empty local") {
672✔
2442
            reset_set({}, {Mixed{values[0]}, Mixed{values[1]}});
84✔
2443
        }
84✔
2444
        SECTION("empty both") {
672✔
2445
            reset_set({}, {});
84✔
2446
        }
84✔
2447
        SECTION("equal suffix") {
672✔
2448
            reset_set({Mixed{values[0]}, Mixed{values[1]}}, {Mixed{values[1]}});
84✔
2449
        }
84✔
2450
        SECTION("equal prefix") {
672✔
2451
            reset_set({Mixed{values[0]}}, {Mixed{values[1]}, Mixed{values[0]}});
84✔
2452
        }
84✔
2453
        SECTION("equal lists") {
672✔
2454
            reset_set({Mixed{values[0]}, Mixed{values[1]}}, {Mixed{values[0]}, Mixed{values[1]}});
84✔
2455
        }
84✔
2456
    }
672✔
2457
}
2,256✔
2458

2459
namespace test_instructions {
2460

2461
struct Add {
2462
    Add(util::Optional<int64_t> key)
2463
        : pk(key)
2464
    {
1,348✔
2465
    }
1,348✔
2466
    util::Optional<int64_t> pk;
2467
};
2468

2469
struct Remove {
2470
    Remove(util::Optional<int64_t> key)
2471
        : pk(key)
2472
    {
824✔
2473
    }
824✔
2474
    util::Optional<int64_t> pk;
2475
};
2476

2477
struct Clear {};
2478

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

2489
struct CreateObject {
2490
    CreateObject(std::string_view name, util::Optional<int64_t> key)
2491
        : pk(key)
2492
        , class_name(name)
2493
    {
100✔
2494
    }
100✔
2495
    util::Optional<int64_t> pk;
2496
    std::string_view class_name;
2497
};
2498

2499
struct Move {
2500
    Move(size_t from_ndx, size_t to_ndx)
2501
        : from(from_ndx)
2502
        , to(to_ndx)
2503
    {
96✔
2504
    }
96✔
2505
    size_t from;
2506
    size_t to;
2507
};
2508

2509
struct Insert {
2510
    Insert(size_t index, util::Optional<int64_t> key)
2511
        : ndx(index)
2512
        , pk(key)
2513
    {
32✔
2514
    }
32✔
2515
    size_t ndx;
2516
    util::Optional<int64_t> pk;
2517
};
2518

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

2597
private:
2598
    mpark::variant<Add, Remove, Clear, RemoveObject, CreateObject, Move, Insert> m_op;
2599
};
2600

2601
} // namespace test_instructions
2602

2603
TEMPLATE_TEST_CASE("client reset collections of links", "[sync][pbs][client reset][links][collections]",
2604
                   cf::ListOfObjects, cf::ListOfMixedLinks, cf::SetOfObjects, cf::SetOfMixedLinks,
2605
                   cf::DictionaryOfObjects, cf::DictionaryOfMixedLinks)
2606
{
1,048✔
2607
    if (!util::EventLoop::has_implementation())
1,048✔
2608
        return;
×
2609

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

524✔
2635
    TestSyncManager init_sync_manager;
1,048✔
2636
    SyncTestFile config(init_sync_manager.app(), "default");
1,048✔
2637
    config.cache = false;
1,048✔
2638
    config.automatic_change_notifications = false;
1,048✔
2639
    config.schema = schema;
1,048✔
2640
    ClientResyncMode test_mode = GENERATE(ClientResyncMode::DiscardLocal, ClientResyncMode::Recover);
1,048✔
2641
    CAPTURE(test_mode);
1,048✔
2642
    config.sync_config->client_resync_mode = test_mode;
1,048✔
2643

524✔
2644
    SyncTestFile config2(init_sync_manager.app(), "default");
1,048✔
2645
    config2.schema = schema;
1,048✔
2646

524✔
2647
    std::unique_ptr<reset_utils::TestClientReset> test_reset =
1,048✔
2648
        reset_utils::make_fake_local_client_reset(config, config2);
1,048✔
2649

524✔
2650
    CppContext c;
1,048✔
2651
    auto create_one_source_object = [&](realm::SharedRealm r, int64_t val, std::vector<ObjLink> links = {}) {
2,096✔
2652
        auto object = Object::create(
2,096✔
2653
            c, r, "source",
2,096✔
2654
            std::any(realm::AnyDict{{valid_pk_name, std::any(val)}, {"realm_id", std::string(partition)}}),
2,096✔
2655
            CreatePolicy::ForceCreate);
2,096✔
2656

1,048✔
2657
        for (auto link : links) {
6,144✔
2658
            test_type.add_link(object.get_obj(), link);
6,144✔
2659
        }
6,144✔
2660
    };
2,096✔
2661

524✔
2662
    auto create_one_dest_object = [&](realm::SharedRealm r, util::Optional<int64_t> val) -> ObjLink {
10,288✔
2663
        std::any v;
10,288✔
2664
        if (val) {
10,288✔
2665
            v = std::any(*val);
10,240✔
2666
        }
10,240✔
2667
        auto obj = Object::create(
10,288✔
2668
            c, r, "dest",
10,288✔
2669
            std::any(realm::AnyDict{{valid_pk_name, std::move(v)}, {"realm_id", std::string(partition)}}),
10,288✔
2670
            CreatePolicy::ForceCreate);
10,288✔
2671
        return ObjLink{obj.get_obj().get_table()->get_key(), obj.get_obj().get_key()};
10,288✔
2672
    };
10,288✔
2673

524✔
2674
    auto require_links_to_match_ids = [&](std::vector<Obj>& links, std::vector<util::Optional<int64_t>>& expected,
1,048✔
2675
                                          bool sorted) {
1,018✔
2676
        std::vector<util::Optional<int64_t>> actual;
988✔
2677
        for (auto obj : links) {
2,396✔
2678
            if (obj.is_null(valid_pk_name)) {
2,396✔
2679
                actual.push_back(util::none);
12✔
2680
            }
12✔
2681
            else {
2,384✔
2682
                actual.push_back(obj.get<Int>(valid_pk_name));
2,384✔
2683
            }
2,384✔
2684
        }
2,396✔
2685
        if (sorted) {
988✔
2686
            std::sort(actual.begin(), actual.end());
592✔
2687
        }
592✔
2688
        REQUIRE(actual == expected);
988!
2689
    };
988✔
2690

524✔
2691
    constexpr int64_t source_pk = 0;
1,048✔
2692
    constexpr util::Optional<int64_t> dest_pk_1 = 1;
1,048✔
2693
    constexpr util::Optional<int64_t> dest_pk_2 = 2;
1,048✔
2694
    constexpr util::Optional<int64_t> dest_pk_3 = 3;
1,048✔
2695
    constexpr util::Optional<int64_t> dest_pk_4 = 4;
1,048✔
2696
    constexpr util::Optional<int64_t> dest_pk_5 = 5;
1,048✔
2697

524✔
2698
    Results results;
1,048✔
2699
    Object object;
1,048✔
2700
    CollectionChangeSet object_changes, results_changes;
1,048✔
2701
    NotificationToken object_token, results_token;
1,048✔
2702
    auto setup_listeners = [&](SharedRealm realm) {
1,018✔
2703
        TableRef source_table = get_table(*realm, "source");
988✔
2704
        ColKey id_col = source_table->get_column_key("_id");
988✔
2705
        results = Results(realm, source_table->where().equal(id_col, source_pk));
988✔
2706
        if (auto obj = results.first()) {
988✔
2707
            object = Object(realm, *obj);
988✔
2708
            object_token = object.add_notification_callback([&](CollectionChangeSet changes) {
1,712✔
2709
                object_changes = std::move(changes);
1,712✔
2710
            });
1,712✔
2711
        }
988✔
2712
        results_token = results.add_notification_callback([&](CollectionChangeSet changes) {
1,712✔
2713
            results_changes = std::move(changes);
1,712✔
2714
        });
1,712✔
2715
    };
988✔
2716

524✔
2717
    auto get_source_object = [&](SharedRealm realm) -> Obj {
5,048✔
2718
        TableRef src_table = get_table(*realm, "source");
5,048✔
2719
        return src_table->try_get_object(src_table->find_primary_key(Mixed{source_pk}));
5,048✔
2720
    };
5,048✔
2721
    auto apply_instructions = [&](SharedRealm realm, std::vector<CollectionOperation>& instructions) {
2,096✔
2722
        TableRef dst_table = get_table(*realm, "dest");
2,096✔
2723
        for (auto& instruction : instructions) {
3,072✔
2724
            Obj src_obj = get_source_object(realm);
3,072✔
2725
            instruction.apply(&test_type, src_obj, dst_table);
3,072✔
2726
        }
3,072✔
2727
    };
2,096✔
2728

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

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

524✔
2840
    test_reset->setup([&](SharedRealm realm) {
1,760✔
2841
        populate_initial_state(realm);
1,760✔
2842
    });
1,760✔
2843

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

144✔
2969
            auto invalidate_object = [&](SharedRealm realm, std::string_view table_name, Mixed pk) {
288✔
2970
                TableRef table = get_table(*realm, table_name);
288✔
2971
                Obj obj = table->get_object_with_primary_key(pk);
288✔
2972
                REALM_ASSERT(obj.is_valid());
288✔
2973
                if (realm->config().path == config.path) {
288✔
2974
                    // the local realm does an invalidation
72✔
2975
                    table->invalidate_object(obj.get_key());
144✔
2976
                }
144✔
2977
                else {
144✔
2978
                    // the remote realm has deleted it
72✔
2979
                    table->remove_object(obj.get_key());
144✔
2980
                }
144✔
2981
            };
288✔
2982

144✔
2983
            invalidate_object(realm, "dest", dest_pk_1);
288✔
2984
        });
288✔
2985

72✔
2986
        SECTION("remote adds a link") {
144✔
2987
            reset_collection({}, {Add{dest_pk_4}}, {dest_pk_2, dest_pk_3, dest_pk_4}, 1);
24✔
2988
        }
24✔
2989
        SECTION("remote removes a link") {
144✔
2990
            reset_collection({}, {Remove{dest_pk_2}}, {dest_pk_3}, 1);
24✔
2991
        }
24✔
2992
        SECTION("remote deletes a dest object that local links to") {
144✔
2993
            reset_collection({Add{dest_pk_4}}, {RemoveObject{"dest", dest_pk_4}}, {dest_pk_2, dest_pk_3}, 2);
24✔
2994
        }
24✔
2995
        SECTION("remote deletes a different dest object") {
144✔
2996
            reset_collection({Add{dest_pk_4}}, {RemoveObject{"dest", dest_pk_2}}, {dest_pk_3, dest_pk_4}, 2);
24✔
2997
        }
24✔
2998
        SECTION("local adds two new links and remote deletes a different dest object") {
144✔
2999
            reset_collection({Add{dest_pk_4}, Add{dest_pk_5}}, {RemoveObject{"dest", dest_pk_2}},
24✔
3000
                             {dest_pk_3, dest_pk_4, dest_pk_5}, 2);
24✔
3001
        }
24✔
3002
        SECTION("remote deletes an object, then removes and adds to the list") {
144✔
3003
            reset_collection({}, {RemoveObject{"dest", dest_pk_2}, Remove{dest_pk_3}, Add{dest_pk_4}}, {dest_pk_4},
24✔
3004
                             2);
24✔
3005
        }
24✔
3006
    }
144✔
3007

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

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

3116
template <typename T>
3117
void set_embedded_list(const std::vector<T>& array_values, LnkLst& list)
3118
{
3,552✔
3119
    for (size_t i = 0; i < array_values.size(); ++i) {
4,896✔
3120
        Obj link;
1,344✔
3121
        if (i >= list.size()) {
1,344✔
3122
            link = list.create_and_insert_linked_object(list.size());
1,052✔
3123
        }
1,052✔
3124
        else {
292✔
3125
            link = list.get_object(i);
292✔
3126
        }
292✔
3127
        array_values[i].assign_to(link);
1,344✔
3128
    }
1,344✔
3129
    if (list.size() > array_values.size()) {
3,552✔
3130
        if (array_values.size() == 0) {
12!
3131
            list.clear();
8✔
3132
        }
8✔
3133
        else {
4✔
3134
            list.remove(array_values.size(), list.size());
4✔
3135
        }
4✔
3136
    }
12✔
3137
}
3,552✔
3138

3139
template <typename T>
3140
void combine_array_values(std::vector<T>& from, const std::vector<T>& to)
3141
{
92✔
3142
    auto it = from.begin();
92✔
3143
    for (auto val : to) {
288✔
3144
        it = ++from.insert(it, val);
288✔
3145
    }
288✔
3146
}
92✔
3147

3148
TEST_CASE("client reset with embedded object", "[sync][pbs][client reset][embedded objects]") {
168✔
3149
    if (!util::EventLoop::has_implementation())
168✔
3150
        return;
×
3151

84✔
3152
    TestSyncManager init_sync_manager;
168✔
3153
    SyncTestFile config(init_sync_manager.app(), "default");
168✔
3154
    config.cache = false;
168✔
3155
    config.automatic_change_notifications = false;
168✔
3156
    ClientResyncMode test_mode = GENERATE(ClientResyncMode::DiscardLocal, ClientResyncMode::Recover);
168✔
3157
    CAPTURE(test_mode);
168✔
3158
    config.sync_config->client_resync_mode = test_mode;
168✔
3159

84✔
3160
    ObjectSchema shared_class = {"object",
168✔
3161
                                 {
168✔
3162
                                     {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
168✔
3163
                                     {"value", PropertyType::Int},
168✔
3164
                                 }};
168✔
3165

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

84✔
3303
    struct EmbeddedContent {
168✔
3304
        std::string name = random_string(10);
168✔
3305
        int64_t int_value = random_int();
168✔
3306
        std::vector<Int> array_vals = {random_int(), random_int(), random_int()};
168✔
3307
        util::Optional<SecondLevelEmbeddedContent> second_level = SecondLevelEmbeddedContent();
168✔
3308
        std::vector<SecondLevelEmbeddedContent> array_of_seconds = {};
168✔
3309
        void apply_recovery_from(const EmbeddedContent& other)
168✔
3310
        {
106✔
3311
            name = other.name;
44✔
3312
            int_value = other.int_value;
44✔
3313
            combine_array_values(array_vals, other.array_vals);
44✔
3314
            if (second_level && other.second_level) {
44✔
3315
                second_level->apply_recovery_from(*other.second_level);
40✔
3316
            }
40✔
3317
            else {
4✔
3318
                second_level = other.second_level;
4✔
3319
            }
4✔
3320
        }
44✔
3321
        void test(const EmbeddedContent& other) const
168✔
3322
        {
1,024✔
3323
            INFO("Checking EmbeddedContent" << name);
1,024✔
3324
            REQUIRE(name == other.name);
1,024!
3325
            REQUIRE(int_value == other.int_value);
1,024!
3326
            REQUIRE(array_vals == other.array_vals);
1,024!
3327
            REQUIRE(array_of_seconds.size() == other.array_of_seconds.size());
1,024!
3328
            for (size_t i = 0; i < array_of_seconds.size(); ++i) {
1,052✔
3329
                array_of_seconds[i].test(other.array_of_seconds[i]);
28✔
3330
            }
28✔
3331
            if (!second_level) {
1,024✔
3332
                REQUIRE(!other.second_level);
2!
3333
            }
2✔
3334
            else {
1,022✔
3335
                REQUIRE(!!other.second_level);
1,022!
3336
                second_level->test(*other.second_level);
1,022✔
3337
            }
1,022✔
3338
        }
1,024✔
3339
        static util::Optional<EmbeddedContent> get_from(Obj embedded)
168✔
3340
        {
1,418✔
3341
            util::Optional<EmbeddedContent> value;
1,418✔
3342
            if (embedded.is_valid()) {
1,418✔
3343
                value = EmbeddedContent{};
1,378✔
3344
                value->name = embedded.get_any("name").get<StringData>();
1,378✔
3345
                value->int_value = embedded.get_any("int_value").get<Int>();
1,378✔
3346
                ColKey list_col = embedded.get_table()->get_column_key("array");
1,378✔
3347
                value->array_vals = embedded.get_list_values<Int>(list_col);
1,378✔
3348

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

100✔
3449
            for (size_t i = 0; i < list.size(); ++i) {
772✔
3450
                Obj link = list.get_object(i);
572✔
3451
                content.array_values.push_back(*EmbeddedContent::get_from(link));
572✔
3452
            }
572✔
3453
            auto dict = obj.get_dictionary("embedded_dict");
200✔
3454
            content.dict_values.clear();
200✔
3455
            for (auto it : dict) {
620✔
3456
                Obj link = dict.get_object(it.first.get_string());
620✔
3457
                content.dict_values.insert({it.first.get_string(), EmbeddedContent::get_from(link)});
620✔
3458
            }
620✔
3459
            return content;
200✔
3460
        }
200✔
3461
        void assign_to(Obj obj) const
168✔
3462
        {
436✔
3463
            ColKey link_col = obj.get_table()->get_column_key("embedded_obj");
436✔
3464
            if (!link_value) {
436✔
3465
                obj.set_null(link_col);
16✔
3466
            }
16✔
3467
            else {
420✔
3468
                Obj embedded_link = obj.get_linked_object(link_col);
420✔
3469
                if (!embedded_link) {
420✔
3470
                    embedded_link = obj.create_and_set_linked_object(link_col);
232✔
3471
                }
232✔
3472
                link_value->assign_to(embedded_link);
420✔
3473
            }
420✔
3474
            auto list = obj.get_linklist("array_of_objs");
436✔
3475
            set_embedded_list(array_values, list);
436✔
3476
            auto dict = obj.get_dictionary("embedded_dict");
436✔
3477
            for (auto it = dict.begin(); it != dict.end();) {
760✔
3478
                if (dict_values.find((*it).first.get_string()) == dict_values.end()) {
324✔
3479
                    it = dict.erase(it);
16✔
3480
                }
16✔
3481
                else {
308✔
3482
                    ++it;
308✔
3483
                }
308✔
3484
            }
324✔
3485
            for (auto it : dict_values) {
1,356✔
3486
                if (it.second) {
1,356✔
3487
                    auto embedded = dict.get_object(it.first);
1,312✔
3488
                    if (!embedded) {
1,312✔
3489
                        embedded = dict.create_and_insert_linked_object(it.first);
1,008✔
3490
                    }
1,008✔
3491
                    it.second->assign_to(embedded);
1,312✔
3492
                }
1,312✔
3493
                else {
44✔
3494
                    dict.insert(it.first, Mixed{});
44✔
3495
                }
44✔
3496
            }
1,356✔
3497
        }
436✔
3498
    };
168✔
3499

84✔
3500
    SyncTestFile config2(init_sync_manager.app(), "default");
168✔
3501
    config2.schema = config.schema;
168✔
3502

84✔
3503
    std::unique_ptr<reset_utils::TestClientReset> test_reset =
168✔
3504
        reset_utils::make_fake_local_client_reset(config, config2);
168✔
3505

84✔
3506
    auto get_top_object = [](SharedRealm realm) {
460✔
3507
        advance_and_notify(*realm);
460✔
3508
        TableRef table = get_table(*realm, "TopLevel");
460✔
3509
        REQUIRE(table->size() == 1);
460!
3510
        Obj obj = *table->begin();
460✔
3511
        return obj;
460✔
3512
    };
460✔
3513

84✔
3514
    using StateList = std::vector<TopLevelContent>;
168✔
3515
    auto reset_embedded_object = [&](StateList local_content, StateList remote_content,
168✔
3516
                                     TopLevelContent expected_recovered) {
134✔
3517
        test_reset
100✔
3518
            ->make_local_changes([&](SharedRealm local_realm) {
100✔
3519
                Obj obj = get_top_object(local_realm);
100✔
3520
                for (auto& s : local_content) {
104✔
3521
                    s.assign_to(obj);
104✔
3522
                }
104✔
3523
            })
100✔
3524
            ->make_remote_changes([&](SharedRealm remote_realm) {
100✔
3525
                Obj obj = get_top_object(remote_realm);
100✔
3526
                for (auto& s : remote_content) {
100✔
3527
                    s.assign_to(obj);
100✔
3528
                }
100✔
3529
            })
100✔
3530
            ->on_post_reset([&](SharedRealm local_realm) {
100✔
3531
                Obj obj = get_top_object(local_realm);
100✔
3532
                TopLevelContent actual = TopLevelContent::get_from(obj);
100✔
3533
                if (test_mode == ClientResyncMode::Recover) {
100✔
3534
                    actual.test(expected_recovered);
50✔
3535
                }
50✔
3536
                else if (test_mode == ClientResyncMode::DiscardLocal) {
50✔
3537
                    REQUIRE(remote_content.size() > 0);
50!
3538
                    actual.test(remote_content.back());
50✔
3539
                }
50✔
3540
                else {
×
3541
                    REALM_UNREACHABLE();
3542
                }
×
3543
            })
100✔
3544
            ->run();
100✔
3545
    };
100✔
3546

84✔
3547
    ObjectId pk_val = ObjectId::gen();
168✔
3548
    test_reset->setup([&pk_val](SharedRealm realm) {
132✔
3549
        auto table = get_table(*realm, "TopLevel");
96✔
3550
        REQUIRE(table);
96!
3551
        auto obj = table->create_object_with_primary_key(pk_val);
96✔
3552
        Obj embedded_link = obj.create_and_set_linked_object(table->get_column_key("embedded_obj"));
96✔
3553
        embedded_link.set<String>("name", "initial name");
96✔
3554
    });
96✔
3555

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

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

56✔
4182
        SECTION("modifications to a preexisting object through two layers of embedded lists triggers a list copy") {
112✔
4183
            SecondLevelEmbeddedContent preexisting_item, local_modified, remote_added;
4✔
4184
            initial.array_values[0].array_of_seconds.push_back(preexisting_item);
4✔
4185
            const size_t initial_item_pos = initial.array_values[0].array_of_seconds.size() - 1;
4✔
4186
            local = initial;
4✔
4187
            remote = initial;
4✔
4188
            local.array_values[0].array_of_seconds[initial_item_pos] = local_modified;
4✔
4189
            remote.array_values[0].array_of_seconds.push_back(remote_added); // overwritten by local!
4✔
4190
            TopLevelContent expected_recovered = local;
4✔
4191
            reset_embedded_object({local}, {remote}, expected_recovered);
4✔
4192
        }
4✔
4193

56✔
4194
        SECTION("add int") {
112✔
4195
            auto add_to_dict_item = [&](SharedRealm realm, std::string key, int64_t addition) {
20✔
4196
                Obj obj = get_top_object(realm);
20✔
4197
                auto dict = obj.get_dictionary("embedded_dict");
20✔
4198
                auto embedded = dict.get_object(key);
20✔
4199
                REQUIRE(!!embedded);
20!
4200
                embedded.add_int("int_value", addition);
20✔
4201
                return TopLevelContent::get_from(obj);
20✔
4202
            };
20✔
4203
            TopLevelContent expected_recovered;
16✔
4204
            const std::string existing_key = "foo";
16✔
4205

8✔
4206
            test_reset->on_post_reset([&](SharedRealm local_realm) {
14✔
4207
                Obj obj = get_top_object(local_realm);
12✔
4208
                TopLevelContent actual = TopLevelContent::get_from(obj);
12✔
4209
                actual.test(test_mode == ClientResyncMode::Recover ? expected_recovered : initial);
9✔
4210
            });
12✔
4211
            int64_t initial_value = initial.dict_values[existing_key]->int_value;
16✔
4212
            std::mt19937_64 engine(std::random_device{}());
16✔
4213
            std::uniform_int_distribution<int64_t> rng(-10'000'000'000, 10'000'000'000);
16✔
4214

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

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