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

realm / realm-core / nicola.cabiddu_1040

26 Sep 2023 05:08PM UTC coverage: 91.056% (-1.9%) from 92.915%
nicola.cabiddu_1040

Pull #6766

Evergreen

nicola-cab
several fixes and final client reset algo for collection in mixed
Pull Request #6766: Client Reset for collections in mixed / nested collections

97128 of 178458 branches covered (0.0%)

1524 of 1603 new or added lines in 5 files covered. (95.07%)

4511 existing lines in 109 files now uncovered.

236619 of 259862 relevant lines covered (91.06%)

7169640.31 hits per line

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

98.73
/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
    {
24✔
55
        std::lock_guard<std::mutex> lock(m_mutex);
24✔
56
        m_error = e;
24✔
57
    }
24✔
58
    operator bool() const
59
    {
3,687✔
60
        std::lock_guard<std::mutex> lock(m_mutex);
3,687✔
61
        return bool(m_error);
3,687✔
62
    }
3,687✔
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
{
16,272✔
104
    return ObjectStore::table_for_object_type(realm.read_group(), object_type);
16,272✔
105
}
16,272✔
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
    std::string base_url = get_base_url();
2✔
126
    REQUIRE(!base_url.empty());
2!
127
    auto server_app_config = minimal_app_config(base_url, "client_reset_tests", schema);
2✔
128
    server_app_config.partition_key = partition_prop;
2✔
129
    TestAppSession test_app_session(create_app(server_app_config));
2✔
130
    auto app = test_app_session.app();
2✔
131

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

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

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

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

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

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

1✔
183
        wait_for_upload(*second_realm);
2✔
184
    }
2✔
185

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

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

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

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

1✔
220
    std::string base_url = get_base_url();
2✔
221
    REQUIRE(!base_url.empty());
2!
222
    auto server_app_config = minimal_app_config(base_url, "client_reset_tests", schema);
2✔
223
    server_app_config.partition_key = partition_prop;
2✔
224
    TestAppSession test_app_session(create_app(server_app_config));
2✔
225
    auto app = test_app_session.app();
2✔
226

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

236
        FAIL(util::format("got error from server: %1", err.status));
×
237
    };
×
238

1✔
239
    auto realm = Realm::get_shared_realm(realm_config);
2✔
240
    auto obj_id = ObjectId::gen();
2✔
241
    {
2✔
242
        realm->begin_transaction();
2✔
243
        CppContext c(realm);
2✔
244
        Object::create(
2✔
245
            c, realm, "object",
2✔
246
            std::any(AnyDict{{"_id", obj_id}, {"value", int64_t(5)}, {partition.property_name, partition.value}}));
2✔
247
        realm->commit_transaction();
2✔
248
        wait_for_upload(*realm);
2✔
249
    }
2✔
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

1✔
256
    reset_utils::trigger_client_reset(test_app_session.app_session(), realm);
2✔
257

1✔
258
    wait_for_download(*realm, std::chrono::minutes(10));
2✔
259
}
2✔
260

261
TEST_CASE("sync: client reset", "[sync][pbs][client reset][baas]") {
80✔
262
    if (!util::EventLoop::has_implementation())
80✔
263
        return;
×
264

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

40✔
313
    // this is just for ease of debugging
40✔
314
    local_config.path = local_config.path + ".local";
80✔
315
    remote_config.path = remote_config.path + ".remote";
80✔
316

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

1✔
342
        make_reset(local_config, remote_config)
2✔
343
            ->on_post_reset([&](SharedRealm) {
2✔
344
                util::EventLoop::main().run_until([&] {
3,632✔
345
                    return bool(err);
3,632✔
346
                });
3,632✔
347
            })
2✔
348
            ->run();
2✔
349

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

40✔
366
    local_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError err) {
80✔
367
        CAPTURE(err.status);
×
368
        CAPTURE(local_config.path);
×
369
        FAIL("Error handler should not have been called");
×
370
    };
×
371

40✔
372
    local_config.cache = false;
80✔
373
    local_config.automatic_change_notifications = false;
80✔
374
    const std::string fresh_path = realm::_impl::ClientResetOperation::get_fresh_path_for(local_config.path);
80✔
375
    size_t before_callback_invocations = 0;
80✔
376
    size_t after_callback_invocations = 0;
80✔
377
    std::mutex mtx;
80✔
378
    local_config.sync_config->notify_before_client_reset = [&](SharedRealm before) {
69✔
379
        std::lock_guard<std::mutex> lock(mtx);
58✔
380
        ++before_callback_invocations;
58✔
381
        REQUIRE(before);
58!
382
        REQUIRE(before->is_frozen());
58!
383
        REQUIRE(before->read_group().get_table("class_object"));
58!
384
        REQUIRE(before->config().path == local_config.path);
58!
385
        REQUIRE_FALSE(before->schema().empty());
58!
386
        REQUIRE(before->schema_version() != ObjectStore::NotVersioned);
58!
387
        REQUIRE(util::File::exists(local_config.path));
58!
388
    };
58✔
389
    local_config.sync_config->notify_after_client_reset = [&](SharedRealm before, ThreadSafeReference after_ref,
80✔
390
                                                              bool) {
64✔
391
        std::lock_guard<std::mutex> lock(mtx);
48✔
392
        SharedRealm after = Realm::get_shared_realm(std::move(after_ref), util::Scheduler::make_default());
48✔
393
        ++after_callback_invocations;
48✔
394
        REQUIRE(before);
48!
395
        REQUIRE(before->is_frozen());
48!
396
        REQUIRE(before->read_group().get_table("class_object"));
48!
397
        REQUIRE(before->config().path == local_config.path);
48!
398
        REQUIRE(after);
48!
399
        REQUIRE(!after->is_frozen());
48!
400
        REQUIRE(after->read_group().get_table("class_object"));
48!
401
        REQUIRE(after->config().path == local_config.path);
48!
402
        REQUIRE(after->current_transaction_version() > before->current_transaction_version());
48!
403
    };
48✔
404

40✔
405
    Results results;
80✔
406
    Object object;
80✔
407
    CollectionChangeSet object_changes, results_changes;
80✔
408
    NotificationToken object_token, results_token;
80✔
409
    auto setup_listeners = [&](SharedRealm realm) {
51✔
410
        results = Results(realm, ObjectStore::table_for_object_type(realm->read_group(), "object"))
22✔
411
                      .sort({{{"value", true}}});
22✔
412
        if (results.size() >= 1) {
22✔
413
            REQUIRE(results.get<Obj>(0).get<Int>("value") == 4);
18!
414

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

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

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

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

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

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

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

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

40✔
744
    SECTION("discard local") {
80✔
745
        local_config.sync_config->client_resync_mode = ClientResyncMode::DiscardLocal;
48✔
746
        std::unique_ptr<reset_utils::TestClientReset> test_reset = make_reset(local_config, remote_config);
48✔
747

24✔
748
        SECTION("modify") {
48✔
749
            test_reset
2✔
750
                ->on_post_local_changes([&](SharedRealm realm) {
2✔
751
                    setup_listeners(realm);
2✔
752
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
753
                    CHECK(results.size() == 1);
2!
754
                    CHECK(results.get<Obj>(0).get<Int>("value") == 4);
2!
755
                })
2✔
756
                ->on_post_reset([&](SharedRealm realm) {
2✔
757
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
758

1✔
759
                    CHECK(before_callback_invocations == 1);
2!
760
                    CHECK(after_callback_invocations == 1);
2!
761
                    CHECK(results.size() == 1);
2!
762
                    CHECK(results.get<Obj>(0).get<Int>("value") == 6);
2!
763
                    CHECK(object.get_obj().get<Int>("value") == 6);
2!
764
                    REQUIRE_INDICES(results_changes.modifications, 0);
2!
765
                    REQUIRE_INDICES(results_changes.insertions);
2!
766
                    REQUIRE_INDICES(results_changes.deletions);
2!
767
                    REQUIRE_INDICES(object_changes.modifications, 0);
2!
768
                    REQUIRE_INDICES(object_changes.insertions);
2!
769
                    REQUIRE_INDICES(object_changes.deletions);
2!
770
                    // make sure that the reset operation has cleaned up after itself
1✔
771
                    REQUIRE(util::File::exists(local_config.path));
2!
772
                    REQUIRE_FALSE(util::File::exists(fresh_path));
2!
773
                })
2✔
774
                ->run();
2✔
775

1✔
776
            SECTION("a Realm can be reset twice") {
2✔
777
                // keep the Realm to reset (config) the same, but change out the remote (config2)
1✔
778
                // to a new path because otherwise it will be reset as well which we don't want
1✔
779
                SyncTestFile config3 = get_valid_config();
2✔
780
                ObjectId to_continue_reset = test_reset->get_pk_of_object_driving_reset();
2✔
781
                test_reset = make_reset(local_config, config3);
2✔
782
                test_reset->set_pk_of_object_driving_reset(to_continue_reset);
2✔
783
                test_reset
2✔
784
                    ->setup([&](SharedRealm realm) {
2✔
785
                        // after a reset we already start with a value of 6
1✔
786
                        TableRef table = get_table(*realm, "object");
2✔
787
                        REQUIRE(table->size() == 1);
2!
788
                        REQUIRE(table->begin()->get<Int>("value") == 6);
2!
789
                        REQUIRE_NOTHROW(advance_and_notify(*object.get_realm()));
2✔
790
                        CHECK(object.get_obj().get<Int>("value") == 6);
2!
791
                        object_changes = {};
2✔
792
                        results_changes = {};
2✔
793
                    })
2✔
794
                    ->on_post_local_changes([&](SharedRealm) {
2✔
795
                        // advance the object's realm because the one passed here is different
1✔
796
                        REQUIRE_NOTHROW(advance_and_notify(*object.get_realm()));
2✔
797
                        // 6 -> 4
1✔
798
                        CHECK(results.size() == 1);
2!
799
                        CHECK(results.get<Obj>(0).get<Int>("value") == 4);
2!
800
                        CHECK(object.get_obj().get<Int>("value") == 4);
2!
801
                        REQUIRE_INDICES(results_changes.modifications, 0);
2!
802
                        REQUIRE_INDICES(results_changes.insertions);
2!
803
                        REQUIRE_INDICES(results_changes.deletions);
2!
804
                        REQUIRE_INDICES(object_changes.modifications, 0);
2!
805
                        REQUIRE_INDICES(object_changes.insertions);
2!
806
                        REQUIRE_INDICES(object_changes.deletions);
2!
807
                        object_changes = {};
2✔
808
                        results_changes = {};
2✔
809
                    })
2✔
810
                    ->on_post_reset([&](SharedRealm) {
2✔
811
                        REQUIRE_NOTHROW(advance_and_notify(*object.get_realm()));
2✔
812
                        CHECK(before_callback_invocations == 2);
2!
813
                        CHECK(after_callback_invocations == 2);
2!
814
                        // 4 -> 6
1✔
815
                        CHECK(results.size() == 1);
2!
816
                        CHECK(results.get<Obj>(0).get<Int>("value") == 6);
2!
817
                        CHECK(object.get_obj().get<Int>("value") == 6);
2!
818
                        REQUIRE_INDICES(results_changes.modifications, 0);
2!
819
                        REQUIRE_INDICES(results_changes.insertions);
2!
820
                        REQUIRE_INDICES(results_changes.deletions);
2!
821
                        REQUIRE_INDICES(object_changes.modifications, 0);
2!
822
                        REQUIRE_INDICES(object_changes.insertions);
2!
823
                        REQUIRE_INDICES(object_changes.deletions);
2!
824
                    })
2✔
825
                    ->run();
2✔
826
            }
2✔
827
        }
2✔
828

24✔
829
        SECTION("can be reset without notifiers") {
48✔
830
            local_config.sync_config->notify_before_client_reset = nullptr;
2✔
831
            local_config.sync_config->notify_after_client_reset = nullptr;
2✔
832
            make_reset(local_config, remote_config)->run();
2✔
833
            REQUIRE(before_callback_invocations == 0);
2!
834
            REQUIRE(after_callback_invocations == 0);
2!
835
        }
2✔
836

24✔
837
        SECTION("callbacks are seeded with Realm instances even if the coordinator dies") {
48✔
838
            auto client_reset_harness = make_reset(local_config, remote_config);
2✔
839
            client_reset_harness->disable_wait_for_reset_completion();
2✔
840
            std::shared_ptr<SyncSession> session;
2✔
841
            client_reset_harness
2✔
842
                ->on_post_local_changes([&](SharedRealm local) {
2✔
843
                    // retain a reference so the sync session completes, even though the Realm is cleaned up
1✔
844
                    session = local->sync_session();
2✔
845
                })
2✔
846
                ->run();
2✔
847
            auto local_coordinator = realm::_impl::RealmCoordinator::get_existing_coordinator(local_config.path);
2✔
848
            REQUIRE(!local_coordinator);
2!
849
            REQUIRE(before_callback_invocations == 0);
2!
850
            REQUIRE(after_callback_invocations == 0);
2!
851
            timed_sleeping_wait_for(
2✔
852
                [&]() -> bool {
140✔
853
                    std::lock_guard<std::mutex> lock(mtx);
140✔
854
                    return after_callback_invocations > 0;
140✔
855
                },
140✔
856
                std::chrono::seconds(60));
2✔
857
            // this test also relies on the test config above to verify the Realm instances in the callbacks
1✔
858
            REQUIRE(before_callback_invocations == 1);
2!
859
            REQUIRE(after_callback_invocations == 1);
2!
860
        }
2✔
861

24✔
862
        SECTION("notifiers work if the session instance changes") {
48✔
863
            // run this test with ASAN to check for use after free
1✔
864
            size_t before_callback_invocations_2 = 0;
2✔
865
            size_t after_callback_invocations_2 = 0;
2✔
866
            std::shared_ptr<SyncSession> session;
2✔
867
            std::unique_ptr<SyncConfig> config_copy;
2✔
868
            {
2✔
869
                SyncTestFile temp_config = get_valid_config();
2✔
870
                temp_config.persist();
2✔
871
                temp_config.sync_config->client_resync_mode = ClientResyncMode::DiscardLocal;
2✔
872
                config_copy = std::make_unique<SyncConfig>(*temp_config.sync_config);
2✔
873
                config_copy->notify_before_client_reset = [&](SharedRealm before_realm) {
1✔
874
                    std::lock_guard<std::mutex> lock(mtx);
×
875
                    REQUIRE(before_realm);
×
876
                    REQUIRE(before_realm->schema_version() != ObjectStore::NotVersioned);
×
877
                    ++before_callback_invocations_2;
×
878
                };
×
879
                config_copy->notify_after_client_reset = [&](SharedRealm, ThreadSafeReference, bool) {
1✔
880
                    std::lock_guard<std::mutex> lock(mtx);
×
881
                    ++after_callback_invocations_2;
×
882
                };
×
883

1✔
884
                temp_config.sync_config->notify_before_client_reset = [&](SharedRealm before_realm) {
2✔
885
                    std::lock_guard<std::mutex> lock(mtx);
2✔
886
                    ++before_callback_invocations;
2✔
887
                    REQUIRE(session);
2!
888
                    REQUIRE(config_copy);
2!
889
                    REQUIRE(before_realm);
2!
890
                    REQUIRE(before_realm->schema_version() != ObjectStore::NotVersioned);
2!
891
                    session->update_configuration(*config_copy);
2✔
892
                };
2✔
893

1✔
894
                auto realm = Realm::get_shared_realm(temp_config);
2✔
895
                wait_for_upload(*realm);
2✔
896

1✔
897
                session = test_app_session.app()->sync_manager()->get_existing_session(temp_config.path);
2✔
898
                REQUIRE(session);
2!
899
            }
2✔
900
            sync::SessionErrorInfo synthetic(Status{ErrorCodes::SyncClientResetRequired, "A fake client reset error"},
2✔
901
                                             sync::IsFatal{true});
2✔
902
            synthetic.server_requests_action = sync::ProtocolErrorInfo::Action::ClientReset;
2✔
903
            SyncSession::OnlyForTesting::handle_error(*session, std::move(synthetic));
2✔
904

1✔
905
            session->revive_if_needed();
2✔
906
            timed_sleeping_wait_for(
2✔
907
                [&]() -> bool {
73✔
908
                    std::lock_guard<std::mutex> lock(mtx);
73✔
909
                    return before_callback_invocations > 0;
73✔
910
                },
73✔
911
                std::chrono::seconds(120));
2✔
912
            millisleep(500); // just make some space for the after callback to be attempted
2✔
913
            REQUIRE(before_callback_invocations == 1);
2!
914
            REQUIRE(after_callback_invocations == 0);
2!
915
            REQUIRE(before_callback_invocations_2 == 0);
2!
916
            REQUIRE(after_callback_invocations_2 == 0);
2!
917
        }
2✔
918

24✔
919
        SECTION("an interrupted reset can recover on the next session") {
48✔
920
            struct SessionInterruption : public std::runtime_error {
2✔
921
                using std::runtime_error::runtime_error;
2✔
922
            };
2✔
923
            try {
2✔
924
                test_reset
2✔
925
                    ->on_post_local_changes([&](SharedRealm) {
2✔
926
                        throw SessionInterruption("fake interruption during reset");
2✔
927
                    })
2✔
928
                    ->run();
2✔
929
            }
2✔
930
            catch (const SessionInterruption&) {
2✔
931
                REQUIRE(before_callback_invocations == 0);
2!
932
                REQUIRE(after_callback_invocations == 0);
2!
933
                test_reset.reset();
2✔
934
                auto realm = Realm::get_shared_realm(local_config);
2✔
935
                timed_sleeping_wait_for(
2✔
936
                    [&]() -> bool {
86✔
937
                        std::lock_guard<std::mutex> lock(mtx);
86✔
938
                        realm->begin_transaction();
86✔
939
                        TableRef table = get_table(*realm, "object");
86✔
940
                        REQUIRE(table);
86!
941
                        REQUIRE(table->size() == 1);
86!
942
                        auto col = table->get_column_key("value");
86✔
943
                        int64_t value = table->begin()->get<Int>(col);
86✔
944
                        realm->cancel_transaction();
86✔
945
                        return value == 6;
86✔
946
                    },
86✔
947
                    std::chrono::seconds(20));
2✔
948
            }
2✔
949
            auto session = test_app_session.app()->sync_manager()->get_existing_session(local_config.path);
2✔
950
            if (session) {
2✔
951
                session->shutdown_and_wait();
2✔
952
            }
2✔
953
            {
2✔
954
                std::lock_guard<std::mutex> lock(mtx);
2✔
955
                REQUIRE(before_callback_invocations == 1);
2!
956
                REQUIRE(after_callback_invocations == 1);
2!
957
            }
2✔
958
        }
2✔
959

24✔
960
        SECTION("an interrupted reset can recover on the next session restart") {
48✔
961
            test_reset->disable_wait_for_reset_completion();
2✔
962
            SharedRealm realm;
2✔
963
            test_reset
2✔
964
                ->on_post_local_changes([&](SharedRealm local) {
2✔
965
                    // retain a reference of the realm.
1✔
966
                    realm = local;
2✔
967
                })
2✔
968
                ->run();
2✔
969

1✔
970
            timed_wait_for([&] {
21,254✔
971
                return util::File::exists(_impl::ClientResetOperation::get_fresh_path_for(local_config.path));
21,254✔
972
            });
21,254✔
973

1✔
974
            // Restart the session before the client reset finishes.
1✔
975
            realm->sync_session()->restart_session();
2✔
976

1✔
977
            REQUIRE(!wait_for_upload(*realm));
2!
978
            REQUIRE(!wait_for_download(*realm));
2!
979
            realm->refresh();
2✔
980

1✔
981
            auto table = realm->read_group().get_table("class_object");
2✔
982
            REQUIRE(table->size() == 1);
2!
983
            auto col = table->get_column_key("value");
2✔
984
            int64_t value = table->begin()->get<Int>(col);
2✔
985
            REQUIRE(value == 6);
2!
986

1✔
987
            {
2✔
988
                std::lock_guard<std::mutex> lock(mtx);
2✔
989
                REQUIRE(before_callback_invocations == 1);
2!
990
                REQUIRE(after_callback_invocations == 1);
2!
991
            }
2✔
992
        }
2✔
993

24✔
994
        SECTION("invalid files at the fresh copy path are cleaned up") {
48✔
995
            ThreadSafeSyncError err;
2✔
996
            local_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) {
1✔
997
                err = error;
×
998
            };
×
999
            std::string fresh_path = realm::_impl::ClientResetOperation::get_fresh_path_for(local_config.path);
2✔
1000
            util::File f(fresh_path, util::File::Mode::mode_Write);
2✔
1001
            f.write("a non empty file");
2✔
1002
            f.sync();
2✔
1003
            f.close();
2✔
1004

1✔
1005
            make_reset(local_config, remote_config)->run();
2✔
1006
            REQUIRE(!err);
2!
1007
            REQUIRE(before_callback_invocations == 1);
2!
1008
            REQUIRE(after_callback_invocations == 1);
2!
1009
        }
2✔
1010

24✔
1011
        SECTION("failing to download a fresh copy results in an error") {
48✔
1012
            ThreadSafeSyncError err;
2✔
1013
            local_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) {
2✔
1014
                err = error;
2✔
1015
            };
2✔
1016
            std::string fresh_path = realm::_impl::ClientResetOperation::get_fresh_path_for(local_config.path);
2✔
1017
            // create a non-empty directory that we'll fail to delete
1✔
1018
            util::make_dir(fresh_path);
2✔
1019
            util::File(util::File::resolve("file", fresh_path), util::File::mode_Write);
2✔
1020

1✔
1021
            REQUIRE(!err);
2!
1022
            make_reset(local_config, remote_config)
2✔
1023
                ->on_post_reset([&](SharedRealm) {
2✔
1024
                    util::EventLoop::main().run_until([&] {
3✔
1025
                        return bool(err);
3✔
1026
                    });
3✔
1027
                })
2✔
1028
                ->run();
2✔
1029
            REQUIRE(err);
2!
1030
            REQUIRE(err.value()->is_client_reset_requested());
2!
1031
        }
2✔
1032

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

1✔
1036
            make_reset(local_config, remote_config)
2✔
1037
                ->on_post_reset([&](SharedRealm realm) {
2✔
1038
                    realm->close();
2✔
1039
                    SharedRealm r_after;
2✔
1040
                    REQUIRE_NOTHROW(r_after = Realm::get_shared_realm(local_config));
2✔
1041
                    CHECK(ObjectStore::table_for_object_type(r_after->read_group(), "object")
2!
1042
                              ->begin()
2✔
1043
                              ->get<Int>("value") == 6);
2✔
1044
                })
2✔
1045
                ->run();
2✔
1046
        }
2✔
1047

24✔
1048
        SECTION("delete and insert new") {
48✔
1049
            constexpr int64_t new_value = 42;
2✔
1050
            test_reset
2✔
1051
                ->make_remote_changes([&](SharedRealm remote) {
2✔
1052
                    auto table = get_table(*remote, "object");
2✔
1053
                    REQUIRE(table);
2!
1054
                    REQUIRE(table->size() == 1);
2!
1055
                    ObjectId different_pk = ObjectId::gen();
2✔
1056
                    table->clear();
2✔
1057
                    auto obj = create_object(*remote, "object", {different_pk}, partition);
2✔
1058
                    auto col = obj.get_table()->get_column_key("value");
2✔
1059
                    obj.set(col, new_value);
2✔
1060
                })
2✔
1061
                ->on_post_local_changes([&](SharedRealm realm) {
2✔
1062
                    setup_listeners(realm);
2✔
1063
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
1064
                    CHECK(results.size() == 1);
2!
1065
                    CHECK(results.get<Obj>(0).get<Int>("value") == 4);
2!
1066
                })
2✔
1067
                ->on_post_reset([&](SharedRealm realm) {
2✔
1068
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
1069
                    CHECK(results.size() == 1);
2!
1070
                    CHECK(results.get<Obj>(0).get<Int>("value") == new_value);
2!
1071
                    CHECK(!object.is_valid());
2!
1072
                    REQUIRE_INDICES(results_changes.modifications);
2!
1073
                    REQUIRE_INDICES(results_changes.insertions, 0);
2!
1074
                    REQUIRE_INDICES(results_changes.deletions, 0);
2!
1075
                    REQUIRE_INDICES(object_changes.modifications);
2!
1076
                    REQUIRE_INDICES(object_changes.insertions);
2!
1077
                    REQUIRE_INDICES(object_changes.deletions, 0);
2!
1078
                })
2✔
1079
                ->run();
2✔
1080
        }
2✔
1081

24✔
1082
        SECTION("delete and insert same pk is reported as modification") {
48✔
1083
            constexpr int64_t new_value = 42;
2✔
1084
            test_reset
2✔
1085
                ->make_remote_changes([&](SharedRealm remote) {
2✔
1086
                    auto table = get_table(*remote, "object");
2✔
1087
                    REQUIRE(table);
2!
1088
                    REQUIRE(table->size() == 1);
2!
1089
                    Mixed orig_pk = table->begin()->get_primary_key();
2✔
1090
                    table->clear();
2✔
1091
                    auto obj = create_object(*remote, "object", {orig_pk.get_object_id()}, partition);
2✔
1092
                    REQUIRE(obj.get_primary_key() == orig_pk);
2!
1093
                    auto col = obj.get_table()->get_column_key("value");
2✔
1094
                    obj.set(col, new_value);
2✔
1095
                })
2✔
1096
                ->on_post_local_changes([&](SharedRealm realm) {
2✔
1097
                    setup_listeners(realm);
2✔
1098
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
1099
                    CHECK(results.size() == 1);
2!
1100
                    CHECK(results.get<Obj>(0).get<Int>("value") == 4);
2!
1101
                })
2✔
1102
                ->on_post_reset([&](SharedRealm realm) {
2✔
1103
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
1104
                    CHECK(results.size() == 1);
2!
1105
                    CHECK(results.get<Obj>(0).get<Int>("value") == new_value);
2!
1106
                    CHECK(object.is_valid());
2!
1107
                    CHECK(object.get_obj().get<Int>("value") == new_value);
2!
1108
                    REQUIRE_INDICES(results_changes.modifications, 0);
2!
1109
                    REQUIRE_INDICES(results_changes.insertions);
2!
1110
                    REQUIRE_INDICES(results_changes.deletions);
2!
1111
                    REQUIRE_INDICES(object_changes.modifications, 0);
2!
1112
                    REQUIRE_INDICES(object_changes.insertions);
2!
1113
                    REQUIRE_INDICES(object_changes.deletions);
2!
1114
                })
2✔
1115
                ->run();
2✔
1116
        }
2✔
1117

24✔
1118
        SECTION("insert in discarded transaction is deleted") {
48✔
1119
            constexpr int64_t new_value = 42;
2✔
1120
            test_reset
2✔
1121
                ->make_local_changes([&](SharedRealm local) {
2✔
1122
                    auto table = get_table(*local, "object");
2✔
1123
                    REQUIRE(table);
2!
1124
                    REQUIRE(table->size() == 1);
2!
1125
                    auto obj = create_object(*local, "object", util::none, partition);
2✔
1126
                    auto col = obj.get_table()->get_column_key("value");
2✔
1127
                    REQUIRE(table->size() == 2);
2!
1128
                    obj.set(col, new_value);
2✔
1129
                })
2✔
1130
                ->on_post_local_changes([&](SharedRealm realm) {
2✔
1131
                    setup_listeners(realm);
2✔
1132
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
1133
                    CHECK(results.size() == 2);
2!
1134
                })
2✔
1135
                ->on_post_reset([&](SharedRealm realm) {
2✔
1136
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
1137
                    CHECK(results.size() == 1);
2!
1138
                    CHECK(results.get<Obj>(0).get<Int>("value") == 6);
2!
1139
                    CHECK(object.is_valid());
2!
1140
                    CHECK(object.get_obj().get<Int>("value") == 6);
2!
1141
                    REQUIRE_INDICES(results_changes.modifications, 0);
2!
1142
                    REQUIRE_INDICES(results_changes.insertions);
2!
1143
                    REQUIRE_INDICES(results_changes.deletions, 1);
2!
1144
                    REQUIRE_INDICES(object_changes.modifications, 0);
2!
1145
                    REQUIRE_INDICES(object_changes.insertions);
2!
1146
                    REQUIRE_INDICES(object_changes.deletions);
2!
1147
                })
2✔
1148
                ->run();
2✔
1149
        }
2✔
1150

24✔
1151
        SECTION("delete in discarded transaction is recovered") {
48✔
1152
            test_reset
2✔
1153
                ->make_local_changes([&](SharedRealm local) {
2✔
1154
                    auto table = get_table(*local, "object");
2✔
1155
                    REQUIRE(table);
2!
1156
                    REQUIRE(table->size() == 1);
2!
1157
                    table->clear();
2✔
1158
                    REQUIRE(table->size() == 0);
2!
1159
                })
2✔
1160
                ->on_post_local_changes([&](SharedRealm realm) {
2✔
1161
                    setup_listeners(realm);
2✔
1162
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
1163
                    CHECK(results.size() == 0);
2!
1164
                })
2✔
1165
                ->on_post_reset([&](SharedRealm realm) {
2✔
1166
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
1167
                    CHECK(results.size() == 1);
2!
1168
                    CHECK(results.get<Obj>(0).get<Int>("value") == 6);
2!
1169
                    CHECK(!object.is_valid());
2!
1170
                    REQUIRE_INDICES(results_changes.modifications);
2!
1171
                    REQUIRE_INDICES(results_changes.insertions, 0);
2!
1172
                    REQUIRE_INDICES(results_changes.deletions);
2!
1173
                })
2✔
1174
                ->run();
2✔
1175
        }
2✔
1176

24✔
1177
        SECTION("extra local table creates a client reset error") {
48✔
1178
            ThreadSafeSyncError err;
2✔
1179
            local_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) {
2✔
1180
                err = error;
2✔
1181
            };
2✔
1182
            make_reset(local_config, remote_config)
2✔
1183
                ->make_local_changes([&](SharedRealm local) {
2✔
1184
                    local->update_schema(
2✔
1185
                        {
2✔
1186
                            {"object2",
2✔
1187
                             {
2✔
1188
                                 {"_id", PropertyType::ObjectId | PropertyType::Nullable, Property::IsPrimary{true}},
2✔
1189
                                 {"realm_id", PropertyType::String | PropertyType::Nullable},
2✔
1190
                             }},
2✔
1191
                        },
2✔
1192
                        0, nullptr, nullptr, true);
2✔
1193
                    create_object(*local, "object2", ObjectId::gen(), partition);
2✔
1194
                    create_object(*local, "object2", ObjectId::gen(), partition);
2✔
1195
                })
2✔
1196
                ->on_post_reset([&](SharedRealm realm) {
2✔
1197
                    util::EventLoop::main().run_until([&] {
3✔
1198
                        return bool(err);
3✔
1199
                    });
3✔
1200
                    REQUIRE_NOTHROW(realm->refresh());
2✔
1201
                })
2✔
1202
                ->run();
2✔
1203
            REQUIRE(err);
2!
1204
            REQUIRE(err.value()->is_client_reset_requested());
2!
1205
            REQUIRE(before_callback_invocations == 1);
2!
1206
            REQUIRE(after_callback_invocations == 0);
2!
1207
        }
2✔
1208

24✔
1209
        SECTION("extra local column creates a client reset error") {
48✔
1210
            ThreadSafeSyncError err;
2✔
1211
            local_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) {
2✔
1212
                err = error;
2✔
1213
            };
2✔
1214
            make_reset(local_config, remote_config)
2✔
1215
                ->make_local_changes([](SharedRealm local) {
2✔
1216
                    local->update_schema(
2✔
1217
                        {
2✔
1218
                            {"object",
2✔
1219
                             {
2✔
1220
                                 {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
1221
                                 {"value2", PropertyType::Int},
2✔
1222
                                 {"array", PropertyType::Int | PropertyType::Array},
2✔
1223
                                 {"link", PropertyType::Object | PropertyType::Nullable, "object"},
2✔
1224
                                 {"realm_id", PropertyType::String | PropertyType::Nullable},
2✔
1225
                             }},
2✔
1226
                        },
2✔
1227
                        0, nullptr, nullptr, true);
2✔
1228
                    auto table = ObjectStore::table_for_object_type(local->read_group(), "object");
2✔
1229
                    table->begin()->set(table->get_column_key("value2"), 123);
2✔
1230
                })
2✔
1231
                ->on_post_reset([&](SharedRealm realm) {
2✔
1232
                    util::EventLoop::main().run_until([&] {
3✔
1233
                        return bool(err);
3✔
1234
                    });
3✔
1235
                    REQUIRE_NOTHROW(realm->refresh());
2✔
1236
                })
2✔
1237
                ->run();
2✔
1238

1✔
1239
            REQUIRE(err);
2!
1240
            REQUIRE(err.value()->is_client_reset_requested());
2!
1241
            REQUIRE(before_callback_invocations == 1);
2!
1242
            REQUIRE(after_callback_invocations == 0);
2!
1243
        }
2✔
1244

24✔
1245
        SECTION("compatible schema changes in both remote and local transactions") {
48✔
1246
            test_reset
2✔
1247
                ->make_local_changes([](SharedRealm local) {
2✔
1248
                    local->update_schema(
2✔
1249
                        {
2✔
1250
                            {"object",
2✔
1251
                             {
2✔
1252
                                 {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
1253
                                 {"value2", PropertyType::Int},
2✔
1254
                                 {"realm_id", PropertyType::String | PropertyType::Nullable},
2✔
1255
                             }},
2✔
1256
                            {"object2",
2✔
1257
                             {
2✔
1258
                                 {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
1259
                                 {"link", PropertyType::Object | PropertyType::Nullable, "object"},
2✔
1260
                                 {"realm_id", PropertyType::String | PropertyType::Nullable},
2✔
1261
                             }},
2✔
1262
                        },
2✔
1263
                        0, nullptr, nullptr, true);
2✔
1264
                })
2✔
1265
                ->make_remote_changes([](SharedRealm remote) {
2✔
1266
                    remote->update_schema(
2✔
1267
                        {
2✔
1268
                            {"object",
2✔
1269
                             {
2✔
1270
                                 {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
1271
                                 {"value2", PropertyType::Int},
2✔
1272
                                 {"realm_id", PropertyType::String | PropertyType::Nullable},
2✔
1273
                             }},
2✔
1274
                            {"object2",
2✔
1275
                             {
2✔
1276
                                 {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
1277
                                 {"link", PropertyType::Object | PropertyType::Nullable, "object"},
2✔
1278
                                 {"realm_id", PropertyType::String | PropertyType::Nullable},
2✔
1279
                             }},
2✔
1280
                        },
2✔
1281
                        0, nullptr, nullptr, true);
2✔
1282
                })
2✔
1283
                ->on_post_reset([](SharedRealm realm) {
2✔
1284
                    REQUIRE_NOTHROW(realm->refresh());
2✔
1285
                    auto table = ObjectStore::table_for_object_type(realm->read_group(), "object2");
2✔
1286
                    REQUIRE(table->get_column_count() == 3);
2!
1287
                    REQUIRE(bool(table->get_column_key("link")));
2!
1288
                })
2✔
1289
                ->run();
2✔
1290
        }
2✔
1291

24✔
1292
        SECTION("incompatible schema changes in remote and local transactions") {
48✔
1293
            ThreadSafeSyncError err;
2✔
1294
            local_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) {
2✔
1295
                err = error;
2✔
1296
            };
2✔
1297
            make_reset(local_config, remote_config)
2✔
1298
                ->make_local_changes([](SharedRealm local) {
2✔
1299
                    local->update_schema(
2✔
1300
                        {
2✔
1301
                            {"object",
2✔
1302
                             {
2✔
1303
                                 {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
1304
                                 {"value2", PropertyType::Float},
2✔
1305
                                 {"realm_id", PropertyType::String | PropertyType::Nullable},
2✔
1306
                             }},
2✔
1307
                        },
2✔
1308
                        0, nullptr, nullptr, true);
2✔
1309
                })
2✔
1310
                ->make_remote_changes([](SharedRealm remote) {
2✔
1311
                    remote->update_schema(
2✔
1312
                        {
2✔
1313
                            {"object",
2✔
1314
                             {
2✔
1315
                                 {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
1316
                                 {"value2", PropertyType::Int},
2✔
1317
                                 {"realm_id", PropertyType::String | PropertyType::Nullable},
2✔
1318
                             }},
2✔
1319
                        },
2✔
1320
                        0, nullptr, nullptr, true);
2✔
1321
                })
2✔
1322
                ->on_post_reset([&](SharedRealm realm) {
2✔
1323
                    util::EventLoop::main().run_until([&] {
3✔
1324
                        return bool(err);
3✔
1325
                    });
3✔
1326
                    REQUIRE_NOTHROW(realm->refresh());
2✔
1327
                })
2✔
1328
                ->run();
2✔
1329
            REQUIRE(err);
2!
1330
            REQUIRE(err.value()->is_client_reset_requested());
2!
1331
        }
2✔
1332

24✔
1333
        SECTION("primary key type cannot be changed") {
48✔
1334
            ThreadSafeSyncError err;
2✔
1335
            local_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) {
2✔
1336
                err = error;
2✔
1337
            };
2✔
1338

1✔
1339
            make_reset(local_config, remote_config)
2✔
1340
                ->make_local_changes([](SharedRealm local) {
2✔
1341
                    local->update_schema(
2✔
1342
                        {
2✔
1343
                            {"new table",
2✔
1344
                             {
2✔
1345
                                 {"_id", PropertyType::Int, Property::IsPrimary{true}},
2✔
1346
                                 {"realm_id", PropertyType::String | PropertyType::Nullable},
2✔
1347
                             }},
2✔
1348
                        },
2✔
1349
                        0, nullptr, nullptr, true);
2✔
1350
                })
2✔
1351
                ->make_remote_changes([](SharedRealm remote) {
2✔
1352
                    remote->update_schema(
2✔
1353
                        {
2✔
1354
                            {"new table",
2✔
1355
                             {
2✔
1356
                                 {"_id", PropertyType::String, Property::IsPrimary{true}},
2✔
1357
                                 {"realm_id", PropertyType::String | PropertyType::Nullable},
2✔
1358
                             }},
2✔
1359
                        },
2✔
1360
                        0, nullptr, nullptr, true);
2✔
1361
                })
2✔
1362
                ->on_post_reset([&](SharedRealm realm) {
2✔
1363
                    util::EventLoop::main().run_until([&] {
3✔
1364
                        return bool(err);
3✔
1365
                    });
3✔
1366
                    REQUIRE_NOTHROW(realm->refresh());
2✔
1367
                })
2✔
1368
                ->run();
2✔
1369
            REQUIRE(err);
2!
1370
            REQUIRE(err.value()->is_client_reset_requested());
2!
1371
        }
2✔
1372

24✔
1373
        SECTION("list operations") {
48✔
1374
            ObjKey k0, k1, k2;
6✔
1375
            test_reset->setup([&](SharedRealm realm) {
6✔
1376
                k0 = create_object(*realm, "link target", ObjectId::gen(), partition).set("value", 1).get_key();
6✔
1377
                k1 = create_object(*realm, "link target", ObjectId::gen(), partition).set("value", 2).get_key();
6✔
1378
                k2 = create_object(*realm, "link target", ObjectId::gen(), partition).set("value", 3).get_key();
6✔
1379
                Obj o = create_object(*realm, "link origin", ObjectId::gen(), partition);
6✔
1380
                auto list = o.get_linklist(o.get_table()->get_column_key("list"));
6✔
1381
                list.add(k0);
6✔
1382
                list.add(k1);
6✔
1383
                list.add(k2);
6✔
1384
            });
6✔
1385
            auto check_links = [&](auto& realm) {
6✔
1386
                auto table = get_table(*realm, "link origin");
6✔
1387
                REQUIRE(table->size() == 1);
6!
1388
                auto list = table->begin()->get_linklist(table->get_column_key("list"));
6✔
1389
                REQUIRE(list.size() == 3);
6!
1390
                REQUIRE(list.get_object(0).template get<Int>("value") == 1);
6!
1391
                REQUIRE(list.get_object(1).template get<Int>("value") == 2);
6!
1392
                REQUIRE(list.get_object(2).template get<Int>("value") == 3);
6!
1393
            };
6✔
1394

3✔
1395
            SECTION("list insertions in local transaction") {
6✔
1396
                test_reset
2✔
1397
                    ->make_local_changes([&](SharedRealm local) {
2✔
1398
                        auto table = get_table(*local, "link origin");
2✔
1399
                        auto list = table->begin()->get_linklist(table->get_column_key("list"));
2✔
1400
                        list.add(k0);
2✔
1401
                        list.insert(0, k2);
2✔
1402
                        list.insert(0, k1);
2✔
1403
                    })
2✔
1404
                    ->on_post_reset([&](SharedRealm realm) {
2✔
1405
                        REQUIRE_NOTHROW(realm->refresh());
2✔
1406
                        check_links(realm);
2✔
1407
                    })
2✔
1408
                    ->run();
2✔
1409
            }
2✔
1410

3✔
1411
            SECTION("list deletions in local transaction") {
6✔
1412
                test_reset
2✔
1413
                    ->make_local_changes([&](SharedRealm local) {
2✔
1414
                        auto table = get_table(*local, "link origin");
2✔
1415
                        auto list = table->begin()->get_linklist(table->get_column_key("list"));
2✔
1416
                        list.remove(1);
2✔
1417
                    })
2✔
1418
                    ->on_post_reset([&](SharedRealm realm) {
2✔
1419
                        REQUIRE_NOTHROW(realm->refresh());
2✔
1420
                        check_links(realm);
2✔
1421
                    })
2✔
1422
                    ->run();
2✔
1423
            }
2✔
1424

3✔
1425
            SECTION("list clear in local transaction") {
6✔
1426
                test_reset
2✔
1427
                    ->make_local_changes([&](SharedRealm local) {
2✔
1428
                        auto table = get_table(*local, "link origin");
2✔
1429
                        auto list = table->begin()->get_linklist(table->get_column_key("list"));
2✔
1430
                        list.clear();
2✔
1431
                    })
2✔
1432
                    ->on_post_reset([&](SharedRealm realm) {
2✔
1433
                        REQUIRE_NOTHROW(realm->refresh());
2✔
1434
                        check_links(realm);
2✔
1435
                    })
2✔
1436
                    ->run();
2✔
1437
            }
2✔
1438
        }
6✔
1439

24✔
1440
        SECTION("conflicting primary key creations") {
48✔
1441
            ObjectId id1 = ObjectId::gen();
2✔
1442
            ObjectId id2 = ObjectId::gen();
2✔
1443
            ObjectId id3 = ObjectId::gen();
2✔
1444
            ObjectId id4 = ObjectId::gen();
2✔
1445
            test_reset
2✔
1446
                ->make_local_changes([&](SharedRealm local) {
2✔
1447
                    auto table = get_table(*local, "object");
2✔
1448
                    table->clear();
2✔
1449
                    create_object(*local, "object", {id1}, partition).set("value", 4);
2✔
1450
                    create_object(*local, "object", {id2}, partition).set("value", 5);
2✔
1451
                    create_object(*local, "object", {id3}, partition).set("value", 6);
2✔
1452
                })
2✔
1453
                ->make_remote_changes([&](SharedRealm remote) {
2✔
1454
                    auto table = get_table(*remote, "object");
2✔
1455
                    table->clear();
2✔
1456
                    create_object(*remote, "object", {id1}, partition).set("value", 4);
2✔
1457
                    create_object(*remote, "object", {id2}, partition).set("value", 7);
2✔
1458
                    create_object(*remote, "object", {id4}, partition).set("value", 8);
2✔
1459
                })
2✔
1460
                ->on_post_local_changes([&](SharedRealm realm) {
2✔
1461
                    setup_listeners(realm);
2✔
1462
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
1463
                    CHECK(results.size() == 3);
2!
1464
                    CHECK(results.get<Obj>(0).get<Int>("value") == 4);
2!
1465
                })
2✔
1466
                ->on_post_reset([&](SharedRealm realm) {
2✔
1467
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
1468
                    CHECK(results.size() == 3);
2!
1469
                    // here we rely on results being sorted by "value"
1✔
1470
                    CHECK(results.get<Obj>(0).get<ObjectId>("_id") == id1);
2!
1471
                    CHECK(results.get<Obj>(0).get<Int>("value") == 4);
2!
1472
                    CHECK(results.get<Obj>(1).get<ObjectId>("_id") == id2);
2!
1473
                    CHECK(results.get<Obj>(1).get<Int>("value") == 7);
2!
1474
                    CHECK(results.get<Obj>(2).get<ObjectId>("_id") == id4);
2!
1475
                    CHECK(results.get<Obj>(2).get<Int>("value") == 8);
2!
1476
                    CHECK(object.is_valid());
2!
1477
                    REQUIRE_INDICES(results_changes.modifications, 1);
2!
1478
                    REQUIRE_INDICES(results_changes.insertions, 2);
2!
1479
                    REQUIRE_INDICES(results_changes.deletions, 2);
2!
1480
                    REQUIRE_INDICES(object_changes.modifications);
2!
1481
                    REQUIRE_INDICES(object_changes.insertions);
2!
1482
                    REQUIRE_INDICES(object_changes.deletions);
2!
1483
                })
2✔
1484
                ->run();
2✔
1485
        }
2✔
1486

24✔
1487
        auto get_key_for_object_with_value = [&](TableRef table, int64_t value) -> ObjKey {
32✔
1488
            REQUIRE(table);
16!
1489
            auto target = std::find_if(table->begin(), table->end(), [&](auto& it) -> bool {
28✔
1490
                return it.template get<Int>("value") == value;
28✔
1491
            });
28✔
1492
            if (target == table->end()) {
16✔
1493
                return {};
×
1494
            }
×
1495
            return target->get_key();
16✔
1496
        };
16✔
1497

24✔
1498
        SECTION("link to remotely deleted object") {
48✔
1499
            test_reset
2✔
1500
                ->setup([&](SharedRealm realm) {
2✔
1501
                    auto k0 =
2✔
1502
                        create_object(*realm, "link target", ObjectId::gen(), partition).set("value", 1).get_key();
2✔
1503
                    create_object(*realm, "link target", ObjectId::gen(), partition).set("value", 2);
2✔
1504
                    create_object(*realm, "link target", ObjectId::gen(), partition).set("value", 3);
2✔
1505

1✔
1506
                    Obj o = create_object(*realm, "link origin", ObjectId::gen(), partition);
2✔
1507
                    o.set("link", k0);
2✔
1508
                })
2✔
1509
                ->make_local_changes([&](SharedRealm local) {
2✔
1510
                    auto target_table = get_table(*local, "link target");
2✔
1511
                    auto key_of_second_target = get_key_for_object_with_value(target_table, 2);
2✔
1512
                    REQUIRE(key_of_second_target);
2!
1513
                    auto table = get_table(*local, "link origin");
2✔
1514
                    table->begin()->set("link", key_of_second_target);
2✔
1515
                })
2✔
1516
                ->make_remote_changes([&](SharedRealm remote) {
2✔
1517
                    auto table = get_table(*remote, "link target");
2✔
1518
                    auto key_of_second_target = get_key_for_object_with_value(table, 2);
2✔
1519
                    table->remove_object(key_of_second_target);
2✔
1520
                })
2✔
1521
                ->on_post_reset([&](SharedRealm realm) {
2✔
1522
                    REQUIRE_NOTHROW(realm->refresh());
2✔
1523
                    auto origin = get_table(*realm, "link origin");
2✔
1524
                    auto target = get_table(*realm, "link target");
2✔
1525
                    REQUIRE(origin->size() == 1);
2!
1526
                    REQUIRE(target->size() == 2);
2!
1527
                    REQUIRE(get_key_for_object_with_value(target, 1));
2!
1528
                    REQUIRE(get_key_for_object_with_value(target, 3));
2!
1529
                    auto key = origin->begin()->get<ObjKey>("link");
2✔
1530
                    auto obj = target->get_object(key);
2✔
1531
                    REQUIRE(obj.get<Int>("value") == 1);
2!
1532
                })
2✔
1533
                ->run();
2✔
1534
        }
2✔
1535

24✔
1536
        SECTION("add remotely deleted object to list") {
48✔
1537
            ObjKey k0, k1, k2;
2✔
1538
            test_reset
2✔
1539
                ->setup([&](SharedRealm realm) {
2✔
1540
                    k0 = create_object(*realm, "link target", ObjectId::gen(), partition).set("value", 1).get_key();
2✔
1541
                    k1 = create_object(*realm, "link target", ObjectId::gen(), partition).set("value", 2).get_key();
2✔
1542
                    k2 = create_object(*realm, "link target", ObjectId::gen(), partition).set("value", 3).get_key();
2✔
1543
                    Obj o = create_object(*realm, "link origin", ObjectId::gen(), partition);
2✔
1544
                    o.get_linklist("list").add(k0);
2✔
1545
                })
2✔
1546
                ->make_local_changes([&](SharedRealm local) {
2✔
1547
                    auto key = get_key_for_object_with_value(get_table(*local, "link target"), 2);
2✔
1548
                    auto table = get_table(*local, "link origin");
2✔
1549
                    auto list = table->begin()->get_linklist("list");
2✔
1550
                    list.add(key);
2✔
1551
                })
2✔
1552
                ->make_remote_changes([&](SharedRealm remote) {
2✔
1553
                    auto table = get_table(*remote, "link target");
2✔
1554
                    auto key = get_key_for_object_with_value(table, 2);
2✔
1555
                    REQUIRE(key);
2!
1556
                    table->remove_object(key);
2✔
1557
                })
2✔
1558
                ->on_post_reset([&](SharedRealm realm) {
2✔
1559
                    REQUIRE_NOTHROW(realm->refresh());
2✔
1560
                    auto table = get_table(*realm, "link origin");
2✔
1561
                    auto target_table = get_table(*realm, "link target");
2✔
1562
                    REQUIRE(table->size() == 1);
2!
1563
                    REQUIRE(target_table->size() == 2);
2!
1564
                    REQUIRE(get_key_for_object_with_value(target_table, 1));
2!
1565
                    REQUIRE(get_key_for_object_with_value(target_table, 3));
2!
1566
                    auto list = table->begin()->get_linklist("list");
2✔
1567
                    REQUIRE(list.size() == 1);
2!
1568
                    REQUIRE(list.get_object(0).get<Int>("value") == 1);
2!
1569
                })
2✔
1570
                ->run();
2✔
1571
        }
2✔
1572
    } // end discard local section
48✔
1573

40✔
1574
    SECTION("cycle detection") {
80✔
1575
        auto has_reset_cycle_flag = [](SharedRealm realm) -> util::Optional<_impl::client_reset::PendingReset> {
10✔
1576
            auto db = TestHelper::get_db(realm);
6✔
1577
            auto rt = db->start_read();
6✔
1578
            return _impl::client_reset::has_pending_reset(rt);
6✔
1579
        };
6✔
1580
        ThreadSafeSyncError err;
14✔
1581
        local_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) {
11✔
1582
            err = error;
8✔
1583
        };
8✔
1584
        auto make_fake_previous_reset = [&local_config](ClientResyncMode type) {
13✔
1585
            local_config.sync_config->notify_before_client_reset = [previous_type = type](SharedRealm realm) {
12✔
1586
                auto db = TestHelper::get_db(realm);
12✔
1587
                auto wt = db->start_write();
12✔
1588
                _impl::client_reset::track_reset(wt, previous_type);
12✔
1589
                wt->commit();
12✔
1590
            };
12✔
1591
        };
12✔
1592
        SECTION("a normal reset adds and removes a cycle detection flag") {
14✔
1593
            local_config.sync_config->client_resync_mode = ClientResyncMode::RecoverOrDiscard;
2✔
1594
            local_config.sync_config->notify_before_client_reset = [&](SharedRealm realm) {
2✔
1595
                auto flag = has_reset_cycle_flag(realm);
2✔
1596
                REQUIRE(!flag);
2!
1597
                std::lock_guard<std::mutex> lock(mtx);
2✔
1598
                ++before_callback_invocations;
2✔
1599
            };
2✔
1600
            local_config.sync_config->notify_after_client_reset = [&](SharedRealm, ThreadSafeReference realm_ref,
2✔
1601
                                                                      bool did_recover) {
2✔
1602
                SharedRealm realm = Realm::get_shared_realm(std::move(realm_ref), util::Scheduler::make_default());
2✔
1603
                auto flag = has_reset_cycle_flag(realm);
2✔
1604
                REQUIRE(bool(flag));
2!
1605
                REQUIRE(flag->type == ClientResyncMode::Recover);
2!
1606
                REQUIRE(did_recover);
2!
1607
                std::lock_guard<std::mutex> lock(mtx);
2✔
1608
                ++after_callback_invocations;
2✔
1609
            };
2✔
1610
            make_reset(local_config, remote_config)
2✔
1611
                ->on_post_local_changes([&](SharedRealm realm) {
2✔
1612
                    auto flag = has_reset_cycle_flag(realm);
2✔
1613
                    REQUIRE(!flag);
2!
1614
                })
2✔
1615
                ->run();
2✔
1616
            REQUIRE(!err);
2!
1617
            REQUIRE(before_callback_invocations == 1);
2!
1618
            REQUIRE(after_callback_invocations == 1);
2!
1619
        }
2✔
1620
        SECTION("In DiscardLocal mode: a previous failed discard reset is detected and generates an error") {
14✔
1621
            local_config.sync_config->client_resync_mode = ClientResyncMode::DiscardLocal;
2✔
1622
            make_fake_previous_reset(ClientResyncMode::DiscardLocal);
2✔
1623
            make_reset(local_config, remote_config)->run();
2✔
1624
            timed_sleeping_wait_for([&]() -> bool {
2✔
1625
                return !!err;
2✔
1626
            });
2✔
1627
            REQUIRE(err.value()->is_client_reset_requested());
2!
1628
        }
2✔
1629
        SECTION("In Recover mode: a previous failed recover reset is detected and generates an error") {
14✔
1630
            local_config.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
1631
            make_fake_previous_reset(ClientResyncMode::Recover);
2✔
1632
            make_reset(local_config, remote_config)->run();
2✔
1633
            timed_sleeping_wait_for([&]() -> bool {
2✔
1634
                return !!err;
2✔
1635
            });
2✔
1636
            REQUIRE(err.value()->is_client_reset_requested());
2!
1637
        }
2✔
1638
        SECTION("In Recover mode: a previous failed discard reset is detected and generates an error") {
14✔
1639
            local_config.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
1640
            make_fake_previous_reset(ClientResyncMode::DiscardLocal);
2✔
1641
            make_reset(local_config, remote_config)->run();
2✔
1642
            timed_sleeping_wait_for([&]() -> bool {
2✔
1643
                return !!err;
2✔
1644
            });
2✔
1645
            REQUIRE(err.value()->is_client_reset_requested());
2!
1646
        }
2✔
1647
        SECTION("In RecoverOrDiscard mode: a previous failed discard reset is detected and generates an error") {
14✔
1648
            local_config.sync_config->client_resync_mode = ClientResyncMode::RecoverOrDiscard;
2✔
1649
            make_fake_previous_reset(ClientResyncMode::DiscardLocal);
2✔
1650
            make_reset(local_config, remote_config)->run();
2✔
1651
            timed_sleeping_wait_for([&]() -> bool {
2✔
1652
                return !!err;
2✔
1653
            });
2✔
1654
            REQUIRE(err.value()->is_client_reset_requested());
2!
1655
        }
2✔
1656
        const ObjectId added_pk = ObjectId::gen();
14✔
1657
        auto has_added_object = [&](SharedRealm realm) -> bool {
11✔
1658
            REQUIRE_NOTHROW(realm->refresh());
8✔
1659
            auto table = get_table(*realm, "object");
8✔
1660
            REQUIRE(table);
8!
1661
            ObjKey key = table->find_primary_key(added_pk);
8✔
1662
            return !!key;
8✔
1663
        };
8✔
1664
        SECTION(
14✔
1665
            "In RecoverOrDiscard mode: a previous failed recovery is detected and triggers a DiscardLocal reset") {
8✔
1666
            local_config.sync_config->client_resync_mode = ClientResyncMode::RecoverOrDiscard;
2✔
1667
            make_fake_previous_reset(ClientResyncMode::Recover);
2✔
1668
            local_config.sync_config->notify_after_client_reset = [&](SharedRealm before,
2✔
1669
                                                                      ThreadSafeReference after_ref,
2✔
1670
                                                                      bool did_recover) {
2✔
1671
                SharedRealm after = Realm::get_shared_realm(std::move(after_ref), util::Scheduler::make_default());
2✔
1672

1✔
1673
                REQUIRE(!did_recover);
2!
1674
                REQUIRE(has_added_object(before));
2!
1675
                REQUIRE(!has_added_object(after)); // discarded insert due to fallback to DiscardLocal mode
2!
1676
                std::lock_guard<std::mutex> lock(mtx);
2✔
1677
                ++after_callback_invocations;
2✔
1678
            };
2✔
1679
            make_reset(local_config, remote_config)
2✔
1680
                ->make_local_changes([&](SharedRealm realm) {
2✔
1681
                    auto table = get_table(*realm, "object");
2✔
1682
                    REQUIRE(table);
2!
1683
                    create_object(*realm, "object", {added_pk}, partition);
2✔
1684
                })
2✔
1685
                ->run();
2✔
1686
            timed_sleeping_wait_for(
2✔
1687
                [&]() -> bool {
2✔
1688
                    std::lock_guard<std::mutex> lock(mtx);
2✔
1689
                    return after_callback_invocations > 0 || err;
2!
1690
                },
2✔
1691
                std::chrono::seconds(120));
2✔
1692
            REQUIRE(!err);
2!
1693
        }
2✔
1694
        SECTION("In DiscardLocal mode: a previous failed recovery does not cause an error") {
14✔
1695
            local_config.sync_config->client_resync_mode = ClientResyncMode::DiscardLocal;
2✔
1696
            make_fake_previous_reset(ClientResyncMode::Recover);
2✔
1697
            local_config.sync_config->notify_after_client_reset = [&](SharedRealm before,
2✔
1698
                                                                      ThreadSafeReference after_ref,
2✔
1699
                                                                      bool did_recover) {
2✔
1700
                SharedRealm after = Realm::get_shared_realm(std::move(after_ref), util::Scheduler::make_default());
2✔
1701

1✔
1702
                REQUIRE(!did_recover);
2!
1703
                REQUIRE(has_added_object(before));
2!
1704
                REQUIRE(!has_added_object(after)); // not recovered
2!
1705
                std::lock_guard<std::mutex> lock(mtx);
2✔
1706
                ++after_callback_invocations;
2✔
1707
            };
2✔
1708
            make_reset(local_config, remote_config)
2✔
1709
                ->make_local_changes([&](SharedRealm realm) {
2✔
1710
                    auto table = get_table(*realm, "object");
2✔
1711
                    REQUIRE(table);
2!
1712
                    create_object(*realm, "object", {added_pk}, partition);
2✔
1713
                })
2✔
1714
                ->run();
2✔
1715
            timed_sleeping_wait_for(
2✔
1716
                [&]() -> bool {
2✔
1717
                    std::lock_guard<std::mutex> lock(mtx);
2✔
1718
                    return after_callback_invocations > 0 || err;
2!
1719
                },
2✔
1720
                std::chrono::seconds(120));
2✔
1721
            REQUIRE(!err);
2!
1722
        }
2✔
1723
    } // end cycle detection
14✔
1724
    SECTION("The server can prohibit recovery") {
80✔
1725
        const realm::AppSession& app_session = test_app_session.app_session();
4✔
1726
        auto sync_service = app_session.admin_api.get_sync_service(app_session.server_app_id);
4✔
1727
        auto sync_config = app_session.admin_api.get_config(app_session.server_app_id, sync_service);
4✔
1728
        REQUIRE(!sync_config.recovery_is_disabled);
4!
1729
        constexpr bool recovery_is_disabled = true;
4✔
1730
        app_session.admin_api.set_disable_recovery_to(app_session.server_app_id, sync_service.id, sync_config,
4✔
1731
                                                      recovery_is_disabled);
4✔
1732
        sync_config = app_session.admin_api.get_config(app_session.server_app_id, sync_service);
4✔
1733
        REQUIRE(sync_config.recovery_is_disabled);
4!
1734

2✔
1735
        SECTION("In Recover mode, a manual client reset is triggered") {
4✔
1736
            local_config.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
1737
            ThreadSafeSyncError err;
2✔
1738
            local_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) {
2✔
1739
                err = error;
2✔
1740
            };
2✔
1741
            make_reset(local_config, remote_config)
2✔
1742
                ->on_post_reset([&](SharedRealm) {
2✔
1743
                    util::EventLoop::main().run_until([&] {
3✔
1744
                        return bool(err);
3✔
1745
                    });
3✔
1746
                })
2✔
1747
                ->run();
2✔
1748
            REQUIRE(err);
2!
1749
            SyncError error = *err.value();
2✔
1750
            REQUIRE(error.is_client_reset_requested());
2!
1751
            REQUIRE(error.user_info.size() >= 2);
2!
1752
            REQUIRE(error.user_info.count(SyncError::c_original_file_path_key) == 1);
2!
1753
            REQUIRE(error.user_info.count(SyncError::c_recovery_file_path_key) == 1);
2!
1754
        }
2✔
1755
        SECTION("In RecoverOrDiscard mode, DiscardLocal is selected") {
4✔
1756
            local_config.sync_config->client_resync_mode = ClientResyncMode::RecoverOrDiscard;
2✔
1757
            constexpr int64_t new_value = 123456;
2✔
1758
            make_reset(local_config, remote_config)
2✔
1759
                ->make_local_changes([&](SharedRealm local) {
2✔
1760
                    auto table = get_table(*local, "object");
2✔
1761
                    REQUIRE(table);
2!
1762
                    REQUIRE(table->size() == 1);
2!
1763
                    auto obj = create_object(*local, "object", ObjectId::gen(), partition);
2✔
1764
                    auto col = obj.get_table()->get_column_key("value");
2✔
1765
                    REQUIRE(table->size() == 2);
2!
1766
                    obj.set(col, new_value);
2✔
1767
                })
2✔
1768
                ->on_post_local_changes([&](SharedRealm realm) {
2✔
1769
                    setup_listeners(realm);
2✔
1770
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
1771
                    CHECK(results.size() == 2);
2!
1772
                })
2✔
1773
                ->on_post_reset([&](SharedRealm realm) {
2✔
1774
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
2✔
1775
                    CHECK(results.size() == 1); // insert was discarded
2!
1776
                    CHECK(results.get<Obj>(0).get<Int>("value") == 6);
2!
1777
                    CHECK(object.is_valid());
2!
1778
                    CHECK(object.get_obj().get<Int>("value") == 6);
2!
1779
                })
2✔
1780
                ->run();
2✔
1781
        }
2✔
1782
    } // end: The server can prohibit recovery
4✔
1783
}
80✔
1784

1785
TEST_CASE("sync: Client reset during async open", "[sync][pbs][client reset][baas]") {
2✔
1786
    const reset_utils::Partition partition{"realm_id", random_string(20)};
2✔
1787
    Property partition_prop = {partition.property_name, PropertyType::String | PropertyType::Nullable};
2✔
1788
    Schema schema{
2✔
1789
        {"object",
2✔
1790
         {
2✔
1791
             {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
1792
             {"value", PropertyType::String},
2✔
1793
             partition_prop,
2✔
1794
         }},
2✔
1795
    };
2✔
1796

1✔
1797
    std::string base_url = get_base_url();
2✔
1798
    REQUIRE(!base_url.empty());
2!
1799
    auto server_app_config = minimal_app_config(base_url, "client_reset_tests", schema);
2✔
1800
    server_app_config.partition_key = partition_prop;
2✔
1801
    TestAppSession test_app_session(create_app(server_app_config));
2✔
1802
    auto app = test_app_session.app();
2✔
1803

1✔
1804
    auto before_callback_called = util::make_promise_future<void>();
2✔
1805
    auto after_callback_called = util::make_promise_future<void>();
2✔
1806
    create_user_and_log_in(app);
2✔
1807
    SyncTestFile realm_config(app->current_user(), partition.value, std::nullopt,
2✔
1808
                              [](std::shared_ptr<SyncSession>, SyncError) { /*noop*/ });
1✔
1809
    realm_config.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
1810

1✔
1811
    realm_config.sync_config->on_sync_client_event_hook =
2✔
1812
        [&, client_reset_triggered = false](std::weak_ptr<SyncSession> weak_sess,
2✔
1813
                                            const SyncClientHookData& event_data) mutable {
8✔
1814
            auto sess = weak_sess.lock();
8✔
1815
            if (!sess) {
8✔
1816
                return SyncClientHookAction::NoAction;
×
1817
            }
×
1818
            if (sess->path() != realm_config.path) {
8✔
1819
                return SyncClientHookAction::NoAction;
4✔
1820
            }
4✔
1821

2✔
1822
            if (event_data.event != SyncClientHookEvent::DownloadMessageReceived) {
4✔
1823
                return SyncClientHookAction::NoAction;
2✔
1824
            }
2✔
1825

1✔
1826
            if (client_reset_triggered) {
2✔
1827
                return SyncClientHookAction::NoAction;
×
1828
            }
×
1829
            client_reset_triggered = true;
2✔
1830
            reset_utils::trigger_client_reset(test_app_session.app_session());
2✔
1831
            return SyncClientHookAction::EarlyReturn;
2✔
1832
        };
2✔
1833

1✔
1834
    // Expected behaviour is that the frozen realm passed in the callback should have no
1✔
1835
    // schema initialized if a client reset happens during an async open and the realm has never been opened before.
1✔
1836
    // SDK's should handle any edge cases which require the use of a schema i.e
1✔
1837
    // calling set_schema_subset(...)
1✔
1838
    realm_config.sync_config->notify_before_client_reset =
2✔
1839
        [promise = util::CopyablePromiseHolder(std::move(before_callback_called.promise))](
2✔
1840
            std::shared_ptr<Realm> realm) mutable {
2✔
1841
            CHECK(realm->schema_version() == ObjectStore::NotVersioned);
2!
1842
            promise.get_promise().emplace_value();
2✔
1843
        };
2✔
1844

1✔
1845
    realm_config.sync_config->notify_after_client_reset =
2✔
1846
        [promise = util::CopyablePromiseHolder(std::move(after_callback_called.promise))](
2✔
1847
            std::shared_ptr<Realm> realm, ThreadSafeReference, bool) mutable {
2✔
1848
            CHECK(realm->schema_version() == ObjectStore::NotVersioned);
2!
1849
            promise.get_promise().emplace_value();
2✔
1850
        };
2✔
1851

1✔
1852
    auto realm_task = Realm::get_synchronized_realm(realm_config);
2✔
1853
    auto realm_pf = util::make_promise_future<SharedRealm>();
2✔
1854
    realm_task->start([promise_holder = util::CopyablePromiseHolder(std::move(realm_pf.promise))](
2✔
1855
                          ThreadSafeReference ref, std::exception_ptr ex) mutable {
2✔
1856
        auto promise = promise_holder.get_promise();
2✔
1857
        if (ex) {
2✔
1858
            try {
×
1859
                std::rethrow_exception(ex);
×
1860
            }
×
1861
            catch (...) {
×
1862
                promise.set_error(exception_to_status());
×
1863
            }
×
1864
            return;
×
1865
        }
2✔
1866
        auto realm = Realm::get_shared_realm(std::move(ref));
2✔
1867
        if (!realm) {
2✔
1868
            promise.set_error({ErrorCodes::RuntimeError, "could not get realm from threadsaferef"});
×
1869
        }
×
1870
        promise.emplace_value(std::move(realm));
2✔
1871
    });
2✔
1872
    auto realm = realm_pf.future.get();
2✔
1873
    before_callback_called.future.get();
2✔
1874
    after_callback_called.future.get();
2✔
1875
}
2✔
1876

1877
#endif // REALM_ENABLE_AUTH_TESTS
1878

1879
namespace cf = realm::collection_fixtures;
1880
TEMPLATE_TEST_CASE("client reset types", "[sync][pbs][client reset]", cf::MixedVal, cf::Int, cf::Bool, cf::Float,
1881
                   cf::Double, cf::String, cf::Binary, cf::Date, cf::OID, cf::Decimal, cf::UUID,
1882
                   cf::BoxedOptional<cf::Int>, cf::BoxedOptional<cf::Bool>, cf::BoxedOptional<cf::Float>,
1883
                   cf::BoxedOptional<cf::Double>, cf::BoxedOptional<cf::OID>, cf::BoxedOptional<cf::UUID>,
1884
                   cf::UnboxedOptional<cf::String>, cf::UnboxedOptional<cf::Binary>, cf::UnboxedOptional<cf::Date>,
1885
                   cf::UnboxedOptional<cf::Decimal>)
1886
{
2,256✔
1887
    auto values = TestType::values();
2,256✔
1888
    using T = typename TestType::Type;
2,256✔
1889

1,128✔
1890
    if (!util::EventLoop::has_implementation())
2,256✔
1891
        return;
×
1892

1,128✔
1893
    TestSyncManager init_sync_manager;
2,256✔
1894
    SyncTestFile config(init_sync_manager.app(), "default");
2,256✔
1895
    config.cache = false;
2,256✔
1896
    config.automatic_change_notifications = false;
2,256✔
1897
    ClientResyncMode test_mode = GENERATE(ClientResyncMode::DiscardLocal, ClientResyncMode::Recover);
2,256✔
1898
    CAPTURE(test_mode);
2,256✔
1899
    config.sync_config->client_resync_mode = test_mode;
2,256✔
1900
    config.schema = Schema{
2,256✔
1901
        {"object",
2,256✔
1902
         {
2,256✔
1903
             {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2,256✔
1904
             {"value", PropertyType::Int},
2,256✔
1905
         }},
2,256✔
1906
        {"test type",
2,256✔
1907
         {{"_id", PropertyType::Int, Property::IsPrimary{true}},
2,256✔
1908
          {"value", TestType::property_type},
2,256✔
1909
          {"list", PropertyType::Array | TestType::property_type},
2,256✔
1910
          {"dictionary", PropertyType::Dictionary | TestType::property_type},
2,256✔
1911
          {"set", PropertyType::Set | TestType::property_type}}},
2,256✔
1912
    };
2,256✔
1913

1,128✔
1914
    SyncTestFile config2(init_sync_manager.app(), "default");
2,256✔
1915
    config2.schema = config.schema;
2,256✔
1916

1,128✔
1917
    Results results;
2,256✔
1918
    Object object;
2,256✔
1919
    CollectionChangeSet object_changes, results_changes;
2,256✔
1920
    NotificationToken object_token, results_token;
2,256✔
1921
    auto setup_listeners = [&](SharedRealm realm) {
2,256✔
1922
        results = Results(realm, ObjectStore::table_for_object_type(realm->read_group(), "test type"))
2,256✔
1923
                      .sort({{{"_id", true}}});
2,256✔
1924
        if (results.size() >= 1) {
2,256✔
1925
            auto obj = *ObjectStore::table_for_object_type(realm->read_group(), "test type")->begin();
2,256✔
1926
            object = Object(realm, obj);
2,256✔
1927
            object_token = object.add_notification_callback([&](CollectionChangeSet changes) {
3,510✔
1928
                object_changes = std::move(changes);
3,510✔
1929
            });
3,510✔
1930
        }
2,256✔
1931
        results_token = results.add_notification_callback([&](CollectionChangeSet changes) {
3,510✔
1932
            results_changes = std::move(changes);
3,510✔
1933
        });
3,510✔
1934
    };
2,256✔
1935

1,128✔
1936
    auto check_list = [&](Obj obj, std::vector<T>& expected) {
3,024✔
1937
        ColKey col = obj.get_table()->get_column_key("list");
3,024✔
1938
        auto actual = obj.get_list_values<T>(col);
3,024✔
1939
        REQUIRE(actual == expected);
3,024!
1940
    };
3,024✔
1941

1,128✔
1942
    auto check_dictionary = [&](Obj obj, std::map<std::string, Mixed>& expected) {
2,136✔
1943
        ColKey col = obj.get_table()->get_column_key("dictionary");
2,016✔
1944
        Dictionary dict = obj.get_dictionary(col);
2,016✔
1945
        REQUIRE(dict.size() == expected.size());
2,016!
1946
        for (auto& pair : expected) {
3,612✔
1947
            auto it = dict.find(pair.first);
3,612✔
1948
            REQUIRE(it != dict.end());
3,612!
1949
            REQUIRE((*it).second == pair.second);
3,612!
1950
        }
3,612✔
1951
    };
2,016✔
1952

1,128✔
1953
    auto check_set = [&](Obj obj, std::set<Mixed>& expected) {
2,688✔
1954
        ColKey col = obj.get_table()->get_column_key("set");
2,688✔
1955
        SetBasePtr set = obj.get_setbase_ptr(col);
2,688✔
1956
        REQUIRE(set->size() == expected.size());
2,688!
1957
        for (auto& value : expected) {
3,024✔
1958
            auto ndx = set->find_any(value);
3,024✔
1959
            CAPTURE(value);
3,024✔
1960
            REQUIRE(ndx != realm::not_found);
3,024!
1961
        }
3,024✔
1962
    };
2,688✔
1963

1,128✔
1964
    std::unique_ptr<reset_utils::TestClientReset> test_reset =
2,256✔
1965
        reset_utils::make_fake_local_client_reset(config, config2);
2,256✔
1966

1,128✔
1967
    SECTION("property") {
2,256✔
1968
        REQUIRE(values.size() >= 2);
324!
1969
        REQUIRE(values[0] != values[1]);
324!
1970
        int64_t pk_val = 0;
324✔
1971
        T initial_value = values[0];
324✔
1972

162✔
1973
        auto set_value = [](SharedRealm realm, T value) {
648✔
1974
            auto table = get_table(*realm, "test type");
648✔
1975
            REQUIRE(table);
648!
1976
            REQUIRE(table->size() == 1);
648!
1977
            ColKey col = table->get_column_key("value");
648✔
1978
            table->begin()->set<T>(col, value);
648✔
1979
        };
648✔
1980
        auto check_value = [](Obj obj, T value) {
1,296✔
1981
            ColKey col = obj.get_table()->get_column_key("value");
1,296✔
1982
            REQUIRE(obj.get<T>(col) == value);
1,296!
1983
        };
1,296✔
1984

162✔
1985
        test_reset->setup([&pk_val, &initial_value](SharedRealm realm) {
648✔
1986
            auto table = get_table(*realm, "test type");
648✔
1987
            REQUIRE(table);
648!
1988
            auto obj = table->create_object_with_primary_key(pk_val);
648✔
1989
            ColKey col = table->get_column_key("value");
648✔
1990
            obj.set<T>(col, initial_value);
648✔
1991
        });
648✔
1992

162✔
1993
        auto reset_property = [&](T local_state, T remote_state) {
324✔
1994
            test_reset
324✔
1995
                ->make_local_changes([&](SharedRealm local_realm) {
324✔
1996
                    set_value(local_realm, local_state);
324✔
1997
                })
324✔
1998
                ->make_remote_changes([&](SharedRealm remote_realm) {
324✔
1999
                    set_value(remote_realm, remote_state);
324✔
2000
                })
324✔
2001
                ->on_post_local_changes([&](SharedRealm realm) {
324✔
2002
                    setup_listeners(realm);
324✔
2003
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
324✔
2004
                    CHECK(results.size() == 1);
324!
2005
                    CHECK(results.get<Obj>(0).get<Int>("_id") == pk_val);
324!
2006
                    CHECK(object.is_valid());
324!
2007
                    check_value(results.get<Obj>(0), local_state);
324✔
2008
                    check_value(object.get_obj(), local_state);
324✔
2009
                })
324✔
2010
                ->on_post_reset([&](SharedRealm realm) {
324✔
2011
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
324✔
2012

162✔
2013
                    CHECK(results.size() == 1);
324!
2014
                    CHECK(object.is_valid());
324!
2015
                    T expected_state = (test_mode == ClientResyncMode::DiscardLocal) ? remote_state : local_state;
324✔
2016
                    check_value(results.get<Obj>(0), expected_state);
324✔
2017
                    check_value(object.get_obj(), expected_state);
324✔
2018
                    if (local_state == expected_state) {
324✔
2019
                        REQUIRE_INDICES(results_changes.modifications);
162!
2020
                        REQUIRE_INDICES(object_changes.modifications);
162!
2021
                    }
162✔
2022
                    else {
162✔
2023
                        REQUIRE_INDICES(results_changes.modifications, 0);
162!
2024
                        REQUIRE_INDICES(object_changes.modifications, 0);
162!
2025
                    }
162✔
2026
                    REQUIRE_INDICES(results_changes.insertions);
324!
2027
                    REQUIRE_INDICES(results_changes.deletions);
324!
2028
                    REQUIRE_INDICES(object_changes.insertions);
324!
2029
                    REQUIRE_INDICES(object_changes.deletions);
324!
2030
                })
324✔
2031
                ->run();
324✔
2032
        };
324✔
2033

162✔
2034
        SECTION("modify") {
324✔
2035
            reset_property(values[0], values[1]);
84✔
2036
        }
84✔
2037
        SECTION("modify opposite") {
324✔
2038
            reset_property(values[1], values[0]);
84✔
2039
        }
84✔
2040
        // verify whatever other test values are provided (type bool only has two)
162✔
2041
        for (size_t i = 2; i < values.size(); ++i) {
1,928✔
2042
            SECTION(util::format("modify to value: %1", i)) {
1,604✔
2043
                reset_property(values[0], values[i]);
156✔
2044
            }
156✔
2045
        }
1,604✔
2046
    }
324✔
2047

1,128✔
2048
    SECTION("lists") {
2,256✔
2049
        REQUIRE(values.size() >= 2);
756!
2050
        REQUIRE(values[0] != values[1]);
756!
2051
        int64_t pk_val = 0;
756✔
2052
        // MSVC doesn't seem to automatically capture a templated variable so
378✔
2053
        // the following lambda is explicit about it's captures
378✔
2054
        T initial_list_value = values[0];
756✔
2055
        test_reset->setup([&pk_val, &initial_list_value](SharedRealm realm) {
1,512✔
2056
            auto table = get_table(*realm, "test type");
1,512✔
2057
            REQUIRE(table);
1,512!
2058
            auto obj = table->create_object_with_primary_key(pk_val);
1,512✔
2059
            ColKey col = table->get_column_key("list");
1,512✔
2060
            obj.template set_list_values<T>(col, {initial_list_value});
1,512✔
2061
        });
1,512✔
2062

378✔
2063
        auto reset_list = [&](std::vector<T>&& local_state, std::vector<T>&& remote_state) {
756✔
2064
            test_reset
756✔
2065
                ->make_local_changes([&](SharedRealm local_realm) {
756✔
2066
                    auto table = get_table(*local_realm, "test type");
756✔
2067
                    REQUIRE(table);
756!
2068
                    REQUIRE(table->size() == 1);
756!
2069
                    ColKey col = table->get_column_key("list");
756✔
2070
                    table->begin()->template set_list_values<T>(col, local_state);
756✔
2071
                })
756✔
2072
                ->make_remote_changes([&](SharedRealm remote_realm) {
756✔
2073
                    auto table = get_table(*remote_realm, "test type");
756✔
2074
                    REQUIRE(table);
756!
2075
                    REQUIRE(table->size() == 1);
756!
2076
                    ColKey col = table->get_column_key("list");
756✔
2077
                    table->begin()->template set_list_values<T>(col, remote_state);
756✔
2078
                })
756✔
2079
                ->on_post_local_changes([&](SharedRealm realm) {
756✔
2080
                    setup_listeners(realm);
756✔
2081
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
756✔
2082
                    CHECK(results.size() == 1);
756!
2083
                    CHECK(results.get<Obj>(0).get<Int>("_id") == pk_val);
756!
2084
                    CHECK(object.is_valid());
756!
2085
                    check_list(results.get<Obj>(0), local_state);
756✔
2086
                    check_list(object.get_obj(), local_state);
756✔
2087
                })
756✔
2088
                ->on_post_reset([&](SharedRealm realm) {
756✔
2089
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
756✔
2090

378✔
2091
                    CHECK(results.size() == 1);
756!
2092
                    CHECK(object.is_valid());
756!
2093
                    std::vector<T>& expected_state = remote_state;
756✔
2094
                    if (test_mode == ClientResyncMode::Recover) {
756✔
2095
                        expected_state = local_state;
378✔
2096
                    }
378✔
2097
                    check_list(results.get<Obj>(0), expected_state);
756✔
2098
                    check_list(object.get_obj(), expected_state);
756✔
2099
                    if (local_state == expected_state) {
756✔
2100
                        REQUIRE_INDICES(results_changes.modifications);
462!
2101
                        REQUIRE_INDICES(object_changes.modifications);
462!
2102
                    }
462✔
2103
                    else {
294✔
2104
                        REQUIRE_INDICES(results_changes.modifications, 0);
294!
2105
                        REQUIRE_INDICES(object_changes.modifications, 0);
294!
2106
                    }
294✔
2107
                    REQUIRE_INDICES(results_changes.insertions);
756!
2108
                    REQUIRE_INDICES(results_changes.deletions);
756!
2109
                    REQUIRE_INDICES(object_changes.insertions);
756!
2110
                    REQUIRE_INDICES(object_changes.deletions);
756!
2111
                })
756✔
2112
                ->run();
756✔
2113
        };
756✔
2114

378✔
2115
        SECTION("modify") {
756✔
2116
            reset_list({values[0]}, {values[1]});
84✔
2117
        }
84✔
2118
        SECTION("modify opposite") {
756✔
2119
            reset_list({values[1]}, {values[0]});
84✔
2120
        }
84✔
2121
        SECTION("empty remote") {
756✔
2122
            reset_list({values[1], values[0], values[1]}, {});
84✔
2123
        }
84✔
2124
        SECTION("empty local") {
756✔
2125
            reset_list({}, {values[0], values[1]});
84✔
2126
        }
84✔
2127
        SECTION("empty both") {
756✔
2128
            reset_list({}, {});
84✔
2129
        }
84✔
2130
        SECTION("equal suffix") {
756✔
2131
            reset_list({values[0], values[0], values[1]}, {values[0], values[1]});
84✔
2132
        }
84✔
2133
        SECTION("equal prefix") {
756✔
2134
            reset_list({values[0]}, {values[0], values[1], values[1]});
84✔
2135
        }
84✔
2136
        SECTION("equal lists") {
756✔
2137
            reset_list({values[0]}, {values[0]});
84✔
2138
        }
84✔
2139
        SECTION("equal middle") {
756✔
2140
            reset_list({values[0], values[1], values[0]}, {values[1], values[1], values[1]});
84✔
2141
        }
84✔
2142
    }
756✔
2143

1,128✔
2144
    SECTION("dictionary") {
2,256✔
2145
        REQUIRE(values.size() >= 2);
504!
2146
        REQUIRE(values[0] != values[1]);
504!
2147
        int64_t pk_val = 0;
504✔
2148
        std::string dict_key = "hello";
504✔
2149
        test_reset->setup([&](SharedRealm realm) {
1,008✔
2150
            auto table = get_table(*realm, "test type");
1,008✔
2151
            REQUIRE(table);
1,008!
2152
            auto obj = table->create_object_with_primary_key(pk_val);
1,008✔
2153
            ColKey col = table->get_column_key("dictionary");
1,008✔
2154
            Dictionary dict = obj.get_dictionary(col);
1,008✔
2155
            dict.insert(dict_key, Mixed{values[0]});
1,008✔
2156
        });
1,008✔
2157

252✔
2158
        auto reset_dictionary = [&](std::map<std::string, Mixed>&& local_state,
504✔
2159
                                    std::map<std::string, Mixed>&& remote_state) {
504✔
2160
            test_reset
504✔
2161
                ->make_local_changes([&](SharedRealm local_realm) {
504✔
2162
                    auto table = get_table(*local_realm, "test type");
504✔
2163
                    REQUIRE(table);
504!
2164
                    REQUIRE(table->size() == 1);
504!
2165
                    ColKey col = table->get_column_key("dictionary");
504✔
2166
                    Dictionary dict = table->begin()->get_dictionary(col);
504✔
2167
                    for (auto& pair : local_state) {
756✔
2168
                        dict.insert(pair.first, pair.second);
756✔
2169
                    }
756✔
2170
                    for (auto it = dict.begin(); it != dict.end();) {
1,428✔
2171
                        auto found = std::any_of(local_state.begin(), local_state.end(), [&](auto pair) {
2,016✔
2172
                            return Mixed{pair.first} == (*it).first && Mixed{pair.second} == (*it).second;
2,016✔
2173
                        });
2,016✔
2174
                        if (!found) {
924✔
2175
                            it = dict.erase(it);
168✔
2176
                        }
168✔
2177
                        else {
756✔
2178
                            ++it;
756✔
2179
                        }
756✔
2180
                    }
924✔
2181
                })
504✔
2182
                ->make_remote_changes([&](SharedRealm remote_realm) {
504✔
2183
                    auto table = get_table(*remote_realm, "test type");
504✔
2184
                    REQUIRE(table);
504!
2185
                    REQUIRE(table->size() == 1);
504!
2186
                    ColKey col = table->get_column_key("dictionary");
504✔
2187
                    Dictionary dict = table->begin()->get_dictionary(col);
504✔
2188
                    for (auto& pair : remote_state) {
1,008✔
2189
                        dict.insert(pair.first, pair.second);
1,008✔
2190
                    }
1,008✔
2191
                    for (auto it = dict.begin(); it != dict.end();) {
1,680✔
2192
                        auto found = std::any_of(remote_state.begin(), remote_state.end(), [&](auto pair) {
2,772✔
2193
                            return Mixed{pair.first} == (*it).first && Mixed{pair.second} == (*it).second;
2,772✔
2194
                        });
2,772✔
2195
                        if (!found) {
1,176✔
2196
                            it = dict.erase(it);
168✔
2197
                        }
168✔
2198
                        else {
1,008✔
2199
                            ++it;
1,008✔
2200
                        }
1,008✔
2201
                    }
1,176✔
2202
                })
504✔
2203
                ->on_post_local_changes([&](SharedRealm realm) {
504✔
2204
                    setup_listeners(realm);
504✔
2205
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
504✔
2206
                    CHECK(results.size() == 1);
504!
2207
                    CHECK(results.get<Obj>(0).get<Int>("_id") == pk_val);
504!
2208
                    CHECK(object.is_valid());
504!
2209
                    check_dictionary(results.get<Obj>(0), local_state);
504✔
2210
                    check_dictionary(object.get_obj(), local_state);
504✔
2211
                })
504✔
2212
                ->on_post_reset([&](SharedRealm realm) {
504✔
2213
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
504✔
2214
                    CHECK(results.size() == 1);
504!
2215
                    CHECK(object.is_valid());
504!
2216

252✔
2217
                    auto& expected_state = remote_state;
504✔
2218
                    if (test_mode == ClientResyncMode::Recover) {
504✔
2219
                        for (auto it : local_state) {
378✔
2220
                            expected_state[it.first] = it.second;
378✔
2221
                        }
378✔
2222
                        if (local_state.find(dict_key) == local_state.end()) {
252✔
2223
                            expected_state.erase(dict_key); // explict erasure of initial state occurred
84✔
2224
                        }
84✔
2225
                    }
252✔
2226
                    check_dictionary(results.get<Obj>(0), expected_state);
504✔
2227
                    check_dictionary(object.get_obj(), expected_state);
504✔
2228
                    if (local_state == expected_state) {
504✔
2229
                        REQUIRE_INDICES(results_changes.modifications);
168!
2230
                        REQUIRE_INDICES(object_changes.modifications);
168!
2231
                    }
168✔
2232
                    else {
336✔
2233
                        REQUIRE_INDICES(results_changes.modifications, 0);
336!
2234
                        REQUIRE_INDICES(object_changes.modifications, 0);
336!
2235
                    }
336✔
2236
                    REQUIRE_INDICES(results_changes.insertions);
504!
2237
                    REQUIRE_INDICES(results_changes.deletions);
504!
2238
                    REQUIRE_INDICES(object_changes.insertions);
504!
2239
                    REQUIRE_INDICES(object_changes.deletions);
504!
2240
                })
504✔
2241
                ->run();
504✔
2242
        };
504✔
2243

252✔
2244
        SECTION("modify") {
504✔
2245
            reset_dictionary({{dict_key, Mixed{values[0]}}}, {{dict_key, Mixed{values[1]}}});
84✔
2246
        }
84✔
2247
        SECTION("modify opposite") {
504✔
2248
            reset_dictionary({{dict_key, Mixed{values[1]}}}, {{dict_key, Mixed{values[0]}}});
84✔
2249
        }
84✔
2250
        SECTION("modify complex") {
504✔
2251
            std::map<std::string, Mixed> local;
84✔
2252
            local.emplace(std::make_pair("adam", Mixed(values[0])));
84✔
2253
            local.emplace(std::make_pair("bernie", Mixed(values[0])));
84✔
2254
            local.emplace(std::make_pair("david", Mixed(values[0])));
84✔
2255
            local.emplace(std::make_pair("eric", Mixed(values[0])));
84✔
2256
            local.emplace(std::make_pair("frank", Mixed(values[1])));
84✔
2257
            std::map<std::string, Mixed> remote;
84✔
2258
            remote.emplace(std::make_pair("adam", Mixed(values[0])));
84✔
2259
            remote.emplace(std::make_pair("bernie", Mixed(values[1])));
84✔
2260
            remote.emplace(std::make_pair("carl", Mixed(values[0])));
84✔
2261
            remote.emplace(std::make_pair("david", Mixed(values[1])));
84✔
2262
            remote.emplace(std::make_pair("frank", Mixed(values[0])));
84✔
2263
            reset_dictionary(std::move(local), std::move(remote));
84✔
2264
        }
84✔
2265
        SECTION("empty remote") {
504✔
2266
            reset_dictionary({{dict_key, Mixed{values[1]}}}, {});
84✔
2267
        }
84✔
2268
        SECTION("empty local") {
504✔
2269
            reset_dictionary({}, {{dict_key, Mixed{values[1]}}});
84✔
2270
        }
84✔
2271
        SECTION("extra values on remote") {
504✔
2272
            reset_dictionary({{dict_key, Mixed{values[0]}}}, {{dict_key, Mixed{values[0]}},
84✔
2273
                                                              {"world", Mixed{values[1]}},
84✔
2274
                                                              {"foo", Mixed{values[1]}},
84✔
2275
                                                              {"aaa", Mixed{values[0]}}});
84✔
2276
        }
84✔
2277
    }
504✔
2278

1,128✔
2279
    SECTION("set") {
2,256✔
2280
        int64_t pk_val = 0;
672✔
2281

336✔
2282
        auto reset_set = [&](std::set<Mixed> local_state, std::set<Mixed> remote_state) {
672✔
2283
            test_reset
672✔
2284
                ->make_local_changes([&](SharedRealm local_realm) {
672✔
2285
                    auto table = get_table(*local_realm, "test type");
672✔
2286
                    REQUIRE(table);
672!
2287
                    ColKey col = table->get_column_key("set");
672✔
2288
                    SetBasePtr set = table->begin()->get_setbase_ptr(col);
672✔
2289
                    for (size_t i = set->size(); i > 0; --i) {
1,344✔
2290
                        Mixed si = set->get_any(i - 1);
672✔
2291
                        if (local_state.find(si) == local_state.end()) {
672✔
2292
                            set->erase_any(si);
252✔
2293
                        }
252✔
2294
                    }
672✔
2295
                    for (auto e : local_state) {
756✔
2296
                        set->insert_any(e);
756✔
2297
                    }
756✔
2298
                })
672✔
2299
                ->make_remote_changes([&](SharedRealm remote_realm) {
672✔
2300
                    auto table = get_table(*remote_realm, "test type");
672✔
2301
                    REQUIRE(table);
672!
2302
                    ColKey col = table->get_column_key("set");
672✔
2303
                    SetBasePtr set = table->begin()->get_setbase_ptr(col);
672✔
2304
                    for (size_t i = set->size(); i > 0; --i) {
1,344✔
2305
                        Mixed si = set->get_any(i - 1);
672✔
2306
                        if (remote_state.find(si) == remote_state.end()) {
672✔
2307
                            set->erase_any(si);
336✔
2308
                        }
336✔
2309
                    }
672✔
2310
                    for (auto e : remote_state) {
756✔
2311
                        set->insert_any(e);
756✔
2312
                    }
756✔
2313
                })
672✔
2314
                ->on_post_local_changes([&](SharedRealm realm) {
672✔
2315
                    setup_listeners(realm);
672✔
2316
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
672✔
2317
                    CHECK(results.size() == 1);
672!
2318
                    CHECK(results.get<Obj>(0).get<Int>("_id") == pk_val);
672!
2319
                    CHECK(object.is_valid());
672!
2320
                    check_set(results.get<Obj>(0), local_state);
672✔
2321
                    check_set(object.get_obj(), local_state);
672✔
2322
                })
672✔
2323
                ->on_post_reset([&](SharedRealm realm) {
672✔
2324
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
672✔
2325
                    CHECK(results.size() == 1);
672!
2326
                    CHECK(object.is_valid());
672!
2327
                    std::set<Mixed>& expected = remote_state;
672✔
2328
                    if (test_mode == ClientResyncMode::Recover) {
672✔
2329
                        bool do_erase_initial = remote_state.find(Mixed{values[0]}) == remote_state.end() ||
336✔
2330
                                                local_state.find(Mixed{values[0]}) == local_state.end();
252✔
2331
                        for (auto& e : local_state) {
378✔
2332
                            expected.insert(e);
378✔
2333
                        }
378✔
2334
                        if (do_erase_initial) {
336✔
2335
                            expected.erase(Mixed{values[0]}); // explicit erase of initial element occurred
252✔
2336
                        }
252✔
2337
                    }
336✔
2338
                    check_set(results.get<Obj>(0), expected);
672✔
2339
                    check_set(object.get_obj(), expected);
672✔
2340
                    if (local_state == expected) {
672✔
2341
                        REQUIRE_INDICES(results_changes.modifications);
210!
2342
                        REQUIRE_INDICES(object_changes.modifications);
210!
2343
                    }
210✔
2344
                    else {
462✔
2345
                        REQUIRE_INDICES(results_changes.modifications, 0);
462!
2346
                        REQUIRE_INDICES(object_changes.modifications, 0);
462!
2347
                    }
462✔
2348
                    REQUIRE_INDICES(results_changes.insertions);
672!
2349
                    REQUIRE_INDICES(results_changes.deletions);
672!
2350
                    REQUIRE_INDICES(object_changes.insertions);
672!
2351
                    REQUIRE_INDICES(object_changes.deletions);
672!
2352
                })
672✔
2353
                ->run();
672✔
2354
        };
672✔
2355

336✔
2356
        REQUIRE(values.size() >= 2);
672!
2357
        REQUIRE(values[0] != values[1]);
672!
2358
        test_reset->setup([&](SharedRealm realm) {
1,344✔
2359
            auto table = get_table(*realm, "test type");
1,344✔
2360
            REQUIRE(table);
1,344!
2361
            auto obj = table->create_object_with_primary_key(pk_val);
1,344✔
2362
            ColKey col = table->get_column_key("set");
1,344✔
2363
            SetBasePtr set = obj.get_setbase_ptr(col);
1,344✔
2364
            set->insert_any(Mixed{values[0]});
1,344✔
2365
        });
1,344✔
2366

336✔
2367
        SECTION("modify") {
672✔
2368
            reset_set({Mixed{values[0]}}, {Mixed{values[1]}});
84✔
2369
        }
84✔
2370
        SECTION("modify opposite") {
672✔
2371
            reset_set({Mixed{values[1]}}, {Mixed{values[0]}});
84✔
2372
        }
84✔
2373
        SECTION("empty remote") {
672✔
2374
            reset_set({Mixed{values[1]}, Mixed{values[0]}}, {});
84✔
2375
        }
84✔
2376
        SECTION("empty local") {
672✔
2377
            reset_set({}, {Mixed{values[0]}, Mixed{values[1]}});
84✔
2378
        }
84✔
2379
        SECTION("empty both") {
672✔
2380
            reset_set({}, {});
84✔
2381
        }
84✔
2382
        SECTION("equal suffix") {
672✔
2383
            reset_set({Mixed{values[0]}, Mixed{values[1]}}, {Mixed{values[1]}});
84✔
2384
        }
84✔
2385
        SECTION("equal prefix") {
672✔
2386
            reset_set({Mixed{values[0]}}, {Mixed{values[1]}, Mixed{values[0]}});
84✔
2387
        }
84✔
2388
        SECTION("equal lists") {
672✔
2389
            reset_set({Mixed{values[0]}, Mixed{values[1]}}, {Mixed{values[0]}, Mixed{values[1]}});
84✔
2390
        }
84✔
2391
    }
672✔
2392
}
2,256✔
2393

2394
namespace test_instructions {
2395

2396
struct Add {
2397
    Add(util::Optional<int64_t> key)
2398
        : pk(key)
2399
    {
956✔
2400
    }
956✔
2401
    util::Optional<int64_t> pk;
2402
};
2403

2404
struct Remove {
2405
    Remove(util::Optional<int64_t> key)
2406
        : pk(key)
2407
    {
752✔
2408
    }
752✔
2409
    util::Optional<int64_t> pk;
2410
};
2411

2412
struct Clear {};
2413

2414
struct RemoveObject {
2415
    RemoveObject(std::string_view name, util::Optional<int64_t> key)
2416
        : pk(key)
2417
        , class_name(name)
2418
    {
180✔
2419
    }
180✔
2420
    util::Optional<int64_t> pk;
2421
    std::string_view class_name;
2422
};
2423

2424
struct CreateObject {
2425
    CreateObject(std::string_view name, util::Optional<int64_t> key)
2426
        : pk(key)
2427
        , class_name(name)
2428
    {
12✔
2429
    }
12✔
2430
    util::Optional<int64_t> pk;
2431
    std::string_view class_name;
2432
};
2433

2434
struct Move {
2435
    Move(size_t from_ndx, size_t to_ndx)
2436
        : from(from_ndx)
2437
        , to(to_ndx)
2438
    {
64✔
2439
    }
64✔
2440
    size_t from;
2441
    size_t to;
2442
};
2443

2444
struct CollectionOperation {
2445
    CollectionOperation(Add op)
2446
        : m_op(op)
2447
    {
956✔
2448
    }
956✔
2449
    CollectionOperation(Remove op)
2450
        : m_op(op)
2451
    {
752✔
2452
    }
752✔
2453
    CollectionOperation(RemoveObject op)
2454
        : m_op(op)
2455
    {
180✔
2456
    }
180✔
2457
    CollectionOperation(CreateObject op)
2458
        : m_op(op)
2459
    {
12✔
2460
    }
12✔
2461
    CollectionOperation(Clear op)
2462
        : m_op(op)
2463
    {
204✔
2464
    }
204✔
2465
    CollectionOperation(Move op)
2466
        : m_op(op)
2467
    {
64✔
2468
    }
64✔
2469
    void apply(collection_fixtures::LinkedCollectionBase* collection, Obj src_obj, TableRef dst_table)
2470
    {
2,168✔
2471
        mpark::visit(
2,168✔
2472
            util::overload{
2,168✔
2473
                [&](Add add_link) {
1,562✔
2474
                    Mixed pk_to_add = add_link.pk ? Mixed{add_link.pk} : Mixed{};
944✔
2475
                    ObjKey dst_key = dst_table->find_primary_key(pk_to_add);
956✔
2476
                    REALM_ASSERT(dst_key);
956✔
2477
                    collection->add_link(src_obj, ObjLink{dst_table->get_key(), dst_key});
956✔
2478
                },
956✔
2479
                [&](Remove remove_link) {
1,460✔
2480
                    Mixed pk_to_remove = remove_link.pk ? Mixed{remove_link.pk} : Mixed{};
752✔
2481
                    ObjKey dst_key = dst_table->find_primary_key(pk_to_remove);
752✔
2482
                    REALM_ASSERT(dst_key);
752✔
2483
                    bool did_remove = collection->remove_link(src_obj, ObjLink{dst_table->get_key(), dst_key});
752✔
2484
                    REALM_ASSERT(did_remove);
752✔
2485
                },
752✔
2486
                [&](RemoveObject remove_object) {
1,174✔
2487
                    Group* group = dst_table->get_parent_group();
180✔
2488
                    Group::TableNameBuffer buffer;
180✔
2489
                    TableRef table =
180✔
2490
                        group->get_table(Group::class_name_to_table_name(remove_object.class_name, buffer));
180✔
2491
                    REALM_ASSERT(table);
180✔
2492
                    ObjKey dst_key = table->find_primary_key(Mixed{remove_object.pk});
180✔
2493
                    REALM_ASSERT(dst_key);
180✔
2494
                    table->remove_object(dst_key);
180✔
2495
                },
180✔
2496
                [&](CreateObject create_object) {
1,090✔
2497
                    Group* group = dst_table->get_parent_group();
12✔
2498
                    Group::TableNameBuffer buffer;
12✔
2499
                    TableRef table =
12✔
2500
                        group->get_table(Group::class_name_to_table_name(create_object.class_name, buffer));
12✔
2501
                    REALM_ASSERT(table);
12✔
2502
                    table->create_object_with_primary_key(Mixed{create_object.pk});
12✔
2503
                },
12✔
2504
                [&](Clear) {
1,186✔
2505
                    collection->clear_collection(src_obj);
204✔
2506
                },
204✔
2507
                [&](Move move) {
1,116✔
2508
                    collection->move(src_obj, move.from, move.to);
64✔
2509
                }},
64✔
2510
            m_op);
2,168✔
2511
    }
2,168✔
2512

2513
private:
2514
    mpark::variant<Add, Remove, Clear, RemoveObject, CreateObject, Move> m_op;
2515
};
2516

2517
} // namespace test_instructions
2518

2519
TEMPLATE_TEST_CASE("client reset collections of links", "[sync][pbs][client reset][links][collections]",
2520
                   cf::ListOfObjects, cf::ListOfMixedLinks, cf::SetOfObjects, cf::SetOfMixedLinks,
2521
                   cf::DictionaryOfObjects, cf::DictionaryOfMixedLinks)
2522
{
800✔
2523
    if (!util::EventLoop::has_implementation())
800✔
2524
        return;
×
2525

400✔
2526
    using namespace test_instructions;
800✔
2527
    const std::string valid_pk_name = "_id";
800✔
2528
    const auto partition = random_string(100);
800✔
2529
    const std::string collection_prop_name = "collection";
800✔
2530
    TestType test_type(collection_prop_name, "dest");
800✔
2531
    constexpr bool test_type_is_array = realm::is_any_v<TestType, cf::ListOfObjects, cf::ListOfMixedLinks>;
800✔
2532
    Schema schema = {
800✔
2533
        {"source",
800✔
2534
         {{valid_pk_name, PropertyType::Int | PropertyType::Nullable, true},
800✔
2535
          {"realm_id", PropertyType::String | PropertyType::Nullable},
800✔
2536
          test_type.property()}},
800✔
2537
        {"dest",
800✔
2538
         {
800✔
2539
             {valid_pk_name, PropertyType::Int | PropertyType::Nullable, true},
800✔
2540
             {"realm_id", PropertyType::String | PropertyType::Nullable},
800✔
2541
         }},
800✔
2542
        {"object",
800✔
2543
         {
800✔
2544
             {valid_pk_name, PropertyType::ObjectId, Property::IsPrimary{true}},
800✔
2545
             {"value", PropertyType::Int},
800✔
2546
             {"realm_id", PropertyType::String | PropertyType::Nullable},
800✔
2547
         }},
800✔
2548
    };
800✔
2549

400✔
2550
    TestSyncManager init_sync_manager;
800✔
2551
    SyncTestFile config(init_sync_manager.app(), "default");
800✔
2552
    config.cache = false;
800✔
2553
    config.automatic_change_notifications = false;
800✔
2554
    config.schema = schema;
800✔
2555
    ClientResyncMode test_mode = GENERATE(ClientResyncMode::DiscardLocal, ClientResyncMode::Recover);
800✔
2556
    CAPTURE(test_mode);
800✔
2557
    config.sync_config->client_resync_mode = test_mode;
800✔
2558

400✔
2559
    SyncTestFile config2(init_sync_manager.app(), "default");
800✔
2560
    config2.schema = schema;
800✔
2561

400✔
2562
    std::unique_ptr<reset_utils::TestClientReset> test_reset =
800✔
2563
        reset_utils::make_fake_local_client_reset(config, config2);
800✔
2564

400✔
2565
    CppContext c;
800✔
2566
    auto create_one_source_object = [&](realm::SharedRealm r, int64_t val, std::vector<ObjLink> links = {}) {
1,600✔
2567
        auto object = Object::create(
1,600✔
2568
            c, r, "source",
1,600✔
2569
            std::any(realm::AnyDict{{valid_pk_name, std::any(val)}, {"realm_id", std::string(partition)}}),
1,600✔
2570
            CreatePolicy::ForceCreate);
1,600✔
2571

800✔
2572
        for (auto link : links) {
4,656✔
2573
            test_type.add_link(object.get_obj(), link);
4,656✔
2574
        }
4,656✔
2575
    };
1,600✔
2576

400✔
2577
    auto create_one_dest_object = [&](realm::SharedRealm r, util::Optional<int64_t> val) -> ObjLink {
7,808✔
2578
        std::any v;
7,808✔
2579
        if (val) {
7,808✔
2580
            v = std::any(*val);
7,760✔
2581
        }
7,760✔
2582
        auto obj = Object::create(
7,808✔
2583
            c, r, "dest",
7,808✔
2584
            std::any(realm::AnyDict{{valid_pk_name, std::move(v)}, {"realm_id", std::string(partition)}}),
7,808✔
2585
            CreatePolicy::ForceCreate);
7,808✔
2586
        return ObjLink{obj.get_obj().get_table()->get_key(), obj.get_obj().get_key()};
7,808✔
2587
    };
7,808✔
2588

400✔
2589
    auto require_links_to_match_ids = [&](std::vector<Obj>& links, std::vector<util::Optional<int64_t>>& expected,
800✔
2590
                                          bool sorted) {
770✔
2591
        std::vector<util::Optional<int64_t>> actual;
740✔
2592
        for (auto obj : links) {
1,800✔
2593
            if (obj.is_null(valid_pk_name)) {
1,800✔
2594
                actual.push_back(util::none);
12✔
2595
            }
12✔
2596
            else {
1,788✔
2597
                actual.push_back(obj.get<Int>(valid_pk_name));
1,788✔
2598
            }
1,788✔
2599
        }
1,800✔
2600
        if (sorted) {
740✔
2601
            std::sort(actual.begin(), actual.end());
456✔
2602
        }
456✔
2603
        REQUIRE(actual == expected);
740!
2604
    };
740✔
2605

400✔
2606
    constexpr int64_t source_pk = 0;
800✔
2607
    constexpr util::Optional<int64_t> dest_pk_1 = 1;
800✔
2608
    constexpr util::Optional<int64_t> dest_pk_2 = 2;
800✔
2609
    constexpr util::Optional<int64_t> dest_pk_3 = 3;
800✔
2610
    constexpr util::Optional<int64_t> dest_pk_4 = 4;
800✔
2611
    constexpr util::Optional<int64_t> dest_pk_5 = 5;
800✔
2612

400✔
2613
    Results results;
800✔
2614
    Object object;
800✔
2615
    CollectionChangeSet object_changes, results_changes;
800✔
2616
    NotificationToken object_token, results_token;
800✔
2617
    auto setup_listeners = [&](SharedRealm realm) {
770✔
2618
        TableRef source_table = get_table(*realm, "source");
740✔
2619
        ColKey id_col = source_table->get_column_key("_id");
740✔
2620
        results = Results(realm, source_table->where().equal(id_col, source_pk));
740✔
2621
        if (auto obj = results.first()) {
740✔
2622
            object = Object(realm, *obj);
740✔
2623
            object_token = object.add_notification_callback([&](CollectionChangeSet changes) {
1,232✔
2624
                object_changes = std::move(changes);
1,232✔
2625
            });
1,232✔
2626
        }
740✔
2627
        results_token = results.add_notification_callback([&](CollectionChangeSet changes) {
1,232✔
2628
            results_changes = std::move(changes);
1,232✔
2629
        });
1,232✔
2630
    };
740✔
2631

400✔
2632
    auto get_source_object = [&](SharedRealm realm) -> Obj {
3,648✔
2633
        TableRef src_table = get_table(*realm, "source");
3,648✔
2634
        return src_table->try_get_object(src_table->find_primary_key(Mixed{source_pk}));
3,648✔
2635
    };
3,648✔
2636
    auto apply_instructions = [&](SharedRealm realm, std::vector<CollectionOperation>& instructions) {
1,600✔
2637
        TableRef dst_table = get_table(*realm, "dest");
1,600✔
2638
        for (auto& instruction : instructions) {
2,168✔
2639
            Obj src_obj = get_source_object(realm);
2,168✔
2640
            instruction.apply(&test_type, src_obj, dst_table);
2,168✔
2641
        }
2,168✔
2642
    };
1,600✔
2643

400✔
2644
    auto reset_collection =
800✔
2645
        [&](std::vector<CollectionOperation>&& local_ops, std::vector<CollectionOperation>&& remote_ops,
800✔
2646
            std::vector<util::Optional<int64_t>>&& expected_recovered_state, size_t num_expected_nulls = 0) {
770✔
2647
            std::vector<util::Optional<int64_t>> remote_pks;
740✔
2648
            std::vector<util::Optional<int64_t>> local_pks;
740✔
2649
            test_reset
740✔
2650
                ->make_local_changes([&](SharedRealm local_realm) {
740✔
2651
                    apply_instructions(local_realm, local_ops);
740✔
2652
                    Obj source_obj = get_source_object(local_realm);
740✔
2653
                    if (source_obj) {
740✔
2654
                        auto local_links = test_type.get_links(source_obj);
740✔
2655
                        std::transform(local_links.begin(), local_links.end(), std::back_inserter(local_pks),
740✔
2656
                                       [](auto obj) -> util::Optional<int64_t> {
1,900✔
2657
                                           Mixed pk = obj.get_primary_key();
1,900✔
2658
                                           return pk.is_null() ? util::none : util::make_optional(pk.get_int());
1,888✔
2659
                                       });
1,900✔
2660
                    }
740✔
2661
                })
740✔
2662
                ->make_remote_changes([&](SharedRealm remote_realm) {
740✔
2663
                    apply_instructions(remote_realm, remote_ops);
740✔
2664
                    Obj source_obj = get_source_object(remote_realm);
740✔
2665
                    if (source_obj) {
740✔
2666
                        auto remote_links = test_type.get_links(source_obj);
728✔
2667
                        std::transform(remote_links.begin(), remote_links.end(), std::back_inserter(remote_pks),
728✔
2668
                                       [](auto obj) -> util::Optional<int64_t> {
1,856✔
2669
                                           Mixed pk = obj.get_primary_key();
1,856✔
2670
                                           return pk.is_null() ? util::none : util::make_optional(pk.get_int());
1,856✔
2671
                                       });
1,856✔
2672
                    }
728✔
2673
                })
740✔
2674
                ->on_post_local_changes([&](SharedRealm realm) {
740✔
2675
                    setup_listeners(realm);
740✔
2676
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
740✔
2677
                    CHECK(results.size() == 1);
740!
2678
                })
740✔
2679
                ->on_post_reset([&](SharedRealm realm) {
740✔
2680
                    object_changes = {};
740✔
2681
                    results_changes = {};
740✔
2682
                    REQUIRE_NOTHROW(advance_and_notify(*realm));
740✔
2683
                    CHECK(results.size() == 1);
740!
2684
                    CHECK(object.is_valid());
740!
2685
                    auto linked_objects = test_type.get_links(results.get(0));
740✔
2686
                    std::vector<util::Optional<int64_t>>& expected_links = remote_pks;
740✔
2687
                    if (test_mode == ClientResyncMode::Recover) {
740✔
2688
                        expected_links = expected_recovered_state;
376✔
2689
                        size_t expected_size = expected_links.size();
376✔
2690
                        if (!test_type.will_erase_removed_object_links()) {
376✔
2691
                            // dictionary size will remain the same because the key is preserved with a null value
58✔
2692
                            expected_size += num_expected_nulls;
116✔
2693
                        }
116✔
2694
                        CHECK(test_type.size_of_collection(results.get(0)) == expected_size);
376!
2695
                    }
376✔
2696
                    if (!test_type_is_array) {
740✔
2697
                        // order should not matter except for lists
228✔
2698
                        std::sort(local_pks.begin(), local_pks.end());
456✔
2699
                        std::sort(expected_links.begin(), expected_links.end());
456✔
2700
                    }
456✔
2701
                    require_links_to_match_ids(linked_objects, expected_links, !test_type_is_array);
740✔
2702
                    if (local_pks == expected_links) {
740✔
2703
                        REQUIRE_INDICES(results_changes.modifications);
248!
2704
                        REQUIRE_INDICES(object_changes.modifications);
248!
2705
                    }
248✔
2706
                    else {
492✔
2707
                        REQUIRE_INDICES(results_changes.modifications, 0);
492!
2708
                        REQUIRE_INDICES(object_changes.modifications, 0);
492!
2709
                    }
492✔
2710
                    REQUIRE_INDICES(results_changes.insertions);
740!
2711
                    REQUIRE_INDICES(results_changes.deletions);
740!
2712
                    REQUIRE_INDICES(object_changes.insertions);
740!
2713
                    REQUIRE_INDICES(object_changes.deletions);
740!
2714
                })
740✔
2715
                ->run();
740✔
2716
        };
740✔
2717

400✔
2718
    auto reset_collection_removing_source_object = [&](std::vector<CollectionOperation>&& local_ops,
800✔
2719
                                                       std::vector<CollectionOperation>&& remote_ops) {
430✔
2720
        test_reset
60✔
2721
            ->make_local_changes([&](SharedRealm local_realm) {
60✔
2722
                apply_instructions(local_realm, local_ops);
60✔
2723
            })
60✔
2724
            ->make_remote_changes([&](SharedRealm remote_realm) {
60✔
2725
                apply_instructions(remote_realm, remote_ops);
60✔
2726
            })
60✔
2727
            ->on_post_reset([&](SharedRealm realm) {
60✔
2728
                REQUIRE_NOTHROW(advance_and_notify(*realm));
60✔
2729
                TableRef table = realm->read_group().get_table("class_source");
60✔
2730
                REQUIRE(!table->find_primary_key(Mixed{source_pk}));
60!
2731
            })
60✔
2732
            ->run();
60✔
2733
    };
60✔
2734

400✔
2735
    test_reset->setup([&](SharedRealm realm) {
1,552✔
2736
        test_type.reset_test_state();
1,552✔
2737
        // add a container collection with three valid links
776✔
2738
        ObjLink dest1 = create_one_dest_object(realm, dest_pk_1);
1,552✔
2739
        ObjLink dest2 = create_one_dest_object(realm, dest_pk_2);
1,552✔
2740
        ObjLink dest3 = create_one_dest_object(realm, dest_pk_3);
1,552✔
2741
        create_one_dest_object(realm, dest_pk_4);
1,552✔
2742
        create_one_dest_object(realm, dest_pk_5);
1,552✔
2743
        create_one_source_object(realm, source_pk, {dest1, dest2, dest3});
1,552✔
2744
    });
1,552✔
2745

400✔
2746
    SECTION("no changes") {
800✔
2747
        reset_collection({}, {}, {dest_pk_1, dest_pk_2, dest_pk_3});
24✔
2748
    }
24✔
2749
    SECTION("remote removes all") {
800✔
2750
        reset_collection({}, {{Remove{dest_pk_3}}, {Remove{dest_pk_2}}, {Remove{dest_pk_1}}}, {});
24✔
2751
    }
24✔
2752
    SECTION("local removes all") { // local client state wins
800✔
2753
        reset_collection({{Remove{dest_pk_3}}, {Remove{dest_pk_2}}, {Remove{dest_pk_1}}}, {}, {});
24✔
2754
    }
24✔
2755
    SECTION("both remove all links") { // local client state wins
800✔
2756
        reset_collection({{Remove{dest_pk_3}}, {Remove{dest_pk_2}}, {Remove{dest_pk_1}}},
24✔
2757
                         {{Remove{dest_pk_3}}, {Remove{dest_pk_2}}, {Remove{dest_pk_1}}}, {});
24✔
2758
    }
24✔
2759
    SECTION("local removes first link") { // local client state wins
800✔
2760
        reset_collection({{Remove{dest_pk_1}}}, {}, {dest_pk_2, dest_pk_3});
24✔
2761
    }
24✔
2762
    SECTION("local removes middle link") { // local client state wins
800✔
2763
        reset_collection({{Remove{dest_pk_2}}}, {}, {dest_pk_1, dest_pk_3});
24✔
2764
    }
24✔
2765
    SECTION("local removes last link") { // local client state wins
800✔
2766
        reset_collection({{Remove{dest_pk_3}}}, {}, {dest_pk_1, dest_pk_2});
24✔
2767
    }
24✔
2768
    SECTION("remote removes first link") {
800✔
2769
        reset_collection({}, {{Remove{dest_pk_1}}}, {dest_pk_2, dest_pk_3});
24✔
2770
    }
24✔
2771
    SECTION("remote removes middle link") {
800✔
2772
        reset_collection({}, {{Remove{dest_pk_2}}}, {dest_pk_1, dest_pk_3});
24✔
2773
    }
24✔
2774
    SECTION("remote removes last link") {
800✔
2775
        reset_collection({}, {{Remove{dest_pk_3}}}, {dest_pk_1, dest_pk_2});
24✔
2776
    }
24✔
2777
    SECTION("local adds a link with a null pk value") {
800✔
2778
        test_reset->setup([&](SharedRealm realm) {
48✔
2779
            test_type.reset_test_state();
48✔
2780
            create_one_dest_object(realm, util::none);
48✔
2781
            create_one_source_object(realm, source_pk, {});
48✔
2782
        });
48✔
2783
        reset_collection({Add{util::none}}, {}, {util::none});
24✔
2784
    }
24✔
2785
    SECTION("removal of different links") {
800✔
2786
        std::vector<util::Optional<int64_t>> expected = {dest_pk_2};
24✔
2787
        if constexpr (test_type_is_array) {
24✔
2788
            expected = {dest_pk_2, dest_pk_3}; // local client state wins
8✔
2789
        }
8✔
2790
        reset_collection({Remove{dest_pk_1}}, {Remove{dest_pk_3}}, std::move(expected));
24✔
2791
    }
24✔
2792
    SECTION("local addition") {
800✔
2793
        reset_collection({Add{dest_pk_4}}, {}, {dest_pk_1, dest_pk_2, dest_pk_3, dest_pk_4});
24✔
2794
    }
24✔
2795
    SECTION("remote addition") {
800✔
2796
        reset_collection({}, {Add{dest_pk_4}}, {dest_pk_1, dest_pk_2, dest_pk_3, dest_pk_4});
24✔
2797
    }
24✔
2798
    SECTION("both addition of different items") {
800✔
2799
        reset_collection({Add{dest_pk_4}}, {Add{dest_pk_5}, Remove{dest_pk_5}, Add{dest_pk_5}},
24✔
2800
                         {dest_pk_1, dest_pk_2, dest_pk_3, dest_pk_4, dest_pk_5});
24✔
2801
    }
24✔
2802
    SECTION("both addition of same items") {
800✔
2803
        std::vector<util::Optional<int64_t>> expected = {dest_pk_1, dest_pk_2, dest_pk_3, dest_pk_4};
24✔
2804
        if constexpr (test_type_is_array) {
24✔
2805
            expected = {dest_pk_1, dest_pk_2, dest_pk_3, dest_pk_4, dest_pk_4};
8✔
2806
        }
8✔
2807
        // dictionary has added the new link to the same key on both sides
12✔
2808
        reset_collection({Add{dest_pk_4}}, {Add{dest_pk_4}}, std::move(expected));
24✔
2809
    }
24✔
2810
    SECTION("local add/delete, remote add/delete/add different") {
800✔
2811
        reset_collection({Add{dest_pk_4}, Remove{dest_pk_4}}, {Add{dest_pk_5}, Remove{dest_pk_5}, Add{dest_pk_5}},
24✔
2812
                         {dest_pk_1, dest_pk_2, dest_pk_3, dest_pk_5});
24✔
2813
    }
24✔
2814
    SECTION("remote add/delete, local add") {
800✔
2815
        reset_collection({Add{dest_pk_4}}, {Add{dest_pk_5}, Remove{dest_pk_5}},
24✔
2816
                         {dest_pk_1, dest_pk_2, dest_pk_3, dest_pk_4});
24✔
2817
    }
24✔
2818
    SECTION("local remove, remote add") {
800✔
2819
        std::vector<util::Optional<int64_t>> expected = {dest_pk_1, dest_pk_3, dest_pk_4, dest_pk_5};
24✔
2820
        if constexpr (test_type_is_array) {
24✔
2821
            expected = {dest_pk_1, dest_pk_3}; // local client state wins
8✔
2822
        }
8✔
2823
        reset_collection({Remove{dest_pk_2}}, {Add{dest_pk_4}, Add{dest_pk_5}}, std::move(expected));
24✔
2824
    }
24✔
2825
    SECTION("local adds link to remotely deleted object") {
800✔
2826
        reset_collection({Add{dest_pk_4}}, {RemoveObject{"dest", dest_pk_4}}, {dest_pk_1, dest_pk_2, dest_pk_3}, 1);
24✔
2827
    }
24✔
2828
    SECTION("local clear") {
800✔
2829
        reset_collection({Clear{}}, {}, {});
24✔
2830
    }
24✔
2831
    SECTION("remote clear") {
800✔
2832
        reset_collection({}, {Clear{}}, {});
24✔
2833
    }
24✔
2834
    SECTION("both clear") {
800✔
2835
        reset_collection({Clear{}}, {Clear{}}, {});
24✔
2836
    }
24✔
2837
    SECTION("both clear and add") {
800✔
2838
        reset_collection({Clear{}, Add{dest_pk_1}}, {Clear{}, Add{dest_pk_2}}, {dest_pk_1});
24✔
2839
    }
24✔
2840
    SECTION("both clear and add/remove/add/add") {
800✔
2841
        reset_collection({Clear{}, Add{dest_pk_1}, Remove{dest_pk_1}, Add{dest_pk_2}, Add{dest_pk_3}},
24✔
2842
                         {Clear{}, Add{dest_pk_1}, Remove{dest_pk_1}, Add{dest_pk_2}, Add{dest_pk_3}},
24✔
2843
                         {dest_pk_2, dest_pk_3});
24✔
2844
    }
24✔
2845
    SECTION("local add to remotely deleted object") {
800✔
2846
        reset_collection({Add{dest_pk_4}}, {Add{dest_pk_4}, RemoveObject{"dest", dest_pk_4}},
24✔
2847
                         {dest_pk_1, dest_pk_2, dest_pk_3}, 1);
24✔
2848
    }
24✔
2849
    SECTION("remote adds link to locally deleted object with link") {
800✔
2850
        reset_collection({Add{dest_pk_4}, RemoveObject{"dest", dest_pk_4}}, {Add{dest_pk_4}, Add{dest_pk_5}},
24✔
2851
                         {dest_pk_1, dest_pk_2, dest_pk_3, dest_pk_5}, 1);
24✔
2852
    }
24✔
2853
    SECTION("remote adds link to locally deleted object without link") {
800✔
2854
        reset_collection({RemoveObject{"dest", dest_pk_4}}, {Add{dest_pk_4}, Add{dest_pk_5}},
24✔
2855
                         {dest_pk_1, dest_pk_2, dest_pk_3, dest_pk_5}, 1);
24✔
2856
    }
24✔
2857
    if (test_mode == ClientResyncMode::Recover) {
800✔
2858
        SECTION("local adds a list item and removes source object, remote modifies list") {
424✔
2859
            reset_collection_removing_source_object({Add{dest_pk_4}, RemoveObject{"source", source_pk}},
12✔
2860
                                                    {Add{dest_pk_5}});
12✔
2861
        }
12✔
2862
        SECTION("local erases list item then removes source object, remote modifies list") {
424✔
2863
            reset_collection_removing_source_object({Remove{dest_pk_1}, RemoveObject{"source", source_pk}},
12✔
2864
                                                    {Add{dest_pk_5}});
12✔
2865
        }
12✔
2866
        SECTION("remote removes source object, recover local modifications") {
424✔
2867
            reset_collection_removing_source_object({Add{dest_pk_4}, Clear{}}, {RemoveObject{"source", source_pk}});
12✔
2868
        }
12✔
2869
        SECTION("remote removes source object, local attempts to ccpy over list state") {
424✔
2870
            reset_collection_removing_source_object({Remove{dest_pk_1}}, {RemoveObject{"source", source_pk}});
12✔
2871
        }
12✔
2872
        SECTION("remote removes source object, local adds it back and modifies it") {
424✔
2873
            reset_collection({Add{dest_pk_4}, RemoveObject{"source", source_pk}, CreateObject{"source", source_pk},
12✔
2874
                              Add{dest_pk_1}},
12✔
2875
                             {RemoveObject{"source", source_pk}}, {dest_pk_1});
12✔
2876
        }
12✔
2877
    }
424✔
2878
    else if (test_mode == ClientResyncMode::DiscardLocal) {
376✔
2879
        SECTION("remote removes source object") {
376✔
2880
            reset_collection_removing_source_object({Add{dest_pk_4}}, {RemoveObject{"source", source_pk}});
12✔
2881
        }
12✔
2882
    }
376✔
2883
    if constexpr (test_type_is_array) {
800✔
2884
        SECTION("local moves on non-added elements causes a diff which overrides server changes") {
304✔
2885
            reset_collection({Move{0, 1}, Add{dest_pk_5}}, {Add{dest_pk_4}},
8✔
2886
                             {dest_pk_2, dest_pk_1, dest_pk_3, dest_pk_5});
8✔
2887
        }
8✔
2888
        SECTION("local moves on added elements can be merged with remote moves") {
304✔
2889
            reset_collection({Add{dest_pk_4}, Add{dest_pk_5}, Move{3, 4}}, {Move{0, 1}},
8✔
2890
                             {dest_pk_2, dest_pk_1, dest_pk_3, dest_pk_5, dest_pk_4});
8✔
2891
        }
8✔
2892
        SECTION("local moves on added elements can be merged with remote additions") {
304✔
2893
            reset_collection({Add{dest_pk_4}, Add{dest_pk_5}, Move{3, 4}}, {Add{dest_pk_1}, Add{dest_pk_2}},
8✔
2894
                             {dest_pk_1, dest_pk_2, dest_pk_3, dest_pk_5, dest_pk_4, dest_pk_1, dest_pk_2});
8✔
2895
        }
8✔
2896
        SECTION("local moves on added elements can be merged with remote deletions") {
304✔
2897
            reset_collection({Add{dest_pk_4}, Add{dest_pk_5}, Move{3, 4}}, {Remove{dest_pk_1}, Remove{dest_pk_2}},
8✔
2898
                             {dest_pk_3, dest_pk_5, dest_pk_4});
8✔
2899
        }
8✔
2900
        SECTION("local move (down) on added elements can be merged with remote deletions") {
304✔
2901
            reset_collection({Add{dest_pk_4}, Add{dest_pk_5}, Move{4, 3}}, {Remove{dest_pk_1}, Remove{dest_pk_2}},
8✔
2902
                             {dest_pk_3, dest_pk_5, dest_pk_4});
8✔
2903
        }
8✔
2904
        SECTION("local move with delete on added elements can be merged with remote deletions") {
304✔
2905
            reset_collection({Add{dest_pk_4}, Add{dest_pk_5}, Move{3, 4}, Remove{dest_pk_5}},
8✔
2906
                             {Remove{dest_pk_1}, Remove{dest_pk_2}}, {dest_pk_3, dest_pk_4});
8✔
2907
        }
8✔
2908
        SECTION("local move (down) with delete on added elements can be merged with remote deletions") {
304✔
2909
            reset_collection({Add{dest_pk_4}, Add{dest_pk_5}, Move{4, 3}, Remove{dest_pk_5}},
8✔
2910
                             {Remove{dest_pk_1}, Remove{dest_pk_2}}, {dest_pk_3, dest_pk_4});
8✔
2911
        }
8✔
2912
    }
304✔
2913
}
800✔
2914

2915
template <typename T>
2916
void set_embedded_list(const std::vector<T>& array_values, LnkLst& list)
2917
{
3,552✔
2918
    for (size_t i = 0; i < array_values.size(); ++i) {
4,896✔
2919
        Obj link;
1,344✔
2920
        if (i >= list.size()) {
1,344✔
2921
            link = list.create_and_insert_linked_object(list.size());
1,052✔
2922
        }
1,052✔
2923
        else {
292✔
2924
            link = list.get_object(i);
292✔
2925
        }
292✔
2926
        array_values[i].assign_to(link);
1,344✔
2927
    }
1,344✔
2928
    if (list.size() > array_values.size()) {
3,552✔
2929
        if (array_values.size() == 0) {
12!
2930
            list.clear();
8✔
2931
        }
8✔
2932
        else {
4✔
2933
            list.remove(array_values.size(), list.size());
4✔
2934
        }
4✔
2935
    }
12✔
2936
}
3,552✔
2937

2938
template <typename T>
2939
void combine_array_values(std::vector<T>& from, const std::vector<T>& to)
2940
{
92✔
2941
    auto it = from.begin();
92✔
2942
    for (auto val : to) {
288✔
2943
        it = ++from.insert(it, val);
288✔
2944
    }
288✔
2945
}
92✔
2946

2947
TEST_CASE("client reset with embedded object", "[sync][pbs][client reset][embedded objects]") {
168✔
2948
    if (!util::EventLoop::has_implementation())
168✔
2949
        return;
×
2950

84✔
2951
    TestSyncManager init_sync_manager;
168✔
2952
    SyncTestFile config(init_sync_manager.app(), "default");
168✔
2953
    config.cache = false;
168✔
2954
    config.automatic_change_notifications = false;
168✔
2955
    ClientResyncMode test_mode = GENERATE(ClientResyncMode::DiscardLocal, ClientResyncMode::Recover);
168✔
2956
    CAPTURE(test_mode);
168✔
2957
    config.sync_config->client_resync_mode = test_mode;
168✔
2958

84✔
2959
    ObjectSchema shared_class = {"object",
168✔
2960
                                 {
168✔
2961
                                     {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
168✔
2962
                                     {"value", PropertyType::Int},
168✔
2963
                                 }};
168✔
2964

84✔
2965
    config.schema = Schema{
168✔
2966
        shared_class,
168✔
2967
        {"TopLevel",
168✔
2968
         {
168✔
2969
             {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
168✔
2970
             {"array_of_objs", PropertyType::Object | PropertyType::Array, "EmbeddedObject"},
168✔
2971
             {"embedded_obj", PropertyType::Object | PropertyType::Nullable, "EmbeddedObject"},
168✔
2972
             {"embedded_dict", PropertyType::Object | PropertyType::Dictionary | PropertyType::Nullable,
168✔
2973
              "EmbeddedObject"},
168✔
2974
             {"any_mixed", PropertyType::Mixed | PropertyType::Nullable},
168✔
2975
         }},
168✔
2976
        {"EmbeddedObject",
168✔
2977
         ObjectSchema::ObjectType::Embedded,
168✔
2978
         {
168✔
2979
             {"array", PropertyType::Int | PropertyType::Array},
168✔
2980
             {"name", PropertyType::String | PropertyType::Nullable},
168✔
2981
             {"link_to_embedded_object2", PropertyType::Object | PropertyType::Nullable, "EmbeddedObject2"},
168✔
2982
             {"array_of_seconds", PropertyType::Object | PropertyType::Array, "EmbeddedObject2"},
168✔
2983
             {"int_value", PropertyType::Int},
168✔
2984
         }},
168✔
2985
        {"EmbeddedObject2",
168✔
2986
         ObjectSchema::ObjectType::Embedded,
168✔
2987
         {
168✔
2988
             {"notes", PropertyType::String | PropertyType::Dictionary | PropertyType::Nullable},
168✔
2989
             {"set_of_ids", PropertyType::Set | PropertyType::ObjectId | PropertyType::Nullable},
168✔
2990
             {"date", PropertyType::Date},
168✔
2991
             {"top_level_link", PropertyType::Object | PropertyType::Nullable, "TopLevel"},
168✔
2992
         }},
168✔
2993
    };
168✔
2994
    struct SecondLevelEmbeddedContent {
168✔
2995
        using DictType = util::FlatMap<std::string, std::string>;
168✔
2996
        DictType dict_values = DictType::container_type{{"key A", random_string(10)}, {"key B", random_string(10)}};
168✔
2997
        std::set<ObjectId> set_of_objects = {ObjectId::gen(), ObjectId::gen()};
168✔
2998
        Timestamp datetime = Timestamp{random_int(), 0};
168✔
2999
        util::Optional<Mixed> pk_of_linked_object;
168✔
3000
        void apply_recovery_from(const SecondLevelEmbeddedContent& other)
168✔
3001
        {
104✔
3002
            datetime = other.datetime;
40✔
3003
            pk_of_linked_object = other.pk_of_linked_object;
40✔
3004
            for (auto it : other.dict_values) {
80✔
3005
                dict_values[it.first] = it.second;
80✔
3006
            }
80✔
3007
            for (auto oid : other.set_of_objects) {
80✔
3008
                set_of_objects.insert(oid);
80✔
3009
            }
80✔
3010
        }
40✔
3011
        void test(const SecondLevelEmbeddedContent& other) const
168✔
3012
        {
1,052✔
3013
            REQUIRE(datetime == other.datetime);
1,052!
3014
            REQUIRE(pk_of_linked_object == other.pk_of_linked_object);
1,052!
3015
            REQUIRE(set_of_objects == other.set_of_objects);
1,052!
3016
            REQUIRE(dict_values.size() == other.dict_values.size());
1,052!
3017
            for (auto kv : dict_values) {
2,100✔
3018
                INFO("dict_value: (" << kv.first << ", " << kv.second << ")");
2,100✔
3019
                auto it = other.dict_values.find(kv.first);
2,100✔
3020
                REQUIRE(it != other.dict_values.end());
2,100!
3021
                REQUIRE(it->second == kv.second);
2,100!
3022
            }
2,100✔
3023
        }
1,052✔
3024
        static SecondLevelEmbeddedContent get_from(Obj second)
168✔
3025
        {
1,418✔
3026
            REALM_ASSERT(second.is_valid());
1,418✔
3027
            SecondLevelEmbeddedContent content{};
1,418✔
3028
            content.datetime = second.get<Timestamp>("date");
1,418✔
3029
            ColKey top_link_col = second.get_table()->get_column_key("top_level_link");
1,418✔
3030
            ObjKey actual_link = second.get<ObjKey>(top_link_col);
1,418✔
3031
            if (actual_link) {
1,418✔
3032
                TableRef top_table = second.get_table()->get_opposite_table(top_link_col);
6✔
3033
                Obj actual_top_obj = top_table->get_object(actual_link);
6✔
3034
                content.pk_of_linked_object = Mixed{actual_top_obj.get_primary_key()};
6✔
3035
            }
6✔
3036
            Dictionary dict = second.get_dictionary("notes");
1,418✔
3037
            content.dict_values.clear();
1,418✔
3038
            for (auto it : dict) {
2,832✔
3039
                content.dict_values.insert({it.first.get_string(), it.second.get_string()});
2,832✔
3040
            }
2,832✔
3041
            Set<ObjectId> set = second.get_set<ObjectId>("set_of_ids");
1,418✔
3042
            content.set_of_objects.clear();
1,418✔
3043
            for (auto oid : set) {
2,836✔
3044
                content.set_of_objects.insert(oid);
2,836✔
3045
            }
2,836✔
3046
            return content;
1,418✔
3047
        }
1,418✔
3048
        void assign_to(Obj second) const
168✔
3049
        {
3,176✔
3050
            if (second.get<Timestamp>("date") != datetime) {
3,176✔
3051
                second.set("date", datetime);
2,508✔
3052
            }
2,508✔
3053
            ColKey top_link_col = second.get_table()->get_column_key("top_level_link");
3,176✔
3054
            if (pk_of_linked_object) {
3,176✔
3055
                TableRef top_table = second.get_table()->get_opposite_table(top_link_col);
8✔
3056
                ObjKey top_link = top_table->find_primary_key(*(pk_of_linked_object));
8✔
3057
                second.set(top_link_col, top_link);
8✔
3058
            }
8✔
3059
            else {
3,168✔
3060
                if (!second.is_null(top_link_col)) {
3,168✔
3061
                    second.set_null(top_link_col);
×
3062
                }
×
3063
            }
3,168✔
3064
            Dictionary dict = second.get_dictionary("notes");
3,176✔
3065
            for (auto it = dict.begin(); it != dict.end(); ++it) {
4,628✔
3066
                if (std::find_if(dict_values.begin(), dict_values.end(), [&](auto& pair) {
2,172✔
3067
                        return pair.first == (*it).first.get_string();
2,172✔
3068
                    }) == dict_values.end()) {
1,092✔
3069
                    dict.erase(it);
12✔
3070
                }
12✔
3071
            }
1,452✔
3072
            for (auto& it : dict_values) {
6,340✔
3073
                auto existing = dict.find(it.first);
6,340✔
3074
                if (existing == dict.end() || (*existing).second.get_string() != it.second) {
6,340✔
3075
                    dict.insert(it.first, it.second);
4,992✔
3076
                }
4,992✔
3077
            }
6,340✔
3078
            Set<ObjectId> set = second.get_set<ObjectId>("set_of_ids");
3,176✔
3079
            if (set_of_objects.empty()) {
3,176✔
3080
                set.clear();
12✔
3081
            }
12✔
3082
            else {
3,164✔
3083
                std::vector<size_t> indices, to_remove;
3,164✔
3084
                set.sort(indices);
3,164✔
3085
                for (size_t ndx : indices) {
2,302✔
3086
                    if (set_of_objects.count(set.get(ndx)) == 0) {
1,440✔
3087
                        to_remove.push_back(ndx);
104✔
3088
                    }
104✔
3089
                }
1,440✔
3090
                std::sort(to_remove.rbegin(), to_remove.rend());
3,164✔
3091
                for (auto ndx : to_remove) {
1,634✔
3092
                    set.erase(set.get(ndx));
104✔
3093
                }
104✔
3094
                for (auto oid : set_of_objects) {
6,328✔
3095
                    if (set.find(oid) == realm::npos) {
6,328✔
3096
                        set.insert(oid);
4,992✔
3097
                    }
4,992✔
3098
                }
6,328✔
3099
            }
3,164✔
3100
        }
3,176✔
3101
    };
168✔
3102

84✔
3103
    struct EmbeddedContent {
168✔
3104
        std::string name = random_string(10);
168✔
3105
        int64_t int_value = random_int();
168✔
3106
        std::vector<Int> array_vals = {random_int(), random_int(), random_int()};
168✔
3107
        util::Optional<SecondLevelEmbeddedContent> second_level = SecondLevelEmbeddedContent();
168✔
3108
        std::vector<SecondLevelEmbeddedContent> array_of_seconds = {};
168✔
3109
        void apply_recovery_from(const EmbeddedContent& other)
168✔
3110
        {
106✔
3111
            name = other.name;
44✔
3112
            int_value = other.int_value;
44✔
3113
            combine_array_values(array_vals, other.array_vals);
44✔
3114
            if (second_level && other.second_level) {
44✔
3115
                second_level->apply_recovery_from(*other.second_level);
40✔
3116
            }
40✔
3117
            else {
4✔
3118
                second_level = other.second_level;
4✔
3119
            }
4✔
3120
        }
44✔
3121
        void test(const EmbeddedContent& other) const
168✔
3122
        {
1,024✔
3123
            INFO("Checking EmbeddedContent" << name);
1,024✔
3124
            REQUIRE(name == other.name);
1,024!
3125
            REQUIRE(int_value == other.int_value);
1,024!
3126
            REQUIRE(array_vals == other.array_vals);
1,024!
3127
            REQUIRE(array_of_seconds.size() == other.array_of_seconds.size());
1,024!
3128
            for (size_t i = 0; i < array_of_seconds.size(); ++i) {
1,052✔
3129
                array_of_seconds[i].test(other.array_of_seconds[i]);
28✔
3130
            }
28✔
3131
            if (!second_level) {
1,024✔
3132
                REQUIRE(!other.second_level);
2!
3133
            }
2✔
3134
            else {
1,022✔
3135
                REQUIRE(!!other.second_level);
1,022!
3136
                second_level->test(*other.second_level);
1,022✔
3137
            }
1,022✔
3138
        }
1,024✔
3139
        static util::Optional<EmbeddedContent> get_from(Obj embedded)
168✔
3140
        {
1,418✔
3141
            util::Optional<EmbeddedContent> value;
1,418✔
3142
            if (embedded.is_valid()) {
1,418✔
3143
                value = EmbeddedContent{};
1,378✔
3144
                value->name = embedded.get_any("name").get<StringData>();
1,378✔
3145
                value->int_value = embedded.get_any("int_value").get<Int>();
1,378✔
3146
                ColKey list_col = embedded.get_table()->get_column_key("array");
1,378✔
3147
                value->array_vals = embedded.get_list_values<Int>(list_col);
1,378✔
3148

689✔
3149
                ColKey link2_col = embedded.get_table()->get_column_key("link_to_embedded_object2");
1,378✔
3150
                Obj second = embedded.get_linked_object(link2_col);
1,378✔
3151
                value->second_level = util::none;
1,378✔
3152
                if (second.is_valid()) {
1,378✔
3153
                    value->second_level = SecondLevelEmbeddedContent::get_from(second);
1,376✔
3154
                }
1,376✔
3155
                auto list = embedded.get_linklist("array_of_seconds");
1,378✔
3156
                for (size_t i = 0; i < list.size(); ++i) {
1,420✔
3157
                    value->array_of_seconds.push_back(SecondLevelEmbeddedContent::get_from(list.get_object(i)));
42✔
3158
                }
42✔
3159
            }
1,378✔
3160
            return value;
1,418✔
3161
        }
1,418✔
3162
        void assign_to(Obj embedded) const
168✔
3163
        {
3,116✔
3164
            if (embedded.get<StringData>("name") != name) {
3,116✔
3165
                embedded.set<StringData>("name", name);
2,468✔
3166
            }
2,468✔
3167
            if (embedded.get<Int>("int_value") != int_value) {
3,116✔
3168
                embedded.set<Int>("int_value", int_value);
2,440✔
3169
            }
2,440✔
3170
            ColKey list_col = embedded.get_table()->get_column_key("array");
3,116✔
3171
            if (embedded.get_list_values<Int>(list_col) != array_vals) {
3,116✔
3172
                embedded.set_list_values<Int>(list_col, array_vals);
2,468✔
3173
            }
2,468✔
3174
            ColKey link2_col = embedded.get_table()->get_column_key("link_to_embedded_object2");
3,116✔
3175
            if (second_level) {
3,116✔
3176
                Obj second = embedded.get_linked_object(link2_col);
3,112✔
3177
                if (!second) {
3,112✔
3178
                    second = embedded.create_and_set_linked_object(link2_col);
2,392✔
3179
                }
2,392✔
3180
                second_level->assign_to(second);
3,112✔
3181
            }
3,112✔
3182
            else {
4✔
3183
                embedded.set_null(link2_col);
4✔
3184
            }
4✔
3185
            auto list = embedded.get_linklist("array_of_seconds");
3,116✔
3186
            set_embedded_list(array_of_seconds, list);
3,116✔
3187
        }
3,116✔
3188
    };
168✔
3189
    struct TopLevelContent {
168✔
3190
        util::Optional<EmbeddedContent> link_value = EmbeddedContent();
168✔
3191
        std::vector<EmbeddedContent> array_values{3};
168✔
3192
        using DictType = util::FlatMap<std::string, util::Optional<EmbeddedContent>>;
168✔
3193
        DictType dict_values = DictType::container_type{
168✔
3194
            {"foo", EmbeddedContent()},
168✔
3195
            {"bar", EmbeddedContent()},
168✔
3196
            {"baz", EmbeddedContent()},
168✔
3197
        };
168✔
3198
        void apply_recovery_from(const TopLevelContent& other)
168✔
3199
        {
108✔
3200
            combine_array_values(array_values, other.array_values);
48✔
3201
            for (auto it : other.dict_values) {
168✔
3202
                dict_values[it.first] = it.second;
168✔
3203
            }
168✔
3204
            if (link_value && other.link_value) {
48✔
3205
                link_value->apply_recovery_from(*other.link_value);
32✔
3206
            }
32✔
3207
            else if (link_value) {
16✔
3208
                link_value = other.link_value;
4✔
3209
            }
4✔
3210
            // assuming starting from an initial value, if the link_value is null, then it was intentionally deleted.
24✔
3211
        }
48✔
3212
        void test(const TopLevelContent& other) const
168✔
3213
        {
154✔
3214
            if (link_value) {
140✔
3215
                INFO("checking TopLevelContent.link_value");
126✔
3216
                REQUIRE(!!other.link_value);
126!
3217
                link_value->test(*other.link_value);
126✔
3218
            }
126✔
3219
            else {
14✔
3220
                REQUIRE(!other.link_value);
14!
3221
            }
14✔
3222
            REQUIRE(array_values.size() == other.array_values.size());
140!
3223
            for (size_t i = 0; i < array_values.size(); ++i) {
600✔
3224
                INFO("checking array_values: " << i);
460✔
3225
                array_values[i].test(other.array_values[i]);
460✔
3226
            }
460✔
3227
            REQUIRE(dict_values.size() == other.dict_values.size());
140!
3228
            for (auto it : dict_values) {
444✔
3229
                INFO("checking dict_values: " << it.first);
444✔
3230
                auto found = other.dict_values.find(it.first);
444✔
3231
                REQUIRE(found != other.dict_values.end());
444!
3232
                if (it.second) {
444✔
3233
                    REQUIRE(!!found->second);
418!
3234
                    it.second->test(*found->second);
418✔
3235
                }
418✔
3236
                else {
26✔
3237
                    REQUIRE(!found->second);
26!
3238
                }
26✔
3239
            }
444✔
3240
        }
140✔
3241
        static TopLevelContent get_from(Obj obj)
168✔
3242
        {
200✔
3243
            TopLevelContent content;
200✔
3244
            Obj embedded_link = obj.get_linked_object("embedded_obj");
200✔
3245
            content.link_value = EmbeddedContent::get_from(embedded_link);
200✔
3246
            auto list = obj.get_linklist("array_of_objs");
200✔
3247
            content.array_values.clear();
200✔
3248

100✔
3249
            for (size_t i = 0; i < list.size(); ++i) {
772✔
3250
                Obj link = list.get_object(i);
572✔
3251
                content.array_values.push_back(*EmbeddedContent::get_from(link));
572✔
3252
            }
572✔
3253
            auto dict = obj.get_dictionary("embedded_dict");
200✔
3254
            content.dict_values.clear();
200✔
3255
            for (auto it : dict) {
620✔
3256
                Obj link = dict.get_object(it.first.get_string());
620✔
3257
                content.dict_values.insert({it.first.get_string(), EmbeddedContent::get_from(link)});
620✔
3258
            }
620✔
3259
            return content;
200✔
3260
        }
200✔
3261
        void assign_to(Obj obj) const
168✔
3262
        {
436✔
3263
            ColKey link_col = obj.get_table()->get_column_key("embedded_obj");
436✔
3264
            if (!link_value) {
436✔
3265
                obj.set_null(link_col);
16✔
3266
            }
16✔
3267
            else {
420✔
3268
                Obj embedded_link = obj.get_linked_object(link_col);
420✔
3269
                if (!embedded_link) {
420✔
3270
                    embedded_link = obj.create_and_set_linked_object(link_col);
232✔
3271
                }
232✔
3272
                link_value->assign_to(embedded_link);
420✔
3273
            }
420✔
3274
            auto list = obj.get_linklist("array_of_objs");
436✔
3275
            set_embedded_list(array_values, list);
436✔
3276
            auto dict = obj.get_dictionary("embedded_dict");
436✔
3277
            for (auto it = dict.begin(); it != dict.end();) {
760✔
3278
                if (dict_values.find((*it).first.get_string()) == dict_values.end()) {
324✔
3279
                    it = dict.erase(it);
16✔
3280
                }
16✔
3281
                else {
308✔
3282
                    ++it;
308✔
3283
                }
308✔
3284
            }
324✔
3285
            for (auto it : dict_values) {
1,356✔
3286
                if (it.second) {
1,356✔
3287
                    auto embedded = dict.get_object(it.first);
1,312✔
3288
                    if (!embedded) {
1,312✔
3289
                        embedded = dict.create_and_insert_linked_object(it.first);
1,008✔
3290
                    }
1,008✔
3291
                    it.second->assign_to(embedded);
1,312✔
3292
                }
1,312✔
3293
                else {
44✔
3294
                    dict.insert(it.first, Mixed{});
44✔
3295
                }
44✔
3296
            }
1,356✔
3297
        }
436✔
3298
    };
168✔
3299

84✔
3300
    SyncTestFile config2(init_sync_manager.app(), "default");
168✔
3301
    config2.schema = config.schema;
168✔
3302

84✔
3303
    std::unique_ptr<reset_utils::TestClientReset> test_reset =
168✔
3304
        reset_utils::make_fake_local_client_reset(config, config2);
168✔
3305

84✔
3306
    auto get_top_object = [](SharedRealm realm) {
460✔
3307
        advance_and_notify(*realm);
460✔
3308
        TableRef table = get_table(*realm, "TopLevel");
460✔
3309
        REQUIRE(table->size() == 1);
460!
3310
        Obj obj = *table->begin();
460✔
3311
        return obj;
460✔
3312
    };
460✔
3313

84✔
3314
    using StateList = std::vector<TopLevelContent>;
168✔
3315
    auto reset_embedded_object = [&](StateList local_content, StateList remote_content,
168✔
3316
                                     TopLevelContent expected_recovered) {
134✔
3317
        test_reset
100✔
3318
            ->make_local_changes([&](SharedRealm local_realm) {
100✔
3319
                Obj obj = get_top_object(local_realm);
100✔
3320
                for (auto& s : local_content) {
104✔
3321
                    s.assign_to(obj);
104✔
3322
                }
104✔
3323
            })
100✔
3324
            ->make_remote_changes([&](SharedRealm remote_realm) {
100✔
3325
                Obj obj = get_top_object(remote_realm);
100✔
3326
                for (auto& s : remote_content) {
100✔
3327
                    s.assign_to(obj);
100✔
3328
                }
100✔
3329
            })
100✔
3330
            ->on_post_reset([&](SharedRealm local_realm) {
100✔
3331
                Obj obj = get_top_object(local_realm);
100✔
3332
                TopLevelContent actual = TopLevelContent::get_from(obj);
100✔
3333
                if (test_mode == ClientResyncMode::Recover) {
100✔
3334
                    actual.test(expected_recovered);
50✔
3335
                }
50✔
3336
                else if (test_mode == ClientResyncMode::DiscardLocal) {
50✔
3337
                    REQUIRE(remote_content.size() > 0);
50!
3338
                    actual.test(remote_content.back());
50✔
3339
                }
50✔
3340
                else {
×
3341
                    REALM_UNREACHABLE();
×
3342
                }
×
3343
            })
100✔
3344
            ->run();
100✔
3345
    };
100✔
3346

84✔
3347
    ObjectId pk_val = ObjectId::gen();
168✔
3348
    test_reset->setup([&pk_val](SharedRealm realm) {
132✔
3349
        auto table = get_table(*realm, "TopLevel");
96✔
3350
        REQUIRE(table);
96!
3351
        auto obj = table->create_object_with_primary_key(pk_val);
96✔
3352
        Obj embedded_link = obj.create_and_set_linked_object(table->get_column_key("embedded_obj"));
96✔
3353
        embedded_link.set<String>("name", "initial name");
96✔
3354
    });
96✔
3355

84✔
3356
    SECTION("identical changes") {
168✔
3357
        TopLevelContent state;
4✔
3358
        TopLevelContent expected_recovered = state;
4✔
3359
        expected_recovered.apply_recovery_from(state);
4✔
3360
        reset_embedded_object({state}, {state}, expected_recovered);
4✔
3361
    }
4✔
3362
    SECTION("modify every embedded property") {
168✔
3363
        TopLevelContent local, remote;
4✔
3364
        TopLevelContent expected_recovered = remote;
4✔
3365
        expected_recovered.apply_recovery_from(local);
4✔
3366
        reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3367
    }
4✔
3368
    SECTION("remote nullifies embedded links") {
168✔
3369
        TopLevelContent local;
4✔
3370
        TopLevelContent remote = local;
4✔
3371
        remote.link_value.reset();
4✔
3372
        for (auto& val : remote.dict_values) {
12✔
3373
            val.second.reset();
12✔
3374
        }
12✔
3375
        remote.array_values.clear();
4✔
3376
        TopLevelContent expected_recovered = remote;
4✔
3377
        expected_recovered.apply_recovery_from(local);
4✔
3378
        reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3379
    }
4✔
3380
    SECTION("local nullifies embedded links") {
168✔
3381
        TopLevelContent local;
4✔
3382
        TopLevelContent remote = local;
4✔
3383
        local.link_value.reset();
4✔
3384
        for (auto& val : local.dict_values) {
12✔
3385
            val.second.reset();
12✔
3386
        }
12✔
3387
        local.array_values.clear();
4✔
3388
        TopLevelContent expected_recovered = remote;
4✔
3389
        expected_recovered.apply_recovery_from(local);
4✔
3390
        reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3391
    }
4✔
3392
    SECTION("remote adds embedded objects") {
168✔
3393
        TopLevelContent local;
4✔
3394
        TopLevelContent remote = local;
4✔
3395
        remote.dict_values["new key1"] = {EmbeddedContent{}};
4✔
3396
        remote.dict_values["new key2"] = {EmbeddedContent{}};
4✔
3397
        remote.dict_values["new key3"] = {};
4✔
3398
        remote.array_values.push_back({EmbeddedContent{}});
4✔
3399
        remote.array_values.push_back({});
4✔
3400
        remote.array_values.push_back({EmbeddedContent{}});
4✔
3401
        TopLevelContent expected_recovered = remote;
4✔
3402
        expected_recovered.apply_recovery_from(local);
4✔
3403
        reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3404
    }
4✔
3405
    SECTION("local adds some embedded objects") {
168✔
3406
        TopLevelContent local;
4✔
3407
        TopLevelContent remote = local;
4✔
3408
        local.dict_values["new key1"] = {EmbeddedContent{}};
4✔
3409
        local.dict_values["new key2"] = {EmbeddedContent{}};
4✔
3410
        local.dict_values["new key3"] = {};
4✔
3411
        local.array_values.push_back({EmbeddedContent{}});
4✔
3412
        local.array_values.push_back({});
4✔
3413
        local.array_values.push_back({EmbeddedContent{}});
4✔
3414
        TopLevelContent expected_recovered = remote;
4✔
3415
        expected_recovered.apply_recovery_from(local);
4✔
3416
        reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3417
    }
4✔
3418
    SECTION("both add conflicting embedded objects") {
168✔
3419
        TopLevelContent local;
4✔
3420
        TopLevelContent remote = local;
4✔
3421
        local.dict_values["new key1"] = {EmbeddedContent{}};
4✔
3422
        local.dict_values["new key2"] = {EmbeddedContent{}};
4✔
3423
        local.dict_values["new key3"] = {};
4✔
3424
        local.array_values.push_back({EmbeddedContent{}});
4✔
3425
        local.array_values.push_back({});
4✔
3426
        local.array_values.push_back({EmbeddedContent{}});
4✔
3427
        remote.dict_values["new key1"] = {EmbeddedContent{}};
4✔
3428
        remote.dict_values["new key2"] = {EmbeddedContent{}};
4✔
3429
        remote.dict_values["new key3"] = {};
4✔
3430
        remote.array_values.push_back({EmbeddedContent{}});
4✔
3431
        remote.array_values.push_back({});
4✔
3432
        remote.array_values.push_back({EmbeddedContent{}});
4✔
3433
        TopLevelContent expected_recovered = remote;
4✔
3434
        expected_recovered.apply_recovery_from(local);
4✔
3435
        reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3436
    }
4✔
3437
    SECTION("local modifies an embedded object which is removed by the remote") {
168✔
3438
        TopLevelContent local, remote;
4✔
3439
        local.link_value->name = "modified value";
4✔
3440
        remote.link_value = util::none;
4✔
3441
        TopLevelContent expected_recovered = remote;
4✔
3442
        expected_recovered.apply_recovery_from(local);
4✔
3443
        reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3444
    }
4✔
3445
    SECTION("local modifies a deep embedded object which is removed by the remote") {
168✔
3446
        TopLevelContent local, remote;
4✔
3447
        local.link_value->second_level->datetime = Timestamp{1, 1};
4✔
3448
        remote.link_value = util::none;
4✔
3449
        TopLevelContent expected_recovered = remote;
4✔
3450
        expected_recovered.apply_recovery_from(local);
4✔
3451
        reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3452
    }
4✔
3453
    SECTION("local modifies a deep embedded object which is removed at the second level by the remote") {
168✔
3454
        TopLevelContent local, remote;
4✔
3455
        local.link_value->second_level->datetime = Timestamp{1, 1};
4✔
3456
        remote.link_value->second_level = util::none;
4✔
3457
        TopLevelContent expected_recovered = remote;
4✔
3458
        expected_recovered.apply_recovery_from(local);
4✔
3459
        reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3460
    }
4✔
3461
    SECTION("with shared initial state") {
168✔
3462
        TopLevelContent initial;
112✔
3463
        test_reset->setup([&](SharedRealm realm) {
224✔
3464
            auto table = get_table(*realm, "TopLevel");
224✔
3465
            REQUIRE(table);
224!
3466
            auto obj = table->create_object_with_primary_key(pk_val);
224✔
3467
            initial.assign_to(obj);
224✔
3468
        });
224✔
3469
        TopLevelContent local = initial;
112✔
3470
        TopLevelContent remote = initial;
112✔
3471

56✔
3472
        SECTION("local modifications to an embedded object through a dictionary which is removed by the remote are "
112✔
3473
                "ignored") {
58✔
3474
            local.dict_values["foo"]->name = "modified";
4✔
3475
            local.dict_values["foo"]->second_level->datetime = Timestamp{1, 1};
4✔
3476
            local.dict_values["foo"]->array_vals.push_back(random_int());
4✔
3477
            local.dict_values["foo"]->array_vals.erase(local.dict_values["foo"]->array_vals.begin());
4✔
3478
            local.dict_values["foo"]->second_level->dict_values.erase(
4✔
3479
                local.dict_values["foo"]->second_level->dict_values.begin());
4✔
3480
            local.dict_values["foo"]->second_level->set_of_objects.clear();
4✔
3481
            remote.dict_values["foo"] = util::none;
4✔
3482
            TopLevelContent expected_recovered = remote;
4✔
3483
            reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3484
        }
4✔
3485
        SECTION("local modifications to an embedded object through a linklist element which is removed by the remote "
112✔
3486
                "triggers a list copy") {
58✔
3487
            local.array_values.begin()->name = "modified";
4✔
3488
            local.array_values.begin()->second_level->datetime = Timestamp{1, 1};
4✔
3489
            local.array_values.begin()->array_vals.push_back(random_int());
4✔
3490
            local.array_values.begin()->array_vals.erase(local.array_values.begin()->array_vals.begin());
4✔
3491
            local.array_values.begin()->second_level->dict_values.erase(
4✔
3492
                local.array_values.begin()->second_level->dict_values.begin());
4✔
3493
            local.array_values.begin()->second_level->set_of_objects.clear();
4✔
3494
            remote.array_values.erase(remote.array_values.begin());
4✔
3495
            TopLevelContent expected_recovered = local;
4✔
3496
            reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3497
        }
4✔
3498
        SECTION("local ArraySet to an embedded object through a deep link->linklist element which is removed by the "
112✔
3499
                "remote "
112✔
3500
                "triggers a list copy") {
58✔
3501
            local.link_value->array_vals[0] = 12345;
4✔
3502
            remote.link_value->array_vals.erase(remote.link_value->array_vals.begin());
4✔
3503
            TopLevelContent expected_recovered = local;
4✔
3504
            reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3505
        }
4✔
3506
        SECTION("local ArrayErase to an embedded object through a deep link->linklist element which is removed by "
112✔
3507
                "the remote "
112✔
3508
                "triggers a list copy") {
58✔
3509
            local.link_value->array_vals.erase(local.link_value->array_vals.begin());
4✔
3510
            remote.link_value->array_vals.clear();
4✔
3511
            TopLevelContent expected_recovered = local;
4✔
3512
            reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3513
        }
4✔
3514
        SECTION("local modifications to an embedded object through a linklist cleared by the remote triggers a list "
112✔
3515
                "copy") {
58✔
3516
            local.array_values.begin()->name = "modified";
4✔
3517
            local.array_values.begin()->second_level->datetime = Timestamp{1, 1};
4✔
3518
            local.array_values.begin()->array_vals.push_back(random_int());
4✔
3519
            local.array_values.begin()->array_vals.erase(local.array_values.begin()->array_vals.begin());
4✔
3520
            local.array_values.begin()->second_level->dict_values.erase(
4✔
3521
                local.array_values.begin()->second_level->dict_values.begin());
4✔
3522
            local.array_values.begin()->second_level->set_of_objects.clear();
4✔
3523
            remote.array_values.clear();
4✔
3524
            TopLevelContent expected_recovered = local;
4✔
3525
            reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3526
        }
4✔
3527
        SECTION("moving preexisting list items triggers a list copy") {
112✔
3528
            test_reset
4✔
3529
                ->make_local_changes([&](SharedRealm local_realm) {
4✔
3530
                    Obj obj = get_top_object(local_realm);
4✔
3531
                    auto list = obj.get_linklist("array_of_objs");
4✔
3532
                    REQUIRE(list.size() == 3);
4!
3533
                    list.move(0, 1);
4✔
3534
                    list.move(1, 2);
4✔
3535
                    list.move(1, 0);
4✔
3536
                })
4✔
3537
                ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
3538
                    Obj obj = get_top_object(remote_realm);
4✔
3539
                    auto list = obj.get_linklist("array_of_objs");
4✔
3540
                    list.remove(0, list.size()); // any change here is lost
4✔
3541
                    remote = TopLevelContent::get_from(obj);
4✔
3542
                })
4✔
3543
                ->on_post_reset([&](SharedRealm local_realm) {
4✔
3544
                    Obj obj = get_top_object(local_realm);
4✔
3545
                    TopLevelContent actual = TopLevelContent::get_from(obj);
4✔
3546
                    if (test_mode == ClientResyncMode::Recover) {
4✔
3547
                        TopLevelContent expected_recovered = local;
2✔
3548
                        std::iter_swap(expected_recovered.array_values.begin(),
2✔
3549
                                       expected_recovered.array_values.begin() + 1);
2✔
3550
                        std::iter_swap(expected_recovered.array_values.begin() + 1,
2✔
3551
                                       expected_recovered.array_values.begin() + 2);
2✔
3552
                        std::iter_swap(expected_recovered.array_values.begin() + 1,
2✔
3553
                                       expected_recovered.array_values.begin());
2✔
3554
                        actual.test(expected_recovered);
2✔
3555
                    }
2✔
3556
                    else {
2✔
3557
                        actual.test(remote);
2✔
3558
                    }
2✔
3559
                })
4✔
3560
                ->run();
4✔
3561
        }
4✔
3562
        SECTION("inserting new embedded objects into a list which has indices modified by the remote are recovered") {
112✔
3563
            EmbeddedContent new_element1, new_element2;
4✔
3564
            local.array_values.insert(local.array_values.end(), new_element1);
4✔
3565
            local.array_values.insert(local.array_values.begin(), new_element2);
4✔
3566
            remote.array_values.erase(remote.array_values.begin());
4✔
3567
            remote.array_values.erase(remote.array_values.begin());
4✔
3568
            test_reset
4✔
3569
                ->make_local_changes([&](SharedRealm local) {
4✔
3570
                    Obj obj = get_top_object(local);
4✔
3571
                    auto list = obj.get_linklist("array_of_objs");
4✔
3572
                    auto embedded = list.create_and_insert_linked_object(3);
4✔
3573
                    new_element1.assign_to(embedded);
4✔
3574
                    embedded = list.create_and_insert_linked_object(0);
4✔
3575
                    new_element2.assign_to(embedded);
4✔
3576
                })
4✔
3577
                ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
3578
                    Obj obj = get_top_object(remote_realm);
4✔
3579
                    auto list = obj.get_linklist("array_of_objs");
4✔
3580
                    list.remove(0, list.size() - 1);
4✔
3581
                    remote = TopLevelContent::get_from(obj);
4✔
3582
                })
4✔
3583
                ->on_post_reset([&](SharedRealm local_realm) {
4✔
3584
                    Obj obj = get_top_object(local_realm);
4✔
3585
                    TopLevelContent actual = TopLevelContent::get_from(obj);
4✔
3586
                    if (test_mode == ClientResyncMode::Recover) {
4✔
3587
                        TopLevelContent expected_recovered = remote;
2✔
3588
                        expected_recovered.array_values.insert(expected_recovered.array_values.end(), new_element1);
2✔
3589
                        expected_recovered.array_values.insert(expected_recovered.array_values.begin(), new_element2);
2✔
3590
                        actual.test(expected_recovered);
2✔
3591
                    }
2✔
3592
                    else {
2✔
3593
                        actual.test(remote);
2✔
3594
                    }
2✔
3595
                })
4✔
3596
                ->run();
4✔
3597
        }
4✔
3598
        SECTION("local list clear removes remotely inserted objects") {
112✔
3599
            EmbeddedContent new_element_local, new_element_remote;
4✔
3600
            local.array_values.clear();
4✔
3601
            TopLevelContent local2 = local;
4✔
3602
            local2.array_values.push_back(new_element_local);
4✔
3603
            remote.array_values.erase(remote.array_values.begin());
4✔
3604
            remote.array_values.push_back(new_element_remote); // lost via local.clear()
4✔
3605
            TopLevelContent expected_recovered = local2;
4✔
3606
            reset_embedded_object({local, local2}, {remote}, expected_recovered);
4✔
3607
        }
4✔
3608
        SECTION("local modification of a dictionary value which is removed by the remote") {
112✔
3609
            local.dict_values["foo"] = EmbeddedContent{};
4✔
3610
            remote.dict_values.erase("foo");
4✔
3611
            TopLevelContent expected_recovered = remote;
4✔
3612
            reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3613
        }
4✔
3614
        SECTION("local delete of a dictionary value which is removed by the remote") {
112✔
3615
            local.dict_values.erase("foo");
4✔
3616
            remote.dict_values.erase("foo");
4✔
3617
            TopLevelContent expected_recovered = remote;
4✔
3618
            reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3619
        }
4✔
3620
        SECTION("local delete of a dictionary value which is modified by the remote") {
112✔
3621
            local.dict_values.erase("foo");
4✔
3622
            remote.dict_values["foo"] = EmbeddedContent{};
4✔
3623
            TopLevelContent expected_recovered = local;
4✔
3624
            reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3625
        }
4✔
3626
        SECTION("both modify a dictionary value") {
112✔
3627
            EmbeddedContent new_local, new_remote;
4✔
3628
            local.dict_values["foo"] = new_local;
4✔
3629
            remote.dict_values["foo"] = new_remote;
4✔
3630
            TopLevelContent expected_recovered = remote;
4✔
3631
            expected_recovered.dict_values["foo"]->apply_recovery_from(*local.dict_values["foo"]);
4✔
3632
            // a verbatim list copy is triggered by modifications to items which were not just inserted
2✔
3633
            expected_recovered.dict_values["foo"]->array_vals = local.dict_values["foo"]->array_vals;
4✔
3634
            reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3635
        }
4✔
3636
        std::vector<std::string> keys = {"new key", "", "\0"};
112✔
3637
        for (auto key : keys) {
336✔
3638
            SECTION(util::format("both add the same dictionary key: '%1'", key)) {
336✔
3639
                EmbeddedContent new_local, new_remote;
8✔
3640
                local.dict_values[key] = new_local;
8✔
3641
                remote.dict_values[key] = new_remote;
8✔
3642
                TopLevelContent expected_recovered = remote;
8✔
3643
                expected_recovered.dict_values[key]->apply_recovery_from(*local.dict_values[key]);
8✔
3644
                // a verbatim list copy is triggered by modifications to items which were not just inserted
4✔
3645
                expected_recovered.dict_values[key]->array_vals = local.dict_values[key]->array_vals;
8✔
3646
                expected_recovered.dict_values[key]->second_level = local.dict_values[key]->second_level;
8✔
3647
                reset_embedded_object({local}, {remote}, expected_recovered);
8✔
3648
            }
8✔
3649
        }
336✔
3650
        SECTION("deep modifications to inserted and swaped list items are recovered") {
112✔
3651
            EmbeddedContent local_added_at_begin, local_added_at_end, local_added_before_end, remote_added;
4✔
3652
            size_t list_end = initial.array_values.size();
4✔
3653
            test_reset
4✔
3654
                ->make_local_changes([&](SharedRealm local) {
4✔
3655
                    Obj obj = get_top_object(local);
4✔
3656
                    auto list = obj.get_linklist("array_of_objs");
4✔
3657
                    auto embedded = list.create_and_insert_linked_object(0);
4✔
3658
                    local_added_at_begin.assign_to(embedded);
4✔
3659
                    embedded = list.create_and_insert_linked_object(list_end - 1);
4✔
3660
                    local_added_before_end.assign_to(embedded); // this item is needed here so that move does not
4✔
3661
                                                                // trigger a copy of the list
2✔
3662
                    embedded = list.create_and_insert_linked_object(list_end);
4✔
3663
                    local_added_at_end.assign_to(embedded);
4✔
3664
                    local->commit_transaction();
4✔
3665
                    local->begin_transaction();
4✔
3666
                    list.swap(0,
4✔
3667
                              list_end); // generates two move instructions, move(0, list_end), move(list_end - 1, 0)
4✔
3668
                    local->commit_transaction();
4✔
3669
                    local->begin_transaction();
4✔
3670
                    local_added_at_end.name = "should be at begin now";
4✔
3671
                    local_added_at_begin.name = "should be at end now";
4✔
3672
                    local_added_at_end.assign_to(list.get_object(0));
4✔
3673
                    local_added_at_begin.assign_to(list.get_object(list_end));
4✔
3674
                })
4✔
3675
                ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
3676
                    Obj obj = get_top_object(remote_realm);
4✔
3677
                    auto list = obj.get_linklist("array_of_objs");
4✔
3678
                    list.remove(0, list.size()); // individual ArrayErase instructions, not a clear.
4✔
3679
                    remote_added.name = "remote added at zero, should end up in the middle of the list";
4✔
3680
                    remote_added.assign_to(list.create_and_insert_linked_object(0));
4✔
3681
                    remote = TopLevelContent::get_from(obj);
4✔
3682
                })
4✔
3683
                ->on_post_reset([&](SharedRealm local_realm) {
4✔
3684
                    Obj obj = get_top_object(local_realm);
4✔
3685
                    if (test_mode == ClientResyncMode::Recover) {
4✔
3686
                        auto list = obj.get_linklist("array_of_objs");
2✔
3687
                        REQUIRE(list.size() == 4);
2!
3688
                        EmbeddedContent embedded_0 = *EmbeddedContent::get_from(list.get_object(0));
2✔
3689
                        EmbeddedContent embedded_1 = *EmbeddedContent::get_from(list.get_object(1));
2✔
3690
                        EmbeddedContent embedded_2 = *EmbeddedContent::get_from(list.get_object(2));
2✔
3691
                        EmbeddedContent embedded_3 = *EmbeddedContent::get_from(list.get_object(3));
2✔
3692
                        embedded_0.test(local_added_at_end); // local added at end, moved to 0
2✔
3693
                        embedded_1.test(remote_added); // remote added at 0, bumped to 1 by recovered insert at 0
2✔
3694
                        embedded_2.test(local_added_before_end); // local added at 2, not moved
2✔
3695
                        embedded_3.test(local_added_at_begin);   // local added at 0, moved to end
2✔
3696
                    }
2✔
3697
                    else {
2✔
3698
                        TopLevelContent actual = TopLevelContent::get_from(obj);
2✔
3699
                        actual.test(remote);
2✔
3700
                    }
2✔
3701
                })
4✔
3702
                ->run();
4✔
3703
        }
4✔
3704
        SECTION("deep modifications to inserted and moved list items are recovered") {
112✔
3705
            EmbeddedContent local_added_at_begin, local_added_at_end, remote_added;
4✔
3706
            test_reset
4✔
3707
                ->make_local_changes([&](SharedRealm local_realm) {
4✔
3708
                    Obj obj = get_top_object(local_realm);
4✔
3709
                    auto list = obj.get_linklist("array_of_objs");
4✔
3710
                    auto embedded = list.create_and_insert_linked_object(0);
4✔
3711
                    local_added_at_begin.assign_to(embedded);
4✔
3712
                    embedded = list.create_and_insert_linked_object(list.size());
4✔
3713
                    local_added_at_end.assign_to(embedded);
4✔
3714
                    local_realm->commit_transaction();
4✔
3715
                    advance_and_notify(*local_realm);
4✔
3716
                    local_realm->begin_transaction();
4✔
3717
                    list.move(list.size() - 1, 0);
4✔
3718
                    local_realm->commit_transaction();
4✔
3719
                    advance_and_notify(*local_realm);
4✔
3720
                    local_realm->begin_transaction();
4✔
3721
                    local_added_at_end.name = "added at end, moved to 0";
4✔
3722
                    local_added_at_begin.name = "added at 0, bumped to 1";
4✔
3723
                    local_added_at_end.assign_to(list.get_object(0));
4✔
3724
                    local_added_at_begin.assign_to(list.get_object(1));
4✔
3725
                })
4✔
3726
                ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
3727
                    Obj obj = get_top_object(remote_realm);
4✔
3728
                    auto list = obj.get_linklist("array_of_objs");
4✔
3729
                    list.remove(0, list.size()); // individual ArrayErase instructions, not a clear.
4✔
3730
                    remote_added.name = "remote added at zero, should end up at the end of the list";
4✔
3731
                    remote_added.assign_to(list.create_and_insert_linked_object(0));
4✔
3732
                    remote = TopLevelContent::get_from(obj);
4✔
3733
                })
4✔
3734
                ->on_post_reset([&](SharedRealm local_realm) {
4✔
3735
                    Obj obj = get_top_object(local_realm);
4✔
3736
                    if (test_mode == ClientResyncMode::Recover) {
4✔
3737
                        auto list = obj.get_linklist("array_of_objs");
2✔
3738
                        REQUIRE(list.size() == 3);
2!
3739
                        EmbeddedContent embedded_0 = *EmbeddedContent::get_from(list.get_object(0));
2✔
3740
                        EmbeddedContent embedded_1 = *EmbeddedContent::get_from(list.get_object(1));
2✔
3741
                        EmbeddedContent embedded_2 = *EmbeddedContent::get_from(list.get_object(2));
2✔
3742
                        embedded_0.test(local_added_at_end);   // local added at end, moved to 0
2✔
3743
                        embedded_1.test(local_added_at_begin); // local added at begin, bumped up by move
2✔
3744
                        embedded_2.test(
2✔
3745
                            remote_added); // remote added at 0, bumped to 2 by recovered insert at 0 and move to 0
2✔
3746
                    }
2✔
3747
                    else {
2✔
3748
                        TopLevelContent actual = TopLevelContent::get_from(obj);
2✔
3749
                        actual.test(remote);
2✔
3750
                    }
2✔
3751
                })
4✔
3752
                ->run();
4✔
3753
        }
4✔
3754
        SECTION("removing an added list item does not trigger a list copy") {
112✔
3755
            EmbeddedContent local_added_and_removed, local_added;
4✔
3756
            test_reset
4✔
3757
                ->make_local_changes([&](SharedRealm local_realm) {
4✔
3758
                    Obj obj = get_top_object(local_realm);
4✔
3759
                    auto list = obj.get_linklist("array_of_objs");
4✔
3760
                    auto embedded = list.create_and_insert_linked_object(0);
4✔
3761
                    local_added_and_removed.assign_to(embedded);
4✔
3762
                    embedded = list.create_and_insert_linked_object(1);
4✔
3763
                    local_added.assign_to(embedded);
4✔
3764
                    local_realm->commit_transaction();
4✔
3765
                    local_realm->begin_transaction();
4✔
3766
                    list.remove(0);
4✔
3767
                })
4✔
3768
                ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
3769
                    Obj obj = get_top_object(remote_realm);
4✔
3770
                    auto list = obj.get_linklist("array_of_objs");
4✔
3771
                    list.remove(0, list.size()); // individual ArrayErase instructions, not a clear.
4✔
3772
                    remote = TopLevelContent::get_from(obj);
4✔
3773
                })
4✔
3774
                ->on_post_reset([&](SharedRealm local_realm) {
4✔
3775
                    Obj obj = get_top_object(local_realm);
4✔
3776
                    TopLevelContent actual = TopLevelContent::get_from(obj);
4✔
3777
                    if (test_mode == ClientResyncMode::Recover) {
4✔
3778
                        TopLevelContent expected_recovered = remote;
2✔
3779
                        expected_recovered.array_values.insert(expected_recovered.array_values.begin(), local_added);
2✔
3780
                        actual.test(expected_recovered);
2✔
3781
                    }
2✔
3782
                    else {
2✔
3783
                        actual.test(remote);
2✔
3784
                    }
2✔
3785
                })
4✔
3786
                ->run();
4✔
3787
        }
4✔
3788
        SECTION("removing a preexisting list item triggers a list copy") {
112✔
3789
            EmbeddedContent remote_updated_item_0, local_added;
4✔
3790
            test_reset
4✔
3791
                ->make_local_changes([&](SharedRealm local_realm) {
4✔
3792
                    Obj obj = get_top_object(local_realm);
4✔
3793
                    auto list = obj.get_linklist("array_of_objs");
4✔
3794
                    list.remove(0);
4✔
3795
                    list.remove(0);
4✔
3796
                    auto embedded = list.create_and_insert_linked_object(1);
4✔
3797
                    local_added.assign_to(embedded);
4✔
3798
                    local = TopLevelContent::get_from(obj);
4✔
3799
                })
4✔
3800
                ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
3801
                    // any change made to the list here is overwritten by the list copy
2✔
3802
                    Obj obj = get_top_object(remote_realm);
4✔
3803
                    auto list = obj.get_linklist("array_of_objs");
4✔
3804
                    list.remove(1, list.size()); // individual ArrayErase instructions, not a clear.
4✔
3805
                    remote_updated_item_0.assign_to(list.get_object(0));
4✔
3806
                    remote = TopLevelContent::get_from(obj);
4✔
3807
                })
4✔
3808
                ->on_post_reset([&](SharedRealm local_realm) {
4✔
3809
                    Obj obj = get_top_object(local_realm);
4✔
3810
                    TopLevelContent actual = TopLevelContent::get_from(obj);
4✔
3811
                    if (test_mode == ClientResyncMode::Recover) {
4✔
3812
                        actual.test(local);
2✔
3813
                    }
2✔
3814
                    else {
2✔
3815
                        actual.test(remote);
2✔
3816
                    }
2✔
3817
                })
4✔
3818
                ->run();
4✔
3819
        }
4✔
3820
        SECTION("adding and removing a list item when the remote removes the base object has no effect") {
112✔
3821
            EmbeddedContent local_added_at_begin;
4✔
3822
            test_reset
4✔
3823
                ->make_local_changes([&](SharedRealm local_realm) {
4✔
3824
                    Obj obj = get_top_object(local_realm);
4✔
3825
                    auto list = obj.get_linklist("array_of_objs");
4✔
3826
                    auto embedded = list.create_and_insert_linked_object(0);
4✔
3827
                    local_added_at_begin.assign_to(embedded);
4✔
3828
                    local_realm->commit_transaction();
4✔
3829
                    local_realm->begin_transaction();
4✔
3830
                    list.remove(0);
4✔
3831
                })
4✔
3832
                ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
3833
                    // any change made to the list here is overwritten by the list copy
2✔
3834
                    Obj obj = get_top_object(remote_realm);
4✔
3835
                    obj.remove();
4✔
3836
                })
4✔
3837
                ->on_post_reset([&](SharedRealm local_realm) {
4✔
3838
                    advance_and_notify(*local_realm);
4✔
3839
                    TableRef table = get_table(*local_realm, "TopLevel");
4✔
3840
                    REQUIRE(table->size() == 0);
4!
3841
                })
4✔
3842
                ->run();
4✔
3843
        }
4✔
3844
        SECTION("removing a preexisting list item when the remote removes the base object has no effect") {
112✔
3845
            test_reset
4✔
3846
                ->make_local_changes([&](SharedRealm local_realm) {
4✔
3847
                    Obj obj = get_top_object(local_realm);
4✔
3848
                    auto list = obj.get_linklist("array_of_objs");
4✔
3849
                    list.remove(0);
4✔
3850
                })
4✔
3851
                ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
3852
                    // any change made to the list here is overwritten by the list copy
2✔
3853
                    Obj obj = get_top_object(remote_realm);
4✔
3854
                    obj.remove();
4✔
3855
                })
4✔
3856
                ->on_post_reset([&](SharedRealm local_realm) {
4✔
3857
                    advance_and_notify(*local_realm);
4✔
3858
                    TableRef table = get_table(*local_realm, "TopLevel");
4✔
3859
                    REQUIRE(table->size() == 0);
4!
3860
                })
4✔
3861
                ->run();
4✔
3862
        }
4✔
3863
        SECTION("modifications to an embedded object are ignored when the base object is removed") {
112✔
3864
            EmbeddedContent local_modifications;
4✔
3865
            test_reset
4✔
3866
                ->make_local_changes([&](SharedRealm local_realm) {
4✔
3867
                    Obj obj = get_top_object(local_realm);
4✔
3868
                    auto list = obj.get_linklist("array_of_objs");
4✔
3869
                    local_modifications.assign_to(list.get_object(0));
4✔
3870
                })
4✔
3871
                ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
3872
                    // any change made to the list here is overwritten by the list copy
2✔
3873
                    Obj obj = get_top_object(remote_realm);
4✔
3874
                    obj.remove();
4✔
3875
                })
4✔
3876
                ->on_post_reset([&](SharedRealm local_realm) {
4✔
3877
                    advance_and_notify(*local_realm);
4✔
3878
                    TableRef table = get_table(*local_realm, "TopLevel");
4✔
3879
                    REQUIRE(table->size() == 0);
4!
3880
                })
4✔
3881
                ->run();
4✔
3882
        }
4✔
3883
        SECTION("changes made through two layers of embedded lists can be recovered") {
112✔
3884
            EmbeddedContent local_added_at_0, local_added_at_1, remote_added;
4✔
3885
            local_added_at_0.name = "added at 0, moved to 1";
4✔
3886
            local_added_at_0.array_of_seconds = {{}, {}};
4✔
3887
            local_added_at_1.name = "added at 1, bumped to 0";
4✔
3888
            local_added_at_1.array_of_seconds = {{}, {}, {}};
4✔
3889
            remote_added.array_of_seconds = {{}, {}};
4✔
3890
            SecondLevelEmbeddedContent modified, inserted;
4✔
3891
            test_reset
4✔
3892
                ->make_local_changes([&](SharedRealm local_realm) {
4✔
3893
                    Obj obj = get_top_object(local_realm);
4✔
3894
                    auto list = obj.get_linklist("array_of_objs");
4✔
3895
                    auto embedded = list.create_and_insert_linked_object(0);
4✔
3896
                    local_added_at_0.assign_to(embedded);
4✔
3897
                    embedded = list.create_and_insert_linked_object(1);
4✔
3898
                    local_added_at_1.assign_to(embedded);
4✔
3899
                    local_realm->commit_transaction();
4✔
3900
                    local_realm->begin_transaction();
4✔
3901
                    auto list_of_seconds = embedded.get_linklist("array_of_seconds");
4✔
3902
                    list_of_seconds.move(0, 1);
4✔
3903
                    std::iter_swap(local_added_at_1.array_of_seconds.begin(),
4✔
3904
                                   local_added_at_1.array_of_seconds.begin() + 1);
4✔
3905
                    local_realm->commit_transaction();
4✔
3906
                    local_realm->begin_transaction();
4✔
3907
                    list.move(0, 1);
4✔
3908
                    local_realm->commit_transaction();
4✔
3909
                    local_realm->begin_transaction();
4✔
3910
                    modified.assign_to(list_of_seconds.get_object(0));
4✔
3911
                    auto new_second = list_of_seconds.create_and_insert_linked_object(0);
4✔
3912
                    inserted.assign_to(new_second);
4✔
3913
                    local_added_at_1.array_of_seconds[0] = modified;
4✔
3914
                    local_added_at_1.array_of_seconds.insert(local_added_at_1.array_of_seconds.begin(), inserted);
4✔
3915
                })
4✔
3916
                ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
3917
                    Obj obj = get_top_object(remote_realm);
4✔
3918
                    auto list = obj.get_linklist("array_of_objs");
4✔
3919
                    list.remove(0, list.size()); // individual ArrayErase instructions, not a clear.
4✔
3920
                    remote_added.name = "remote added at zero, should end up at the end of the list";
4✔
3921
                    remote_added.assign_to(list.create_and_insert_linked_object(0));
4✔
3922
                    remote = TopLevelContent::get_from(obj);
4✔
3923
                })
4✔
3924
                ->on_post_reset([&](SharedRealm local_realm) {
4✔
3925
                    Obj obj = get_top_object(local_realm);
4✔
3926
                    if (test_mode == ClientResyncMode::Recover) {
4✔
3927
                        auto list = obj.get_linklist("array_of_objs");
2✔
3928
                        REQUIRE(list.size() == 3);
2!
3929
                        EmbeddedContent embedded_0 = *EmbeddedContent::get_from(list.get_object(0));
2✔
3930
                        EmbeddedContent embedded_1 = *EmbeddedContent::get_from(list.get_object(1));
2✔
3931
                        EmbeddedContent embedded_2 = *EmbeddedContent::get_from(list.get_object(2));
2✔
3932
                        embedded_0.test(local_added_at_1); // local added at end, moved to 0
2✔
3933
                        embedded_1.test(local_added_at_0); // local added at begin, bumped up by move
2✔
3934
                        embedded_2.test(remote_added);     // remote added at 0, bumped to 2 by recovered
2✔
3935
                    }
2✔
3936
                    else {
2✔
3937
                        TopLevelContent actual = TopLevelContent::get_from(obj);
2✔
3938
                        actual.test(remote);
2✔
3939
                    }
2✔
3940
                })
4✔
3941
                ->run();
4✔
3942
        }
4✔
3943
        SECTION("insertions to a preexisting object through two layers of embedded lists triggers a list copy") {
112✔
3944
            SecondLevelEmbeddedContent local_added, remote_added;
4✔
3945
            test_reset
4✔
3946
                ->make_local_changes([&](SharedRealm local_realm) {
4✔
3947
                    Obj obj = get_top_object(local_realm);
4✔
3948
                    auto list = obj.get_linklist("array_of_objs");
4✔
3949
                    local_added.assign_to(
4✔
3950
                        list.get_object(0).get_linklist("array_of_seconds").create_and_insert_linked_object(0));
4✔
3951
                })
4✔
3952
                ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
3953
                    Obj obj = get_top_object(remote_realm);
4✔
3954
                    auto list = obj.get_linklist("array_of_objs");
4✔
3955
                    remote_added.assign_to(
4✔
3956
                        list.get_object(0).get_linklist("array_of_seconds").create_and_insert_linked_object(0));
4✔
3957
                    list.move(0, 1);
4✔
3958
                    remote = TopLevelContent::get_from(obj);
4✔
3959
                })
4✔
3960
                ->on_post_reset([&](SharedRealm local_realm) {
4✔
3961
                    Obj obj = get_top_object(local_realm);
4✔
3962
                    if (test_mode == ClientResyncMode::Recover) {
4✔
3963
                        auto list = obj.get_linklist("array_of_objs");
2✔
3964
                        REQUIRE(list.size() == 3);
2!
3965
                        EmbeddedContent embedded_0 = *EmbeddedContent::get_from(list.get_object(0));
2✔
3966
                        EmbeddedContent embedded_1 = *EmbeddedContent::get_from(list.get_object(1));
2✔
3967
                        EmbeddedContent embedded_2 = *EmbeddedContent::get_from(list.get_object(2));
2✔
3968
                        REQUIRE(embedded_0.array_of_seconds.size() == 1);
2!
3969
                        embedded_0.array_of_seconds[0].test(local_added);
2✔
3970
                        REQUIRE(embedded_1.array_of_seconds.size() ==
2!
3971
                                0); // remote changes overwritten by local list copy
2✔
3972
                        REQUIRE(embedded_2.array_of_seconds.size() == 0);
2!
3973
                    }
2✔
3974
                    else {
2✔
3975
                        TopLevelContent actual = TopLevelContent::get_from(obj);
2✔
3976
                        actual.test(remote);
2✔
3977
                    }
2✔
3978
                })
4✔
3979
                ->run();
4✔
3980
        }
4✔
3981

56✔
3982
        SECTION("modifications to a preexisting object through two layers of embedded lists triggers a list copy") {
112✔
3983
            SecondLevelEmbeddedContent preexisting_item, local_modified, remote_added;
4✔
3984
            initial.array_values[0].array_of_seconds.push_back(preexisting_item);
4✔
3985
            const size_t initial_item_pos = initial.array_values[0].array_of_seconds.size() - 1;
4✔
3986
            local = initial;
4✔
3987
            remote = initial;
4✔
3988
            local.array_values[0].array_of_seconds[initial_item_pos] = local_modified;
4✔
3989
            remote.array_values[0].array_of_seconds.push_back(remote_added); // overwritten by local!
4✔
3990
            TopLevelContent expected_recovered = local;
4✔
3991
            reset_embedded_object({local}, {remote}, expected_recovered);
4✔
3992
        }
4✔
3993

56✔
3994
        SECTION("add int") {
112✔
3995
            auto add_to_dict_item = [&](SharedRealm realm, std::string key, int64_t addition) {
20✔
3996
                Obj obj = get_top_object(realm);
20✔
3997
                auto dict = obj.get_dictionary("embedded_dict");
20✔
3998
                auto embedded = dict.get_object(key);
20✔
3999
                REQUIRE(embedded);
20!
4000
                embedded.add_int("int_value", addition);
20✔
4001
                return TopLevelContent::get_from(obj);
20✔
4002
            };
20✔
4003
            TopLevelContent expected_recovered;
16✔
4004
            const std::string existing_key = "foo";
16✔
4005

8✔
4006
            test_reset->on_post_reset([&](SharedRealm local_realm) {
14✔
4007
                Obj obj = get_top_object(local_realm);
12✔
4008
                TopLevelContent actual = TopLevelContent::get_from(obj);
12✔
4009
                actual.test(test_mode == ClientResyncMode::Recover ? expected_recovered : initial);
9✔
4010
            });
12✔
4011
            int64_t initial_value = initial.dict_values[existing_key]->int_value;
16✔
4012
            std::mt19937_64 engine(std::random_device{}());
16✔
4013
            std::uniform_int_distribution<int64_t> rng(-10'000'000'000, 10'000'000'000);
16✔
4014

8✔
4015
            int64_t addition = rng(engine);
16✔
4016
            SECTION("local add_int to an existing dictionary item") {
16✔
4017
                INFO("adding " << initial_value << " with " << addition);
4✔
4018
                expected_recovered = initial;
4✔
4019
                expected_recovered.dict_values[existing_key]->int_value += addition;
4✔
4020
                test_reset
4✔
4021
                    ->make_local_changes([&](SharedRealm local) {
4✔
4022
                        add_to_dict_item(local, existing_key, addition);
4✔
4023
                    })
4✔
4024
                    ->run();
4✔
4025
            }
4✔
4026
            SECTION("local and remote both create the same dictionary item and add to it") {
16✔
4027
                int64_t remote_addition = rng(engine);
4✔
4028
                INFO("adding " << initial_value << " with local " << addition << " and remote " << remote_addition);
4✔
4029
                expected_recovered = initial;
4✔
4030
                expected_recovered.dict_values[existing_key]->int_value += (addition + remote_addition);
4✔
4031
                test_reset
4✔
4032
                    ->make_local_changes([&](SharedRealm local) {
4✔
4033
                        add_to_dict_item(local, existing_key, addition);
4✔
4034
                    })
4✔
4035
                    ->make_remote_changes([&](SharedRealm remote) {
4✔
4036
                        initial = add_to_dict_item(remote, existing_key, remote_addition);
4✔
4037
                    })
4✔
4038
                    ->run();
4✔
4039
            }
4✔
4040
            SECTION("local add_int on a dictionary item which the remote removed is ignored") {
16✔
4041
                INFO("adding " << initial_value << " with " << addition);
4✔
4042
                test_reset
4✔
4043
                    ->make_local_changes([&](SharedRealm local) {
4✔
4044
                        add_to_dict_item(local, existing_key, addition);
4✔
4045
                    })
4✔
4046
                    ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
4047
                        Obj obj = get_top_object(remote_realm);
4✔
4048
                        auto dict = obj.get_dictionary("embedded_dict");
4✔
4049
                        dict.erase(Mixed{existing_key});
4✔
4050
                        initial = TopLevelContent::get_from(obj);
4✔
4051
                        expected_recovered = initial;
4✔
4052
                    })
4✔
4053
                    ->run();
4✔
4054
            }
4✔
4055
            SECTION("local add_int on a dictionary item when the entire root object is removed by the remote removed "
16✔
4056
                    "is ignored") {
10✔
4057
                INFO("adding " << initial_value << " with " << addition);
4✔
4058
                test_reset
4✔
4059
                    ->make_local_changes([&](SharedRealm local) {
4✔
4060
                        add_to_dict_item(local, existing_key, addition);
4✔
4061
                    })
4✔
4062
                    ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
4063
                        Obj obj = get_top_object(remote_realm);
4✔
4064
                        TableRef table = obj.get_table();
4✔
4065
                        obj.remove();
4✔
4066
                        REQUIRE(table->size() == 0);
4!
4067
                    })
4✔
4068
                    ->on_post_reset([&](SharedRealm local_realm) {
4✔
4069
                        advance_and_notify(*local_realm);
4✔
4070
                        TableRef table = get_table(*local_realm, "TopLevel");
4✔
4071
                        REQUIRE(table->size() == 0);
4!
4072
                    })
4✔
4073
                    ->run();
4✔
4074
            }
4✔
4075
        }
16✔
4076
    }
112✔
4077
    SECTION("remote adds a top level link cycle") {
168✔
4078
        TopLevelContent local;
4✔
4079
        TopLevelContent remote = local;
4✔
4080
        remote.link_value->second_level->pk_of_linked_object = Mixed{pk_val};
4✔
4081
        TopLevelContent expected_recovered = remote;
4✔
4082
        expected_recovered.apply_recovery_from(local);
4✔
4083
        // the remote change exists because no local instruction set the value to anything (default)
2✔
4084
        expected_recovered.link_value->second_level->pk_of_linked_object = Mixed{pk_val};
4✔
4085
        reset_embedded_object({local}, {remote}, expected_recovered);
4✔
4086
    }
4✔
4087
    SECTION("local adds a top level link cycle") {
168✔
4088
        TopLevelContent local;
4✔
4089
        TopLevelContent remote = local;
4✔
4090
        local.link_value->second_level->pk_of_linked_object = Mixed{pk_val};
4✔
4091
        TopLevelContent expected_recovered = remote;
4✔
4092
        expected_recovered.apply_recovery_from(local);
4✔
4093
        reset_embedded_object({local}, {remote}, expected_recovered);
4✔
4094
    }
4✔
4095
    SECTION("server adds embedded object classes") {
168✔
4096
        SyncTestFile config2(init_sync_manager.app(), "default");
4✔
4097
        config2.schema = config.schema;
4✔
4098
        config.schema = Schema{shared_class};
4✔
4099
        test_reset = reset_utils::make_fake_local_client_reset(config, config2);
4✔
4100
        TopLevelContent remote_content;
4✔
4101

2✔
4102
        test_reset
4✔
4103
            ->make_remote_changes([&](SharedRealm remote) {
4✔
4104
                advance_and_notify(*remote);
4✔
4105
                TableRef table = get_table(*remote, "TopLevel");
4✔
4106
                auto obj = table->create_object_with_primary_key(pk_val);
4✔
4107
                REQUIRE(table->size() == 1);
4!
4108
                remote_content.assign_to(obj);
4✔
4109
            })
4✔
4110
            ->on_post_reset([&](SharedRealm local) {
4✔
4111
                advance_and_notify(*local);
4✔
4112
                TableRef table = get_table(*local, "TopLevel");
4✔
4113
                REQUIRE(table->size() == 1);
4!
4114
                Obj obj = *table->begin();
4✔
4115
                TopLevelContent actual = TopLevelContent::get_from(obj);
4✔
4116
                actual.test(remote_content);
4✔
4117
            })
4✔
4118
            ->run();
4✔
4119
    }
4✔
4120
    SECTION("client adds embedded object classes") {
168✔
4121
        SyncTestFile config2(init_sync_manager.app(), "default");
4✔
4122
        config2.schema = Schema{shared_class};
4✔
4123
        test_reset = reset_utils::make_fake_local_client_reset(config, config2);
4✔
4124
        TopLevelContent local_content;
4✔
4125
        test_reset->make_local_changes([&](SharedRealm local) {
4✔
4126
            TableRef table = get_table(*local, "TopLevel");
4✔
4127
            auto obj = table->create_object_with_primary_key(pk_val);
4✔
4128
            REQUIRE(table->size() == 1);
4!
4129
            local_content.assign_to(obj);
4✔
4130
        });
4✔
4131
        if (test_mode == ClientResyncMode::DiscardLocal) {
4✔
4132
            REQUIRE_THROWS_WITH(test_reset->run(), "Client reset cannot recover when classes have been removed: "
2✔
4133
                                                   "{EmbeddedObject, EmbeddedObject2, TopLevel}");
2✔
4134
        }
2✔
4135
        else {
2✔
4136
            // In recovery mode, AddTable should succeed if the server is in dev mode, and fail
1✔
4137
            // if the server is in production which in that case the changes will be rejected.
1✔
4138
            // Since this is a fake reset, it always succeeds here.
1✔
4139
            test_reset
2✔
4140
                ->on_post_reset([&](SharedRealm local) {
2✔
4141
                    TableRef table = get_table(*local, "TopLevel");
2✔
4142
                    REQUIRE(table->size() == 1);
2!
4143
                })
2✔
4144
                ->run();
2✔
4145
        }
2✔
4146
    }
4✔
4147
}
168✔
4148

4149
TEST_CASE("client reset with nested collection", "[client reset][local][nested collection]") {
72✔
4150

36✔
4151
    if (!util::EventLoop::has_implementation())
72✔
NEW
4152
        return;
×
4153

36✔
4154
    TestSyncManager init_sync_manager;
72✔
4155
    SyncTestFile config(init_sync_manager.app(), "default");
72✔
4156
    config.cache = false;
72✔
4157
    config.automatic_change_notifications = false;
72✔
4158
    ClientResyncMode test_mode = GENERATE(ClientResyncMode::DiscardLocal, ClientResyncMode::Recover);
72✔
4159
    CAPTURE(test_mode);
72✔
4160
    config.sync_config->client_resync_mode = test_mode;
72✔
4161

36✔
4162
    ObjectSchema shared_class = {"object",
72✔
4163
                                 {
72✔
4164
                                     {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
72✔
4165
                                     {"value", PropertyType::Int},
72✔
4166
                                 }};
72✔
4167

36✔
4168
    config.schema = Schema{shared_class,
72✔
4169
                           {"TopLevel",
72✔
4170
                            {
72✔
4171
                                {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
72✔
4172
                                {"any_mixed", PropertyType::Mixed | PropertyType::Nullable},
72✔
4173
                            }}};
72✔
4174

36✔
4175
    SECTION("add nested collection locally") {
72✔
4176
        ObjectId pk_val = ObjectId::gen();
4✔
4177
        SyncTestFile config2(init_sync_manager.app(), "default");
4✔
4178
        config2.schema = Schema{shared_class};
4✔
4179

2✔
4180
        auto test_reset = reset_utils::make_fake_local_client_reset(config, config2);
4✔
4181
        test_reset->make_local_changes([&](SharedRealm local) {
4✔
4182
            advance_and_notify(*local);
4✔
4183
            TableRef table = get_table(*local, "TopLevel");
4✔
4184
            auto obj = table->create_object_with_primary_key(pk_val);
4✔
4185
            auto col = table->get_column_key("any_mixed");
4✔
4186
            obj.set_collection(col, CollectionType::List);
4✔
4187
            List list{local, obj, col};
4✔
4188
            list.insert_collection(0, CollectionType::List);
4✔
4189
            auto nlist = list.get_list(0);
4✔
4190
            nlist.add(Mixed{10});
4✔
4191
            nlist.add(Mixed{"Test"});
4✔
4192
            REQUIRE(table->size() == 1);
4!
4193
        });
4✔
4194
        if (test_mode == ClientResyncMode::DiscardLocal) {
4✔
4195
            REQUIRE_THROWS_WITH(test_reset->run(), "Client reset cannot recover when classes have been removed: "
2✔
4196
                                                   "{TopLevel}");
2✔
4197
        }
2✔
4198
        else {
2✔
4199
            test_reset
2✔
4200
                ->on_post_reset([&](SharedRealm local) {
2✔
4201
                    advance_and_notify(*local);
2✔
4202
                    TableRef table = get_table(*local, "TopLevel");
2✔
4203
                    REQUIRE(table->size() == 1);
2!
4204
                    auto obj = table->get_object(0);
2✔
4205
                    auto col = table->get_column_key("any_mixed");
2✔
4206
                    List list{local, obj, col};
2✔
4207
                    REQUIRE(list.size() == 1);
2!
4208
                    auto nlist = list.get_list(0);
2✔
4209
                    REQUIRE(nlist.size() == 2);
2!
4210
                    REQUIRE(nlist.get_any(0).get_int() == 10);
2!
4211
                    REQUIRE(nlist.get_any(1).get_string() == "Test");
2!
4212
                })
2✔
4213
                ->run();
2✔
4214
        }
2✔
4215
    }
4✔
4216
    SECTION("server adds nested collection. List of nested collections") {
72✔
4217
        ObjectId pk_val = ObjectId::gen();
4✔
4218
        SyncTestFile config2(init_sync_manager.app(), "default");
4✔
4219
        config2.schema = config.schema;
4✔
4220
        config.schema = Schema{shared_class};
4✔
4221
        auto test_reset = reset_utils::make_fake_local_client_reset(config, config2);
4✔
4222

2✔
4223
        test_reset
4✔
4224
            ->make_remote_changes([&](SharedRealm remote) {
4✔
4225
                advance_and_notify(*remote);
4✔
4226
                TableRef table = get_table(*remote, "TopLevel");
4✔
4227
                auto obj = table->create_object_with_primary_key(pk_val);
4✔
4228
                auto col = table->get_column_key("any_mixed");
4✔
4229
                // List
2✔
4230
                obj.set_collection(col, CollectionType::List);
4✔
4231
                List list{remote, obj, col};
4✔
4232
                // primitive type
2✔
4233
                list.add(Mixed{42});
4✔
4234
                // List<List<Mixed>>
2✔
4235
                list.insert_collection(1, CollectionType::List);
4✔
4236
                auto nlist = list.get_list(1);
4✔
4237
                nlist.add(Mixed{10});
4✔
4238
                nlist.add(Mixed{"Test"});
4✔
4239
                // List<Dictionary>
2✔
4240
                list.insert_collection(2, CollectionType::Dictionary);
4✔
4241
                auto n_dict = list.get_dictionary(2);
4✔
4242
                n_dict.insert("Test", Mixed{"10"});
4✔
4243
                n_dict.insert("Test1", Mixed{10});
4✔
4244
                // List<Set<Mixed>>
2✔
4245
                list.insert_collection(3, CollectionType::Set);
4✔
4246
                auto n_set = list.get_set(3);
4✔
4247
                n_set.insert(Mixed{"Hello"});
4✔
4248
                n_set.insert(Mixed{"World"});
4✔
4249
                REQUIRE(list.size() == 4);
4!
4250
                REQUIRE(table->size() == 1);
4!
4251
            })
4✔
4252
            ->on_post_reset([&](SharedRealm local) {
4✔
4253
                advance_and_notify(*local);
4✔
4254
                TableRef table = get_table(*local, "TopLevel");
4✔
4255
                REQUIRE(table->size() == 1);
4!
4256
                auto obj = table->get_object(0);
4✔
4257
                auto col = table->get_column_key("any_mixed");
4✔
4258
                List list{local, obj, col};
4✔
4259
                REQUIRE(list.size() == 4);
4!
4260
                auto mixed = list.get_any(0);
4✔
4261
                REQUIRE(mixed.get_int() == 42);
4!
4262
                auto nlist = list.get_list(1);
4✔
4263
                REQUIRE(nlist.size() == 2);
4!
4264
                REQUIRE(nlist.get_any(0).get_int() == 10);
4!
4265
                REQUIRE(nlist.get_any(1).get_string() == "Test");
4!
4266
                auto n_dict = list.get_dictionary(2);
4✔
4267
                REQUIRE(n_dict.size() == 2);
4!
4268
                REQUIRE(n_dict.get<Mixed>("Test").get_string() == "10");
4!
4269
                REQUIRE(n_dict.get<Mixed>("Test1").get_int() == 10);
4!
4270
                auto n_set = list.get_set(3);
4✔
4271
                REQUIRE(n_set.size() == 2);
4!
4272
                REQUIRE(n_set.find_any("Hello") == 0);
4!
4273
                REQUIRE(n_set.find_any("World") == 1);
4!
4274
            })
4✔
4275
            ->run();
4✔
4276
    }
4✔
4277
    SECTION("server adds nested collection. Dictionary of nested collections") {
72✔
4278
        ObjectId pk_val = ObjectId::gen();
4✔
4279
        SyncTestFile config2(init_sync_manager.app(), "default");
4✔
4280
        config2.schema = config.schema;
4✔
4281
        config.schema = Schema{shared_class};
4✔
4282
        auto test_reset = reset_utils::make_fake_local_client_reset(config, config2);
4✔
4283
        test_reset
4✔
4284
            ->make_remote_changes([&](SharedRealm remote) {
4✔
4285
                advance_and_notify(*remote);
4✔
4286
                TableRef table = get_table(*remote, "TopLevel");
4✔
4287
                auto obj = table->create_object_with_primary_key(pk_val);
4✔
4288
                auto col = table->get_column_key("any_mixed");
4✔
4289
                // List
2✔
4290
                obj.set_collection(col, CollectionType::Dictionary);
4✔
4291
                object_store::Dictionary dict{remote, obj, col};
4✔
4292
                // primitive type
2✔
4293
                dict.insert("Scalar", Mixed{42});
4✔
4294
                // Dictionary<List<Mixed>>
2✔
4295
                dict.insert_collection("List", CollectionType::List);
4✔
4296
                auto nlist = dict.get_list("List");
4✔
4297
                nlist.add(Mixed{10});
4✔
4298
                nlist.add(Mixed{"Test"});
4✔
4299
                // Dictionary<Dictionary>
2✔
4300
                dict.insert_collection("Dict", CollectionType::Dictionary);
4✔
4301
                auto n_dict = dict.get_dictionary("Dict");
4✔
4302
                n_dict.insert("Test", Mixed{"10"});
4✔
4303
                n_dict.insert("Test1", Mixed{10});
4✔
4304
                // List<Set<Mixed>>
2✔
4305
                dict.insert_collection("Set", CollectionType::Set);
4✔
4306
                auto n_set = dict.get_set("Set");
4✔
4307
                n_set.insert(Mixed{"Hello"});
4✔
4308
                n_set.insert(Mixed{"World"});
4✔
4309
                REQUIRE(dict.size() == 4);
4!
4310
                REQUIRE(table->size() == 1);
4!
4311
            })
4✔
4312
            ->on_post_reset([&](SharedRealm local) {
4✔
4313
                advance_and_notify(*local);
4✔
4314
                TableRef table = get_table(*local, "TopLevel");
4✔
4315
                REQUIRE(table->size() == 1);
4!
4316
                auto obj = table->get_object(0);
4✔
4317
                auto col = table->get_column_key("any_mixed");
4✔
4318
                object_store::Dictionary dict{local, obj, col};
4✔
4319
                REQUIRE(dict.size() == 4);
4!
4320
                auto mixed = dict.get_any("Scalar");
4✔
4321
                REQUIRE(mixed.get_int() == 42);
4!
4322
                auto nlist = dict.get_list("List");
4✔
4323
                REQUIRE(nlist.size() == 2);
4!
4324
                REQUIRE(nlist.get_any(0).get_int() == 10);
4!
4325
                REQUIRE(nlist.get_any(1).get_string() == "Test");
4!
4326
                auto n_dict = dict.get_dictionary("Dict");
4✔
4327
                REQUIRE(n_dict.size() == 2);
4!
4328
                REQUIRE(n_dict.get<Mixed>("Test").get_string() == "10");
4!
4329
                REQUIRE(n_dict.get<Mixed>("Test1").get_int() == 10);
4!
4330
                auto n_set = dict.get_set("Set");
4✔
4331
                REQUIRE(n_set.size() == 2);
4!
4332
                REQUIRE(n_set.find_any("Hello") == 0);
4!
4333
                REQUIRE(n_set.find_any("World") == 1);
4!
4334
            })
4✔
4335
            ->run();
4✔
4336
    }
4✔
4337
    SECTION("add nested collection both locally and remotely List vs Set") {
72✔
4338
        ObjectId pk_val = ObjectId::gen();
4✔
4339
        SyncTestFile config2(init_sync_manager.app(), "default");
4✔
4340
        config2.schema = config.schema;
4✔
4341
        auto test_reset = reset_utils::make_fake_local_client_reset(config, config2);
4✔
4342
        test_reset
4✔
4343
            ->make_local_changes([&](SharedRealm local) {
4✔
4344
                advance_and_notify(*local);
4✔
4345
                auto table = get_table(*local, "TopLevel");
4✔
4346
                auto obj = table->create_object_with_primary_key(pk_val);
4✔
4347
                auto col = table->get_column_key("any_mixed");
4✔
4348
                obj.set_collection(col, CollectionType::List);
4✔
4349
                List list{local, obj, col};
4✔
4350
                list.insert(0, Mixed{30});
4✔
4351
                REQUIRE(list.size() == 1);
4!
4352
            })
4✔
4353
            ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
4354
                advance_and_notify(*remote_realm);
4✔
4355
                auto table = get_table(*remote_realm, "TopLevel");
4✔
4356
                auto obj = table->create_object_with_primary_key(pk_val);
4✔
4357
                auto col = table->get_column_key("any_mixed");
4✔
4358
                obj.set_collection(col, CollectionType::Set);
4✔
4359
                object_store::Set set{remote_realm, obj, col};
4✔
4360
                set.insert(Mixed{40});
4✔
4361
                REQUIRE(set.size() == 1);
4!
4362
            })
4✔
4363
            ->on_post_reset([&](SharedRealm local_realm) {
4✔
4364
                advance_and_notify(*local_realm);
4✔
4365
                if (test_mode == ClientResyncMode::DiscardLocal) {
4✔
4366
                    TableRef table = get_table(*local_realm, "TopLevel");
2✔
4367
                    REQUIRE(table->size() == 1);
2!
4368
                    auto obj = table->get_object(0);
2✔
4369
                    auto col = table->get_column_key("any_mixed");
2✔
4370
                    object_store::Set set{local_realm, obj, col};
2✔
4371
                    REQUIRE(set.size() == 1);
2!
4372
                    REQUIRE(set.get_any(0).get_int() == 40);
2!
4373
                }
2✔
4374
                else {
2✔
4375
                    TableRef table = get_table(*local_realm, "TopLevel");
2✔
4376
                    REQUIRE(table->size() == 1);
2!
4377
                    auto obj = table->get_object(0);
2✔
4378
                    auto col = table->get_column_key("any_mixed");
2✔
4379
                    List list{local_realm, obj, col};
2✔
4380
                    REQUIRE(list.size() == 1);
2!
4381
                    REQUIRE(list.get_any(0).get_int() == 30);
2!
4382
                }
2✔
4383
            })
4✔
4384
            ->run();
4✔
4385
    }
4✔
4386
    SECTION("add nested collection both locally and remotely List vs Dictionary") {
72✔
4387
        ObjectId pk_val = ObjectId::gen();
4✔
4388
        SyncTestFile config2(init_sync_manager.app(), "default");
4✔
4389
        config2.schema = config.schema;
4✔
4390
        auto test_reset = reset_utils::make_fake_local_client_reset(config, config2);
4✔
4391
        test_reset
4✔
4392
            ->make_local_changes([&](SharedRealm local) {
4✔
4393
                advance_and_notify(*local);
4✔
4394
                auto table = get_table(*local, "TopLevel");
4✔
4395
                auto obj = table->create_object_with_primary_key(pk_val);
4✔
4396
                auto col = table->get_column_key("any_mixed");
4✔
4397
                obj.set_collection(col, CollectionType::List);
4✔
4398
                List list{local, obj, col};
4✔
4399
                list.insert(0, Mixed{30});
4✔
4400
                REQUIRE(list.size() == 1);
4!
4401
            })
4✔
4402
            ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
4403
                advance_and_notify(*remote_realm);
4✔
4404
                auto table = get_table(*remote_realm, "TopLevel");
4✔
4405
                auto obj = table->create_object_with_primary_key(pk_val);
4✔
4406
                auto col = table->get_column_key("any_mixed");
4✔
4407
                obj.set_collection(col, CollectionType::Dictionary);
4✔
4408
                object_store::Dictionary dict{remote_realm, obj, col};
4✔
4409
                dict.insert("Test", Mixed{40});
4✔
4410
                REQUIRE(dict.size() == 1);
4!
4411
            })
4✔
4412
            ->on_post_reset([&](SharedRealm local_realm) {
4✔
4413
                advance_and_notify(*local_realm);
4✔
4414
                if (test_mode == ClientResyncMode::DiscardLocal) {
4✔
4415
                    TableRef table = get_table(*local_realm, "TopLevel");
2✔
4416
                    REQUIRE(table->size() == 1);
2!
4417
                    auto obj = table->get_object(0);
2✔
4418
                    auto col = table->get_column_key("any_mixed");
2✔
4419
                    object_store::Dictionary dictionary{local_realm, obj, col};
2✔
4420
                    REQUIRE(dictionary.size() == 1);
2!
4421
                    REQUIRE(dictionary.get_any("Test").get_int() == 40);
2!
4422
                }
2✔
4423
                else {
2✔
4424
                    TableRef table = get_table(*local_realm, "TopLevel");
2✔
4425
                    REQUIRE(table->size() == 1);
2!
4426
                    auto obj = table->get_object(0);
2✔
4427
                    auto col = table->get_column_key("any_mixed");
2✔
4428
                    List list{local_realm, obj, col};
2✔
4429
                    REQUIRE(list.size() == 1);
2!
4430
                    REQUIRE(list.get_any(0) == 30);
2!
4431
                }
2✔
4432
            })
4✔
4433
            ->run();
4✔
4434
    }
4✔
4435
    SECTION("add nested collection both locally and remotely. Nesting levels mismatch List vs Dictionary") {
72✔
4436
        ObjectId pk_val = ObjectId::gen();
4✔
4437
        SyncTestFile config2(init_sync_manager.app(), "default");
4✔
4438
        config2.schema = config.schema;
4✔
4439
        auto test_reset = reset_utils::make_fake_local_client_reset(config, config2);
4✔
4440
        test_reset
4✔
4441
            ->make_local_changes([&](SharedRealm local) {
4✔
4442
                advance_and_notify(*local);
4✔
4443
                auto table = get_table(*local, "TopLevel");
4✔
4444
                auto obj = table->create_object_with_primary_key(pk_val);
4✔
4445
                auto col = table->get_column_key("any_mixed");
4✔
4446
                obj.set_collection(col, CollectionType::List);
4✔
4447
                List list{local, obj, col};
4✔
4448
                list.insert_collection(0, CollectionType::Dictionary);
4✔
4449
                auto dict = list.get_dictionary(0);
4✔
4450
                dict.insert("Test", Mixed{30});
4✔
4451
                REQUIRE(list.size() == 1);
4!
4452
            })
4✔
4453
            ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
4454
                advance_and_notify(*remote_realm);
4✔
4455
                auto table = get_table(*remote_realm, "TopLevel");
4✔
4456
                auto obj = table->create_object_with_primary_key(pk_val);
4✔
4457
                auto col = table->get_column_key("any_mixed");
4✔
4458
                obj.set_collection(col, CollectionType::List);
4✔
4459
                List list{remote_realm, obj, col};
4✔
4460
                list.insert_collection(0, CollectionType::List);
4✔
4461
                auto nlist = list.get_list(0);
4✔
4462
                nlist.insert(0, Mixed{30});
4✔
4463
                REQUIRE(nlist.size() == 1);
4!
4464
            })
4✔
4465
            ->on_post_reset([&](SharedRealm local_realm) {
4✔
4466
                advance_and_notify(*local_realm);
4✔
4467
                if (test_mode == ClientResyncMode::DiscardLocal) {
4✔
4468
                    TableRef table = get_table(*local_realm, "TopLevel");
2✔
4469
                    REQUIRE(table->size() == 1);
2!
4470
                    auto obj = table->get_object(0);
2✔
4471
                    auto col = table->get_column_key("any_mixed");
2✔
4472
                    List list{local_realm, obj, col};
2✔
4473
                    REQUIRE(list.size() == 1);
2!
4474
                    auto nlist = list.get_list(0);
2✔
4475
                    REQUIRE(nlist.size() == 1);
2!
4476
                    REQUIRE(nlist.get<Mixed>(0).get_int() == 30);
2!
4477
                }
2✔
4478
                else {
2✔
4479
                    TableRef table = get_table(*local_realm, "TopLevel");
2✔
4480
                    REQUIRE(table->size() == 1);
2!
4481
                    auto obj = table->get_object(0);
2✔
4482
                    auto col = table->get_column_key("any_mixed");
2✔
4483
                    List list{local_realm, obj, col};
2✔
4484
                    REQUIRE(list.size() == 2);
2!
4485
                    auto n_dict = list.get_dictionary(0);
2✔
4486
                    REQUIRE(n_dict.size() == 1);
2!
4487
                    REQUIRE(n_dict.get<Mixed>("Test").get_int() == 30);
2!
4488
                    auto n_list = list.get_list(1);
2✔
4489
                    REQUIRE(n_list.size() == 1);
2!
4490
                    REQUIRE(n_list.get_any(0) == 30);
2!
4491
                }
2✔
4492
            })
4✔
4493
            ->run();
4✔
4494
    }
4✔
4495
    SECTION("add nested collection both locally and remotely. Collections matched. Merge collections if not discard "
72✔
4496
            "local") {
38✔
4497
        ObjectId pk_val = ObjectId::gen();
4✔
4498
        SyncTestFile config2(init_sync_manager.app(), "default");
4✔
4499
        config2.schema = config.schema;
4✔
4500
        auto test_reset = reset_utils::make_fake_local_client_reset(config, config2);
4✔
4501
        test_reset
4✔
4502
            ->make_local_changes([&](SharedRealm local) {
4✔
4503
                advance_and_notify(*local);
4✔
4504
                auto table = get_table(*local, "TopLevel");
4✔
4505
                auto obj = table->create_object_with_primary_key(pk_val);
4✔
4506
                auto col = table->get_column_key("any_mixed");
4✔
4507
                obj.set_collection(col, CollectionType::List);
4✔
4508
                List list{local, obj, col};
4✔
4509
                list.insert_collection(0, CollectionType::List);
4✔
4510
                auto n_list = list.get_list(0);
4✔
4511
                n_list.insert(0, Mixed{30});
4✔
4512
                list.insert_collection(1, CollectionType::Dictionary);
4✔
4513
                auto dict = list.get_dictionary(1);
4✔
4514
                dict.insert("Test", Mixed{10});
4✔
4515
                REQUIRE(list.size() == 2);
4!
4516
            })
4✔
4517
            ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
4518
                advance_and_notify(*remote_realm);
4✔
4519
                auto table = get_table(*remote_realm, "TopLevel");
4✔
4520
                auto obj = table->create_object_with_primary_key(pk_val);
4✔
4521
                auto col = table->get_column_key("any_mixed");
4✔
4522
                obj.set_collection(col, CollectionType::List);
4✔
4523
                List list{remote_realm, obj, col};
4✔
4524
                list.insert_collection(0, CollectionType::List);
4✔
4525
                auto n_list = list.get_list(0);
4✔
4526
                n_list.insert(0, Mixed{40});
4✔
4527
                list.insert_collection(1, CollectionType::Dictionary);
4✔
4528
                auto dict = list.get_dictionary(1);
4✔
4529
                dict.insert("Test1", Mixed{11});
4✔
4530
                REQUIRE(list.size() == 2);
4!
4531
            })
4✔
4532
            ->on_post_reset([&](SharedRealm local_realm) {
4✔
4533
                advance_and_notify(*local_realm);
4✔
4534
                TableRef table = get_table(*local_realm, "TopLevel");
4✔
4535
                REQUIRE(table->size() == 1);
4!
4536
                auto obj = table->get_object(0);
4✔
4537
                auto col = table->get_column_key("any_mixed");
4✔
4538
                List list{local_realm, obj, col};
4✔
4539
                if (test_mode == ClientResyncMode::DiscardLocal) {
4✔
4540
                    REQUIRE(list.size() == 2);
2!
4541
                    auto n_list = list.get_list(0);
2✔
4542
                    REQUIRE(n_list.get_any(0).get_int() == 40);
2!
4543
                    auto n_dict = list.get_dictionary(1);
2✔
4544
                    REQUIRE(n_dict.size() == 1);
2!
4545
                    REQUIRE(n_dict.get<Mixed>("Test1").get_int() == 11);
2!
4546
                }
2✔
4547
                else {
2✔
4548
                    REQUIRE(list.size() == 4);
2!
4549
                    auto n_list = list.get_list(0);
2✔
4550
                    REQUIRE(n_list.size() == 1);
2!
4551
                    REQUIRE(n_list.get_any(0).get_int() == 30);
2!
4552
                    auto n_dict = list.get_dictionary(1);
2✔
4553
                    REQUIRE(n_dict.size() == 1);
2!
4554
                    REQUIRE(n_dict.get<Mixed>("Test").get_int() == 10);
2!
4555
                    auto n_list1 = list.get_list(2);
2✔
4556
                    REQUIRE(n_list1.size() == 1);
2!
4557
                    REQUIRE(n_list1.get_any(0).get_int() == 40);
2!
4558
                    auto n_dict1 = list.get_dictionary(3);
2✔
4559
                    REQUIRE(n_dict1.size() == 1);
2!
4560
                    REQUIRE(n_dict1.get<Mixed>("Test1").get_int() == 11);
2!
4561
                }
2✔
4562
            })
4✔
4563
            ->run();
4✔
4564
    }
4✔
4565
    SECTION("add nested collection both locally and remotely. Collections matched. Mix collections with values") {
72✔
4566
        ObjectId pk_val = ObjectId::gen();
4✔
4567
        SyncTestFile config2(init_sync_manager.app(), "default");
4✔
4568
        config2.schema = config.schema;
4✔
4569
        auto test_reset = reset_utils::make_fake_local_client_reset(config, config2);
4✔
4570
        test_reset
4✔
4571
            ->make_local_changes([&](SharedRealm local) {
4✔
4572
                advance_and_notify(*local);
4✔
4573
                auto table = get_table(*local, "TopLevel");
4✔
4574
                auto obj = table->create_object_with_primary_key(pk_val);
4✔
4575
                auto col = table->get_column_key("any_mixed");
4✔
4576
                obj.set_collection(col, CollectionType::List);
4✔
4577
                List list{local, obj, col};
4✔
4578
                list.insert_collection(0, CollectionType::List);
4✔
4579
                auto n_list = list.get_list(0);
4✔
4580
                n_list.insert(0, Mixed{30});
4✔
4581
                list.insert_collection(1, CollectionType::Dictionary);
4✔
4582
                auto dict = list.get_dictionary(1);
4✔
4583
                dict.insert("Test", Mixed{10});
4✔
4584
                list.insert(0, Mixed{2}); // this shifts all the other collections by 1
4✔
4585
                REQUIRE(list.size() == 3);
4!
4586
            })
4✔
4587
            ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
4588
                advance_and_notify(*remote_realm);
4✔
4589
                auto table = get_table(*remote_realm, "TopLevel");
4✔
4590
                auto obj = table->create_object_with_primary_key(pk_val);
4✔
4591
                auto col = table->get_column_key("any_mixed");
4✔
4592
                obj.set_collection(col, CollectionType::List);
4✔
4593
                List list{remote_realm, obj, col};
4✔
4594
                list.insert_collection(0, CollectionType::List);
4✔
4595
                auto n_list = list.get_list(0);
4✔
4596
                n_list.insert(0, Mixed{40});
4✔
4597
                list.insert_collection(1, CollectionType::Dictionary);
4✔
4598
                auto dict = list.get_dictionary(1);
4✔
4599
                dict.insert("Test1", Mixed{11});
4✔
4600
                list.insert(0, Mixed{30}); // this shifts all the other collections by 1
4✔
4601
                REQUIRE(list.size() == 3);
4!
4602
            })
4✔
4603
            ->on_post_reset([&](SharedRealm local_realm) {
4✔
4604
                advance_and_notify(*local_realm);
4✔
4605
                TableRef table = get_table(*local_realm, "TopLevel");
4✔
4606
                REQUIRE(table->size() == 1);
4!
4607
                auto obj = table->get_object(0);
4✔
4608
                auto col = table->get_column_key("any_mixed");
4✔
4609
                List list{local_realm, obj, col};
4✔
4610
                if (test_mode == ClientResyncMode::DiscardLocal) {
4✔
4611
                    REQUIRE(list.size() == 3);
2!
4612
                    REQUIRE(list.get_any(0).get_int() == 30);
2!
4613
                    auto n_list = list.get_list(1);
2✔
4614
                    REQUIRE(n_list.get_any(0).get_int() == 40);
2!
4615
                    auto n_dict = list.get_dictionary(2);
2✔
4616
                    REQUIRE(n_dict.size() == 1);
2!
4617
                    REQUIRE(n_dict.get<Mixed>("Test1").get_int() == 11);
2!
4618
                }
2✔
4619
                else {
2✔
4620
                    // local
1✔
4621
                    REQUIRE(list.size() == 6);
2!
4622
                    REQUIRE(list.get_any(0).get_int() == 2);
2!
4623
                    auto n_list = list.get_list(1);
2✔
4624
                    REQUIRE(n_list.size() == 1);
2!
4625
                    REQUIRE(n_list.get_any(0).get_int() == 30);
2!
4626
                    auto n_dict = list.get_dictionary(2);
2✔
4627
                    REQUIRE(n_dict.size() == 1);
2!
4628
                    REQUIRE(n_dict.get<Mixed>("Test").get_int() == 10);
2!
4629
                    // remote
1✔
4630
                    REQUIRE(list.get_any(3).get_int() == 30);
2!
4631
                    auto n_list1 = list.get_list(4);
2✔
4632
                    REQUIRE(n_list1.size() == 1);
2!
4633
                    REQUIRE(n_list1.get_any(0).get_int() == 40);
2!
4634
                    auto n_dict1 = list.get_dictionary(5);
2✔
4635
                    REQUIRE(n_dict1.size() == 1);
2!
4636
                    REQUIRE(n_dict1.get<Mixed>("Test1").get_int() == 11);
2!
4637
                }
2✔
4638
            })
4✔
4639
            ->run();
4✔
4640
    }
4✔
4641
    SECTION("add nested collection both locally and remotely. Collections do not match") {
72✔
4642
        ObjectId pk_val = ObjectId::gen();
4✔
4643
        SyncTestFile config2(init_sync_manager.app(), "default");
4✔
4644
        config2.schema = config.schema;
4✔
4645
        auto test_reset = reset_utils::make_fake_local_client_reset(config, config2);
4✔
4646
        test_reset
4✔
4647
            ->make_local_changes([&](SharedRealm local) {
4✔
4648
                advance_and_notify(*local);
4✔
4649
                auto table = get_table(*local, "TopLevel");
4✔
4650
                auto obj = table->create_object_with_primary_key(pk_val);
4✔
4651
                auto col = table->get_column_key("any_mixed");
4✔
4652
                obj.set_collection(col, CollectionType::List);
4✔
4653
                List list{local, obj, col};
4✔
4654
                list.insert_collection(0, CollectionType::List);
4✔
4655
                auto n_list = list.get_list(0);
4✔
4656
                n_list.insert(0, Mixed{30});
4✔
4657
            })
4✔
4658
            ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
4659
                advance_and_notify(*remote_realm);
4✔
4660
                auto table = get_table(*remote_realm, "TopLevel");
4✔
4661
                auto obj = table->create_object_with_primary_key(pk_val);
4✔
4662
                auto col = table->get_column_key("any_mixed");
4✔
4663
                obj.set_collection(col, CollectionType::Dictionary);
4✔
4664
                object_store::Dictionary dict{remote_realm, obj, col};
4✔
4665
                dict.insert_collection("List", CollectionType::List);
4✔
4666
                auto n_list = dict.get_list("List");
4✔
4667
                n_list.insert(0, Mixed{30});
4✔
4668
            })
4✔
4669
            ->on_post_reset([&](SharedRealm local_realm) {
4✔
4670
                advance_and_notify(*local_realm);
4✔
4671
                TableRef table = get_table(*local_realm, "TopLevel");
4✔
4672
                REQUIRE(table->size() == 1);
4!
4673
                auto obj = table->get_object(0);
4✔
4674
                auto col = table->get_column_key("any_mixed");
4✔
4675
                if (test_mode == ClientResyncMode::DiscardLocal) {
4✔
4676
                    object_store::Dictionary dict{local_realm, obj, col};
2✔
4677
                    REQUIRE(dict.size() == 1);
2!
4678
                    auto n_list = dict.get_list("List");
2✔
4679
                    REQUIRE(n_list.size() == 1);
2!
4680
                    REQUIRE(n_list.get_any(0).get_int() == 30);
2!
4681
                }
2✔
4682
                else {
2✔
4683
                    List list{local_realm, obj, col};
2✔
4684
                    REQUIRE(list.size() == 1);
2!
4685
                    auto n_list = list.get_list(0);
2✔
4686
                    REQUIRE(n_list.size() == 1);
2!
4687
                    REQUIRE(n_list.get_any(0).get_int() == 30);
2!
4688
                }
2✔
4689
            })
4✔
4690
            ->run();
4✔
4691
    }
4✔
4692
    SECTION("delete collection remotely and add locally. Collections do not match") {
72✔
4693
        ObjectId pk_val = ObjectId::gen();
4✔
4694
        SyncTestFile config2(init_sync_manager.app(), "default");
4✔
4695
        config2.schema = config.schema;
4✔
4696
        auto test_reset = reset_utils::make_fake_local_client_reset(config, config2);
4✔
4697
        test_reset
4✔
4698
            ->setup([&](SharedRealm realm) {
8✔
4699
                auto table = get_table(*realm, "TopLevel");
8✔
4700
                auto obj = table->create_object_with_primary_key(pk_val);
8✔
4701
                auto col = table->get_column_key("any_mixed");
8✔
4702
                obj.set_collection(col, CollectionType::List);
8✔
4703
                List list{realm, obj, col};
8✔
4704
                list.insert_collection(0, CollectionType::List);
8✔
4705
                auto n_list = list.get_list(0);
8✔
4706
                n_list.insert(0, Mixed{30});
8✔
4707
                list.insert_collection(1, CollectionType::List);
8✔
4708
                n_list = list.get_list(0);
8✔
4709
                n_list.insert(0, Mixed{31});
8✔
4710
            })
8✔
4711
            ->make_local_changes([&](SharedRealm local_realm) {
4✔
4712
                advance_and_notify(*local_realm);
4✔
4713
                TableRef table = get_table(*local_realm, "TopLevel");
4✔
4714
                REQUIRE(table->size() == 1);
4!
4715
                auto obj = table->get_object(0);
4✔
4716
                auto col = table->get_column_key("any_mixed");
4✔
4717
                List list{local_realm, obj, col};
4✔
4718
                list.insert_collection(0, CollectionType::List);
4✔
4719
                auto n_list = list.get_list(0);
4✔
4720
                n_list.insert(0, Mixed{50});
4✔
4721
                REQUIRE(list.size() == 3);
4!
4722
            })
4✔
4723
            ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
4724
                advance_and_notify(*remote_realm);
4✔
4725
                TableRef table = get_table(*remote_realm, "TopLevel");
4✔
4726
                REQUIRE(table->size() == 1);
4!
4727
                auto obj = table->get_object(0);
4✔
4728
                auto col = table->get_column_key("any_mixed");
4✔
4729
                List list{remote_realm, obj, col};
4✔
4730
                REQUIRE(list.size() == 2);
4!
4731
                list.remove(0);
4✔
4732
            })
4✔
4733
            ->on_post_reset([&](SharedRealm local_realm) {
4✔
4734
                advance_and_notify(*local_realm);
4✔
4735
                TableRef table = get_table(*local_realm, "TopLevel");
4✔
4736
                REQUIRE(table->size() == 1);
4!
4737
                auto obj = table->get_object(0);
4✔
4738
                auto col = table->get_column_key("any_mixed");
4✔
4739
                if (test_mode == ClientResyncMode::DiscardLocal) {
4✔
4740
                    List list{local_realm, obj, col};
2✔
4741
                    REQUIRE(list.size() == 1);
2!
4742
                }
2✔
4743
                else {
2✔
4744
                    List list{local_realm, obj, col};
2✔
4745
                    REQUIRE(list.size() == 2);
2!
4746
                }
2✔
4747
            })
4✔
4748
            ->run();
4✔
4749
    }
4✔
4750
    SECTION("delete collection remotely and add locally same index.") {
72✔
4751
        ObjectId pk_val = ObjectId::gen();
4✔
4752
        SyncTestFile config2(init_sync_manager.app(), "default");
4✔
4753
        config2.schema = config.schema;
4✔
4754
        auto test_reset = reset_utils::make_fake_local_client_reset(config, config2);
4✔
4755
        test_reset
4✔
4756
            ->setup([&](SharedRealm realm) {
8✔
4757
                auto table = get_table(*realm, "TopLevel");
8✔
4758
                auto obj = table->create_object_with_primary_key(pk_val);
8✔
4759
                auto col = table->get_column_key("any_mixed");
8✔
4760
                obj.set_collection(col, CollectionType::List);
8✔
4761
                List list{realm, obj, col};
8✔
4762
                list.insert_collection(0, CollectionType::List);
8✔
4763
                auto n_list = list.get_list(0);
8✔
4764
                n_list.insert(0, Mixed{30});
8✔
4765
            })
8✔
4766
            ->make_local_changes([&](SharedRealm local_realm) {
4✔
4767
                advance_and_notify(*local_realm);
4✔
4768
                TableRef table = get_table(*local_realm, "TopLevel");
4✔
4769
                REQUIRE(table->size() == 1);
4!
4770
                auto obj = table->get_object(0);
4✔
4771
                auto col = table->get_column_key("any_mixed");
4✔
4772
                List list{local_realm, obj, col};
4✔
4773
                list.insert_collection(0, CollectionType::List);
4✔
4774
                auto n_list = list.get_list(0);
4✔
4775
                n_list.insert(0, Mixed{50});
4✔
4776
            })
4✔
4777
            ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
4778
                advance_and_notify(*remote_realm);
4✔
4779
                TableRef table = get_table(*remote_realm, "TopLevel");
4✔
4780
                REQUIRE(table->size() == 1);
4!
4781
                auto obj = table->get_object(0);
4✔
4782
                auto col = table->get_column_key("any_mixed");
4✔
4783
                List list{remote_realm, obj, col};
4✔
4784
                REQUIRE(list.size() == 1);
4!
4785
                list.remove(0);
4✔
4786
            })
4✔
4787
            ->on_post_reset([&](SharedRealm local_realm) {
4✔
4788
                advance_and_notify(*local_realm);
4✔
4789
                if (test_mode == ClientResyncMode::DiscardLocal) {
4✔
4790
                    TableRef table = get_table(*local_realm, "TopLevel");
2✔
4791
                    REQUIRE(table->size() == 1);
2!
4792
                    auto obj = table->get_object(0);
2✔
4793
                    auto col = table->get_column_key("any_mixed");
2✔
4794
                    List list{local_realm, obj, col};
2✔
4795
                    REQUIRE(list.size() == 0);
2!
4796
                }
2✔
4797
                else {
2✔
4798
                    TableRef table = get_table(*local_realm, "TopLevel");
2✔
4799
                    REQUIRE(table->size() == 1);
2!
4800
                    auto obj = table->get_object(0);
2✔
4801
                    auto col = table->get_column_key("any_mixed");
2✔
4802
                    List list{local_realm, obj, col};
2✔
4803
                    REQUIRE(list.size() == 1);
2!
4804
                }
2✔
4805
            })
4✔
4806
            ->run();
4✔
4807
    }
4✔
4808
    SECTION("shift collection remotely and locally") {
72✔
4809
        ObjectId pk_val = ObjectId::gen();
4✔
4810
        SyncTestFile config2(init_sync_manager.app(), "default");
4✔
4811
        config2.schema = config.schema;
4✔
4812
        auto test_reset = reset_utils::make_fake_local_client_reset(config, config2);
4✔
4813
        test_reset
4✔
4814
            ->setup([&](SharedRealm realm) {
8✔
4815
                auto table = get_table(*realm, "TopLevel");
8✔
4816
                auto obj = table->create_object_with_primary_key(pk_val);
8✔
4817
                auto col = table->get_column_key("any_mixed");
8✔
4818
                obj.set_collection(col, CollectionType::List);
8✔
4819
                List list{realm, obj, col};
8✔
4820
                list.insert_collection(0, CollectionType::List);
8✔
4821
                auto n_list = list.get_list(0);
8✔
4822
                n_list.insert(0, Mixed{30});
8✔
4823
            })
8✔
4824
            ->make_local_changes([&](SharedRealm local_realm) {
4✔
4825
                advance_and_notify(*local_realm);
4✔
4826
                TableRef table = get_table(*local_realm, "TopLevel");
4✔
4827
                REQUIRE(table->size() == 1);
4!
4828
                auto obj = table->get_object(0);
4✔
4829
                auto col = table->get_column_key("any_mixed");
4✔
4830
                List list{local_realm, obj, col};
4✔
4831
                auto n_list = list.get_list(0);
4✔
4832
                n_list.insert(0, Mixed{50});
4✔
4833
                list.insert_collection(0, CollectionType::List); // shift
4✔
4834
                auto n_list1 = list.get_list(0);
4✔
4835
                n_list1.insert(0, Mixed{150});
4✔
4836
            })
4✔
4837
            ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
4838
                advance_and_notify(*remote_realm);
4✔
4839
                TableRef table = get_table(*remote_realm, "TopLevel");
4✔
4840
                REQUIRE(table->size() == 1);
4!
4841
                auto obj = table->get_object(0);
4✔
4842
                auto col = table->get_column_key("any_mixed");
4✔
4843
                List list{remote_realm, obj, col};
4✔
4844
                auto n_list = list.get_list(0);
4✔
4845
                n_list.insert(1, Mixed{100});
4✔
4846
                list.insert_collection(0, CollectionType::List); // shift
4✔
4847
                auto n_list1 = list.get_list(0);
4✔
4848
                n_list1.insert(0, Mixed{42});
4✔
4849
            })
4✔
4850
            ->on_post_reset([&](SharedRealm local_realm) {
4✔
4851
                advance_and_notify(*local_realm);
4✔
4852
                TableRef table = get_table(*local_realm, "TopLevel");
4✔
4853
                REQUIRE(table->size() == 1);
4!
4854
                auto obj = table->get_object(0);
4✔
4855
                auto col = table->get_column_key("any_mixed");
4✔
4856
                if (test_mode == ClientResyncMode::DiscardLocal) {
4✔
4857
                    List list{local_realm, obj, col};
2✔
4858
                    REQUIRE(list.size() == 2);
2!
4859
                    auto n_list = list.get_list(0);
2✔
4860
                    auto n_list1 = list.get_list(1);
2✔
4861
                    REQUIRE(n_list.size() == 1);
2!
4862
                    REQUIRE(n_list1.size() == 2);
2!
4863
                    REQUIRE(n_list1.get_any(0).get_int() == 30);
2!
4864
                    REQUIRE(n_list1.get_any(1).get_int() == 100);
2!
4865
                    REQUIRE(n_list.get_any(0).get_int() == 42);
2!
4866
                }
2✔
4867
                else {
2✔
4868
                    List list{local_realm, obj, col};
2✔
4869
                    REQUIRE(list.size() == 3);
2!
4870
                    auto n_list = list.get_list(0);
2✔
4871
                    auto n_list1 = list.get_list(1);
2✔
4872
                    auto n_list2 = list.get_list(2);
2✔
4873
                    REQUIRE(n_list.size() == 1);
2!
4874
                    REQUIRE(n_list1.size() == 2);
2!
4875
                    REQUIRE(n_list2.size() == 2);
2!
4876
                    REQUIRE(n_list.get_any(0).get_int() == 150);
2!
4877
                    REQUIRE(n_list1.get_any(0).get_int() == 50);
2!
4878
                    REQUIRE(n_list1.get_any(1).get_int() == 42);
2!
4879
                    REQUIRE(n_list2.get_any(0).get_int() == 30);
2!
4880
                    REQUIRE(n_list2.get_any(1).get_int() == 100);
2!
4881
                }
2✔
4882
            })
4✔
4883
            ->run();
4✔
4884
    }
4✔
4885
    SECTION("delete collection locally (list). Local should win") {
72✔
4886
        ObjectId pk_val = ObjectId::gen();
4✔
4887
        SyncTestFile config2(init_sync_manager.app(), "default");
4✔
4888
        config2.schema = config.schema;
4✔
4889
        auto test_reset = reset_utils::make_fake_local_client_reset(config, config2);
4✔
4890
        test_reset
4✔
4891
            ->setup([&](SharedRealm realm) {
8✔
4892
                auto table = get_table(*realm, "TopLevel");
8✔
4893
                auto obj = table->create_object_with_primary_key(pk_val);
8✔
4894
                auto col = table->get_column_key("any_mixed");
8✔
4895
                obj.set_collection(col, CollectionType::List);
8✔
4896
                List list{realm, obj, col};
8✔
4897
                list.insert_collection(0, CollectionType::List);
8✔
4898
                auto n_list = list.get_list(0);
8✔
4899
                n_list.insert(0, Mixed{30});
8✔
4900
            })
8✔
4901
            ->make_local_changes([&](SharedRealm local_realm) {
4✔
4902
                advance_and_notify(*local_realm);
4✔
4903
                TableRef table = get_table(*local_realm, "TopLevel");
4✔
4904
                REQUIRE(table->size() == 1);
4!
4905
                auto obj = table->get_object(0);
4✔
4906
                auto col = table->get_column_key("any_mixed");
4✔
4907
                List list{local_realm, obj, col};
4✔
4908
                REQUIRE(list.size() == 1);
4!
4909
                list.remove(0);
4✔
4910
            })
4✔
4911
            ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
4912
                advance_and_notify(*remote_realm);
4✔
4913
                TableRef table = get_table(*remote_realm, "TopLevel");
4✔
4914
                REQUIRE(table->size() == 1);
4!
4915
                auto obj = table->get_object(0);
4✔
4916
                auto col = table->get_column_key("any_mixed");
4✔
4917
                List list{remote_realm, obj, col};
4✔
4918
                list.add(Mixed{10});
4✔
4919
                REQUIRE(list.size() == 2);
4!
4920
            })
4✔
4921
            ->on_post_reset([&](SharedRealm local_realm) {
4✔
4922
                advance_and_notify(*local_realm);
4✔
4923
                TableRef table = get_table(*local_realm, "TopLevel");
4✔
4924
                REQUIRE(table->size() == 1);
4!
4925
                auto obj = table->get_object(0);
4✔
4926
                auto col = table->get_column_key("any_mixed");
4✔
4927
                if (test_mode == ClientResyncMode::DiscardLocal) {
4✔
4928
                    List list{local_realm, obj, col};
2✔
4929
                    REQUIRE(list.size() == 2);
2!
4930
                    auto n_list1 = list.get_list(0);
2✔
4931
                    auto mixed = list.get_any(1);
2✔
4932
                    REQUIRE(n_list1.size() == 1);
2!
4933
                    REQUIRE(mixed.get_int() == 10);
2!
4934
                    REQUIRE(n_list1.get_any(0).get_int() == 30);
2!
4935
                }
2✔
4936
                else {
2✔
4937
                    List list{local_realm, obj, col};
2✔
4938
                    REQUIRE(list.size() == 0);
2!
4939
                }
2✔
4940
            })
4✔
4941
            ->run();
4✔
4942
    }
4✔
4943
    SECTION("move collection locally (list). Local should win") {
72✔
4944
        ObjectId pk_val = ObjectId::gen();
4✔
4945
        SyncTestFile config2(init_sync_manager.app(), "default");
4✔
4946
        config2.schema = config.schema;
4✔
4947
        auto test_reset = reset_utils::make_fake_local_client_reset(config, config2);
4✔
4948
        test_reset
4✔
4949
            ->setup([&](SharedRealm realm) {
8✔
4950
                auto table = get_table(*realm, "TopLevel");
8✔
4951
                auto obj = table->create_object_with_primary_key(pk_val);
8✔
4952
                auto col = table->get_column_key("any_mixed");
8✔
4953
                obj.set_collection(col, CollectionType::List);
8✔
4954
                List list{realm, obj, col};
8✔
4955
                list.insert_collection(0, CollectionType::List);
8✔
4956
                auto n_list = list.get_list(0);
8✔
4957
                n_list.insert(0, Mixed{30});
8✔
4958
                n_list.insert(1, Mixed{10});
8✔
4959
            })
8✔
4960
            ->make_local_changes([&](SharedRealm local_realm) {
4✔
4961
                advance_and_notify(*local_realm);
4✔
4962
                TableRef table = get_table(*local_realm, "TopLevel");
4✔
4963
                REQUIRE(table->size() == 1);
4!
4964
                auto obj = table->get_object(0);
4✔
4965
                auto col = table->get_column_key("any_mixed");
4✔
4966
                List list{local_realm, obj, col};
4✔
4967
                auto nlist = list.get_list(0);
4✔
4968
                nlist.move(0, 1); // move value 30 in pos 1.
4✔
4969
                REQUIRE(nlist.size() == 2);
4!
4970
                REQUIRE(nlist.get_any(0).get_int() == 10);
4!
4971
                REQUIRE(nlist.get_any(1).get_int() == 30);
4!
4972
            })
4✔
4973
            ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
4974
                advance_and_notify(*remote_realm);
4✔
4975
                TableRef table = get_table(*remote_realm, "TopLevel");
4✔
4976
                REQUIRE(table->size() == 1);
4!
4977
                auto obj = table->get_object(0);
4✔
4978
                auto col = table->get_column_key("any_mixed");
4✔
4979
                List list{remote_realm, obj, col};
4✔
4980
                REQUIRE(list.size() == 1);
4!
4981
                auto nlist = list.get_list(0);
4✔
4982
                REQUIRE(nlist.size() == 2);
4!
4983
                nlist.add(Mixed{2});
4✔
4984
                REQUIRE(nlist.size() == 3);
4!
4985
            })
4✔
4986
            ->on_post_reset([&](SharedRealm local_realm) {
4✔
4987
                advance_and_notify(*local_realm);
4✔
4988
                TableRef table = get_table(*local_realm, "TopLevel");
4✔
4989
                REQUIRE(table->size() == 1);
4!
4990
                auto obj = table->get_object(0);
4✔
4991
                auto col = table->get_column_key("any_mixed");
4✔
4992
                if (test_mode == ClientResyncMode::DiscardLocal) {
4✔
4993
                    // local state is preserved
1✔
4994
                    List list{local_realm, obj, col};
2✔
4995
                    REQUIRE(list.size() == 1);
2!
4996
                    auto nlist = list.get_list(0);
2✔
4997
                    REQUIRE(nlist.size() == 3);
2!
4998
                    REQUIRE(nlist.get_any(0).get_int() == 30);
2!
4999
                    REQUIRE(nlist.get_any(1).get_int() == 10);
2!
5000
                    REQUIRE(nlist.get_any(2).get_int() == 2);
2!
5001
                }
2✔
5002
                else {
2✔
5003
                    // local change wins
1✔
5004
                    List list{local_realm, obj, col};
2✔
5005
                    REQUIRE(list.size() == 1);
2!
5006
                    auto nlist = list.get_list(0);
2✔
5007
                    REQUIRE(nlist.size() == 2);
2!
5008
                    REQUIRE(nlist.get_any(0).get_int() == 10);
2!
5009
                    REQUIRE(nlist.get_any(1).get_int() == 30);
2!
5010
                }
2✔
5011
            })
4✔
5012
            ->run();
4✔
5013
    }
4✔
5014
    SECTION("delete collection locally (dictionary). Local should win") {
72✔
5015
        ObjectId pk_val = ObjectId::gen();
4✔
5016
        SyncTestFile config2(init_sync_manager.app(), "default");
4✔
5017
        config2.schema = config.schema;
4✔
5018
        auto test_reset = reset_utils::make_fake_local_client_reset(config, config2);
4✔
5019
        test_reset
4✔
5020
            ->setup([&](SharedRealm realm) {
8✔
5021
                auto table = get_table(*realm, "TopLevel");
8✔
5022
                auto obj = table->create_object_with_primary_key(pk_val);
8✔
5023
                auto col = table->get_column_key("any_mixed");
8✔
5024
                obj.set_collection(col, CollectionType::Dictionary);
8✔
5025
                object_store::Dictionary dictionary{realm, obj, col};
8✔
5026
                dictionary.insert_collection("Test", CollectionType::Dictionary);
8✔
5027
                auto n_dictionary = dictionary.get_dictionary("Test");
8✔
5028
                n_dictionary.insert("Val", 30);
8✔
5029
            })
8✔
5030
            ->make_local_changes([&](SharedRealm local_realm) {
4✔
5031
                advance_and_notify(*local_realm);
4✔
5032
                TableRef table = get_table(*local_realm, "TopLevel");
4✔
5033
                REQUIRE(table->size() == 1);
4!
5034
                auto obj = table->get_object(0);
4✔
5035
                auto col = table->get_column_key("any_mixed");
4✔
5036
                object_store::Dictionary dictionary{local_realm, obj, col};
4✔
5037
                REQUIRE(dictionary.size() == 1);
4!
5038
                dictionary.erase("Test");
4✔
5039
            })
4✔
5040
            ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
5041
                advance_and_notify(*remote_realm);
4✔
5042
                TableRef table = get_table(*remote_realm, "TopLevel");
4✔
5043
                REQUIRE(table->size() == 1);
4!
5044
                auto obj = table->get_object(0);
4✔
5045
                auto col = table->get_column_key("any_mixed");
4✔
5046
                object_store::Dictionary dictionary{remote_realm, obj, col};
4✔
5047
                REQUIRE(dictionary.size() == 1);
4!
5048
                auto n_dictionary = dictionary.get_dictionary("Test");
4✔
5049
                n_dictionary.insert("Val1", 31);
4✔
5050
            })
4✔
5051
            ->on_post_reset([&](SharedRealm local_realm) {
4✔
5052
                advance_and_notify(*local_realm);
4✔
5053
                TableRef table = get_table(*local_realm, "TopLevel");
4✔
5054
                REQUIRE(table->size() == 1);
4!
5055
                auto obj = table->get_object(0);
4✔
5056
                auto col = table->get_column_key("any_mixed");
4✔
5057
                if (test_mode == ClientResyncMode::DiscardLocal) {
4✔
5058
                    object_store::Dictionary dictionary{local_realm, obj, col};
2✔
5059
                    REQUIRE(dictionary.size() == 1);
2!
5060
                    auto n_dictionary = dictionary.get_dictionary("Test");
2✔
5061
                    REQUIRE(n_dictionary.get_any("Val").get_int() == 30);
2!
5062
                    REQUIRE(n_dictionary.get_any("Val1").get_int() == 31);
2!
5063
                }
2✔
5064
                else {
2✔
5065
                    // local change wins
1✔
5066
                    object_store::Dictionary dictionary{local_realm, obj, col};
2✔
5067
                    REQUIRE(dictionary.size() == 0);
2!
5068
                }
2✔
5069
            })
4✔
5070
            ->run();
4✔
5071
    }
4✔
5072
    // testing copying logic for nested collections
36✔
5073
    SECTION("Verify copy logic for collections in mixed.") {
72✔
5074
        ObjectId pk_val = ObjectId::gen();
4✔
5075
        SyncTestFile config2(init_sync_manager.app(), "default");
4✔
5076
        config2.schema = config.schema;
4✔
5077
        auto test_reset = reset_utils::make_fake_local_client_reset(config, config2);
4✔
5078
        test_reset
4✔
5079
            ->setup([&](SharedRealm realm) {
8✔
5080
                auto table = get_table(*realm, "TopLevel");
8✔
5081
                auto obj = table->create_object_with_primary_key(pk_val);
8✔
5082
                auto col = table->get_column_key("any_mixed");
8✔
5083
                obj.set_collection(col, CollectionType::List);
8✔
5084
                List list{realm, obj, col};
8✔
5085
                list.insert_collection(0, CollectionType::List);
8✔
5086
                list.insert_collection(1, CollectionType::Dictionary);
8✔
5087
                auto nlist = list.get_list(0);
8✔
5088
                auto ndict = list.get_dictionary(1);
8✔
5089
                nlist.add(Mixed{1});
8✔
5090
                nlist.add(Mixed{"Test"});
8✔
5091
                ndict.insert("Int", Mixed(3));
8✔
5092
                ndict.insert("String", Mixed("Test"));
8✔
5093
            })
8✔
5094
            ->make_local_changes([&](SharedRealm local_realm) {
4✔
5095
                advance_and_notify(*local_realm);
4✔
5096
                TableRef table = get_table(*local_realm, "TopLevel");
4✔
5097
                REQUIRE(table->size() == 1);
4!
5098
                auto obj = table->get_object(0);
4✔
5099
                auto col = table->get_column_key("any_mixed");
4✔
5100
                List list{local_realm, obj, col};
4✔
5101
                REQUIRE(list.size() == 2);
4!
5102
                auto nlist = list.get_list(0);
4✔
5103
                nlist.add(Mixed{4});
4✔
5104
                auto ndict = list.get_dictionary(1);
4✔
5105
                ndict.insert("Int2", 6);
4✔
5106
            })
4✔
5107
            ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
5108
                advance_and_notify(*remote_realm);
4✔
5109
                TableRef table = get_table(*remote_realm, "TopLevel");
4✔
5110
                REQUIRE(table->size() == 1);
4!
5111
                auto obj = table->get_object(0);
4✔
5112
                auto col = table->get_column_key("any_mixed");
4✔
5113
                List list{remote_realm, obj, col};
4✔
5114
                REQUIRE(list.size() == 2);
4!
5115
                auto nlist = list.get_list(0);
4✔
5116
                nlist.add(Mixed{7});
4✔
5117
                auto ndict = list.get_dictionary(1);
4✔
5118
                ndict.insert("Int3", Mixed{9});
4✔
5119
            })
4✔
5120
            ->on_post_reset([&](SharedRealm local_realm) {
4✔
5121
                advance_and_notify(*local_realm);
4✔
5122
                TableRef table = get_table(*local_realm, "TopLevel");
4✔
5123
                REQUIRE(table->size() == 1);
4!
5124
                auto obj = table->get_object(0);
4✔
5125
                auto col = table->get_column_key("any_mixed");
4✔
5126
                if (test_mode == ClientResyncMode::DiscardLocal) {
4✔
5127
                    // db must be equal to remote
1✔
5128
                    List list{local_realm, obj, col};
2✔
5129
                    REQUIRE(list.size() == 2);
2!
5130
                    auto nlist = list.get_list(0);
2✔
5131
                    auto ndict = list.get_dictionary(1);
2✔
5132
                    REQUIRE(nlist.size() == 3);
2!
5133
                    REQUIRE(ndict.size() == 3);
2!
5134
                    REQUIRE(nlist.get_any(0).get_int() == 1);
2!
5135
                    REQUIRE(nlist.get_any(1).get_string() == "Test");
2!
5136
                    REQUIRE(nlist.get_any(2).get_int() == 7);
2!
5137
                    REQUIRE(ndict.get_any("Int").get_int() == 3);
2!
5138
                    REQUIRE(ndict.get_any("String").get_string() == "Test");
2!
5139
                    REQUIRE(ndict.get_any("Int3").get_int() == 9);
2!
5140
                }
2✔
5141
                else {
2✔
5142
                    List list{local_realm, obj, col};
2✔
5143
                    REQUIRE(list.size() == 2);
2!
5144
                    auto nlist = list.get_list(0);
2✔
5145
                    auto ndict = list.get_dictionary(1);
2✔
5146
                    REQUIRE(nlist.size() == 4);
2!
5147
                    REQUIRE(ndict.size() == 4);
2!
5148
                    REQUIRE(nlist.get_any(0).get_int() == 1);
2!
5149
                    REQUIRE(nlist.get_any(1).get_string() == "Test");
2!
5150
                    REQUIRE(nlist.get_any(2).get_int() == 4);
2!
5151
                    REQUIRE(nlist.get_any(3).get_int() == 7);
2!
5152
                    REQUIRE(ndict.get_any("Int").get_int() == 3);
2!
5153
                    REQUIRE(ndict.get_any("String").get_string() == "Test");
2!
5154
                    REQUIRE(ndict.get_any("Int2").get_int() == 6);
2!
5155
                    REQUIRE(ndict.get_any("Int3").get_int() == 9);
2!
5156
                }
2✔
5157
            })
4✔
5158
            ->run();
4✔
5159
    }
4✔
5160
    SECTION("Verify copy logic for collections in mixed. Mismatch at index i") {
72✔
5161
        ObjectId pk_val = ObjectId::gen();
4✔
5162
        SyncTestFile config2(init_sync_manager.app(), "default");
4✔
5163
        config2.schema = config.schema;
4✔
5164
        auto test_reset = reset_utils::make_fake_local_client_reset(config, config2);
4✔
5165
        test_reset
4✔
5166
            ->setup([&](SharedRealm realm) {
8✔
5167
                auto table = get_table(*realm, "TopLevel");
8✔
5168
                auto obj = table->create_object_with_primary_key(pk_val);
8✔
5169
                auto col = table->get_column_key("any_mixed");
8✔
5170
                obj.set_collection(col, CollectionType::List);
8✔
5171
            })
8✔
5172
            ->make_local_changes([&](SharedRealm local_realm) {
4✔
5173
                advance_and_notify(*local_realm);
4✔
5174
                TableRef table = get_table(*local_realm, "TopLevel");
4✔
5175
                REQUIRE(table->size() == 1);
4!
5176
                auto obj = table->get_object(0);
4✔
5177
                auto col = table->get_column_key("any_mixed");
4✔
5178
                List list{local_realm, obj, col};
4✔
5179
                list.insert_collection(0, CollectionType::List);
4✔
5180
                auto nlist = list.get_list(0);
4✔
5181
                nlist.add(Mixed{"Local"});
4✔
5182
            })
4✔
5183
            ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
5184
                advance_and_notify(*remote_realm);
4✔
5185
                TableRef table = get_table(*remote_realm, "TopLevel");
4✔
5186
                REQUIRE(table->size() == 1);
4!
5187
                auto obj = table->get_object(0);
4✔
5188
                auto col = table->get_column_key("any_mixed");
4✔
5189
                List list{remote_realm, obj, col};
4✔
5190
                list.insert_collection(0, CollectionType::Dictionary);
4✔
5191
                auto ndict = list.get_dictionary(0);
4✔
5192
                ndict.insert("Test", Mixed{"Remote"});
4✔
5193
            })
4✔
5194
            ->on_post_reset([&](SharedRealm local_realm) {
4✔
5195
                advance_and_notify(*local_realm);
4✔
5196
                TableRef table = get_table(*local_realm, "TopLevel");
4✔
5197
                REQUIRE(table->size() == 1);
4!
5198
                auto obj = table->get_object(0);
4✔
5199
                auto col = table->get_column_key("any_mixed");
4✔
5200
                if (test_mode == ClientResyncMode::DiscardLocal) {
4✔
5201
                    // db must be equal to remote
1✔
5202
                    List list{local_realm, obj, col};
2✔
5203
                    REQUIRE(list.size() == 1);
2!
5204
                    auto ndict = list.get_dictionary(0);
2✔
5205
                    REQUIRE(ndict.size() == 1);
2!
5206
                    REQUIRE(ndict.get_any("Test").get_string() == "Remote");
2!
5207
                }
2✔
5208
                else {
2✔
5209
                    List list{local_realm, obj, col};
2✔
5210
                    REQUIRE(list.size() == 2);
2!
5211
                    auto nlist = list.get_list(0);
2✔
5212
                    auto ndict = list.get_dictionary(1);
2✔
5213
                    REQUIRE(ndict.get_any("Test").get_string() == "Remote");
2!
5214
                    REQUIRE(nlist.get_any(0).get_string() == "Local");
2!
5215
                }
2✔
5216
            })
4✔
5217
            ->run();
4✔
5218
    }
4✔
5219
    SECTION("Verify copy and notification logic for List with scalar mixed types and nested collections") {
72✔
5220
        Results results;
4✔
5221
        Object object;
4✔
5222
        List list_listener, nlist_setup_listener, nlist_local_listener;
4✔
5223
        CollectionChangeSet list_changes, nlist_setup_changes, nlist_local_changes;
4✔
5224
        NotificationToken list_token, nlist_setup_token, nlist_local_token;
4✔
5225

2✔
5226
        ObjectId pk_val = ObjectId::gen();
4✔
5227
        SyncTestFile config2(init_sync_manager.app(), "default");
4✔
5228
        config2.schema = config.schema;
4✔
5229
        auto test_reset = reset_utils::make_fake_local_client_reset(config, config2);
4✔
5230
        test_reset
4✔
5231
            ->setup([&](SharedRealm realm) {
8✔
5232
                auto table = get_table(*realm, "TopLevel");
8✔
5233
                auto obj = table->create_object_with_primary_key(pk_val);
8✔
5234
                auto col = table->get_column_key("any_mixed");
8✔
5235
                obj.set_collection(col, CollectionType::List);
8✔
5236
                List list{realm, obj, col};
8✔
5237
                list.insert_collection(0, CollectionType::List);
8✔
5238
                list.add(Mixed{"Setup"});
8✔
5239
                auto nlist = list.get_list(0);
8✔
5240
                nlist.add(Mixed{"Setup"});
8✔
5241
            })
8✔
5242
            ->make_local_changes([&](SharedRealm local_realm) {
4✔
5243
                advance_and_notify(*local_realm);
4✔
5244
                TableRef table = get_table(*local_realm, "TopLevel");
4✔
5245
                REQUIRE(table->size() == 1);
4!
5246
                auto obj = table->get_object(0);
4✔
5247
                auto col = table->get_column_key("any_mixed");
4✔
5248
                List list{local_realm, obj, col};
4✔
5249
                REQUIRE(list.size() == 2);
4!
5250
                list.insert_collection(0, CollectionType::List);
4✔
5251
                list.add(Mixed{"Local"});
4✔
5252
                auto nlist = list.get_list(0);
4✔
5253
                nlist.add(Mixed{"Local"});
4✔
5254
            })
4✔
5255
            ->on_post_local_changes([&](SharedRealm realm) {
4✔
5256
                TableRef table = get_table(*realm, "TopLevel");
4✔
5257
                REQUIRE(table->size() == 1);
4!
5258
                auto obj = table->get_object(0);
4✔
5259
                auto col = table->get_column_key("any_mixed");
4✔
5260
                list_listener = List{realm, obj, col};
4✔
5261
                REQUIRE(list_listener.size() == 4);
4!
5262
                list_token = list_listener.add_notification_callback([&](CollectionChangeSet changes) {
4✔
5263
                    list_changes = std::move(changes);
4✔
5264
                });
4✔
5265
                auto nlist_setup = list_listener.get_list(1);
4✔
5266
                REQUIRE(nlist_setup.size() == 1);
4!
5267
                REQUIRE(nlist_setup.get_any(0) == Mixed{"Setup"});
4!
5268
                nlist_setup_listener = nlist_setup;
4✔
5269
                nlist_setup_token = nlist_setup_listener.add_notification_callback([&](CollectionChangeSet changes) {
4✔
5270
                    nlist_setup_changes = std::move(changes);
4✔
5271
                });
4✔
5272
                auto nlist_local = list_listener.get_list(0);
4✔
5273
                REQUIRE(nlist_local.size() == 1);
4!
5274
                REQUIRE(nlist_local.get_any(0) == Mixed{"Local"});
4!
5275
                nlist_local_listener = nlist_local;
4✔
5276
                nlist_local_token = nlist_local_listener.add_notification_callback([&](CollectionChangeSet changes) {
4✔
5277
                    nlist_local_changes = std::move(changes);
4✔
5278
                });
4✔
5279
            })
4✔
5280
            ->make_remote_changes([&](SharedRealm remote_realm) {
4✔
5281
                advance_and_notify(*remote_realm);
4✔
5282
                TableRef table = get_table(*remote_realm, "TopLevel");
4✔
5283
                REQUIRE(table->size() == 1);
4!
5284
                auto obj = table->get_object(0);
4✔
5285
                auto col = table->get_column_key("any_mixed");
4✔
5286
                List list{remote_realm, obj, col};
4✔
5287
                REQUIRE(list.size() == 2);
4!
5288
                list.insert_collection(0, CollectionType::List);
4✔
5289
                list.add(Mixed{"Remote"});
4✔
5290
                auto nlist = list.get_list(0);
4✔
5291
                nlist.add(Mixed{"Remote"});
4✔
5292
            })
4✔
5293
            ->on_post_reset([&](SharedRealm local_realm) {
4✔
5294
                advance_and_notify(*local_realm);
4✔
5295
                TableRef table = get_table(*local_realm, "TopLevel");
4✔
5296
                REQUIRE(table->size() == 1);
4!
5297
                auto obj = table->get_object(0);
4✔
5298
                auto col = table->get_column_key("any_mixed");
4✔
5299

2✔
5300
                if (test_mode == ClientResyncMode::DiscardLocal) {
4✔
5301
                    // db must be equal to remote
1✔
5302
                    List list{local_realm, obj, col};
2✔
5303
                    REQUIRE(list.size() == 4);
2!
5304
                    auto nlist_remote = list.get_list(0);
2✔
5305
                    auto nlist_setup = list.get_list(1);
2✔
5306
                    auto mixed_setup = list.get_any(2);
2✔
5307
                    auto mixed_remote = list.get_any(3);
2✔
5308
                    REQUIRE(nlist_remote.size() == 1);
2!
5309
                    REQUIRE(nlist_setup.size() == 1);
2!
5310
                    REQUIRE(mixed_setup.get_string() == "Setup");
2!
5311
                    REQUIRE(mixed_remote.get_string() == "Remote");
2!
5312
                    REQUIRE(nlist_remote.get_any(0).get_string() == "Remote");
2!
5313
                    REQUIRE(nlist_setup.get_any(0).get_string() == "Setup");
2!
5314
                    REQUIRE(list_listener.is_valid());
2!
5315
                    REQUIRE_INDICES(list_changes.deletions);  // old nested collection deleted
2!
5316
                    REQUIRE_INDICES(list_changes.insertions); // new nested collection inserted
2!
5317
                    REQUIRE_INDICES(list_changes.modifications, 0,
2!
5318
                                    3); // replace Local with Remote at position 0 and 3
2✔
5319
                    REQUIRE(!nlist_local_changes.collection_root_was_deleted); // original local collection deleted
2!
5320
                    REQUIRE(!nlist_setup_changes.collection_root_was_deleted);
2!
5321
                    REQUIRE_INDICES(nlist_setup_changes.insertions); // there are no new insertions or deletions
2!
5322
                    REQUIRE_INDICES(nlist_setup_changes.deletions);
2!
5323
                    REQUIRE_INDICES(nlist_setup_changes.modifications);
2!
5324
                }
2✔
5325
                else {
2✔
5326
                    List list{local_realm, obj, col};
2✔
5327
                    REQUIRE(list.size() == 6);
2!
5328
                    auto nlist_local = list.get_list(0);
2✔
5329
                    auto nlist_remote = list.get_list(1);
2✔
5330
                    auto nlist_setup = list.get_list(2);
2✔
5331
                    auto mixed_local = list.get_any(3);
2✔
5332
                    auto mixed_setup = list.get_any(4);
2✔
5333
                    auto mixed_remote = list.get_any(5);
2✔
5334
                    // local, remote changes are kept
1✔
5335
                    REQUIRE(nlist_remote.size() == 1);
2!
5336
                    REQUIRE(nlist_setup.size() == 1);
2!
5337
                    REQUIRE(nlist_local.size() == 1);
2!
5338
                    REQUIRE(mixed_setup.get_string() == "Setup");
2!
5339
                    REQUIRE(mixed_remote.get_string() == "Remote");
2!
5340
                    REQUIRE(mixed_local.get_string() == "Local");
2!
5341
                    REQUIRE(nlist_remote.get_any(0).get_string() == "Remote");
2!
5342
                    REQUIRE(nlist_local.get_any(0).get_string() == "Local");
2!
5343
                    REQUIRE(nlist_setup.get_any(0).get_string() == "Setup");
2!
5344
                    // notifications
1✔
5345
                    REQUIRE(list_listener.is_valid());
2!
5346
                    // src is [ [Local],[Remote],[Setup], Local, Setup, Remote ]
1✔
5347
                    // dst is [ [Local], [Setup], Setup, Local]
1✔
5348
                    // no deletions
1✔
5349
                    REQUIRE_INDICES(list_changes.deletions);
2!
5350
                    // inserted "Setup" and "Remote" at the end
1✔
5351
                    REQUIRE_INDICES(list_changes.insertions, 4, 5);
2!
5352
                    // changed [Setup] ==> [Remote] and Setup ==> [Setup]
1✔
5353
                    REQUIRE_INDICES(list_changes.modifications, 1, 2);
2!
5354
                    REQUIRE(!nlist_local_changes.collection_root_was_deleted);
2!
5355
                    REQUIRE_INDICES(nlist_local_changes.insertions);
2!
5356
                    REQUIRE_INDICES(nlist_local_changes.deletions);
2!
5357
                    REQUIRE(!nlist_setup_changes.collection_root_was_deleted);
2!
5358
                    REQUIRE_INDICES(nlist_setup_changes.insertions);
2!
5359
                    REQUIRE_INDICES(nlist_setup_changes.deletions);
2!
5360
                }
2✔
5361
            })
4✔
5362
            ->run();
4✔
5363
    }
4✔
5364
}
72✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc