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

realm / realm-core / 1668

13 Sep 2023 03:21PM UTC coverage: 91.258% (+0.04%) from 91.218%
1668

push

Evergreen

GitHub
Merge pull request #6974 from realm/release/13.20.1

95956 of 175880 branches covered (0.0%)

233793 of 256190 relevant lines covered (91.26%)

7274240.23 hits per line

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

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

19
#include <util/event_loop.hpp>
20
#include <util/test_file.hpp>
21
#include <util/test_utils.hpp>
22
#include <../util/semaphore.hpp>
23

24
#include <realm/db.hpp>
25
#include <realm/history.hpp>
26

27
#include <realm/impl/simulated_failure.hpp>
28

29
#include <realm/object-store/binding_context.hpp>
30
#include <realm/object-store/keypath_helpers.hpp>
31
#include <realm/object-store/object_schema.hpp>
32
#include <realm/object-store/object_store.hpp>
33
#include <realm/object-store/property.hpp>
34
#include <realm/object-store/results.hpp>
35
#include <realm/object-store/schema.hpp>
36
#include <realm/object-store/thread_safe_reference.hpp>
37
#include <realm/object-store/impl/realm_coordinator.hpp>
38
#include <realm/object-store/util/event_loop_dispatcher.hpp>
39
#include <realm/object-store/util/scheduler.hpp>
40

41
#include <realm/util/base64.hpp>
42
#include <realm/util/fifo_helper.hpp>
43
#include <realm/util/scope_exit.hpp>
44

45
#if REALM_ENABLE_SYNC
46
#include <util/sync/flx_sync_harness.hpp>
47

48
#include <realm/object-store/sync/async_open_task.hpp>
49
#include <realm/object-store/sync/impl/sync_metadata.hpp>
50

51
#include <realm/sync/noinst/client_history_impl.hpp>
52
#include <realm/sync/subscriptions.hpp>
53
#endif
54

55
#include <catch2/catch_all.hpp>
56
#include <catch2/matchers/catch_matchers_string.hpp>
57

58
#include <external/json/json.hpp>
59

60
#include <array>
61
#if REALM_HAVE_UV
62
#include <uv.h>
63
#endif
64

65
namespace realm {
66
class TestHelper {
67
public:
68
    static DBRef& get_db(SharedRealm const& shared_realm)
69
    {
6,500✔
70
        return Realm::Internal::get_db(*shared_realm);
6,500✔
71
    }
6,500✔
72

73
    static void begin_read(SharedRealm const& shared_realm, VersionID version)
74
    {
2✔
75
        Realm::Internal::begin_read(*shared_realm, version);
2✔
76
    }
2✔
77
};
78

79
static bool operator==(IndexSet const& a, IndexSet const& b)
80
{
2✔
81
    return std::equal(a.as_indexes().begin(), a.as_indexes().end(), b.as_indexes().begin(), b.as_indexes().end());
2✔
82
}
2✔
83
} // namespace realm
84

85
using namespace realm;
86

87
namespace {
88
class Observer : public BindingContext {
89
public:
90
    Observer(Obj& obj)
91
    {
2✔
92
        m_result.push_back(ObserverState{obj.get_table()->get_key(), obj.get_key(), nullptr});
2✔
93
    }
2✔
94

95
    IndexSet array_change(size_t index, ColKey col_key) const noexcept
96
    {
2✔
97
        auto& changes = m_result[index].changes;
2✔
98
        auto col = changes.find(col_key.value);
2✔
99
        return col == changes.end() ? IndexSet{} : col->second.indices;
2✔
100
    }
2✔
101

102
private:
103
    std::vector<ObserverState> m_result;
104
    std::vector<void*> m_invalidated;
105

106
    std::vector<ObserverState> get_observed_rows() override
107
    {
4✔
108
        return m_result;
4✔
109
    }
4✔
110

111
    void did_change(std::vector<ObserverState> const& observers, std::vector<void*> const& invalidated, bool) override
112
    {
4✔
113
        m_invalidated = invalidated;
4✔
114
        m_result = observers;
4✔
115
    }
4✔
116
};
117
} // namespace
118

119
TEST_CASE("SharedRealm: get_shared_realm()") {
76✔
120
    TestFile config;
76✔
121
    config.schema_version = 1;
76✔
122
    config.schema = Schema{
76✔
123
        {"object", {{"value", PropertyType::Int}}},
76✔
124
    };
76✔
125

38✔
126
    SECTION("should return the same instance when caching is enabled") {
76✔
127
        config.cache = true;
2✔
128
        auto realm1 = Realm::get_shared_realm(config);
2✔
129
        auto realm2 = Realm::get_shared_realm(config);
2✔
130
        REQUIRE(realm1.get() == realm2.get());
2!
131
    }
2✔
132

38✔
133
    SECTION("should return different instances when caching is disabled") {
76✔
134
        config.cache = false;
2✔
135
        auto realm1 = Realm::get_shared_realm(config);
2✔
136
        auto realm2 = Realm::get_shared_realm(config);
2✔
137
        REQUIRE(realm1.get() != realm2.get());
2!
138
    }
2✔
139

38✔
140
    SECTION("should validate that the config is sensible") {
76✔
141
        SECTION("bad encryption key") {
18✔
142
            config.encryption_key = std::vector<char>(2, 0);
2✔
143
            REQUIRE_EXCEPTION(Realm::get_shared_realm(config), InvalidEncryptionKey,
2✔
144
                              "Encryption key must be 64 bytes.");
2✔
145
        }
2✔
146

9✔
147
        SECTION("schema without schema version") {
18✔
148
            config.schema_version = ObjectStore::NotVersioned;
2✔
149
            REQUIRE_EXCEPTION(Realm::get_shared_realm(config), IllegalCombination,
2✔
150
                              "A schema version must be specified when the schema is specified");
2✔
151
        }
2✔
152

9✔
153
        SECTION("migration function for immutable") {
18✔
154
            config.schema_mode = SchemaMode::Immutable;
2✔
155
            config.migration_function = [](auto, auto, auto) {};
1✔
156
            REQUIRE_EXCEPTION(Realm::get_shared_realm(config), IllegalCombination,
2✔
157
                              "Realms opened in immutable mode do not use a migration function");
2✔
158
        }
2✔
159

9✔
160
        SECTION("migration function for read-only") {
18✔
161
            config.schema_mode = SchemaMode::ReadOnly;
2✔
162
            config.migration_function = [](auto, auto, auto) {};
1✔
163
            REQUIRE_EXCEPTION(Realm::get_shared_realm(config), IllegalCombination,
2✔
164
                              "Realms opened in read-only mode do not use a migration function");
2✔
165
        }
2✔
166

9✔
167
        SECTION("migration function for additive discovered") {
18✔
168
            config.schema_mode = SchemaMode::AdditiveDiscovered;
2✔
169
            config.migration_function = [](auto, auto, auto) {};
1✔
170
            REQUIRE_EXCEPTION(Realm::get_shared_realm(config), IllegalCombination,
2✔
171
                              "Realms opened in Additive-only schema mode do not use a migration function");
2✔
172
        }
2✔
173

9✔
174
        SECTION("migration function for additive explicit") {
18✔
175
            config.schema_mode = SchemaMode::AdditiveExplicit;
2✔
176
            config.migration_function = [](auto, auto, auto) {};
1✔
177
            REQUIRE_EXCEPTION(Realm::get_shared_realm(config), IllegalCombination,
2✔
178
                              "Realms opened in Additive-only schema mode do not use a migration function");
2✔
179
        }
2✔
180

9✔
181
        SECTION("initialization function for immutable") {
18✔
182
            config.schema_mode = SchemaMode::Immutable;
2✔
183
            config.initialization_function = [](auto) {};
1✔
184
            REQUIRE_EXCEPTION(Realm::get_shared_realm(config), IllegalCombination,
2✔
185
                              "Realms opened in immutable mode do not use an initialization function");
2✔
186
        }
2✔
187

9✔
188
        SECTION("initialization function for read-only") {
18✔
189
            config.schema_mode = SchemaMode::ReadOnly;
2✔
190
            config.initialization_function = [](auto) {};
1✔
191
            REQUIRE_EXCEPTION(Realm::get_shared_realm(config), IllegalCombination,
2✔
192
                              "Realms opened in read-only mode do not use an initialization function");
2✔
193
        }
2✔
194
        SECTION("in-memory encrypted realms are rejected") {
18✔
195
            config.in_memory = true;
2✔
196
            config.encryption_key = make_test_encryption_key();
2✔
197
            REQUIRE_EXCEPTION(Realm::get_shared_realm(config), IllegalCombination,
2✔
198
                              "Encryption is not supported for in-memory realms");
2✔
199
        }
2✔
200
    }
18✔
201

38✔
202
    SECTION("should reject mismatched config") {
76✔
203
        config.encryption_key.clear(); // may be set already when encrypting all
8✔
204

4✔
205
        SECTION("schema version") {
8✔
206
            auto realm = Realm::get_shared_realm(config);
2✔
207
            config.schema_version = 2;
2✔
208
            REQUIRE_EXCEPTION(
2✔
209
                Realm::get_shared_realm(config), MismatchedConfig,
2✔
210
                Catch::Matchers::Matches("Realm at path '.*' already opened with different schema version."));
2✔
211

1✔
212
            config.schema = util::none;
2✔
213
            config.schema_version = ObjectStore::NotVersioned;
2✔
214
            REQUIRE_NOTHROW(Realm::get_shared_realm(config));
2✔
215
        }
2✔
216

4✔
217
        SECTION("schema mode") {
8✔
218
            auto realm = Realm::get_shared_realm(config);
2✔
219
            config.schema_mode = SchemaMode::Manual;
2✔
220
            REQUIRE_EXCEPTION(
2✔
221
                Realm::get_shared_realm(config), MismatchedConfig,
2✔
222
                Catch::Matchers::Matches("Realm at path '.*' already opened with a different schema mode."));
2✔
223
        }
2✔
224

4✔
225
        SECTION("durability") {
8✔
226
            auto realm = Realm::get_shared_realm(config);
2✔
227
            config.in_memory = true;
2✔
228
            REQUIRE_EXCEPTION(
2✔
229
                Realm::get_shared_realm(config), MismatchedConfig,
2✔
230
                Catch::Matchers::Matches("Realm at path '.*' already opened with different inMemory settings."));
2✔
231
        }
2✔
232

4✔
233
        SECTION("schema") {
8✔
234
            auto realm = Realm::get_shared_realm(config);
2✔
235
            config.schema = Schema{
2✔
236
                {"object", {{"value", PropertyType::Int}, {"value2", PropertyType::Int}}},
2✔
237
            };
2✔
238
            REQUIRE_EXCEPTION(
2✔
239
                Realm::get_shared_realm(config), SchemaMismatch,
2✔
240
                Catch::Matchers::ContainsSubstring("Migration is required due to the following errors:"));
2✔
241
        }
2✔
242
    }
8✔
243

38✔
244

38✔
245
// Windows doesn't use fifos
38✔
246
#ifndef _WIN32
76✔
247
    SECTION("should be able to set a FIFO fallback path") {
76✔
248
        std::string fallback_dir = util::make_temp_dir() + "/fallback/";
2✔
249
        realm::util::try_make_dir(fallback_dir);
2✔
250
        TestFile config;
2✔
251
        config.fifo_files_fallback_path = fallback_dir;
2✔
252
        config.schema_version = 1;
2✔
253
        config.schema = Schema{
2✔
254
            {"object", {{"value", PropertyType::Int}}},
2✔
255
        };
2✔
256

1✔
257
        realm::util::make_dir(config.path + ".note");
2✔
258
        auto realm = Realm::get_shared_realm(config);
2✔
259
        auto fallback_file = util::format("%1realm_%2.note", fallback_dir,
2✔
260
                                          std::hash<std::string>()(config.path)); // Mirror internal implementation
2✔
261
        REQUIRE(util::File::exists(fallback_file));
2!
262
        realm::util::remove_dir(config.path + ".note");
2✔
263
        REQUIRE(realm::util::try_remove_dir_recursive(fallback_dir));
2!
264
    }
2✔
265

38✔
266
    SECTION("automatically append dir separator to end of fallback path") {
76✔
267
        std::string fallback_dir = util::make_temp_dir() + "/fallback";
2✔
268
        realm::util::try_make_dir(fallback_dir);
2✔
269
        TestFile config;
2✔
270
        config.fifo_files_fallback_path = fallback_dir;
2✔
271
        config.schema_version = 1;
2✔
272
        config.schema = Schema{
2✔
273
            {"object", {{"value", PropertyType::Int}}},
2✔
274
        };
2✔
275

1✔
276
        realm::util::make_dir(config.path + ".note");
2✔
277
        auto realm = Realm::get_shared_realm(config);
2✔
278
        auto fallback_file = util::format("%1/realm_%2.note", fallback_dir,
2✔
279
                                          std::hash<std::string>()(config.path)); // Mirror internal implementation
2✔
280
        REQUIRE(util::File::exists(fallback_file));
2!
281
        realm::util::remove_dir(config.path + ".note");
2✔
282
        REQUIRE(realm::util::try_remove_dir_recursive(fallback_dir));
2!
283
    }
2✔
284
#endif
76✔
285

38✔
286
    SECTION("should verify that the schema is valid") {
76✔
287
        config.schema =
2✔
288
            Schema{{"object",
2✔
289
                    {{"value", PropertyType::Int}},
2✔
290
                    {{"invalid backlink", PropertyType::LinkingObjects | PropertyType::Array, "object", "value"}}}};
2✔
291
        REQUIRE_THROWS_CONTAINING(Realm::get_shared_realm(config), "origin of linking objects property");
2✔
292
    }
2✔
293

38✔
294
    SECTION("should apply the schema if one is supplied") {
76✔
295
        Realm::get_shared_realm(config);
2✔
296

1✔
297
        {
2✔
298
            Group g(config.path, config.encryption_key.data());
2✔
299
            auto table = ObjectStore::table_for_object_type(g, "object");
2✔
300
            REQUIRE(table);
2!
301
            REQUIRE(table->get_column_count() == 1);
2!
302
            REQUIRE(table->get_column_name(*table->get_column_keys().begin()) == "value");
2!
303
        }
2✔
304

1✔
305
        config.schema_version = 2;
2✔
306
        config.schema = Schema{
2✔
307
            {"object", {{"value", PropertyType::Int}, {"value2", PropertyType::Int}}},
2✔
308
        };
2✔
309
        bool migration_called = false;
2✔
310
        config.migration_function = [&](SharedRealm old_realm, SharedRealm new_realm, Schema&) {
2✔
311
            migration_called = true;
2✔
312
            REQUIRE_FALSE(old_realm->auto_refresh());
2!
313
            REQUIRE(ObjectStore::table_for_object_type(old_realm->read_group(), "object")->get_column_count() == 1);
2!
314
            REQUIRE(ObjectStore::table_for_object_type(new_realm->read_group(), "object")->get_column_count() == 2);
2!
315
        };
2✔
316
        Realm::get_shared_realm(config);
2✔
317
        REQUIRE(migration_called);
2!
318
    }
2✔
319

38✔
320
    SECTION("should properly roll back from migration errors") {
76✔
321
        Realm::get_shared_realm(config);
2✔
322

1✔
323
        config.schema_version = 2;
2✔
324
        config.schema = Schema{
2✔
325
            {"object", {{"value", PropertyType::Int}, {"value2", PropertyType::Int}}},
2✔
326
        };
2✔
327
        bool migration_called = false;
2✔
328
        config.migration_function = [&](SharedRealm old_realm, SharedRealm new_realm, Schema&) {
4✔
329
            REQUIRE_FALSE(old_realm->auto_refresh());
4!
330
            REQUIRE(ObjectStore::table_for_object_type(old_realm->read_group(), "object")->get_column_count() == 1);
4!
331
            REQUIRE(ObjectStore::table_for_object_type(new_realm->read_group(), "object")->get_column_count() == 2);
4!
332
            if (!migration_called) {
4✔
333
                migration_called = true;
2✔
334
                throw "error";
2✔
335
            }
2✔
336
        };
4✔
337
        REQUIRE_THROWS_WITH(Realm::get_shared_realm(config), "error");
2✔
338
        REQUIRE(migration_called);
2!
339
        REQUIRE_NOTHROW(Realm::get_shared_realm(config));
2✔
340
    }
2✔
341

38✔
342
    SECTION("should read the schema from the file if none is supplied") {
76✔
343
        Realm::get_shared_realm(config);
2✔
344

1✔
345
        config.schema = util::none;
2✔
346
        auto realm = Realm::get_shared_realm(config);
2✔
347
        REQUIRE(realm->schema().size() == 1);
2!
348
        auto it = realm->schema().find("object");
2✔
349
        auto table = realm->read_group().get_table("class_object");
2✔
350
        REQUIRE(it != realm->schema().end());
2!
351
        REQUIRE(it->table_key == table->get_key());
2!
352
        REQUIRE(it->persisted_properties.size() == 1);
2!
353
        REQUIRE(it->persisted_properties[0].name == "value");
2!
354
        REQUIRE(it->persisted_properties[0].column_key == table->get_column_key("value"));
2!
355
    }
2✔
356

38✔
357
    SECTION("should read the proper schema from the file if a custom version is supplied") {
76✔
358
        Realm::get_shared_realm(config);
2✔
359

1✔
360
        config.schema = util::none;
2✔
361
        config.schema_mode = SchemaMode::AdditiveExplicit;
2✔
362
        config.schema_version = 0;
2✔
363

1✔
364
        auto realm = Realm::get_shared_realm(config);
2✔
365
        REQUIRE(realm->schema().size() == 1);
2!
366

1✔
367
        auto& db = TestHelper::get_db(realm);
2✔
368
        auto rt = db->start_read();
2✔
369
        VersionID old_version = rt->get_version_of_current_transaction();
2✔
370
        realm->close();
2✔
371

1✔
372
        config.schema = Schema{
2✔
373
            {"object", {{"value", PropertyType::Int}}},
2✔
374
            {"object1", {{"value", PropertyType::Int}}},
2✔
375
        };
2✔
376
        config.schema_version = 1;
2✔
377
        realm = Realm::get_shared_realm(config);
2✔
378
        REQUIRE(realm->schema().size() == 2);
2!
379

1✔
380
        config.schema = util::none;
2✔
381
        auto old_realm = Realm::get_shared_realm(config);
2✔
382
        // must retain 'rt' until after opening for reading at that version
1✔
383
        TestHelper::begin_read(old_realm, old_version);
2✔
384
        rt = nullptr;
2✔
385
        REQUIRE(old_realm->schema().size() == 1);
2!
386
    }
2✔
387

38✔
388
    SECTION("should sensibly handle opening an uninitialized file without a schema specified") {
76✔
389
        SECTION("cached") {
4✔
390
        }
2✔
391
        SECTION("uncached") {
4✔
392
            config.cache = false;
2✔
393
        }
2✔
394

2✔
395
        // create an empty file
2✔
396
        util::File(config.path, util::File::mode_Write);
4✔
397

2✔
398
        // open the empty file, but don't initialize the schema
2✔
399
        Realm::Config config_without_schema = config;
4✔
400
        config_without_schema.schema = util::none;
4✔
401
        config_without_schema.schema_version = ObjectStore::NotVersioned;
4✔
402
        auto realm = Realm::get_shared_realm(config_without_schema);
4✔
403
        REQUIRE(realm->schema().empty());
4!
404
        REQUIRE(realm->schema_version() == ObjectStore::NotVersioned);
4!
405
        // verify that we can get another Realm instance
2✔
406
        REQUIRE_NOTHROW(Realm::get_shared_realm(config_without_schema));
4✔
407

2✔
408
        // verify that we can also still open the file with a proper schema
2✔
409
        auto realm2 = Realm::get_shared_realm(config);
4✔
410
        REQUIRE_FALSE(realm2->schema().empty());
4!
411
        REQUIRE(realm2->schema_version() == 1);
4!
412
    }
4✔
413

38✔
414
    SECTION("should populate the table columns in the schema when opening as immutable") {
76✔
415
        Realm::get_shared_realm(config);
2✔
416

1✔
417
        config.schema_mode = SchemaMode::Immutable;
2✔
418
        auto realm = Realm::get_shared_realm(config);
2✔
419
        auto it = realm->schema().find("object");
2✔
420
        auto table = realm->read_group().get_table("class_object");
2✔
421
        REQUIRE(it != realm->schema().end());
2!
422
        REQUIRE(it->table_key == table->get_key());
2!
423
        REQUIRE(it->persisted_properties.size() == 1);
2!
424
        REQUIRE(it->persisted_properties[0].name == "value");
2!
425
        REQUIRE(it->persisted_properties[0].column_key == table->get_column_key("value"));
2!
426

1✔
427
        SECTION("refreshing an immutable Realm throws") {
2✔
428
            REQUIRE_THROWS_WITH(realm->refresh(), "Can't refresh an immutable Realm.");
2✔
429
        }
2✔
430
    }
2✔
431

38✔
432
    SECTION("should support using different table subsets on different threads") {
76✔
433
        auto realm1 = Realm::get_shared_realm(config);
2✔
434

1✔
435
        config.schema = Schema{
2✔
436
            {"object 2", {{"value", PropertyType::Int}}},
2✔
437
        };
2✔
438
        auto realm2 = Realm::get_shared_realm(config);
2✔
439

1✔
440
        config.schema = util::none;
2✔
441
        auto realm3 = Realm::get_shared_realm(config);
2✔
442

1✔
443
        config.schema = Schema{
2✔
444
            {"object", {{"value", PropertyType::Int}}},
2✔
445
        };
2✔
446
        auto realm4 = Realm::get_shared_realm(config);
2✔
447

1✔
448
        realm1->refresh();
2✔
449
        realm2->refresh();
2✔
450

1✔
451
        REQUIRE(realm1->schema().size() == 1);
2!
452
        REQUIRE(realm1->schema().find("object") != realm1->schema().end());
2!
453
        REQUIRE(realm2->schema().size() == 1);
2!
454
        REQUIRE(realm2->schema().find("object 2") != realm2->schema().end());
2!
455
        REQUIRE(realm3->schema().size() == 2);
2!
456
        REQUIRE(realm3->schema().find("object") != realm3->schema().end());
2!
457
        REQUIRE(realm3->schema().find("object 2") != realm3->schema().end());
2!
458
        REQUIRE(realm4->schema().size() == 1);
2!
459
        REQUIRE(realm4->schema().find("object") != realm4->schema().end());
2!
460
    }
2✔
461
#ifndef _WIN32
76✔
462
    SECTION("should throw when creating the notification pipe fails") {
76✔
463
        // The ExternalCommitHelper implementation on Windows doesn't rely on FIFOs
1✔
464
        std::string expected_path = config.path + ".note";
2✔
465
        REQUIRE(util::try_make_dir(config.path + ".note"));
2!
466
        if (auto tmp_dir = DBOptions::get_sys_tmp_dir(); !tmp_dir.empty()) {
2✔
467
            expected_path = util::format("%1realm_%2.note", util::normalize_dir(tmp_dir),
2✔
468
                                         std::hash<std::string>()(config.path)); // Mirror internal implementation
2✔
469
            REQUIRE(util::try_make_dir(expected_path));
2!
470
        }
2✔
471
        REQUIRE_EXCEPTION(
2✔
472
            Realm::get_shared_realm(config), FileAlreadyExists,
2✔
473
            util::format("Cannot create fifo at path '%1': a non-fifo entry already exists at that path.",
2✔
474
                         expected_path));
2✔
475
        util::remove_dir(config.path + ".note");
2✔
476
        util::try_remove_dir(expected_path);
2✔
477
    }
2✔
478
#endif
76✔
479

38✔
480
    SECTION("should get different instances on different threads") {
76✔
481
        config.cache = true;
2✔
482
        auto realm1 = Realm::get_shared_realm(config);
2✔
483
        std::thread([&] {
2✔
484
            auto realm2 = Realm::get_shared_realm(config);
2✔
485
            REQUIRE(realm1 != realm2);
2!
486
        }).join();
2✔
487
    }
2✔
488

38✔
489
    SECTION("should detect use of Realm on incorrect thread") {
76✔
490
        auto realm = Realm::get_shared_realm(config);
2✔
491
        std::thread([&] {
2✔
492
            REQUIRE_THROWS_MATCHES(realm->verify_thread(), LogicError,
2✔
493
                                   Catch::Matchers::Message("Realm accessed from incorrect thread."));
2✔
494
        }).join();
2✔
495
    }
2✔
496

38✔
497
    // Our test scheduler uses a simple integer identifier to allow cross thread scheduling
38✔
498
    class SimpleScheduler : public util::Scheduler {
76✔
499
    public:
76✔
500
        SimpleScheduler(size_t id)
76✔
501
            : Scheduler()
76✔
502
            , m_id(id)
76✔
503
        {
42✔
504
        }
8✔
505

38✔
506
        bool is_on_thread() const noexcept override
76✔
507
        {
39✔
508
            return true;
2✔
509
        }
2✔
510
        bool is_same_as(const Scheduler* other) const noexcept override
76✔
511
        {
42✔
512
            const SimpleScheduler* o = dynamic_cast<const SimpleScheduler*>(other);
8✔
513
            return (o && (o->m_id == m_id));
8✔
514
        }
8✔
515
        bool can_invoke() const noexcept override
76✔
516
        {
38✔
517
            return false;
×
518
        }
×
519
        void invoke(util::UniqueFunction<void()>&&) override {}
41✔
520

38✔
521
    protected:
76✔
522
        size_t m_id;
76✔
523
    };
76✔
524

38✔
525
    SECTION("should get different instances for different explicitly different schedulers") {
76✔
526
        config.cache = true;
2✔
527
        config.scheduler = std::make_shared<SimpleScheduler>(1);
2✔
528
        auto realm1 = Realm::get_shared_realm(config);
2✔
529
        config.scheduler = std::make_shared<SimpleScheduler>(2);
2✔
530
        auto realm2 = Realm::get_shared_realm(config);
2✔
531
        REQUIRE(realm1 != realm2);
2!
532

1✔
533
        config.scheduler = nullptr;
2✔
534
        auto realm3 = Realm::get_shared_realm(config);
2✔
535
        REQUIRE(realm1 != realm3);
2!
536
        REQUIRE(realm2 != realm3);
2!
537
    }
2✔
538

38✔
539
    SECTION("can use Realm with explicit scheduler on different thread") {
76✔
540
        config.cache = true;
2✔
541
        config.scheduler = std::make_shared<SimpleScheduler>(1);
2✔
542
        auto realm = Realm::get_shared_realm(config);
2✔
543
        std::thread([&] {
2✔
544
            REQUIRE_NOTHROW(realm->verify_thread());
2✔
545
        }).join();
2✔
546
    }
2✔
547

38✔
548
    SECTION("should get same instance for same explicit execution context on different thread") {
76✔
549
        config.cache = true;
2✔
550
        config.scheduler = std::make_shared<SimpleScheduler>(1);
2✔
551
        auto realm1 = Realm::get_shared_realm(config);
2✔
552
        std::thread([&] {
2✔
553
            auto realm2 = Realm::get_shared_realm(config);
2✔
554
            REQUIRE(realm1 == realm2);
2!
555
        }).join();
2✔
556
    }
2✔
557

38✔
558
    SECTION("should not modify the schema when fetching from the cache") {
76✔
559
        config.cache = true;
2✔
560
        auto realm = Realm::get_shared_realm(config);
2✔
561
        auto object_schema = &*realm->schema().find("object");
2✔
562
        Realm::get_shared_realm(config);
2✔
563
        REQUIRE(object_schema == &*realm->schema().find("object"));
2!
564
    }
2✔
565

38✔
566
    SECTION("should reuse cached frozen Realm if versions match") {
76✔
567
        config.cache = true;
2✔
568
        auto realm = Realm::get_shared_realm(config);
2✔
569
        realm->read_group();
2✔
570
        auto frozen = realm->freeze();
2✔
571
        frozen->read_group();
2✔
572

1✔
573
        REQUIRE(frozen != realm);
2!
574
        REQUIRE(realm->read_transaction_version() == frozen->read_transaction_version());
2!
575

1✔
576
        REQUIRE(realm->freeze() == frozen);
2!
577
        REQUIRE(Realm::get_frozen_realm(config, realm->read_transaction_version()) == frozen);
2!
578
    }
2✔
579

38✔
580
    SECTION("should not use cached frozen Realm if versions don't match") {
76✔
581
        config.cache = true;
2✔
582
        auto realm = Realm::get_shared_realm(config);
2✔
583
        realm->read_group();
2✔
584
        auto frozen1 = realm->freeze();
2✔
585
        frozen1->read_group();
2✔
586

1✔
587
        REQUIRE(frozen1 != realm);
2!
588
        REQUIRE(realm->read_transaction_version() == frozen1->read_transaction_version());
2!
589

1✔
590
        auto table = realm->read_group().get_table("class_object");
2✔
591
        realm->begin_transaction();
2✔
592
        table->create_object();
2✔
593
        realm->commit_transaction();
2✔
594

1✔
595
        REQUIRE(realm->read_transaction_version() > frozen1->read_transaction_version());
2!
596

1✔
597
        auto frozen2 = realm->freeze();
2✔
598
        frozen2->read_group();
2✔
599

1✔
600
        REQUIRE(frozen2 != frozen1);
2!
601
        REQUIRE(frozen2 != realm);
2!
602
        REQUIRE(realm->read_transaction_version() == frozen2->read_transaction_version());
2!
603
        REQUIRE(frozen2->read_transaction_version() > frozen1->read_transaction_version());
2!
604
    }
2✔
605

38✔
606
    SECTION("frozen realm should have the same schema as originating realm") {
76✔
607
        auto full_schema = Schema{
2✔
608
            {"object1", {{"value", PropertyType::Int}}},
2✔
609
            {"object2", {{"value", PropertyType::Int}}},
2✔
610
        };
2✔
611

1✔
612
        auto subset_schema = Schema{
2✔
613
            {"object1", {{"value", PropertyType::Int}}},
2✔
614
        };
2✔
615

1✔
616
        config.schema = full_schema;
2✔
617

1✔
618
        auto realm = Realm::get_shared_realm(config);
2✔
619
        realm->close();
2✔
620

1✔
621
        config.schema = subset_schema;
2✔
622

1✔
623
        realm = Realm::get_shared_realm(config);
2✔
624
        realm->read_group();
2✔
625
        auto frozen_realm = realm->freeze();
2✔
626
        auto frozen_schema = frozen_realm->schema();
2✔
627

1✔
628
        REQUIRE(full_schema != subset_schema);
2!
629
        REQUIRE(realm->schema() == subset_schema);
2!
630
        REQUIRE(frozen_schema == subset_schema);
2!
631
    }
2✔
632

38✔
633
    SECTION("frozen realm should have the correct schema even if more properties are added later") {
76✔
634
        config.schema_mode = SchemaMode::AdditiveExplicit;
2✔
635
        auto full_schema = Schema{
2✔
636
            {"object", {{"value1", PropertyType::Int}, {"value2", PropertyType::Int}}},
2✔
637
        };
2✔
638

1✔
639
        auto subset_schema = Schema{
2✔
640
            {"object", {{"value1", PropertyType::Int}}},
2✔
641
        };
2✔
642

1✔
643
        config.schema = subset_schema;
2✔
644
        auto realm = Realm::get_shared_realm(config);
2✔
645
        realm->read_group();
2✔
646

1✔
647
        config.schema = full_schema;
2✔
648
        auto realm2 = Realm::get_shared_realm(config);
2✔
649
        realm2->read_group();
2✔
650

1✔
651
        auto frozen_realm = realm->freeze();
2✔
652
        REQUIRE(realm->schema() == subset_schema);
2!
653
        REQUIRE(realm2->schema() == full_schema);
2!
654
        REQUIRE(frozen_realm->schema() == subset_schema);
2!
655
    }
2✔
656

38✔
657
    SECTION("freeze with orphaned embedded tables") {
76✔
658
        auto schema = Schema{
2✔
659
            {"object1", {{"value", PropertyType::Int}}},
2✔
660
            {"object2", ObjectSchema::ObjectType::Embedded, {{"value", PropertyType::Int}}},
2✔
661
        };
2✔
662
        config.schema = schema;
2✔
663
        config.schema_mode = SchemaMode::AdditiveDiscovered;
2✔
664
        auto realm = Realm::get_shared_realm(config);
2✔
665
        realm->read_group();
2✔
666
        auto frozen_realm = realm->freeze();
2✔
667
        REQUIRE(frozen_realm->schema() == schema);
2!
668
    }
2✔
669
}
76✔
670

671
TEST_CASE("SharedRealm: schema_subset_mode") {
28✔
672
    TestFile config;
28✔
673
    config.schema_mode = SchemaMode::AdditiveExplicit;
28✔
674
    config.schema_version = 1;
28✔
675
    config.schema_subset_mode = SchemaSubsetMode::Complete;
28✔
676
    config.encryption_key.clear();
28✔
677

14✔
678
    // Use a DB directly to simulate changes made by another process
14✔
679
    auto db = DB::create(make_in_realm_history(), config.path);
28✔
680

14✔
681
    // Changing the schema version results in update_schema() hitting a very
14✔
682
    // different code path for Additive modes, so test both with the schema version
14✔
683
    // matching and not matching
14✔
684
    auto set_schema_version = GENERATE(false, true);
28✔
685
    INFO("Matching schema version: " << set_schema_version);
28✔
686
    if (set_schema_version) {
28✔
687
        auto tr = db->start_write();
14✔
688
        ObjectStore::set_schema_version(*tr, 1);
14✔
689
        tr->commit();
14✔
690
    }
14✔
691

14✔
692
    SECTION("additional properties are added at the end") {
28✔
693
        {
4✔
694
            auto tr = db->start_write();
4✔
695
            auto table = tr->add_table("class_object");
4✔
696
            for (int i = 0; i < 5; ++i) {
24✔
697
                table->add_column(type_Int, util::format("col %1", i));
20✔
698
            }
20✔
699
            tr->commit();
4✔
700
        }
4✔
701

2✔
702
        // missing col 0 and 4, and order is different from column order
2✔
703
        config.schema = Schema{{"object",
4✔
704
                                {
4✔
705
                                    {"col 2", PropertyType::Int},
4✔
706
                                    {"col 3", PropertyType::Int},
4✔
707
                                    {"col 1", PropertyType::Int},
4✔
708
                                }}};
4✔
709

2✔
710
        auto realm = Realm::get_shared_realm(config);
4✔
711
        auto& properties = realm->schema().find("object")->persisted_properties;
4✔
712
        REQUIRE(properties.size() == 5);
4!
713
        REQUIRE(properties[0].name == "col 2");
4!
714
        REQUIRE(properties[1].name == "col 3");
4!
715
        REQUIRE(properties[2].name == "col 1");
4!
716
        REQUIRE(properties[3].name == "col 0");
4!
717
        REQUIRE(properties[4].name == "col 4");
4!
718

2✔
719
        for (auto& property : properties) {
20✔
720
            REQUIRE(property.column_key != ColKey{});
20!
721
        }
20✔
722

2✔
723
        config.schema_subset_mode.include_properties = false;
4✔
724
        realm = Realm::get_shared_realm(config);
4✔
725
        REQUIRE(realm->schema().find("object")->persisted_properties.size() == 3);
4!
726
    }
4✔
727

14✔
728
    SECTION("additional tables are added in sorted order") {
28✔
729
        {
4✔
730
            auto tr = db->start_write();
4✔
731
            // In reverse order so that just using the table order doesn't
2✔
732
            // work accidentally
2✔
733
            tr->add_table("class_F")->add_column(type_Int, "value");
4✔
734
            tr->add_table("class_E")->add_column(type_Int, "value");
4✔
735
            tr->add_table("class_D")->add_column(type_Int, "value");
4✔
736
            tr->add_table("class_C")->add_column(type_Int, "value");
4✔
737
            tr->add_table("class_B")->add_column(type_Int, "value");
4✔
738
            tr->add_table("class_A")->add_column(type_Int, "value");
4✔
739
            tr->commit();
4✔
740
        }
4✔
741

2✔
742
        config.schema = Schema{
4✔
743
            {"A", {{"value", PropertyType::Int}}},
4✔
744
            {"E", {{"value", PropertyType::Int}}},
4✔
745
            {"D", {{"value", PropertyType::Int}}},
4✔
746
        };
4✔
747
        auto realm = Realm::get_shared_realm(config);
4✔
748
        auto& schema = realm->schema();
4✔
749
        REQUIRE(schema.size() == 6);
4!
750
        REQUIRE(std::is_sorted(schema.begin(), schema.end(), [](auto& a, auto& b) {
4!
751
            return a.name < b.name;
4✔
752
        }));
4✔
753

2✔
754
        config.schema_subset_mode.include_types = false;
4✔
755
        realm = Realm::get_shared_realm(config);
4✔
756
        REQUIRE(realm->schema().size() == 3);
4!
757
    }
4✔
758

14✔
759
    SECTION("schema is updated when refreshing over a schema change") {
28✔
760
        config.schema = Schema{{"object", {{"value", PropertyType::Int}}}};
4✔
761
        auto realm = Realm::get_shared_realm(config);
4✔
762
        realm->read_group();
4✔
763
        auto& schema = realm->schema();
4✔
764

2✔
765
        {
4✔
766
            auto tr = db->start_write();
4✔
767
            tr->get_table("class_object")->add_column(type_Int, "value 2");
4✔
768
            tr->commit();
4✔
769
        }
4✔
770

2✔
771
        REQUIRE(schema.find("object")->persisted_properties.size() == 1);
4!
772
        realm->refresh();
4✔
773
        REQUIRE(schema.find("object")->persisted_properties.size() == 2);
4!
774

2✔
775
        {
4✔
776
            auto tr = db->start_write();
4✔
777
            tr->add_table("class_object 2")->add_column(type_Int, "value");
4✔
778
            tr->commit();
4✔
779
        }
4✔
780

2✔
781
        REQUIRE(schema.size() == 1);
4!
782
        realm->refresh();
4✔
783
        REQUIRE(schema.size() == 2);
4!
784
    }
4✔
785

14✔
786
    SECTION("schema is updated when schema is modified while not in a read transaction") {
28✔
787
        config.schema = Schema{{"object", {{"value", PropertyType::Int}}}};
4✔
788
        auto realm = Realm::get_shared_realm(config);
4✔
789
        auto& schema = realm->schema();
4✔
790

2✔
791
        {
4✔
792
            auto tr = db->start_write();
4✔
793
            tr->get_table("class_object")->add_column(type_Int, "value 2");
4✔
794
            tr->commit();
4✔
795
        }
4✔
796

2✔
797
        REQUIRE(schema.find("object")->persisted_properties.size() == 1);
4!
798
        realm->read_group();
4✔
799
        REQUIRE(schema.find("object")->persisted_properties.size() == 2);
4!
800
        realm->invalidate();
4✔
801

2✔
802
        {
4✔
803
            auto tr = db->start_write();
4✔
804
            tr->add_table("class_object 2")->add_column(type_Int, "value");
4✔
805
            tr->commit();
4✔
806
        }
4✔
807

2✔
808
        REQUIRE(schema.size() == 1);
4!
809
        realm->read_group();
4✔
810
        REQUIRE(schema.size() == 2);
4!
811
    }
4✔
812

14✔
813
    SECTION("frozen Realm sees the correct schema for each version") {
28✔
814
        config.schema = Schema{{"object", {{"value", PropertyType::Int}}}};
8✔
815
        std::vector<std::shared_ptr<Realm>> realms;
8✔
816
        for (int i = 0; i < 10; ++i) {
88✔
817
            realms.push_back(Realm::get_shared_realm(config));
80✔
818
            realms.back()->read_group();
80✔
819
            auto tr = db->start_write();
80✔
820
            tr->add_table(util::format("class_object %1", i))->add_column(type_Int, "value");
80✔
821
            tr->commit();
80✔
822
        }
80✔
823

4✔
824
        auto reset_schema = GENERATE(false, true);
8✔
825
        if (reset_schema) {
8✔
826
            config.schema.reset();
4✔
827
        }
4✔
828

4✔
829
        for (size_t i = 0; i < 10; ++i) {
88✔
830
            auto& r = *realms[i];
80✔
831
            REQUIRE(r.schema().size() == i + 1);
80!
832
            auto frozen = r.freeze();
80✔
833
            REQUIRE(frozen->schema().size() == i + 1);
80!
834
            REQUIRE(frozen->schema_version() == config.schema_version);
80!
835
            frozen = Realm::get_frozen_realm(config, r.read_transaction_version());
80✔
836
            REQUIRE(frozen->schema().size() == i + 1);
80!
837
            REQUIRE(frozen->schema_version() == config.schema_version);
80!
838
        }
80✔
839

4✔
840
        SECTION("schema not set in config") {
8✔
841
            config.schema = std::nullopt;
8✔
842
            for (size_t i = 0; i < 10; ++i) {
88✔
843
                auto& r = *realms[i];
80✔
844
                REQUIRE(r.schema().size() == i + 1);
80!
845
                REQUIRE(r.freeze()->schema().size() == i + 1);
80!
846
                REQUIRE(Realm::get_frozen_realm(config, r.read_transaction_version())->schema().size() == i + 1);
80!
847
            }
80✔
848
        }
8✔
849
    }
8✔
850

14✔
851
    SECTION("obtaining a frozen realm with an incompatible schema throws") {
28✔
852
        config.schema = Schema{{"object", {{"value", PropertyType::Int}}}};
4✔
853
        auto old_realm = Realm::get_shared_realm(config);
4✔
854
        {
4✔
855
            auto tr = db->start_write();
4✔
856
            auto table = tr->get_table("class_object");
4✔
857
            table->create_object();
4✔
858
            tr->commit();
4✔
859
        }
4✔
860
        old_realm->read_group();
4✔
861

2✔
862
        {
4✔
863
            auto tr = db->start_write();
4✔
864
            auto table = tr->add_table("class_object 2");
4✔
865
            ColKey val_col = table->add_column(type_Int, "value");
4✔
866
            table->create_object().set(val_col, 1);
4✔
867
            tr->commit();
4✔
868
        }
4✔
869

2✔
870
        config.schema = Schema{
4✔
871
            {"object", {{"value", PropertyType::Int}}},
4✔
872
            {"object 2", {{"value", PropertyType::Int}}},
4✔
873
        };
4✔
874
        auto new_realm = Realm::get_shared_realm(config);
4✔
875
        new_realm->read_group();
4✔
876

2✔
877
        REQUIRE(old_realm->freeze()->schema().size() == 1);
4!
878
        REQUIRE(new_realm->freeze()->schema().size() == 2);
4!
879
        REQUIRE(Realm::get_frozen_realm(config, new_realm->read_transaction_version())->schema().size() == 2);
4!
880
        // An additive change is allowed, the unknown table is empty
2✔
881
        REQUIRE(Realm::get_frozen_realm(config, old_realm->read_transaction_version())->schema().size() == 2);
4!
882

2✔
883
        config.schema = Schema{{"object", {{"value", PropertyType::String}}}}; // int -> string
4✔
884
        // Fails because the schema has an invalid breaking change
2✔
885
        REQUIRE_THROWS_AS(Realm::get_frozen_realm(config, new_realm->read_transaction_version()),
4✔
886
                          InvalidReadOnlySchemaChangeException);
4✔
887
        REQUIRE_THROWS_AS(Realm::get_frozen_realm(config, old_realm->read_transaction_version()),
4✔
888
                          InvalidReadOnlySchemaChangeException);
4✔
889
        config.schema = Schema{
4✔
890
            {"object", {{"value", PropertyType::Int}}},
4✔
891
            {"object 2", {{"value", PropertyType::String}}}, // int -> string
4✔
892
        };
4✔
893
        // fails due to invalid change on object 2 type
2✔
894
        REQUIRE_THROWS_AS(Realm::get_frozen_realm(config, new_realm->read_transaction_version()),
4✔
895
                          InvalidReadOnlySchemaChangeException);
4✔
896
        // opening the old state does not fail because the schema is an additive change
2✔
897
        auto frozen_old = Realm::get_frozen_realm(config, old_realm->read_transaction_version());
4✔
898
        REQUIRE(frozen_old->schema().size() == 2);
4!
899
        {
4✔
900
            TableRef table = frozen_old->read_group().get_table("class_object");
4✔
901
            Results results(frozen_old, table);
4✔
902
            REQUIRE(results.is_frozen());
4!
903
            REQUIRE(results.size() == 1);
4!
904
        }
4✔
905
        {
4✔
906
            TableRef table = frozen_old->read_group().get_table("class_object 2");
4✔
907
            REQUIRE(!table);
4!
908
            Results results(frozen_old, table);
4✔
909
            REQUIRE(results.is_frozen());
4!
910
            REQUIRE(results.size() == 0);
4!
911
        }
4✔
912
        config.schema = Schema{
4✔
913
            {"object", {{"value", PropertyType::Int}, {"value 2", PropertyType::String}}}, // add property
4✔
914
        };
4✔
915
        // fails due to additional property on object
2✔
916
        REQUIRE_THROWS_AS(Realm::get_frozen_realm(config, old_realm->read_transaction_version()),
4✔
917
                          InvalidReadOnlySchemaChangeException);
4✔
918
        REQUIRE_THROWS_AS(Realm::get_frozen_realm(config, new_realm->read_transaction_version()),
4✔
919
                          InvalidReadOnlySchemaChangeException);
4✔
920
    }
4✔
921
}
28✔
922

923
#if REALM_ENABLE_SYNC
924
TEST_CASE("Get Realm using Async Open", "[sync][pbs][async open]") {
30✔
925
    if (!util::EventLoop::has_implementation())
30✔
926
        return;
×
927

15✔
928
    TestSyncManager init_sync_manager;
30✔
929
    SyncTestFile config(init_sync_manager.app(), "default");
30✔
930
    config.cache = false;
30✔
931
    ObjectSchema object_schema = {"object",
30✔
932
                                  {
30✔
933
                                      {"_id", PropertyType::Int, Property::IsPrimary{true}},
30✔
934
                                      {"value", PropertyType::Int},
30✔
935
                                  }};
30✔
936
    config.schema = Schema{object_schema};
30✔
937
    SyncTestFile config2(init_sync_manager.app(), "default");
30✔
938
    config2.schema = config.schema;
30✔
939

15✔
940
    std::mutex mutex;
30✔
941

15✔
942
    auto async_open_realm = [&](const Realm::Config& config) {
26✔
943
        ThreadSafeReference realm_ref;
22✔
944
        std::exception_ptr error;
22✔
945
        auto task = Realm::get_synchronized_realm(config);
22✔
946
        task->start([&](ThreadSafeReference&& ref, std::exception_ptr e) {
22✔
947
            std::lock_guard lock(mutex);
22✔
948
            realm_ref = std::move(ref);
22✔
949
            error = e;
22✔
950
        });
22✔
951
        util::EventLoop::main().run_until([&] {
240,454✔
952
            std::lock_guard lock(mutex);
240,454✔
953
            return realm_ref || error;
240,454✔
954
        });
240,454✔
955
        return std::pair(std::move(realm_ref), error);
22✔
956
    };
22✔
957

15✔
958
    SECTION("can open synced Realms that don't already exist") {
30✔
959
        auto [ref, error] = async_open_realm(config);
2✔
960
        REQUIRE(ref);
2!
961
        REQUIRE_FALSE(error);
2!
962
        REQUIRE(Realm::get_shared_realm(std::move(ref))->read_group().get_table("class_object"));
2!
963
    }
2✔
964

15✔
965
    SECTION("can write a realm file without client file id") {
30✔
966
        ThreadSafeReference realm_ref;
2✔
967
        SyncTestFile config3(init_sync_manager.app(), "default");
2✔
968
        config3.schema = config.schema;
2✔
969
        uint64_t client_file_id;
2✔
970

1✔
971
        // Create some content
1✔
972
        auto origin = Realm::get_shared_realm(config);
2✔
973
        origin->begin_transaction();
2✔
974
        origin->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
975
        origin->commit_transaction();
2✔
976
        wait_for_upload(*origin);
2✔
977

1✔
978
        // Create realm file without client file id
1✔
979
        {
2✔
980
            auto [ref, error] = async_open_realm(config);
2✔
981
            REQUIRE(ref);
2!
982
            REQUIRE_FALSE(error);
2!
983
            // Write some data
1✔
984
            SharedRealm realm = Realm::get_shared_realm(std::move(ref));
2✔
985
            realm->begin_transaction();
2✔
986
            realm->read_group().get_table("class_object")->create_object_with_primary_key(2);
2✔
987
            realm->commit_transaction();
2✔
988
            wait_for_upload(*realm);
2✔
989
            wait_for_download(*realm);
2✔
990
            client_file_id = realm->read_group().get_sync_file_id();
2✔
991

1✔
992
            realm->convert(config3);
2✔
993
        }
2✔
994

1✔
995
        // Create some more content on the server
1✔
996
        origin->begin_transaction();
2✔
997
        origin->read_group().get_table("class_object")->create_object_with_primary_key(7);
2✔
998
        origin->commit_transaction();
2✔
999
        wait_for_upload(*origin);
2✔
1000

1✔
1001
        // Now open a realm based on the realm file created above
1✔
1002
        auto realm = Realm::get_shared_realm(config3);
2✔
1003
        wait_for_download(*realm);
2✔
1004
        wait_for_upload(*realm);
2✔
1005

1✔
1006
        // Make sure we have got a new client file id
1✔
1007
        REQUIRE(realm->read_group().get_sync_file_id() != client_file_id);
2!
1008
        REQUIRE(realm->read_group().get_table("class_object")->size() == 3);
2!
1009

1✔
1010
        // Check that we can continue committing to this realm
1✔
1011
        realm->begin_transaction();
2✔
1012
        realm->read_group().get_table("class_object")->create_object_with_primary_key(5);
2✔
1013
        realm->commit_transaction();
2✔
1014
        wait_for_upload(*realm);
2✔
1015

1✔
1016
        // Check that this change is now in the original realm
1✔
1017
        wait_for_download(*origin);
2✔
1018
        origin->refresh();
2✔
1019
        REQUIRE(origin->read_group().get_table("class_object")->size() == 4);
2!
1020
    }
2✔
1021

15✔
1022
    SECTION("downloads Realms which exist on the server") {
30✔
1023
        {
2✔
1024
            auto realm = Realm::get_shared_realm(config2);
2✔
1025
            realm->begin_transaction();
2✔
1026
            realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1027
            realm->commit_transaction();
2✔
1028
            wait_for_upload(*realm);
2✔
1029
        }
2✔
1030

1✔
1031
        auto [ref, error] = async_open_realm(config);
2✔
1032
        REQUIRE(ref);
2!
1033
        REQUIRE_FALSE(error);
2!
1034
        REQUIRE(Realm::get_shared_realm(std::move(ref))->read_group().get_table("class_object"));
2!
1035
    }
2✔
1036

15✔
1037
    SECTION("progress notifiers of a task are cancelled if the task is cancelled") {
30✔
1038
        bool progress_notifier1_called = false;
2✔
1039
        bool task1_completed = false;
2✔
1040
        bool progress_notifier2_called = false;
2✔
1041
        bool task2_completed = false;
2✔
1042
        {
2✔
1043
            auto realm = Realm::get_shared_realm(config2);
2✔
1044
            realm->begin_transaction();
2✔
1045
            realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1046
            realm->commit_transaction();
2✔
1047
            wait_for_upload(*realm);
2✔
1048
        }
2✔
1049

1✔
1050
        DBOptions options;
2✔
1051
        options.encryption_key = config.encryption_key.data();
2✔
1052
        auto db = DB::create(sync::make_client_replication(), config.path, options);
2✔
1053
        auto write = db->start_write(); // block sync from writing until we cancel
2✔
1054

1✔
1055
        std::shared_ptr<AsyncOpenTask> task = Realm::get_synchronized_realm(config);
2✔
1056
        std::shared_ptr<AsyncOpenTask> task2 = Realm::get_synchronized_realm(config);
2✔
1057
        REQUIRE(task);
2!
1058
        REQUIRE(task2);
2!
1059
        task->register_download_progress_notifier([&](uint64_t, uint64_t) {
2✔
1060
            std::lock_guard<std::mutex> guard(mutex);
×
1061
            REQUIRE(!task1_completed);
×
1062
            progress_notifier1_called = true;
×
1063
        });
×
1064
        task2->register_download_progress_notifier([&](uint64_t, uint64_t) {
2✔
1065
            std::lock_guard<std::mutex> guard(mutex);
2✔
1066
            REQUIRE(!task2_completed);
2!
1067
            progress_notifier2_called = true;
2✔
1068
        });
2✔
1069
        task->start([&](ThreadSafeReference realm_ref, std::exception_ptr err) {
1✔
1070
            REQUIRE(!err);
×
1071
            SharedRealm realm = Realm::get_shared_realm(std::move(realm_ref));
×
1072
            REQUIRE(realm);
×
1073
            std::lock_guard<std::mutex> guard(mutex);
×
1074
            task1_completed = true;
×
1075
        });
×
1076
        task->cancel();
2✔
1077
        task2->start([&](ThreadSafeReference realm_ref, std::exception_ptr err) {
2✔
1078
            REQUIRE(!err);
2!
1079
            SharedRealm realm = Realm::get_shared_realm(std::move(realm_ref));
2✔
1080
            REQUIRE(realm);
2!
1081
            std::lock_guard<std::mutex> guard(mutex);
2✔
1082
            task2_completed = true;
2✔
1083
        });
2✔
1084
        write = nullptr; // unblock sync
2✔
1085
        util::EventLoop::main().run_until([&] {
16,005✔
1086
            std::lock_guard<std::mutex> guard(mutex);
16,005✔
1087
            return task2_completed;
16,005✔
1088
        });
16,005✔
1089
        std::lock_guard<std::mutex> guard(mutex);
2✔
1090
        REQUIRE(!progress_notifier1_called);
2!
1091
        REQUIRE(!task1_completed);
2!
1092
        REQUIRE(progress_notifier2_called);
2!
1093
        REQUIRE(task2_completed);
2!
1094
    }
2✔
1095

15✔
1096
    SECTION("downloads latest state for Realms which already exist locally") {
30✔
1097
        wait_for_upload(*Realm::get_shared_realm(config));
2✔
1098

1✔
1099
        {
2✔
1100
            auto realm = Realm::get_shared_realm(config2);
2✔
1101
            realm->begin_transaction();
2✔
1102
            realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1103
            realm->commit_transaction();
2✔
1104
            wait_for_upload(*realm);
2✔
1105
        }
2✔
1106

1✔
1107
        auto [ref, error] = async_open_realm(config);
2✔
1108
        REQUIRE(ref);
2!
1109
        REQUIRE_FALSE(error);
2!
1110
        REQUIRE(Realm::get_shared_realm(std::move(ref))->read_group().get_table("class_object")->size() == 1);
2!
1111
    }
2✔
1112

15✔
1113
    SECTION("can download multiple Realms at a time") {
30✔
1114
        SyncTestFile config1(init_sync_manager.app(), "realm1");
2✔
1115
        SyncTestFile config2(init_sync_manager.app(), "realm2");
2✔
1116
        SyncTestFile config3(init_sync_manager.app(), "realm3");
2✔
1117
        SyncTestFile config4(init_sync_manager.app(), "realm4");
2✔
1118

1✔
1119
        std::vector<std::shared_ptr<AsyncOpenTask>> tasks = {
2✔
1120
            Realm::get_synchronized_realm(config1),
2✔
1121
            Realm::get_synchronized_realm(config2),
2✔
1122
            Realm::get_synchronized_realm(config3),
2✔
1123
            Realm::get_synchronized_realm(config4),
2✔
1124
        };
2✔
1125

1✔
1126
        std::atomic<int> completed{0};
2✔
1127
        for (auto& task : tasks) {
8✔
1128
            task->start([&](auto, auto) {
8✔
1129
                ++completed;
8✔
1130
            });
8✔
1131
        }
8✔
1132
        util::EventLoop::main().run_until([&] {
39,878✔
1133
            return completed == 4;
39,878✔
1134
        });
39,878✔
1135
    }
2✔
1136

15✔
1137
    // Create a token which can be parsed as a JWT but is not valid
15✔
1138
    std::string unencoded_body = nlohmann::json({{"exp", 123}, {"iat", 456}}).dump();
30✔
1139
    std::string encoded_body;
30✔
1140
    encoded_body.resize(util::base64_encoded_size(unencoded_body.size()));
30✔
1141
    util::base64_encode(unencoded_body.data(), unencoded_body.size(), &encoded_body[0], encoded_body.size());
30✔
1142
    auto invalid_token = "." + encoded_body + ".";
30✔
1143

15✔
1144
    // Token refreshing requires that we have app metadata and we can't fetch
15✔
1145
    // it normally, so just stick some fake values in
15✔
1146
    init_sync_manager.app()->sync_manager()->perform_metadata_update([&](SyncMetadataManager& manager) {
30✔
1147
        manager.set_app_metadata("GLOBAL", "location", "hostname", "ws_hostname");
30✔
1148
    });
30✔
1149

15✔
1150
    SECTION("can async open while waiting for a token refresh") {
30✔
1151
        SyncTestFile config(init_sync_manager.app(), "realm");
2✔
1152
        auto valid_token = config.sync_config->user->access_token();
2✔
1153
        config.sync_config->user->update_access_token(std::move(invalid_token));
2✔
1154

1✔
1155
        std::atomic<bool> called{false};
2✔
1156
        auto task = Realm::get_synchronized_realm(config);
2✔
1157
        task->start([&](auto ref, auto error) {
2✔
1158
            std::lock_guard<std::mutex> lock(mutex);
2✔
1159
            REQUIRE(ref);
2!
1160
            REQUIRE(!error);
2!
1161
            called = true;
2✔
1162
        });
2✔
1163

1✔
1164
        auto body = nlohmann::json({{"access_token", valid_token}}).dump();
2✔
1165
        init_sync_manager.network_callback(app::Response{200, 0, {}, body});
2✔
1166
        util::EventLoop::main().run_until([&] {
15,334✔
1167
            return called.load();
15,334✔
1168
        });
15,334✔
1169
        std::lock_guard<std::mutex> lock(mutex);
2✔
1170
        REQUIRE(called);
2!
1171
    }
2✔
1172

15✔
1173
    SECTION("cancels download and reports an error on auth error") {
30✔
1174
        struct Transport : realm::app::GenericNetworkTransport {
2✔
1175
            void send_request_to_server(
2✔
1176
                const realm::app::Request&,
2✔
1177
                realm::util::UniqueFunction<void(const realm::app::Response&)>&& completion) override
2✔
1178
            {
2✔
1179
                completion(app::Response{403});
2✔
1180
            }
2✔
1181
        };
2✔
1182
        TestSyncManager::Config tsm_config;
2✔
1183
        tsm_config.transport = std::make_shared<Transport>();
2✔
1184
        TestSyncManager tsm(tsm_config);
2✔
1185

1✔
1186
        SyncTestFile config(tsm.app(), "realm");
2✔
1187
        config.sync_config->user->update_refresh_token(std::string(invalid_token));
2✔
1188
        config.sync_config->user->update_access_token(std::move(invalid_token));
2✔
1189

1✔
1190
        bool got_error = false;
2✔
1191
        config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError) {
2✔
1192
            got_error = true;
2✔
1193
        };
2✔
1194
        std::atomic<bool> called{false};
2✔
1195
        auto task = Realm::get_synchronized_realm(config);
2✔
1196
        task->start([&](auto ref, auto error) {
2✔
1197
            std::lock_guard<std::mutex> lock(mutex);
2✔
1198
            REQUIRE(error);
2!
1199
            REQUIRE_EXCEPTION(
2✔
1200
                std::rethrow_exception(error), HTTPError,
2✔
1201
                "Unable to refresh the user access token: http error code considered fatal. Client Error: 403");
2✔
1202
            REQUIRE(!ref);
2!
1203
            called = true;
2✔
1204
        });
2✔
1205
        util::EventLoop::main().run_until([&] {
3✔
1206
            return called.load();
3✔
1207
        });
3✔
1208
        std::lock_guard<std::mutex> lock(mutex);
2✔
1209
        REQUIRE(called);
2!
1210
        REQUIRE(got_error);
2!
1211
    }
2✔
1212

15✔
1213
    SECTION("read-only mode sets the schema version") {
30✔
1214
        {
2✔
1215
            SharedRealm realm = Realm::get_shared_realm(config);
2✔
1216
            wait_for_upload(*realm);
2✔
1217
            realm->close();
2✔
1218
        }
2✔
1219

1✔
1220
        config2.schema_mode = SchemaMode::ReadOnly;
2✔
1221
        auto [ref, error] = async_open_realm(config2);
2✔
1222
        REQUIRE(ref);
2!
1223
        REQUIRE_FALSE(error);
2!
1224
        REQUIRE(Realm::get_shared_realm(std::move(ref))->schema_version() == 1);
2!
1225
    }
2✔
1226

15✔
1227
    Schema with_added_object = Schema{object_schema,
30✔
1228
                                      {"added",
30✔
1229
                                       {
30✔
1230
                                           {"_id", PropertyType::Int, Property::IsPrimary{true}},
30✔
1231
                                       }}};
30✔
1232

15✔
1233
    SECTION("read-only mode applies remote schema changes") {
30✔
1234
        // Create the local file without "added"
1✔
1235
        Realm::get_shared_realm(config2);
2✔
1236

1✔
1237
        // Add the table server-side
1✔
1238
        config.schema = with_added_object;
2✔
1239
        config2.schema = with_added_object;
2✔
1240
        {
2✔
1241
            SharedRealm realm = Realm::get_shared_realm(config);
2✔
1242
            wait_for_upload(*realm);
2✔
1243
            realm->close();
2✔
1244
        }
2✔
1245

1✔
1246
        // Verify that the table gets added when reopening
1✔
1247
        config2.schema_mode = SchemaMode::ReadOnly;
2✔
1248
        auto [ref, error] = async_open_realm(config2);
2✔
1249
        REQUIRE(ref);
2!
1250
        REQUIRE_FALSE(error);
2!
1251
        auto realm = Realm::get_shared_realm(std::move(ref));
2✔
1252
        REQUIRE(realm->schema().find("added") != realm->schema().end());
2!
1253
        REQUIRE(realm->read_group().get_table("class_added"));
2!
1254
    }
2✔
1255

15✔
1256
    SECTION("read-only mode does not create tables not present on the server") {
30✔
1257
        // Create the local file without "added"
1✔
1258
        Realm::get_shared_realm(config2);
2✔
1259

1✔
1260
        config2.schema = with_added_object;
2✔
1261
        config2.schema_mode = SchemaMode::ReadOnly;
2✔
1262
        auto [ref, error] = async_open_realm(config2);
2✔
1263
        REQUIRE(ref);
2!
1264
        REQUIRE_FALSE(error);
2!
1265
        auto realm = Realm::get_shared_realm(std::move(ref));
2✔
1266
        REQUIRE(realm->schema().find("added") != realm->schema().end());
2!
1267
        REQUIRE_FALSE(realm->read_group().get_table("class_added"));
2!
1268
    }
2✔
1269

15✔
1270
    SECTION("adding a property to a newly downloaded read-only Realm reports an error") {
30✔
1271
        // Create the Realm on the server
1✔
1272
        wait_for_upload(*Realm::get_shared_realm(config2));
2✔
1273

1✔
1274
        config.schema_mode = SchemaMode::ReadOnly;
2✔
1275
        config.schema = Schema{{"object",
2✔
1276
                                {
2✔
1277
                                    {"_id", PropertyType::Int, Property::IsPrimary{true}},
2✔
1278
                                    {"value", PropertyType::Int},
2✔
1279
                                    {"value2", PropertyType::Int},
2✔
1280
                                }}};
2✔
1281

1✔
1282
        auto [ref, error] = async_open_realm(config);
2✔
1283
        REQUIRE_FALSE(ref);
2!
1284
        REQUIRE(error);
2!
1285
        REQUIRE_THROWS_CONTAINING(std::rethrow_exception(error), "Property 'object.value2' has been added.");
2✔
1286
    }
2✔
1287

15✔
1288
    SECTION("adding a property to an existing read-only Realm reports an error") {
30✔
1289
        Realm::get_shared_realm(config);
2✔
1290

1✔
1291
        config.schema_mode = SchemaMode::ReadOnly;
2✔
1292
        config.schema = Schema{{"object",
2✔
1293
                                {
2✔
1294
                                    {"_id", PropertyType::Int, Property::IsPrimary{true}},
2✔
1295
                                    {"value", PropertyType::Int},
2✔
1296
                                    {"value2", PropertyType::Int},
2✔
1297
                                }}};
2✔
1298
        REQUIRE_THROWS_CONTAINING(Realm::get_shared_realm(config), "Property 'object.value2' has been added.");
2✔
1299

1✔
1300
        auto [ref, error] = async_open_realm(config);
2✔
1301
        REQUIRE_FALSE(ref);
2!
1302
        REQUIRE(error);
2!
1303
        REQUIRE_THROWS_CONTAINING(std::rethrow_exception(error), "Property 'object.value2' has been added.");
2✔
1304
    }
2✔
1305

15✔
1306
    SECTION("removing a property from a newly downloaded read-only Realm leaves the column in place") {
30✔
1307
        // Create the Realm on the server
1✔
1308
        wait_for_upload(*Realm::get_shared_realm(config2));
2✔
1309

1✔
1310
        // Remove the "value" property from the schema
1✔
1311
        config.schema_mode = SchemaMode::ReadOnly;
2✔
1312
        config.schema = Schema{{"object",
2✔
1313
                                {
2✔
1314
                                    {"_id", PropertyType::Int, Property::IsPrimary{true}},
2✔
1315
                                }}};
2✔
1316

1✔
1317
        auto [ref, error] = async_open_realm(config);
2✔
1318
        REQUIRE(ref);
2!
1319
        REQUIRE_FALSE(error);
2!
1320
        REQUIRE(Realm::get_shared_realm(std::move(ref))
2!
1321
                    ->read_group()
2✔
1322
                    .get_table("class_object")
2✔
1323
                    ->get_column_key("value") != ColKey{});
2✔
1324
    }
2✔
1325

15✔
1326
    SECTION("removing a property from a existing read-only Realm leaves the column in place") {
30✔
1327
        Realm::get_shared_realm(config);
2✔
1328

1✔
1329
        // Remove the "value" property from the schema
1✔
1330
        config.schema_mode = SchemaMode::ReadOnly;
2✔
1331
        config.schema = Schema{{"object",
2✔
1332
                                {
2✔
1333
                                    {"_id", PropertyType::Int, Property::IsPrimary{true}},
2✔
1334
                                }}};
2✔
1335

1✔
1336
        auto [ref, error] = async_open_realm(config);
2✔
1337
        REQUIRE(ref);
2!
1338
        REQUIRE_FALSE(error);
2!
1339
        REQUIRE(Realm::get_shared_realm(std::move(ref))
2!
1340
                    ->read_group()
2✔
1341
                    .get_table("class_object")
2✔
1342
                    ->get_column_key("value") != ColKey{});
2✔
1343
    }
2✔
1344
}
30✔
1345

1346
TEST_CASE("SharedRealm: convert", "[sync][pbs][convert]") {
8✔
1347
    TestSyncManager tsm;
8✔
1348
    ObjectSchema object_schema = {"object",
8✔
1349
                                  {
8✔
1350
                                      {"_id", PropertyType::Int, Property::IsPrimary{true}},
8✔
1351
                                      {"value", PropertyType::Int},
8✔
1352
                                  }};
8✔
1353
    Schema schema{object_schema};
8✔
1354

4✔
1355
    SyncTestFile sync_config1(tsm.app(), "default");
8✔
1356
    sync_config1.schema = schema;
8✔
1357
    TestFile local_config1;
8✔
1358
    local_config1.schema = schema;
8✔
1359
    local_config1.schema_version = sync_config1.schema_version;
8✔
1360

4✔
1361
    SECTION("can copy a synced realm to a synced realm") {
8✔
1362
        auto sync_realm1 = Realm::get_shared_realm(sync_config1);
2✔
1363
        sync_realm1->begin_transaction();
2✔
1364
        sync_realm1->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1365
        sync_realm1->commit_transaction();
2✔
1366
        wait_for_upload(*sync_realm1);
2✔
1367
        wait_for_download(*sync_realm1);
2✔
1368

1✔
1369
        // Copy to a new sync config
1✔
1370
        SyncTestFile sync_config2(tsm.app(), "default");
2✔
1371
        sync_config2.schema = schema;
2✔
1372

1✔
1373
        sync_realm1->convert(sync_config2);
2✔
1374

1✔
1375
        auto sync_realm2 = Realm::get_shared_realm(sync_config2);
2✔
1376

1✔
1377
        // Check that the data also exists in the new realm
1✔
1378
        REQUIRE(sync_realm2->read_group().get_table("class_object")->size() == 1);
2!
1379

1✔
1380
        // Verify that sync works and objects created in the new copy will get
1✔
1381
        // synchronized to the old copy
1✔
1382
        sync_realm2->begin_transaction();
2✔
1383
        sync_realm2->read_group().get_table("class_object")->create_object_with_primary_key(1);
2✔
1384
        sync_realm2->commit_transaction();
2✔
1385
        wait_for_upload(*sync_realm2);
2✔
1386
        wait_for_download(*sync_realm1);
2✔
1387

1✔
1388
        sync_realm1->refresh();
2✔
1389
        REQUIRE(sync_realm1->read_group().get_table("class_object")->size() == 2);
2!
1390
    }
2✔
1391

4✔
1392
    SECTION("can convert a synced realm to a local realm") {
8✔
1393
        auto sync_realm = Realm::get_shared_realm(sync_config1);
2✔
1394
        sync_realm->begin_transaction();
2✔
1395
        sync_realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1396
        sync_realm->commit_transaction();
2✔
1397
        wait_for_upload(*sync_realm);
2✔
1398
        wait_for_download(*sync_realm);
2✔
1399

1✔
1400
        sync_realm->convert(local_config1);
2✔
1401

1✔
1402
        auto local_realm = Realm::get_shared_realm(local_config1);
2✔
1403

1✔
1404
        // Check that the data also exists in the new realm
1✔
1405
        REQUIRE(local_realm->read_group().get_table("class_object")->size() == 1);
2!
1406
    }
2✔
1407

4✔
1408
    SECTION("can convert a local realm to a synced realm") {
8✔
1409
        auto local_realm = Realm::get_shared_realm(local_config1);
2✔
1410
        local_realm->begin_transaction();
2✔
1411
        local_realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1412
        local_realm->commit_transaction();
2✔
1413

1✔
1414
        // Copy to a new sync config
1✔
1415
        local_realm->convert(sync_config1);
2✔
1416

1✔
1417
        auto sync_realm = Realm::get_shared_realm(sync_config1);
2✔
1418

1✔
1419
        // Check that the data also exists in the new realm
1✔
1420
        REQUIRE(sync_realm->read_group().get_table("class_object")->size() == 1);
2!
1421
    }
2✔
1422

4✔
1423
    SECTION("can copy a local realm to a local realm") {
8✔
1424
        auto local_realm1 = Realm::get_shared_realm(local_config1);
2✔
1425
        local_realm1->begin_transaction();
2✔
1426
        local_realm1->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1427
        local_realm1->commit_transaction();
2✔
1428

1✔
1429
        // Copy to a new local config
1✔
1430
        TestFile local_config2;
2✔
1431
        local_config2.schema = schema;
2✔
1432
        local_config2.schema_version = local_config1.schema_version;
2✔
1433
        local_realm1->convert(local_config2);
2✔
1434

1✔
1435
        auto local_realm2 = Realm::get_shared_realm(local_config2);
2✔
1436

1✔
1437
        // Check that the data also exists in the new realm
1✔
1438
        REQUIRE(local_realm2->read_group().get_table("class_object")->size() == 1);
2!
1439
    }
2✔
1440
}
8✔
1441

1442
TEST_CASE("SharedRealm: convert - embedded objects", "[sync][pbs][convert][embedded objects]") {
16✔
1443
    TestSyncManager tsm;
16✔
1444
    ObjectSchema object_schema = {"object",
16✔
1445
                                  {
16✔
1446
                                      {"_id", PropertyType::Int, Property::IsPrimary{true}},
16✔
1447
                                      {"value", PropertyType::Int},
16✔
1448
                                      {"embedded_link", PropertyType::Object | PropertyType::Nullable, "embedded"},
16✔
1449
                                  }};
16✔
1450
    ObjectSchema embedded_schema = {"embedded",
16✔
1451
                                    ObjectSchema::ObjectType::Embedded,
16✔
1452
                                    {
16✔
1453
                                        {"name", PropertyType::String | PropertyType::Nullable},
16✔
1454
                                    }};
16✔
1455
    Schema schema{object_schema, embedded_schema};
16✔
1456

8✔
1457
    SyncTestFile sync_config1(tsm.app(), "default");
16✔
1458
    sync_config1.schema = schema;
16✔
1459
    TestFile local_config1;
16✔
1460
    local_config1.schema = schema;
16✔
1461
    local_config1.schema_version = sync_config1.schema_version;
16✔
1462

8✔
1463
    SECTION("can copy a synced realm to a synced realm") {
16✔
1464
        auto sync_realm1 = Realm::get_shared_realm(sync_config1);
4✔
1465
        sync_realm1->begin_transaction();
4✔
1466

2✔
1467
        SECTION("null embedded object") {
4✔
1468
            sync_realm1->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1469
        }
2✔
1470

2✔
1471
        SECTION("embedded object") {
4✔
1472
            auto obj = sync_realm1->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1473
            auto col_key = sync_realm1->read_group().get_table("class_object")->get_column_key("embedded_link");
2✔
1474
            obj.create_and_set_linked_object(col_key);
2✔
1475
        }
2✔
1476

2✔
1477
        sync_realm1->commit_transaction();
4✔
1478
        wait_for_upload(*sync_realm1);
4✔
1479
        wait_for_download(*sync_realm1);
4✔
1480

2✔
1481
        // Copy to a new sync config
2✔
1482
        SyncTestFile sync_config2(tsm.app(), "default");
4✔
1483
        sync_config2.schema = schema;
4✔
1484

2✔
1485
        sync_realm1->convert(sync_config2);
4✔
1486

2✔
1487
        auto sync_realm2 = Realm::get_shared_realm(sync_config2);
4✔
1488

2✔
1489
        // Check that the data also exists in the new realm
2✔
1490
        REQUIRE(sync_realm2->read_group().get_table("class_object")->size() == 1);
4!
1491

2✔
1492
        // Verify that sync works and objects created in the new copy will get
2✔
1493
        // synchronized to the old copy
2✔
1494
        sync_realm2->begin_transaction();
4✔
1495
        sync_realm2->read_group().get_table("class_object")->create_object_with_primary_key(1);
4✔
1496
        sync_realm2->commit_transaction();
4✔
1497
        wait_for_upload(*sync_realm2);
4✔
1498
        wait_for_download(*sync_realm1);
4✔
1499

2✔
1500
        sync_realm1->refresh();
4✔
1501
        REQUIRE(sync_realm1->read_group().get_table("class_object")->size() == 2);
4!
1502
    }
4✔
1503

8✔
1504
    SECTION("can convert a synced realm to a local realm") {
16✔
1505
        auto sync_realm = Realm::get_shared_realm(sync_config1);
4✔
1506
        sync_realm->begin_transaction();
4✔
1507

2✔
1508
        SECTION("null embedded object") {
4✔
1509
            sync_realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1510
        }
2✔
1511

2✔
1512
        SECTION("embedded object") {
4✔
1513
            auto obj = sync_realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1514
            auto col_key = sync_realm->read_group().get_table("class_object")->get_column_key("embedded_link");
2✔
1515
            obj.create_and_set_linked_object(col_key);
2✔
1516
        }
2✔
1517

2✔
1518
        sync_realm->commit_transaction();
4✔
1519
        wait_for_upload(*sync_realm);
4✔
1520
        wait_for_download(*sync_realm);
4✔
1521

2✔
1522
        sync_realm->convert(local_config1);
4✔
1523

2✔
1524
        auto local_realm = Realm::get_shared_realm(local_config1);
4✔
1525

2✔
1526
        // Check that the data also exists in the new realm
2✔
1527
        REQUIRE(local_realm->read_group().get_table("class_object")->size() == 1);
4!
1528
    }
4✔
1529

8✔
1530
    SECTION("can convert a local realm to a synced realm") {
16✔
1531
        auto local_realm = Realm::get_shared_realm(local_config1);
4✔
1532
        local_realm->begin_transaction();
4✔
1533

2✔
1534
        SECTION("null embedded object") {
4✔
1535
            local_realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1536
        }
2✔
1537

2✔
1538
        SECTION("embedded object") {
4✔
1539
            auto obj = local_realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1540
            auto col_key = local_realm->read_group().get_table("class_object")->get_column_key("embedded_link");
2✔
1541
            obj.create_and_set_linked_object(col_key);
2✔
1542
        }
2✔
1543

2✔
1544
        local_realm->commit_transaction();
4✔
1545

2✔
1546
        // Copy to a new sync config
2✔
1547
        local_realm->convert(sync_config1);
4✔
1548

2✔
1549
        auto sync_realm = Realm::get_shared_realm(sync_config1);
4✔
1550

2✔
1551
        // Check that the data also exists in the new realm
2✔
1552
        REQUIRE(sync_realm->read_group().get_table("class_object")->size() == 1);
4!
1553
    }
4✔
1554

8✔
1555
    SECTION("can copy a local realm to a local realm") {
16✔
1556
        auto local_realm1 = Realm::get_shared_realm(local_config1);
4✔
1557
        local_realm1->begin_transaction();
4✔
1558

2✔
1559
        SECTION("null embedded object") {
4✔
1560
            local_realm1->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1561
        }
2✔
1562

2✔
1563
        SECTION("embedded object") {
4✔
1564
            auto obj = local_realm1->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1565
            auto col_key = local_realm1->read_group().get_table("class_object")->get_column_key("embedded_link");
2✔
1566
            obj.create_and_set_linked_object(col_key);
2✔
1567
        }
2✔
1568

2✔
1569
        local_realm1->commit_transaction();
4✔
1570

2✔
1571
        // Copy to a new local config
2✔
1572
        TestFile local_config2;
4✔
1573
        local_config2.schema = schema;
4✔
1574
        local_config2.schema_version = local_config1.schema_version;
4✔
1575
        local_realm1->convert(local_config2);
4✔
1576

2✔
1577
        auto local_realm2 = Realm::get_shared_realm(local_config2);
4✔
1578

2✔
1579
        // Check that the data also exists in the new realm
2✔
1580
        REQUIRE(local_realm2->read_group().get_table("class_object")->size() == 1);
4!
1581
    }
4✔
1582
}
16✔
1583
#endif // REALM_ENABLE_SYNC
1584

1585
TEST_CASE("SharedRealm: async writes") {
104✔
1586
    _impl::RealmCoordinator::assert_no_open_realms();
104✔
1587
    if (!util::EventLoop::has_implementation())
104✔
1588
        return;
×
1589

52✔
1590
    TestFile config;
104✔
1591
    config.schema_version = 0;
104✔
1592
    config.schema = Schema{
104✔
1593
        {"object", {{"value", PropertyType::Int}, {"ints", PropertyType::Array | PropertyType::Int}}},
104✔
1594
    };
104✔
1595
    bool done = false;
104✔
1596
    auto realm = Realm::get_shared_realm(config);
104✔
1597
    auto table = realm->read_group().get_table("class_object");
104✔
1598
    auto col = table->get_column_key("value");
104✔
1599
    int write_nr = 0;
104✔
1600
    int commit_nr = 0;
104✔
1601

52✔
1602
    auto wait_for_done = [&]() {
100✔
1603
        util::EventLoop::main().run_until([&] {
109,477✔
1604
            return done;
109,477✔
1605
        });
109,477✔
1606
        REQUIRE(done);
96!
1607
    };
96✔
1608

52✔
1609
    SECTION("async commit transaction") {
104✔
1610
        realm->async_begin_transaction([&]() {
2✔
1611
            REQUIRE(write_nr == 0);
2!
1612
            ++write_nr;
2✔
1613
            table->create_object().set(col, 45);
2✔
1614
            realm->async_commit_transaction([&](std::exception_ptr) {
2✔
1615
                REQUIRE(commit_nr == 0);
2!
1616
                ++commit_nr;
2✔
1617
            });
2✔
1618
        });
2✔
1619
        for (int expected = 1; expected < 1000; ++expected) {
2,000✔
1620
            realm->async_begin_transaction([&, expected]() {
1,998✔
1621
                REQUIRE(write_nr == expected);
1,998!
1622
                ++write_nr;
1,998✔
1623
                auto o = table->get_object(0);
1,998✔
1624
                o.set(col, o.get<int64_t>(col) + 37);
1,998✔
1625
                realm->async_commit_transaction(
1,998✔
1626
                    [&](auto) {
1,998✔
1627
                        ++commit_nr;
1,998✔
1628
                        done = commit_nr == 1000;
1,998✔
1629
                    },
1,998✔
1630
                    true);
1,998✔
1631
            });
1,998✔
1632
        }
1,998✔
1633
        wait_for_done();
2✔
1634
    }
2✔
1635

52✔
1636
    auto verify_persisted_count = [&](size_t expected) {
78✔
1637
        if (realm)
52✔
1638
            realm->close();
50✔
1639
        _impl::RealmCoordinator::assert_no_open_realms();
52✔
1640

26✔
1641
        auto new_realm = Realm::get_shared_realm(config);
52✔
1642
        auto table = new_realm->read_group().get_table("class_object");
52✔
1643
        REQUIRE(table->size() == expected);
52!
1644
    };
52✔
1645

52✔
1646
    using RealmCloseFunction = void (Realm::*)();
104✔
1647
    static RealmCloseFunction close_functions[] = {&Realm::close, &Realm::invalidate};
104✔
1648
    static const char* close_function_names[] = {"close()", "invalidate()"};
104✔
1649
    for (int i = 0; i < 2; ++i) {
312✔
1650
        SECTION(close_function_names[i]) {
208✔
1651
            bool persisted = false;
40✔
1652
            SECTION("before write lock is acquired") {
40✔
1653
                DBOptions options;
4✔
1654
                options.encryption_key = config.encryption_key.data();
4✔
1655
                // Acquire the write lock with a different DB instance so that we'll
2✔
1656
                // be stuck in the Requesting stage
2✔
1657
                realm::test_util::BowlOfStonesSemaphore sema;
4✔
1658
                JoiningThread thread([&] {
4✔
1659
                    auto db = DB::create(make_in_realm_history(), config.path, options);
4✔
1660
                    auto write = db->start_write();
4✔
1661
                    sema.add_stone();
4✔
1662

2✔
1663
                    // Wait until the main thread is waiting for the lock.
2✔
1664
                    while (!db->other_writers_waiting_for_lock()) {
8✔
1665
                        millisleep(1);
4✔
1666
                    }
4✔
1667
                    write->close();
4✔
1668
                });
4✔
1669

2✔
1670
                // Wait for the background thread to have acquired the lock
2✔
1671
                sema.get_stone();
4✔
1672

2✔
1673
                auto scheduler = realm->scheduler();
4✔
1674
                realm->async_begin_transaction([&] {
2✔
1675
                    // We should never get here as the realm is closed
1676
                    FAIL();
×
1677
                });
×
1678

2✔
1679
                // close() should block until we can acquire the write lock
2✔
1680
                std::invoke(close_functions[i], *realm);
4✔
1681

2✔
1682
                {
4✔
1683
                    // Verify that we released the write lock
2✔
1684
                    auto db = DB::create(make_in_realm_history(), config.path, options);
4✔
1685
                    REQUIRE(db->start_write(/* nonblocking */ true));
4!
1686
                }
4✔
1687

2✔
1688
                // Verify that the transaction callback never got enqueued
2✔
1689
                scheduler->invoke([&] {
4✔
1690
                    done = true;
4✔
1691
                });
4✔
1692
                wait_for_done();
4✔
1693
            }
4✔
1694
            SECTION("before async_begin_transaction() callback") {
40✔
1695
                auto scheduler = realm->scheduler();
4✔
1696
                realm->async_begin_transaction([&] {
2✔
1697
                    // We should never get here as the realm is closed
1698
                    FAIL();
×
1699
                });
×
1700
                std::invoke(close_functions[i], *realm);
4✔
1701
                scheduler->invoke([&] {
4✔
1702
                    done = true;
4✔
1703
                });
4✔
1704
                wait_for_done();
4✔
1705
                verify_persisted_count(0);
4✔
1706
            }
4✔
1707
            SECTION("inside async_begin_transaction() callback before commit") {
40✔
1708
                realm->async_begin_transaction([&] {
4✔
1709
                    table->create_object().set(col, 45);
4✔
1710
                    std::invoke(close_functions[i], *realm);
4✔
1711
                    done = true;
4✔
1712
                });
4✔
1713
                wait_for_done();
4✔
1714
                verify_persisted_count(0);
4✔
1715
            }
4✔
1716
            SECTION("inside async_begin_transaction() callback after sync commit") {
40✔
1717
                realm->async_begin_transaction([&] {
4✔
1718
                    table->create_object().set(col, 45);
4✔
1719
                    realm->commit_transaction();
4✔
1720
                    std::invoke(close_functions[i], *realm);
4✔
1721
                    done = true;
4✔
1722
                });
4✔
1723
                wait_for_done();
4✔
1724
                verify_persisted_count(1);
4✔
1725
            }
4✔
1726
            SECTION("inside async_begin_transaction() callback after async commit") {
40✔
1727
                realm->async_begin_transaction([&] {
4✔
1728
                    table->create_object().set(col, 45);
4✔
1729
                    realm->async_commit_transaction([&](std::exception_ptr) {
4✔
1730
                        persisted = true;
4✔
1731
                    });
4✔
1732
                    std::invoke(close_functions[i], *realm);
4✔
1733
                    REQUIRE(persisted);
4!
1734
                    done = true;
4✔
1735
                });
4✔
1736
                wait_for_done();
4✔
1737
                verify_persisted_count(1);
4✔
1738
            }
4✔
1739
            SECTION("inside async commit completion") {
40✔
1740
                realm->async_begin_transaction([&] {
4✔
1741
                    table->create_object().set(col, 45);
4✔
1742
                    realm->async_commit_transaction([&](std::exception_ptr) {
4✔
1743
                        done = true;
4✔
1744
                        std::invoke(close_functions[i], *realm);
4✔
1745
                    });
4✔
1746
                });
4✔
1747
                wait_for_done();
4✔
1748
                verify_persisted_count(1);
4✔
1749
            }
4✔
1750
            SECTION("between commit and sync") {
40✔
1751
                realm->async_begin_transaction([&] {
4✔
1752
                    table->create_object().set(col, 45);
4✔
1753
                    realm->async_commit_transaction([&](std::exception_ptr) {
4✔
1754
                        persisted = true;
4✔
1755
                    });
4✔
1756
                    done = true;
4✔
1757
                });
4✔
1758
                wait_for_done();
4✔
1759
                std::invoke(close_functions[i], *realm);
4✔
1760
                REQUIRE(persisted);
4!
1761
                verify_persisted_count(1);
4✔
1762
            }
4✔
1763
            SECTION("with multiple pending commits") {
40✔
1764
                int complete_count = 0;
4✔
1765
                realm->async_begin_transaction([&] {
4✔
1766
                    table->create_object().set(col, 45);
4✔
1767
                    realm->async_commit_transaction([&](std::exception_ptr) {
4✔
1768
                        ++complete_count;
4✔
1769
                    });
4✔
1770
                });
4✔
1771
                realm->async_begin_transaction([&] {
4✔
1772
                    table->create_object().set(col, 45);
4✔
1773
                    realm->async_commit_transaction(
4✔
1774
                        [&](auto) {
4✔
1775
                            ++complete_count;
4✔
1776
                        },
4✔
1777
                        true);
4✔
1778
                });
4✔
1779
                realm->async_begin_transaction([&] {
4✔
1780
                    table->create_object().set(col, 45);
4✔
1781
                    realm->async_commit_transaction(
4✔
1782
                        [&](auto) {
4✔
1783
                            ++complete_count;
4✔
1784
                        },
4✔
1785
                        true);
4✔
1786
                    done = true;
4✔
1787
                });
4✔
1788

2✔
1789
                wait_for_done();
4✔
1790
                std::invoke(close_functions[i], *realm);
4✔
1791
                REQUIRE(complete_count == 3);
4!
1792
                verify_persisted_count(3);
4✔
1793
            }
4✔
1794
            SECTION("inside async_begin_transaction() with pending commits") {
40✔
1795
                int complete_count = 0;
4✔
1796
                realm->async_begin_transaction([&] {
4✔
1797
                    table->create_object().set(col, 45);
4✔
1798
                    realm->async_commit_transaction([&](std::exception_ptr) {
4✔
1799
                        ++complete_count;
4✔
1800
                    });
4✔
1801
                });
4✔
1802
                realm->async_begin_transaction([&] {
4✔
1803
                    // This create should be discarded
2✔
1804
                    table->create_object().set(col, 45);
4✔
1805
                    std::invoke(close_functions[i], *realm);
4✔
1806
                    done = true;
4✔
1807
                });
4✔
1808

2✔
1809
                wait_for_done();
4✔
1810
                std::invoke(close_functions[i], *realm);
4✔
1811
                REQUIRE(complete_count == 1);
4!
1812
                verify_persisted_count(1);
4✔
1813
            }
4✔
1814
            SECTION("within did_change()") {
40✔
1815
                struct Context : public BindingContext {
4✔
1816
                    int i;
4✔
1817
                    Context(int i)
4✔
1818
                        : i(i)
4✔
1819
                    {
4✔
1820
                    }
4✔
1821
                    void did_change(std::vector<ObserverState> const&, std::vector<void*> const&, bool) override
4✔
1822
                    {
6✔
1823
                        std::invoke(close_functions[i], *realm.lock());
6✔
1824
                    }
6✔
1825
                };
4✔
1826
                realm->m_binding_context.reset(new Context(i));
4✔
1827
                realm->m_binding_context->realm = realm;
4✔
1828

2✔
1829
                realm->async_begin_transaction([&] {
4✔
1830
                    table->create_object().set(col, 45);
4✔
1831
                    realm->async_commit_transaction([&](std::exception_ptr) {
4✔
1832
                        done = true;
4✔
1833
                    });
4✔
1834
                });
4✔
1835

2✔
1836
                wait_for_done();
4✔
1837
                verify_persisted_count(1);
4✔
1838
            }
4✔
1839
        }
40✔
1840
    }
208✔
1841

52✔
1842
    SECTION("notify only with no further actions") {
104✔
1843
        realm->async_begin_transaction(
2✔
1844
            [&] {
2✔
1845
                done = true;
2✔
1846
            },
2✔
1847
            true);
2✔
1848
        wait_for_done();
2✔
1849
        realm->cancel_transaction();
2✔
1850
    }
2✔
1851
    SECTION("notify only with synchronous commit") {
104✔
1852
        realm->async_begin_transaction(
2✔
1853
            [&] {
2✔
1854
                done = true;
2✔
1855
            },
2✔
1856
            true);
2✔
1857
        wait_for_done();
2✔
1858
        table->create_object();
2✔
1859
        realm->commit_transaction();
2✔
1860
    }
2✔
1861
    SECTION("schedule async commits after notify only") {
104✔
1862
        realm->async_begin_transaction(
2✔
1863
            [&] {
2✔
1864
                done = true;
2✔
1865
            },
2✔
1866
            true);
2✔
1867
        wait_for_done();
2✔
1868
        done = false;
2✔
1869
        realm->async_begin_transaction([&] {
2✔
1870
            table->create_object();
2✔
1871
            done = true;
2✔
1872
            realm->commit_transaction();
2✔
1873
        });
2✔
1874
        table->create_object();
2✔
1875
        realm->commit_transaction();
2✔
1876
        REQUIRE(table->size() == 1);
2!
1877
        wait_for_done();
2✔
1878
        REQUIRE(table->size() == 2);
2!
1879
    }
2✔
1880
    SECTION("exception thrown during transaction with error handler") {
104✔
1881
        Realm::AsyncHandle h = 7;
2✔
1882
        bool called = false;
2✔
1883
        realm->set_async_error_handler([&](Realm::AsyncHandle handle, std::exception_ptr error) {
2✔
1884
            REQUIRE(error);
2!
1885
            REQUIRE_THROWS_CONTAINING(std::rethrow_exception(error), "an error");
2✔
1886
            CHECK(handle == h);
2!
1887
            called = true;
2✔
1888
        });
2✔
1889
        h = realm->async_begin_transaction([&] {
2✔
1890
            table->create_object();
2✔
1891
            done = true;
2✔
1892
            throw std::runtime_error("an error");
2✔
1893
        });
2✔
1894
        wait_for_done();
2✔
1895

1✔
1896
        // Transaction should have been rolled back
1✔
1897
        REQUIRE_FALSE(realm->is_in_transaction());
2!
1898
        REQUIRE(table->size() == 0);
2!
1899
        REQUIRE(called);
2!
1900

1✔
1901
        // Should be able to perform another write afterwards
1✔
1902
        done = false;
2✔
1903
        called = false;
2✔
1904
        h = realm->async_begin_transaction([&] {
2✔
1905
            table->create_object();
2✔
1906
            realm->commit_transaction();
2✔
1907
            done = true;
2✔
1908
        });
2✔
1909
        wait_for_done();
2✔
1910
        REQUIRE(table->size() == 1);
2!
1911
        REQUIRE_FALSE(called);
2!
1912
    }
2✔
1913
#ifndef _WIN32
104✔
1914
    SECTION("exception thrown during transaction without error handler") {
104✔
1915
        realm->set_async_error_handler(nullptr);
2✔
1916
        realm->async_begin_transaction([&] {
2✔
1917
            table->create_object();
2✔
1918
            throw std::runtime_error("an error");
2✔
1919
        });
2✔
1920
        REQUIRE_THROWS_CONTAINING(util::EventLoop::main().run_until([&] {
2✔
1921
            return false;
2✔
1922
        }),
2✔
1923
                                  "an error");
2✔
1924

1✔
1925
        // Transaction should have been rolled back
1✔
1926
        REQUIRE_FALSE(realm->is_in_transaction());
2!
1927
        REQUIRE(table->size() == 0);
2!
1928

1✔
1929
        // Should be able to perform another write afterwards
1✔
1930
        realm->async_begin_transaction([&, realm] {
2✔
1931
            table->create_object();
2✔
1932
            realm->commit_transaction();
2✔
1933
            done = true;
2✔
1934
        });
2✔
1935
        wait_for_done();
2✔
1936
        REQUIRE(table->size() == 1);
2!
1937
    }
2✔
1938
    SECTION("exception thrown during transaction without error handler after closing Realm") {
104✔
1939
        realm->set_async_error_handler(nullptr);
2✔
1940
        realm->async_begin_transaction([&] {
2✔
1941
            realm->close();
2✔
1942
            throw std::runtime_error("an error");
2✔
1943
        });
2✔
1944
        REQUIRE_THROWS_CONTAINING(util::EventLoop::main().run_until([&] {
2✔
1945
            return false;
2✔
1946
        }),
2✔
1947
                                  "an error");
2✔
1948
        REQUIRE(realm->is_closed());
2!
1949
    }
2✔
1950
#endif
104✔
1951
    SECTION("exception thrown from async commit completion callback with error handler") {
104✔
1952
        Realm::AsyncHandle h;
2✔
1953
        realm->set_async_error_handler([&](Realm::AsyncHandle handle, std::exception_ptr error) {
2✔
1954
            REQUIRE(error);
2!
1955
            REQUIRE_THROWS_CONTAINING(std::rethrow_exception(error), "an error");
2✔
1956
            CHECK(handle == h);
2!
1957
            done = true;
2✔
1958
        });
2✔
1959

1✔
1960
        realm->begin_transaction();
2✔
1961
        table->create_object();
2✔
1962
        h = realm->async_commit_transaction([&](std::exception_ptr) {
2✔
1963
            throw std::runtime_error("an error");
2✔
1964
        });
2✔
1965
        wait_for_done();
2✔
1966
        verify_persisted_count(1);
2✔
1967
    }
2✔
1968
#ifndef _WIN32
104✔
1969
    SECTION("exception thrown from async commit completion callback without error handler") {
104✔
1970
        realm->begin_transaction();
2✔
1971
        table->create_object();
2✔
1972
        realm->async_commit_transaction([&](std::exception_ptr) {
2✔
1973
            throw std::runtime_error("an error");
2✔
1974
        });
2✔
1975
        REQUIRE_THROWS_CONTAINING(util::EventLoop::main().run_until([&] {
2✔
1976
            return false;
2✔
1977
        }),
2✔
1978
                                  "an error");
2✔
1979
        REQUIRE(table->size() == 1);
2!
1980
    }
2✔
1981
#endif
104✔
1982

52✔
1983
    if (_impl::SimulatedFailure::is_enabled()) {
104✔
1984
        SECTION("error in the synchronous part of async commit") {
104✔
1985
            realm->begin_transaction();
2✔
1986
            table->create_object();
2✔
1987

1✔
1988
            using sf = _impl::SimulatedFailure;
2✔
1989
            sf::OneShotPrimeGuard pg(sf::shared_group__grow_reader_mapping);
2✔
1990
            REQUIRE_THROWS_AS(realm->async_commit_transaction([&](std::exception_ptr) {
2✔
1991
                FAIL("should not call completion");
2✔
1992
            }),
2✔
1993
                              _impl::SimulatedFailure);
2✔
1994
            REQUIRE_FALSE(realm->is_in_transaction());
2!
1995
        }
2✔
1996
        SECTION("error in the async part of async commit") {
104✔
1997
            realm->begin_transaction();
2✔
1998
            table->create_object();
2✔
1999

1✔
2000
            using sf = _impl::SimulatedFailure;
2✔
2001
            sf::set_thread_local(false);
2✔
2002
            sf::OneShotPrimeGuard pg(sf::group_writer__commit);
2✔
2003
            realm->async_commit_transaction([&](std::exception_ptr e) {
2✔
2004
                REQUIRE(e);
2!
2005
                REQUIRE_THROWS_AS(std::rethrow_exception(e), _impl::SimulatedFailure);
2✔
2006
                done = true;
2✔
2007
            });
2✔
2008
            wait_for_done();
2✔
2009
            sf::set_thread_local(true);
2✔
2010
        }
2✔
2011
    }
104✔
2012
    SECTION("throw exception from did_change()") {
104✔
2013
        struct Context : public BindingContext {
2✔
2014
            void did_change(std::vector<ObserverState> const&, std::vector<void*> const&, bool) override
2✔
2015
            {
2✔
2016
                throw std::runtime_error("expected error");
2✔
2017
            }
2✔
2018
        };
2✔
2019
        realm->m_binding_context.reset(new Context);
2✔
2020

1✔
2021
        realm->begin_transaction();
2✔
2022
        auto table = realm->read_group().get_table("class_object");
2✔
2023
        table->create_object();
2✔
2024
        REQUIRE_THROWS_WITH(realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2025
            done = true;
2✔
2026
        }),
2✔
2027
                            "expected error");
2✔
2028
        wait_for_done();
2✔
2029
    }
2✔
2030
    SECTION("cancel scheduled async transaction") {
104✔
2031
        auto handle = realm->async_begin_transaction([&, realm]() {
1✔
2032
            table->create_object().set(col, 45);
×
2033
            realm->async_commit_transaction(
×
2034
                [&](auto) {
×
2035
                    done = true;
×
2036
                },
×
2037
                true);
×
2038
        });
×
2039
        realm->async_begin_transaction([&, realm]() {
2✔
2040
            table->create_object().set(col, 90);
2✔
2041
            realm->async_commit_transaction(
2✔
2042
                [&](auto) {
2✔
2043
                    done = true;
2✔
2044
                },
2✔
2045
                true);
2✔
2046
        });
2✔
2047
        realm->async_cancel_transaction(handle);
2✔
2048
        wait_for_done();
2✔
2049
        auto table = realm->read_group().get_table("class_object");
2✔
2050
        REQUIRE(table->size() == 1);
2!
2051
        REQUIRE(table->begin()->get<Int>("value") == 90);
2!
2052
    }
2✔
2053
    SECTION("synchronous cancel inside async transaction") {
104✔
2054
        realm->async_begin_transaction([&, realm]() {
2✔
2055
            REQUIRE(table->size() == 0);
2!
2056
            table->create_object().set(col, 45);
2✔
2057
            REQUIRE(table->size() == 1);
2!
2058
            realm->cancel_transaction();
2✔
2059
            REQUIRE(table->size() == 0);
2!
2060
            done = true;
2✔
2061
        });
2✔
2062
        wait_for_done();
2✔
2063
    }
2✔
2064
    SECTION("synchronous commit of async transaction after async commit which allows grouping") {
104✔
2065
        realm->async_begin_transaction([&, realm]() {
2✔
2066
            table->create_object().set(col, 45);
2✔
2067
            realm->async_commit_transaction(
2✔
2068
                [&](auto) {
2✔
2069
                    done = true;
2✔
2070
                },
2✔
2071
                true);
2✔
2072
        });
2✔
2073
        realm->async_begin_transaction([&, realm]() {
2✔
2074
            table->create_object().set(col, 45);
2✔
2075
            realm->commit_transaction();
2✔
2076
        });
2✔
2077
        wait_for_done();
2✔
2078
        auto table = realm->read_group().get_table("class_object");
2✔
2079
        REQUIRE(table->size() == 2);
2!
2080
    }
2✔
2081
    SECTION("synchronous transaction after async transaction with no commit") {
104✔
2082
        realm->async_begin_transaction([&]() {
2✔
2083
            table->create_object().set(col, 80);
2✔
2084
            done = true;
2✔
2085
        });
2✔
2086
        wait_for_done();
2✔
2087
        realm->begin_transaction();
2✔
2088
        table->create_object().set(col, 90);
2✔
2089
        realm->commit_transaction();
2✔
2090
        verify_persisted_count(1);
2✔
2091
    }
2✔
2092
    SECTION("synchronous transaction with scheduled async transaction with no commit") {
104✔
2093
        realm->async_begin_transaction([&]() {
2✔
2094
            table->create_object().set(col, 80);
2✔
2095
            done = true;
2✔
2096
        });
2✔
2097
        realm->begin_transaction();
2✔
2098
        table->create_object().set(col, 90);
2✔
2099
        realm->commit_transaction();
2✔
2100
        wait_for_done();
2✔
2101
        verify_persisted_count(1);
2✔
2102
    }
2✔
2103
    SECTION("synchronous transaction with scheduled async transaction") {
104✔
2104
        realm->async_begin_transaction([&, realm]() {
2✔
2105
            table->create_object().set(col, 80);
2✔
2106
            realm->commit_transaction();
2✔
2107
            done = true;
2✔
2108
        });
2✔
2109
        realm->begin_transaction();
2✔
2110
        table->create_object().set(col, 90);
2✔
2111
        realm->commit_transaction();
2✔
2112
        wait_for_done();
2✔
2113
        REQUIRE(table->size() == 2);
2!
2114
        REQUIRE(table->get_object(0).get<Int>(col) == 90);
2!
2115
        REQUIRE(table->get_object(1).get<Int>(col) == 80);
2!
2116
    }
2✔
2117
    SECTION("synchronous transaction with async write") {
104✔
2118
        realm->begin_transaction();
2✔
2119
        table->create_object().set(col, 45);
2✔
2120
        realm->async_commit_transaction();
2✔
2121

1✔
2122
        realm->begin_transaction();
2✔
2123
        table->create_object().set(col, 90);
2✔
2124
        realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2125
            done = true;
2✔
2126
        });
2✔
2127
        wait_for_done();
2✔
2128
        verify_persisted_count(2);
2✔
2129
    }
2✔
2130
    SECTION("synchronous transaction mixed with async transactions") {
104✔
2131
        realm->async_begin_transaction([&, realm]() {
2✔
2132
            table->create_object().set(col, 45);
2✔
2133
            done = true;
2✔
2134
            realm->async_commit_transaction();
2✔
2135
        });
2✔
2136
        realm->async_begin_transaction([&, realm]() {
2✔
2137
            table->create_object().set(col, 45);
2✔
2138
            realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2139
                done = true;
2✔
2140
            });
2✔
2141
        });
2✔
2142
        wait_for_done();
2✔
2143
        realm->begin_transaction(); // Here syncing of first async tr has not completed
2✔
2144
        REQUIRE(table->size() == 1);
2!
2145
        table->create_object().set(col, 90);
2✔
2146
        realm->commit_transaction(); // Will re-initiate async writes
2✔
2147

1✔
2148
        done = false;
2✔
2149
        wait_for_done();
2✔
2150
        verify_persisted_count(3);
2✔
2151
    }
2✔
2152
    SECTION("asynchronous transaction mixed with sync transaction that is cancelled") {
104✔
2153
        bool persisted = false;
2✔
2154
        realm->async_begin_transaction([&, realm]() {
2✔
2155
            table->create_object().set(col, 45);
2✔
2156
            done = true;
2✔
2157
            realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2158
                persisted = true;
2✔
2159
            });
2✔
2160
        });
2✔
2161
        realm->async_begin_transaction([&, realm]() {
2✔
2162
            table->create_object().set(col, 45);
2✔
2163
            auto handle = realm->async_commit_transaction([&](std::exception_ptr) {
1✔
2164
                FAIL();
×
2165
            });
×
2166
            realm->async_cancel_transaction(handle);
2✔
2167
        });
2✔
2168
        wait_for_done();
2✔
2169
        realm->begin_transaction();
2✔
2170
        CHECK(persisted);
2!
2171
        persisted = false;
2✔
2172
        REQUIRE(table->size() == 1);
2!
2173
        table->create_object().set(col, 90);
2✔
2174
        realm->cancel_transaction();
2✔
2175

1✔
2176
        util::EventLoop::main().run_until([&] {
865✔
2177
            return !realm->is_in_async_transaction();
865✔
2178
        });
865✔
2179

1✔
2180
        REQUIRE(table->size() == 2);
2!
2181
        REQUIRE(!table->find_first_int(col, 90));
2!
2182
    }
2✔
2183
    SECTION("cancelled sync transaction with pending async transaction") {
104✔
2184
        realm->async_begin_transaction([&, realm]() {
2✔
2185
            table->create_object().set(col, 45);
2✔
2186
            realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2187
                done = true;
2✔
2188
            });
2✔
2189
        });
2✔
2190
        realm->begin_transaction();
2✔
2191
        REQUIRE(table->size() == 0);
2!
2192
        table->create_object();
2✔
2193
        realm->cancel_transaction();
2✔
2194
        REQUIRE(table->size() == 0);
2!
2195
        wait_for_done();
2✔
2196
        verify_persisted_count(1);
2✔
2197
    }
2✔
2198
    SECTION("cancelled sync transaction with pending async commit") {
104✔
2199
        bool persisted = false;
2✔
2200
        realm->async_begin_transaction([&, realm]() {
2✔
2201
            table->create_object().set(col, 45);
2✔
2202
            done = true;
2✔
2203
            realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2204
                persisted = true;
2✔
2205
            });
2✔
2206
        });
2✔
2207
        wait_for_done();
2✔
2208
        realm->begin_transaction();
2✔
2209
        REQUIRE(table->size() == 1);
2!
2210
        table->create_object();
2✔
2211
        realm->cancel_transaction();
2✔
2212

1✔
2213
        util::EventLoop::main().run_until([&] {
3✔
2214
            return persisted;
3✔
2215
        });
3✔
2216
        verify_persisted_count(1);
2✔
2217
    }
2✔
2218
    SECTION("sync commit of async transaction with subsequent pending async transaction") {
104✔
2219
        realm->async_begin_transaction([&, realm]() {
2✔
2220
            table->create_object();
2✔
2221
            realm->commit_transaction();
2✔
2222
        });
2✔
2223
        realm->async_begin_transaction([&, realm]() {
2✔
2224
            table->create_object();
2✔
2225
            realm->commit_transaction();
2✔
2226
            done = true;
2✔
2227
        });
2✔
2228
        wait_for_done();
2✔
2229
        REQUIRE(table->size() == 2);
2!
2230
    }
2✔
2231
    SECTION("release reference to Realm after async begin") {
104✔
2232
        std::weak_ptr<Realm> weak_realm = realm;
2✔
2233
        realm->async_begin_transaction([&]() {
2✔
2234
            table->create_object().set(col, 45);
2✔
2235
            weak_realm.lock()->async_commit_transaction([&](std::exception_ptr) {
2✔
2236
                done = true;
2✔
2237
            });
2✔
2238
        });
2✔
2239
        realm = nullptr;
2✔
2240
        wait_for_done();
2✔
2241
        verify_persisted_count(1);
2✔
2242
    }
2✔
2243
    SECTION("object change information") {
104✔
2244
        realm->begin_transaction();
2✔
2245
        auto col = table->get_column_key("ints");
2✔
2246
        auto obj = table->create_object();
2✔
2247
        auto list = obj.get_list<Int>(col);
2✔
2248
        for (int i = 0; i < 3; ++i)
8✔
2249
            list.add(i);
6✔
2250
        realm->commit_transaction();
2✔
2251

1✔
2252
        Observer observer(obj);
2✔
2253
        observer.realm = realm;
2✔
2254
        realm->m_binding_context.reset(&observer);
2✔
2255

1✔
2256
        realm->async_begin_transaction([&]() {
2✔
2257
            list.clear();
2✔
2258
            done = true;
2✔
2259
        });
2✔
2260
        wait_for_done();
2✔
2261
        REQUIRE(observer.array_change(0, col) == IndexSet{0, 1, 2});
2!
2262
        realm->m_binding_context.release();
2✔
2263
    }
2✔
2264

52✔
2265
    SECTION("begin_transaction() from within did_change()") {
104✔
2266
        struct Context : public BindingContext {
2✔
2267
            void did_change(std::vector<ObserverState> const&, std::vector<void*> const&, bool) override
2✔
2268
            {
4✔
2269
                auto r = realm.lock();
4✔
2270
                r->begin_transaction();
4✔
2271
                auto table = r->read_group().get_table("class_object");
4✔
2272
                table->create_object();
4✔
2273
                if (++change_count == 1) {
4✔
2274
                    r->commit_transaction();
2✔
2275
                }
2✔
2276
                else {
2✔
2277
                    r->cancel_transaction();
2✔
2278
                }
2✔
2279
            }
4✔
2280
            int change_count = 0;
2✔
2281
        };
2✔
2282

1✔
2283
        realm->m_binding_context.reset(new Context());
2✔
2284
        realm->m_binding_context->realm = realm;
2✔
2285

1✔
2286
        realm->begin_transaction();
2✔
2287
        auto table = realm->read_group().get_table("class_object");
2✔
2288
        table->create_object();
2✔
2289
        bool persisted = false;
2✔
2290
        realm->async_commit_transaction([&persisted](auto) {
2✔
2291
            persisted = true;
2✔
2292
        });
2✔
2293
        REQUIRE(table->size() == 2);
2!
2294
        REQUIRE(persisted);
2!
2295
    }
2✔
2296

52✔
2297
    SECTION("async write grouping") {
104✔
2298
        size_t completion_calls = 0;
2✔
2299
        for (size_t i = 0; i < 41; ++i) {
84✔
2300
            realm->async_begin_transaction([&, i, realm] {
82✔
2301
                // The top ref in the Realm file should only be updated once every 20 commits
41✔
2302
                CHECK(Group(config.path, config.encryption_key.data()).get_table("class_object")->size() ==
82!
2303
                      (i / 20) * 20);
82✔
2304

41✔
2305
                table->create_object();
82✔
2306
                realm->async_commit_transaction(
82✔
2307
                    [&](std::exception_ptr) {
82✔
2308
                        ++completion_calls;
82✔
2309
                    },
82✔
2310
                    true);
82✔
2311
            });
82✔
2312
        }
82✔
2313
        util::EventLoop::main().run_until([&] {
5,624✔
2314
            return completion_calls == 41;
5,624✔
2315
        });
5,624✔
2316
    }
2✔
2317

52✔
2318
    SECTION("async write grouping with manual barriers") {
104✔
2319
        size_t completion_calls = 0;
2✔
2320
        for (size_t i = 0; i < 41; ++i) {
84✔
2321
            realm->async_begin_transaction([&, i, realm] {
82✔
2322
                // The top ref in the Realm file should only be updated once every 6 commits
41✔
2323
                CHECK(Group(config.path, config.encryption_key.data()).get_table("class_object")->size() ==
82!
2324
                      (i / 6) * 6);
82✔
2325

41✔
2326
                table->create_object();
82✔
2327
                realm->async_commit_transaction(
82✔
2328
                    [&](std::exception_ptr) {
82✔
2329
                        ++completion_calls;
82✔
2330
                    },
82✔
2331
                    (i + 1) % 6 != 0);
82✔
2332
            });
82✔
2333
        }
82✔
2334
        util::EventLoop::main().run_until([&] {
10,531✔
2335
            return completion_calls == 41;
10,531✔
2336
        });
10,531✔
2337
    }
2✔
2338

52✔
2339
    SECTION("async writes scheduled inside sync write") {
104✔
2340
        realm->begin_transaction();
2✔
2341
        realm->async_begin_transaction([&] {
2✔
2342
            REQUIRE(table->size() == 1);
2!
2343
            table->create_object();
2✔
2344
            realm->async_commit_transaction();
2✔
2345
        });
2✔
2346
        realm->async_begin_transaction([&] {
2✔
2347
            REQUIRE(table->size() == 2);
2!
2348
            table->create_object();
2✔
2349
            realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2350
                done = true;
2✔
2351
            });
2✔
2352
        });
2✔
2353
        REQUIRE(table->size() == 0);
2!
2354
        table->create_object();
2✔
2355
        realm->commit_transaction();
2✔
2356
        wait_for_done();
2✔
2357
        REQUIRE(table->size() == 3);
2!
2358
    }
2✔
2359

52✔
2360
    SECTION("async writes scheduled inside multiple sync write") {
104✔
2361
        realm->begin_transaction();
2✔
2362
        realm->async_begin_transaction([&] {
2✔
2363
            REQUIRE(table->size() == 2);
2!
2364
            table->create_object();
2✔
2365
            realm->async_commit_transaction();
2✔
2366
        });
2✔
2367
        realm->async_begin_transaction([&] {
2✔
2368
            REQUIRE(table->size() == 3);
2!
2369
            table->create_object();
2✔
2370
            realm->async_commit_transaction();
2✔
2371
        });
2✔
2372
        REQUIRE(table->size() == 0);
2!
2373
        table->create_object();
2✔
2374
        realm->commit_transaction();
2✔
2375

1✔
2376
        realm->begin_transaction();
2✔
2377
        realm->async_begin_transaction([&] {
2✔
2378
            REQUIRE(table->size() == 4);
2!
2379
            table->create_object();
2✔
2380
            realm->async_commit_transaction();
2✔
2381
        });
2✔
2382
        realm->async_begin_transaction([&] {
2✔
2383
            REQUIRE(table->size() == 5);
2!
2384
            table->create_object();
2✔
2385
            realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2386
                done = true;
2✔
2387
            });
2✔
2388
        });
2✔
2389
        REQUIRE(table->size() == 1);
2!
2390
        table->create_object();
2✔
2391
        realm->commit_transaction();
2✔
2392

1✔
2393

1✔
2394
        wait_for_done();
2✔
2395
        REQUIRE(table->size() == 6);
2!
2396
    }
2✔
2397

52✔
2398
    SECTION("async writes which would run inside sync writes are deferred") {
104✔
2399
        realm->async_begin_transaction([&] {
2✔
2400
            done = true;
2✔
2401
        });
2✔
2402

1✔
2403
        // Wait for the background thread to hold the write lock (without letting
1✔
2404
        // the event loop run so that the scheduled task isn't run)
1✔
2405
        DBOptions options;
2✔
2406
        options.encryption_key = config.encryption_key.data();
2✔
2407
        auto db = DB::create(make_in_realm_history(), config.path, options);
2✔
2408
        while (db->start_write(true))
2✔
2409
            millisleep(1);
×
2410

1✔
2411
        realm->begin_transaction();
2✔
2412

1✔
2413
        // Invoke the pending callback
1✔
2414
        util::EventLoop::main().run_pending();
2✔
2415
        // Should not have run the async write block
1✔
2416
        REQUIRE(done == false);
2!
2417

1✔
2418
        // Should run the async write block once the synchronous transaction is done
1✔
2419
        realm->cancel_transaction();
2✔
2420
        REQUIRE(done == false);
2!
2421
        util::EventLoop::main().run_pending();
2✔
2422
        REQUIRE(done == true);
2!
2423
    }
2✔
2424

52✔
2425
    util::EventLoop::main().run_until([&] {
162✔
2426
        return !realm || !realm->has_pending_async_work();
162✔
2427
    });
162✔
2428

52✔
2429
    _impl::RealmCoordinator::clear_all_caches();
104✔
2430
}
104✔
2431
// Our libuv scheduler currently does not support background threads, so we can
2432
// only run this on apple platforms
2433
#if REALM_PLATFORM_APPLE
2434
TEST_CASE("SharedRealm: async writes on multiple threads") {
5✔
2435
    _impl::RealmCoordinator::assert_no_open_realms();
5✔
2436

2437
    TestFile config;
5✔
2438
    config.cache = true;
5✔
2439
    config.schema_version = 0;
5✔
2440
    config.schema = Schema{{"object", {{"value", PropertyType::Int}}}};
5✔
2441
    auto realm = Realm::get_shared_realm(config);
5✔
2442
    auto table_key = realm->read_group().get_table("class_object")->get_key();
5✔
2443
    realm->close();
5✔
2444

2445
    struct QueueState {
5✔
2446
        dispatch_queue_t queue;
5✔
2447
        Realm::Config config;
5✔
2448
    };
5✔
2449
    std::vector<QueueState> queues;
5✔
2450
    for (int i = 0; i < 10; ++i) {
55✔
2451
        auto queue = dispatch_queue_create(util::format("queue %1", i).c_str(), 0);
50✔
2452
        Realm::Config queue_config = config;
50✔
2453
        queue_config.scheduler = util::Scheduler::make_dispatch(static_cast<void*>(queue));
50✔
2454
        queues.push_back({queue, std::move(queue_config)});
50✔
2455
    }
50✔
2456

2457
    std::atomic<size_t> completions = 0;
5✔
2458
    // Capturing by reference when mixing lambda and blocks is weird, so capture
2459
    // a pointer instead
2460
    auto completions_ptr = &completions;
5✔
2461

2462
    auto async_write_and_async_commit = [=](const Realm::Config& config) {
124✔
2463
        Realm::get_shared_realm(config)->async_begin_transaction([=] {
124✔
2464
            auto realm = Realm::get_shared_realm(config);
124✔
2465
            realm->read_group().get_table(table_key)->create_object();
124✔
2466
            realm->async_commit_transaction([=](std::exception_ptr) {
124✔
2467
                ++*completions_ptr;
124✔
2468
            });
124✔
2469
        });
124✔
2470
    };
124✔
2471
    auto async_write_and_sync_commit = [=](const Realm::Config& config) {
124✔
2472
        Realm::get_shared_realm(config)->async_begin_transaction([=] {
124✔
2473
            auto realm = Realm::get_shared_realm(config);
124✔
2474
            realm->read_group().get_table(table_key)->create_object();
124✔
2475
            realm->commit_transaction();
124✔
2476
            ++*completions_ptr;
124✔
2477
        });
124✔
2478
    };
124✔
2479
    auto sync_write_and_async_commit = [=](const Realm::Config& config) {
124✔
2480
        auto realm = Realm::get_shared_realm(config);
124✔
2481
        realm->begin_transaction();
124✔
2482
        realm->read_group().get_table(table_key)->create_object();
124✔
2483
        realm->async_commit_transaction([=](std::exception_ptr) {
124✔
2484
            ++*completions_ptr;
124✔
2485
        });
124✔
2486
    };
124✔
2487
    auto sync_write_and_sync_commit = [=](const Realm::Config& config) {
124✔
2488
        auto realm = Realm::get_shared_realm(config);
124✔
2489
        realm->begin_transaction();
124✔
2490
        realm->read_group().get_table(table_key)->create_object();
124✔
2491
        realm->commit_transaction();
124✔
2492
        ++*completions_ptr;
124✔
2493
    };
124✔
2494

2495
    SECTION("async begin and async commit") {
5✔
2496
        for (auto& queue : queues) {
10✔
2497
            dispatch_async(queue.queue, ^{
10✔
2498
                for (int i = 0; i < 10; ++i) {
110✔
2499
                    async_write_and_async_commit(queue.config);
100✔
2500
                }
100✔
2501
            });
10✔
2502
        }
10✔
2503
        util::EventLoop::main().run_until([&] {
134✔
2504
            return completions == 100;
134✔
2505
        });
134✔
2506
    }
1✔
2507
    SECTION("async begin and sync commit") {
5✔
2508
        for (auto& queue : queues) {
10✔
2509
            dispatch_async(queue.queue, ^{
10✔
2510
                for (int i = 0; i < 10; ++i) {
110✔
2511
                    async_write_and_sync_commit(queue.config);
100✔
2512
                }
100✔
2513
            });
10✔
2514
        }
10✔
2515
        util::EventLoop::main().run_until([&] {
130✔
2516
            return completions == 100;
130✔
2517
        });
130✔
2518
    }
1✔
2519
    SECTION("sync begin and async commit") {
5✔
2520
        for (auto& queue : queues) {
10✔
2521
            dispatch_async(queue.queue, ^{
10✔
2522
                for (int i = 0; i < 10; ++i) {
110✔
2523
                    sync_write_and_async_commit(queue.config);
100✔
2524
                }
100✔
2525
            });
10✔
2526
        }
10✔
2527
        util::EventLoop::main().run_until([&] {
138✔
2528
            return completions == 100;
138✔
2529
        });
138✔
2530
    }
1✔
2531
    SECTION("sync begin and sync commit") {
5✔
2532
        for (auto& queue : queues) {
10✔
2533
            dispatch_async(queue.queue, ^{
10✔
2534
                for (int i = 0; i < 10; ++i) {
110✔
2535
                    sync_write_and_sync_commit(queue.config);
100✔
2536
                }
100✔
2537
            });
10✔
2538
        }
10✔
2539
        util::EventLoop::main().run_until([&] {
134✔
2540
            return completions == 100;
134✔
2541
        });
134✔
2542
    }
1✔
2543
    SECTION("mixed sync and async") {
5✔
2544
        // Test every permutation of each of the variants
2545
        struct IndexedOp {
1✔
2546
            int index;
1✔
2547
            std::function<void(const Realm::Config& config)> fn;
1✔
2548
        };
1✔
2549
        std::array<IndexedOp, 4> functions{{
1✔
2550
            {0, async_write_and_async_commit},
1✔
2551
            {1, sync_write_and_async_commit},
1✔
2552
            {2, async_write_and_sync_commit},
1✔
2553
            {3, sync_write_and_sync_commit},
1✔
2554
        }};
1✔
2555
        size_t i = 0;
1✔
2556
        size_t expected_completions = 0;
1✔
2557
        do {
24✔
2558
            auto& queue = queues[i++ % 10];
24✔
2559
            auto functions_copy = functions;
24✔
2560
            dispatch_async(queue.queue, ^{
24✔
2561
                for (auto& fn : functions_copy) {
96✔
2562
                    fn.fn(queue.config);
96✔
2563
                }
96✔
2564
            });
24✔
2565
            expected_completions += 4;
24✔
2566
        } while (std::next_permutation(functions.begin(), functions.end(), [](auto& a, auto& b) {
70✔
2567
            return a.index < b.index;
70✔
2568
        }));
70✔
2569

2570
        util::EventLoop::main().run_until([&] {
142✔
2571
            return completions == expected_completions;
142✔
2572
        });
142✔
2573
    }
1✔
2574

2575

2576
    realm = Realm::get_shared_realm(config);
5✔
2577
    REQUIRE(realm->read_group().get_table(table_key)->size() == completions);
5!
2578

2579
    for (auto& queue : queues) {
50✔
2580
        dispatch_sync(queue.queue, ^{
50✔
2581
                      });
50✔
2582
    }
50✔
2583
}
5✔
2584
#endif
2585

2586
class LooperDelegate {
2587
public:
2588
    LooperDelegate() {}
2✔
2589
    void run_once()
2590
    {
1,735✔
2591
        for (auto it = m_tasks.begin(); it != m_tasks.end(); ++it) {
2,650✔
2592
            if (it->may_run && *it->may_run) {
921✔
2593
                it->the_job();
6✔
2594
                m_tasks.erase(it);
6✔
2595
                return;
6✔
2596
            }
6✔
2597
        }
921✔
2598
    }
1,735✔
2599
    std::shared_ptr<bool> add_task(util::UniqueFunction<void()>&& the_job)
2600
    {
6✔
2601
        m_tasks.push_back(Task{std::make_shared<bool>(false), std::move(the_job)});
6✔
2602
        return m_tasks.back().may_run;
6✔
2603
    }
6✔
2604
    bool has_tasks()
2605
    {
×
2606
        return !m_tasks.empty();
×
2607
    }
×
2608

2609
private:
2610
    struct Task {
2611
        std::shared_ptr<bool> may_run;
2612
        util::UniqueFunction<void()> the_job;
2613
    };
2614
    std::vector<Task> m_tasks;
2615
};
2616

2617
#ifndef _WIN32
2618
TEST_CASE("SharedRealm: async_writes_2") {
2✔
2619
    _impl::RealmCoordinator::assert_no_open_realms();
2✔
2620
    if (!util::EventLoop::has_implementation())
2✔
2621
        return;
×
2622

1✔
2623
    TestFile config;
2✔
2624
    config.cache = false;
2✔
2625
    config.schema_version = 0;
2✔
2626
    config.schema = Schema{
2✔
2627
        {"object", {{"value", PropertyType::Int}}},
2✔
2628
    };
2✔
2629
    bool done = false;
2✔
2630
    auto realm = Realm::get_shared_realm(config);
2✔
2631
    int write_nr = 0;
2✔
2632
    int commit_nr = 0;
2✔
2633
    auto table = realm->read_group().get_table("class_object");
2✔
2634
    auto col = table->get_column_key("value");
2✔
2635
    LooperDelegate ld;
2✔
2636
    std::shared_ptr<bool> t1_rdy = ld.add_task([&, realm]() {
2✔
2637
        REQUIRE(write_nr == 0);
2!
2638
        ++write_nr;
2✔
2639
        table->create_object().set(col, 45);
2✔
2640
        realm->cancel_transaction();
2✔
2641
    });
2✔
2642
    std::shared_ptr<bool> t2_rdy = ld.add_task([&, realm]() {
2✔
2643
        REQUIRE(write_nr == 1);
2!
2644
        ++write_nr;
2✔
2645
        table->create_object().set(col, 45);
2✔
2646
        realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2647
            REQUIRE(commit_nr == 0);
2!
2648
            ++commit_nr;
2✔
2649
        });
2✔
2650
    });
2✔
2651
    std::shared_ptr<bool> t3_rdy = ld.add_task([&, realm]() {
2✔
2652
        ++write_nr;
2✔
2653
        auto o = table->get_object(0);
2✔
2654
        o.set(col, o.get<int64_t>(col) + 37);
2✔
2655
        realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2656
            ++commit_nr;
2✔
2657
            done = true;
2✔
2658
        });
2✔
2659
    });
2✔
2660

1✔
2661
    // Make some notify_only transactions
1✔
2662
    realm->async_begin_transaction(
2✔
2663
        [&]() {
2✔
2664
            *t1_rdy = true;
2✔
2665
        },
2✔
2666
        true);
2✔
2667
    realm->async_begin_transaction(
2✔
2668
        [&]() {
2✔
2669
            *t2_rdy = true;
2✔
2670
        },
2✔
2671
        true);
2✔
2672
    realm->async_begin_transaction(
2✔
2673
        [&]() {
2✔
2674
            *t3_rdy = true;
2✔
2675
        },
2✔
2676
        true);
2✔
2677

1✔
2678
    util::EventLoop::main().run_until([&, realm] {
1,735✔
2679
        ld.run_once();
1,735✔
2680
        return done;
1,735✔
2681
    });
1,735✔
2682
    REQUIRE(done);
2!
2683
}
2✔
2684
#endif
2685

2686
TEST_CASE("SharedRealm: notifications") {
12✔
2687
    if (!util::EventLoop::has_implementation())
12✔
2688
        return;
×
2689

6✔
2690
    TestFile config;
12✔
2691
    config.schema_version = 0;
12✔
2692
    config.schema = Schema{
12✔
2693
        {"object", {{"value", PropertyType::Int}}},
12✔
2694
    };
12✔
2695

6✔
2696
    struct Context : BindingContext {
12✔
2697
        size_t* change_count;
12✔
2698
        util::UniqueFunction<void()> did_change_fn;
12✔
2699
        util::UniqueFunction<void()> changes_available_fn;
12✔
2700

6✔
2701
        Context(size_t* out)
12✔
2702
            : change_count(out)
12✔
2703
        {
12✔
2704
        }
12✔
2705

6✔
2706
        void did_change(std::vector<ObserverState> const&, std::vector<void*> const&, bool) override
12✔
2707
        {
20✔
2708
            ++*change_count;
20✔
2709
            if (did_change_fn)
20✔
2710
                did_change_fn();
12✔
2711
        }
20✔
2712

6✔
2713
        void changes_available() override
12✔
2714
        {
9✔
2715
            if (changes_available_fn)
6✔
2716
                changes_available_fn();
2✔
2717
        }
6✔
2718
    };
12✔
2719

6✔
2720
    size_t change_count = 0;
12✔
2721
    auto realm = Realm::get_shared_realm(config);
12✔
2722
    realm->read_group();
12✔
2723
    auto context = new Context{&change_count};
12✔
2724
    realm->m_binding_context.reset(context);
12✔
2725
    realm->m_binding_context->realm = realm;
12✔
2726

6✔
2727
    SECTION("local notifications are sent synchronously") {
12✔
2728
        realm->begin_transaction();
2✔
2729
        REQUIRE(change_count == 0);
2!
2730
        realm->commit_transaction();
2✔
2731
        REQUIRE(change_count == 1);
2!
2732
    }
2✔
2733
#ifndef _WIN32
12✔
2734
    SECTION("remote notifications are sent asynchronously") {
12✔
2735
        auto r2 = Realm::get_shared_realm(config);
2✔
2736
        r2->begin_transaction();
2✔
2737
        r2->commit_transaction();
2✔
2738
        REQUIRE(change_count == 0);
2!
2739
        util::EventLoop::main().run_until([&] {
9✔
2740
            return change_count > 0;
9✔
2741
        });
9✔
2742
        REQUIRE(change_count == 1);
2!
2743
    }
2✔
2744

6✔
2745
    SECTION("notifications created in async transaction are sent synchronously") {
12✔
2746
        realm->async_begin_transaction([&] {
2✔
2747
            REQUIRE(change_count == 0);
2!
2748
            realm->async_commit_transaction();
2✔
2749
            REQUIRE(change_count == 1);
2!
2750
        });
2✔
2751
        REQUIRE(change_count == 0);
2!
2752
        util::EventLoop::main().run_until([&] {
12✔
2753
            return change_count > 0;
12✔
2754
        });
12✔
2755
        REQUIRE(change_count == 1);
2!
2756
        util::EventLoop::main().run_until([&] {
1,425✔
2757
            return !realm->has_pending_async_work();
1,425✔
2758
        });
1,425✔
2759
    }
2✔
2760
#endif
12✔
2761
    SECTION("refresh() from within changes_available() refreshes") {
12✔
2762
        context->changes_available_fn = [&] {
2✔
2763
            REQUIRE(realm->refresh());
2!
2764
        };
2✔
2765
        realm->set_auto_refresh(false);
2✔
2766

1✔
2767
        auto r2 = Realm::get_shared_realm(config);
2✔
2768
        r2->begin_transaction();
2✔
2769
        r2->commit_transaction();
2✔
2770
        realm->notify();
2✔
2771
        // Should return false as the realm was already advanced
1✔
2772
        REQUIRE_FALSE(realm->refresh());
2!
2773
    }
2✔
2774

6✔
2775
    SECTION("refresh() from within did_change() is a no-op") {
12✔
2776
        context->did_change_fn = [&] {
4✔
2777
            if (change_count > 1)
4✔
2778
                return;
2✔
2779

1✔
2780
            // Create another version so that refresh() advances the version
1✔
2781
            auto r2 = Realm::get_shared_realm(realm->config());
2✔
2782
            r2->begin_transaction();
2✔
2783
            r2->commit_transaction();
2✔
2784

1✔
2785
            REQUIRE_FALSE(realm->refresh());
2!
2786
        };
2✔
2787

1✔
2788
        auto r2 = Realm::get_shared_realm(config);
2✔
2789
        r2->begin_transaction();
2✔
2790
        r2->commit_transaction();
2✔
2791

1✔
2792
        REQUIRE(realm->refresh());
2!
2793
        REQUIRE(change_count == 1);
2!
2794

1✔
2795
        REQUIRE(realm->refresh());
2!
2796
        REQUIRE(change_count == 2);
2!
2797
        REQUIRE_FALSE(realm->refresh());
2!
2798
    }
2✔
2799

6✔
2800
    SECTION("begin_write() from within did_change() produces recursive notifications") {
12✔
2801
        context->did_change_fn = [&] {
8✔
2802
            if (realm->is_in_transaction())
8✔
2803
                realm->cancel_transaction();
6✔
2804
            if (change_count > 3)
8✔
2805
                return;
2✔
2806

3✔
2807
            // Create another version so that begin_write() advances the version
3✔
2808
            auto r2 = Realm::get_shared_realm(realm->config());
6✔
2809
            r2->begin_transaction();
6✔
2810
            r2->commit_transaction();
6✔
2811

3✔
2812
            realm->begin_transaction();
6✔
2813
            REQUIRE(change_count == 4);
6!
2814
        };
6✔
2815

1✔
2816
        auto r2 = Realm::get_shared_realm(config);
2✔
2817
        r2->begin_transaction();
2✔
2818
        r2->commit_transaction();
2✔
2819
        REQUIRE(realm->refresh());
2!
2820
        REQUIRE(change_count == 4);
2!
2821
        REQUIRE_FALSE(realm->refresh());
2!
2822
    }
2✔
2823
}
12✔
2824

2825
TEST_CASE("SharedRealm: schema updating from external changes") {
14✔
2826
    TestFile config;
14✔
2827
    config.schema_version = 0;
14✔
2828
    config.schema_mode = SchemaMode::AdditiveExplicit;
14✔
2829
    config.schema = Schema{
14✔
2830
        {"object",
14✔
2831
         {
14✔
2832
             {"value", PropertyType::Int, Property::IsPrimary{true}},
14✔
2833
             {"value 2", PropertyType::Int, Property::IsPrimary{false}, Property::IsIndexed{true}},
14✔
2834
         }},
14✔
2835
    };
14✔
2836

7✔
2837
    SECTION("newly added columns update table columns but are not added to properties") {
14✔
2838
        // Does this test add any value when column keys are stable?
2✔
2839
        auto r1 = Realm::get_shared_realm(config);
4✔
2840
        auto r2 = Realm::get_shared_realm(config);
4✔
2841
        auto test = [&] {
4✔
2842
            r2->begin_transaction();
4✔
2843
            r2->read_group().get_table("class_object")->add_column(type_String, "new col");
4✔
2844
            r2->commit_transaction();
4✔
2845

2✔
2846
            auto& object_schema = *r1->schema().find("object");
4✔
2847
            REQUIRE(object_schema.persisted_properties.size() == 2);
4!
2848
            ColKey col = object_schema.persisted_properties[0].column_key;
4✔
2849
            r1->refresh();
4✔
2850
            REQUIRE(object_schema.persisted_properties[0].column_key == col);
4!
2851
        };
4✔
2852
        SECTION("with an active read transaction") {
4✔
2853
            r1->read_group();
2✔
2854
            test();
2✔
2855
        }
2✔
2856
        SECTION("without an active read transaction") {
4✔
2857
            r1->invalidate();
2✔
2858
            test();
2✔
2859
        }
2✔
2860
    }
4✔
2861

7✔
2862
    SECTION("beginning a read transaction checks for incompatible changes") {
14✔
2863
        auto r = Realm::get_shared_realm(config);
10✔
2864
        r->invalidate();
10✔
2865

5✔
2866
        auto& db = TestHelper::get_db(r);
10✔
2867
        WriteTransaction wt(db);
10✔
2868
        auto& table = *wt.get_table("class_object");
10✔
2869

5✔
2870
        SECTION("removing a property") {
10✔
2871
            table.remove_column(table.get_column_key("value"));
2✔
2872
            wt.commit();
2✔
2873
            REQUIRE_THROWS_CONTAINING(r->refresh(), "Property 'object.value' has been removed.");
2✔
2874
        }
2✔
2875

5✔
2876
        SECTION("change property type") {
10✔
2877
            table.remove_column(table.get_column_key("value 2"));
2✔
2878
            table.add_column(type_Float, "value 2");
2✔
2879
            wt.commit();
2✔
2880
            REQUIRE_THROWS_CONTAINING(r->refresh(),
2✔
2881
                                      "Property 'object.value 2' has been changed from 'int' to 'float'");
2✔
2882
        }
2✔
2883

5✔
2884
        SECTION("make property optional") {
10✔
2885
            table.remove_column(table.get_column_key("value 2"));
2✔
2886
            table.add_column(type_Int, "value 2", true);
2✔
2887
            wt.commit();
2✔
2888
            REQUIRE_THROWS_CONTAINING(r->refresh(), "Property 'object.value 2' has been made optional");
2✔
2889
        }
2✔
2890

5✔
2891
        SECTION("recreate column with no changes") {
10✔
2892
            table.remove_column(table.get_column_key("value 2"));
2✔
2893
            table.add_column(type_Int, "value 2");
2✔
2894
            wt.commit();
2✔
2895
            REQUIRE_NOTHROW(r->refresh());
2✔
2896
        }
2✔
2897

5✔
2898
        SECTION("remove index from non-PK") {
10✔
2899
            table.remove_search_index(table.get_column_key("value 2"));
2✔
2900
            wt.commit();
2✔
2901
            REQUIRE_NOTHROW(r->refresh());
2✔
2902
        }
2✔
2903
    }
10✔
2904
}
14✔
2905

2906
TEST_CASE("SharedRealm: close()") {
4✔
2907
    TestFile config;
4✔
2908
    config.schema_version = 1;
4✔
2909
    config.schema = Schema{
4✔
2910
        {"object", {{"value", PropertyType::Int}}},
4✔
2911
        {"list", {{"list", PropertyType::Object | PropertyType::Array, "object"}}},
4✔
2912
    };
4✔
2913

2✔
2914
    auto realm = Realm::get_shared_realm(config);
4✔
2915

2✔
2916
    SECTION("all functions throw ClosedRealmException after close") {
4✔
2917
        const char* msg = "Cannot access realm that has been closed.";
2✔
2918

1✔
2919
        realm->close();
2✔
2920
        REQUIRE(realm->is_closed());
2!
2921
        REQUIRE_EXCEPTION(realm->verify_open(), ClosedRealm, msg);
2✔
2922

1✔
2923
        REQUIRE_EXCEPTION(realm->update_schema(Schema{}), ClosedRealm, msg);
2✔
2924
        REQUIRE_EXCEPTION(realm->rename_property(Schema{}, "", "", ""), ClosedRealm, msg);
2✔
2925
        REQUIRE_EXCEPTION(realm->set_schema_subset(Schema{}), ClosedRealm, msg);
2✔
2926

1✔
2927
        REQUIRE_EXCEPTION(realm->begin_transaction(), ClosedRealm, msg);
2✔
2928
        REQUIRE_EXCEPTION(realm->commit_transaction(), ClosedRealm, msg);
2✔
2929
        REQUIRE_EXCEPTION(realm->cancel_transaction(), ClosedRealm, msg);
2✔
2930
        REQUIRE(!realm->is_in_transaction());
2!
2931

1✔
2932
        REQUIRE_EXCEPTION(realm->async_begin_transaction(nullptr), ClosedRealm, msg);
2✔
2933
        REQUIRE_EXCEPTION(realm->async_commit_transaction(nullptr), ClosedRealm, msg);
2✔
2934
        REQUIRE_EXCEPTION(realm->async_cancel_transaction(0), ClosedRealm, msg);
2✔
2935
        REQUIRE_FALSE(realm->is_in_async_transaction());
2!
2936

1✔
2937
        REQUIRE_EXCEPTION(realm->freeze(), ClosedRealm, msg);
2✔
2938
        REQUIRE_FALSE(realm->is_frozen());
2!
2939
        REQUIRE_EXCEPTION(realm->get_number_of_versions(), ClosedRealm, msg);
2✔
2940
        REQUIRE_EXCEPTION(realm->read_transaction_version(), ClosedRealm, msg);
2✔
2941
        REQUIRE_EXCEPTION(realm->duplicate(), ClosedRealm, msg);
2✔
2942

1✔
2943
        REQUIRE_EXCEPTION(realm->enable_wait_for_change(), ClosedRealm, msg);
2✔
2944
        REQUIRE_EXCEPTION(realm->wait_for_change(), ClosedRealm, msg);
2✔
2945
        REQUIRE_EXCEPTION(realm->wait_for_change_release(), ClosedRealm, msg);
2✔
2946

1✔
2947
        REQUIRE_NOTHROW(realm->notify());
2✔
2948
        REQUIRE_EXCEPTION(realm->refresh(), ClosedRealm, msg);
2✔
2949
        REQUIRE_EXCEPTION(realm->invalidate(), ClosedRealm, msg);
2✔
2950
        REQUIRE_EXCEPTION(realm->compact(), ClosedRealm, msg);
2✔
2951
        REQUIRE_EXCEPTION(realm->convert(realm->config()), ClosedRealm, msg);
2✔
2952
        REQUIRE_EXCEPTION(realm->write_copy(), ClosedRealm, msg);
2✔
2953

1✔
2954
#if REALM_ENABLE_SYNC
2✔
2955
        REQUIRE_FALSE(realm->sync_session());
2!
2956
        msg = "Flexible sync is not enabled";
2✔
2957
        REQUIRE_EXCEPTION(realm->get_latest_subscription_set(), IllegalOperation, msg);
2✔
2958
        REQUIRE_EXCEPTION(realm->get_active_subscription_set(), IllegalOperation, msg);
2✔
2959
#endif
2✔
2960
    }
2✔
2961

2✔
2962
    SECTION("fully closes database file even with live notifiers") {
4✔
2963
        auto& group = realm->read_group();
2✔
2964
        realm->begin_transaction();
2✔
2965
        auto obj = ObjectStore::table_for_object_type(group, "list")->create_object();
2✔
2966
        realm->commit_transaction();
2✔
2967

1✔
2968
        Results results(realm, ObjectStore::table_for_object_type(group, "object"));
2✔
2969
        List list(realm, obj.get_linklist("list"));
2✔
2970
        Object object(realm, obj);
2✔
2971

1✔
2972
        auto obj_token = object.add_notification_callback([](CollectionChangeSet) {});
2✔
2973
        auto list_token = list.add_notification_callback([](CollectionChangeSet) {});
2✔
2974
        auto results_token = results.add_notification_callback([](CollectionChangeSet) {});
2✔
2975

1✔
2976
        // Perform a dummy transaction to ensure the notifiers actually acquire
1✔
2977
        // resources that need to be closed
1✔
2978
        realm->begin_transaction();
2✔
2979
        realm->commit_transaction();
2✔
2980

1✔
2981
        realm->close();
2✔
2982

1✔
2983
        // Verify that we're able to acquire an exclusive lock
1✔
2984
        REQUIRE(DB::call_with_lock(config.path, [](auto) {}));
2!
2985
    }
2✔
2986
}
4✔
2987

2988
TEST_CASE("Realm::delete_files()") {
12✔
2989
    TestFile config;
12✔
2990
    config.schema_version = 1;
12✔
2991
    config.schema = Schema{{"object", {{"value", PropertyType::Int}}}};
12✔
2992
    auto realm = Realm::get_shared_realm(config);
12✔
2993
    auto path = config.path;
12✔
2994

6✔
2995
    // We need to create some additional files that might not be present
6✔
2996
    // for a freshly opened realm but need to be tested for as the will
6✔
2997
    // be created during a Realm's life cycle.
6✔
2998
    (void)util::File(path + ".log", util::File::mode_Write);
12✔
2999

6✔
3000
    SECTION("Deleting files of a closed Realm succeeds.") {
12✔
3001
        realm->close();
2✔
3002
        bool did_delete = false;
2✔
3003
        Realm::delete_files(path, &did_delete);
2✔
3004
        REQUIRE(did_delete);
2!
3005
        REQUIRE_FALSE(util::File::exists(path));
2!
3006
        REQUIRE_FALSE(util::File::exists(path + ".management"));
2!
3007
        REQUIRE_FALSE(util::File::exists(path + ".note"));
2!
3008
        REQUIRE_FALSE(util::File::exists(path + ".log"));
2!
3009

1✔
3010
        // Deleting the .lock file is not safe. It must still exist.
1✔
3011
        REQUIRE(util::File::exists(path + ".lock"));
2!
3012
    }
2✔
3013

6✔
3014
    SECTION("Trying to delete files of an open Realm fails.") {
12✔
3015
        REQUIRE_EXCEPTION(Realm::delete_files(path), ErrorCodes::DeleteOnOpenRealm,
2✔
3016
                          util::format("Cannot delete files of an open Realm: '%1' is still in use.", path));
2✔
3017
        REQUIRE(util::File::exists(path + ".lock"));
2!
3018
        REQUIRE(util::File::exists(path));
2!
3019
        REQUIRE(util::File::exists(path + ".management"));
2!
3020
#ifndef _WIN32
2✔
3021
        REQUIRE(util::File::exists(path + ".note"));
2!
3022
#endif
2✔
3023
        REQUIRE(util::File::exists(path + ".log"));
2!
3024
    }
2✔
3025

6✔
3026
    SECTION("Deleting the same Realm multiple times.") {
12✔
3027
        realm->close();
2✔
3028
        Realm::delete_files(path);
2✔
3029
        Realm::delete_files(path);
2✔
3030
        Realm::delete_files(path);
2✔
3031
    }
2✔
3032

6✔
3033
    SECTION("Calling delete on a folder that does not exist.") {
12✔
3034
        auto fake_path = "/tmp/doesNotExist/realm.424242";
2✔
3035
        bool did_delete = false;
2✔
3036
        Realm::delete_files(fake_path, &did_delete);
2✔
3037
        REQUIRE_FALSE(did_delete);
2!
3038
    }
2✔
3039

6✔
3040
    SECTION("passing did_delete is optional") {
12✔
3041
        realm->close();
2✔
3042
        Realm::delete_files(path, nullptr);
2✔
3043
    }
2✔
3044

6✔
3045
    SECTION("Deleting a Realm which does not exist does not set did_delete") {
12✔
3046
        TestFile new_config;
2✔
3047
        bool did_delete = false;
2✔
3048
        Realm::delete_files(new_config.path, &did_delete);
2✔
3049
        REQUIRE_FALSE(did_delete);
2!
3050
    }
2✔
3051
}
12✔
3052

3053
TEST_CASE("ShareRealm: in-memory mode from buffer") {
2✔
3054
    TestFile config;
2✔
3055
    config.schema_version = 1;
2✔
3056
    config.schema = Schema{
2✔
3057
        {"object", {{"value", PropertyType::Int}}},
2✔
3058
    };
2✔
3059

1✔
3060
    SECTION("Save and open Realm from in-memory buffer") {
2✔
3061
        // Write in-memory copy of Realm to a buffer
1✔
3062
        auto realm = Realm::get_shared_realm(config);
2✔
3063
        OwnedBinaryData realm_buffer = realm->write_copy();
2✔
3064

1✔
3065
        // Open the buffer as a new (immutable in-memory) Realm
1✔
3066
        realm::Realm::Config config2;
2✔
3067
        config2.in_memory = true;
2✔
3068
        config2.schema_mode = SchemaMode::Immutable;
2✔
3069
        config2.realm_data = realm_buffer.get();
2✔
3070

1✔
3071
        auto realm2 = Realm::get_shared_realm(config2);
2✔
3072

1✔
3073
        // Verify that it can read the schema and that it is the same
1✔
3074
        REQUIRE(realm->schema().size() == 1);
2!
3075
        auto it = realm->schema().find("object");
2✔
3076
        auto table = realm->read_group().get_table("class_object");
2✔
3077
        REQUIRE(it != realm->schema().end());
2!
3078
        REQUIRE(it->table_key == table->get_key());
2!
3079
        REQUIRE(it->persisted_properties.size() == 1);
2!
3080
        REQUIRE(it->persisted_properties[0].name == "value");
2!
3081
        REQUIRE(it->persisted_properties[0].column_key == table->get_column_key("value"));
2!
3082

1✔
3083
        // Test invalid configs
1✔
3084
        realm::Realm::Config config3;
2✔
3085
        config3.realm_data = realm_buffer.get();
2✔
3086
        REQUIRE_EXCEPTION(Realm::get_shared_realm(config3), IllegalCombination,
2✔
3087
                          "In-memory realms initialized from memory buffers can only be opened in read-only mode");
2✔
3088

1✔
3089
        config3.in_memory = true;
2✔
3090
        config3.schema_mode = SchemaMode::Immutable;
2✔
3091
        config3.path = "path";
2✔
3092
        REQUIRE_EXCEPTION(Realm::get_shared_realm(config3), IllegalCombination,
2✔
3093
                          "Specifying both memory buffer and path is invalid");
2✔
3094

1✔
3095
        config3.path = "";
2✔
3096
        config3.encryption_key = std::vector<char>(64, 'a');
2✔
3097
        REQUIRE_EXCEPTION(Realm::get_shared_realm(config3), IllegalCombination,
2✔
3098
                          "Memory buffers do not support encryption");
2✔
3099
    }
2✔
3100
}
2✔
3101

3102
TEST_CASE("ShareRealm: realm closed in did_change callback") {
6✔
3103
    TestFile config;
6✔
3104
    config.schema_version = 1;
6✔
3105
    config.schema = Schema{
6✔
3106
        {"object", {{"value", PropertyType::Int}}},
6✔
3107
    };
6✔
3108
    config.automatic_change_notifications = false;
6✔
3109
    auto r1 = Realm::get_shared_realm(config);
6✔
3110

3✔
3111
    r1->begin_transaction();
6✔
3112
    auto table = r1->read_group().get_table("class_object");
6✔
3113
    table->create_object();
6✔
3114
    r1->commit_transaction();
6✔
3115

3✔
3116
    struct Context : public BindingContext {
6✔
3117
        Context(std::shared_ptr<Realm>& realm)
6✔
3118
            : realm(&realm)
6✔
3119
        {
6✔
3120
        }
6✔
3121
        std::shared_ptr<Realm>* realm;
6✔
3122
        void did_change(std::vector<ObserverState> const&, std::vector<void*> const&, bool) override
6✔
3123
        {
6✔
3124
            auto realm = this->realm; // close() will delete `this`
6✔
3125
            (*realm)->close();
6✔
3126
            realm->reset();
6✔
3127
        }
6✔
3128
    };
6✔
3129

3✔
3130
    SECTION("did_change") {
6✔
3131
        r1->m_binding_context.reset(new Context(r1));
2✔
3132
        r1->invalidate();
2✔
3133

1✔
3134
        auto r2 = Realm::get_shared_realm(config);
2✔
3135
        r2->begin_transaction();
2✔
3136
        r2->read_group().get_table("class_object")->create_object();
2✔
3137
        r2->commit_transaction();
2✔
3138
        r2.reset();
2✔
3139

1✔
3140
        r1->notify();
2✔
3141
    }
2✔
3142

3✔
3143
    SECTION("did_change with async results") {
6✔
3144
        r1->m_binding_context.reset(new Context(r1));
2✔
3145
        Results results(r1, table->where());
2✔
3146
        auto token = results.add_notification_callback([&](CollectionChangeSet) {
1✔
3147
            // Should not be called.
3148
            REQUIRE(false);
×
3149
        });
×
3150

1✔
3151
        auto r2 = Realm::get_shared_realm(config);
2✔
3152
        r2->begin_transaction();
2✔
3153
        r2->read_group().get_table("class_object")->create_object();
2✔
3154
        r2->commit_transaction();
2✔
3155
        r2.reset();
2✔
3156

1✔
3157
        auto coordinator = _impl::RealmCoordinator::get_coordinator(config.path);
2✔
3158
        coordinator->on_change();
2✔
3159

1✔
3160
        r1->notify();
2✔
3161
    }
2✔
3162

3✔
3163
    SECTION("refresh") {
6✔
3164
        r1->m_binding_context.reset(new Context(r1));
2✔
3165

1✔
3166
        auto r2 = Realm::get_shared_realm(config);
2✔
3167
        r2->begin_transaction();
2✔
3168
        r2->read_group().get_table("class_object")->create_object();
2✔
3169
        r2->commit_transaction();
2✔
3170
        r2.reset();
2✔
3171

1✔
3172
        REQUIRE_FALSE(r1->refresh());
2!
3173
    }
2✔
3174
}
6✔
3175

3176
TEST_CASE("RealmCoordinator: schema cache") {
16✔
3177
    TestFile config;
16✔
3178
    auto coordinator = _impl::RealmCoordinator::get_coordinator(config.path);
16✔
3179

8✔
3180
    Schema cache_schema;
16✔
3181
    uint64_t cache_sv = -1, cache_tv = -1;
16✔
3182

8✔
3183
    Schema schema{
16✔
3184
        {"object", {{"value", PropertyType::Int}}},
16✔
3185
    };
16✔
3186
    Schema schema2{
16✔
3187
        {"object",
16✔
3188
         {
16✔
3189
             {"value", PropertyType::Int},
16✔
3190
         }},
16✔
3191
        {"object 2",
16✔
3192
         {
16✔
3193
             {"value", PropertyType::Int},
16✔
3194
         }},
16✔
3195
    };
16✔
3196

8✔
3197
    SECTION("valid initial schema sets cache") {
16✔
3198
        coordinator->cache_schema(schema, 5, 10);
2✔
3199
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3200
        REQUIRE(cache_schema == schema);
2!
3201
        REQUIRE(cache_sv == 5);
2!
3202
        REQUIRE(cache_tv == 10);
2!
3203
    }
2✔
3204

8✔
3205
    SECTION("cache can be updated with newer schema") {
16✔
3206
        coordinator->cache_schema(schema, 5, 10);
2✔
3207
        coordinator->cache_schema(schema2, 6, 11);
2✔
3208
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3209
        REQUIRE(cache_schema == schema2);
2!
3210
        REQUIRE(cache_sv == 6);
2!
3211
        REQUIRE(cache_tv == 11);
2!
3212
    }
2✔
3213

8✔
3214
    SECTION("empty schema is ignored") {
16✔
3215
        coordinator->cache_schema(Schema{}, 5, 10);
2✔
3216
        REQUIRE_FALSE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3217

1✔
3218
        coordinator->cache_schema(schema, 5, 10);
2✔
3219
        coordinator->cache_schema(Schema{}, 5, 10);
2✔
3220
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3221
        REQUIRE(cache_schema == schema);
2!
3222
        REQUIRE(cache_sv == 5);
2!
3223
        REQUIRE(cache_tv == 10);
2!
3224
    }
2✔
3225

8✔
3226
    SECTION("schema for older transaction is ignored") {
16✔
3227
        coordinator->cache_schema(schema, 5, 10);
2✔
3228
        coordinator->cache_schema(schema2, 4, 8);
2✔
3229

1✔
3230
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3231
        REQUIRE(cache_schema == schema);
2!
3232
        REQUIRE(cache_sv == 5);
2!
3233
        REQUIRE(cache_tv == 10);
2!
3234

1✔
3235
        coordinator->advance_schema_cache(10, 20);
2✔
3236
        coordinator->cache_schema(schema, 6, 15);
2✔
3237
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3238
        REQUIRE(cache_tv == 20); // should not have dropped to 15
2!
3239
    }
2✔
3240

8✔
3241
    SECTION("advance_schema() from transaction version bumps transaction version") {
16✔
3242
        coordinator->cache_schema(schema, 5, 10);
2✔
3243
        coordinator->advance_schema_cache(10, 12);
2✔
3244
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3245
        REQUIRE(cache_schema == schema);
2!
3246
        REQUIRE(cache_sv == 5);
2!
3247
        REQUIRE(cache_tv == 12);
2!
3248
    }
2✔
3249

8✔
3250
    SECTION("advance_schema() ending before transaction version does nothing") {
16✔
3251
        coordinator->cache_schema(schema, 5, 10);
2✔
3252
        coordinator->advance_schema_cache(8, 9);
2✔
3253
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3254
        REQUIRE(cache_schema == schema);
2!
3255
        REQUIRE(cache_sv == 5);
2!
3256
        REQUIRE(cache_tv == 10);
2!
3257
    }
2✔
3258

8✔
3259
    SECTION("advance_schema() extending over transaction version bumps version") {
16✔
3260
        coordinator->cache_schema(schema, 5, 10);
2✔
3261
        coordinator->advance_schema_cache(3, 15);
2✔
3262
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3263
        REQUIRE(cache_schema == schema);
2!
3264
        REQUIRE(cache_sv == 5);
2!
3265
        REQUIRE(cache_tv == 15);
2!
3266
    }
2✔
3267

8✔
3268
    SECTION("advance_schema() with no cahced schema does nothing") {
16✔
3269
        coordinator->advance_schema_cache(3, 15);
2✔
3270
        REQUIRE_FALSE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3271
    }
2✔
3272
}
16✔
3273

3274
TEST_CASE("SharedRealm: coordinator schema cache") {
26✔
3275
    TestFile config;
26✔
3276
    auto r = Realm::get_shared_realm(config);
26✔
3277
    auto coordinator = _impl::RealmCoordinator::get_coordinator(config.path);
26✔
3278

13✔
3279
    Schema cache_schema;
26✔
3280
    uint64_t cache_sv = -1, cache_tv = -1;
26✔
3281

13✔
3282
    Schema schema{
26✔
3283
        {"object", {{"value", PropertyType::Int}}},
26✔
3284
    };
26✔
3285
    Schema schema2{
26✔
3286
        {"object",
26✔
3287
         {
26✔
3288
             {"value", PropertyType::Int},
26✔
3289
         }},
26✔
3290
        {"object 2",
26✔
3291
         {
26✔
3292
             {"value", PropertyType::Int},
26✔
3293
         }},
26✔
3294
    };
26✔
3295

13✔
3296
    class ExternalWriter {
26✔
3297
    private:
26✔
3298
        std::shared_ptr<Realm> m_realm;
26✔
3299

13✔
3300
    public:
26✔
3301
        WriteTransaction wt;
26✔
3302
        ExternalWriter(Realm::Config const& config)
26✔
3303
            : m_realm([&] {
22✔
3304
                auto c = config;
18✔
3305
                c.scheduler = util::Scheduler::make_frozen(VersionID());
18✔
3306
                return _impl::RealmCoordinator::get_coordinator(c.path)->get_realm(c, util::none);
18✔
3307
            }())
18✔
3308
            , wt(TestHelper::get_db(m_realm))
26✔
3309
        {
22✔
3310
        }
18✔
3311
    };
26✔
3312

13✔
3313
    auto external_write = [&](Realm::Config const& config, auto&& fn) {
21✔
3314
        ExternalWriter wt(config);
16✔
3315
        fn(wt.wt);
16✔
3316
        wt.wt.commit();
16✔
3317
    };
16✔
3318

13✔
3319
    SECTION("is initially empty for uninitialized file") {
26✔
3320
        REQUIRE_FALSE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3321
    }
2✔
3322
    r->update_schema(schema);
26✔
3323

13✔
3324
    SECTION("is populated after calling update_schema()") {
26✔
3325
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3326
        REQUIRE(cache_sv == 0);
2!
3327
        REQUIRE(cache_schema == schema);
2!
3328
        REQUIRE(cache_schema.begin()->persisted_properties[0].column_key != ColKey{});
2!
3329
    }
2✔
3330

13✔
3331
    coordinator = nullptr;
26✔
3332
    r = nullptr;
26✔
3333
    r = Realm::get_shared_realm(config);
26✔
3334
    coordinator = _impl::RealmCoordinator::get_coordinator(config.path);
26✔
3335
    REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
26!
3336

13✔
3337
    SECTION("is populated after opening an initialized file") {
26✔
3338
        REQUIRE(cache_sv == 0);
2!
3339
        REQUIRE(cache_tv == 2); // with in-realm history the version doesn't reset
2!
3340
        REQUIRE(cache_schema == schema);
2!
3341
        REQUIRE(cache_schema.begin()->persisted_properties[0].column_key != ColKey{});
2!
3342
    }
2✔
3343

13✔
3344
    SECTION("transaction version is bumped after a local write") {
26✔
3345
        auto tv = cache_tv;
2✔
3346
        r->begin_transaction();
2✔
3347
        r->commit_transaction();
2✔
3348
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3349
        REQUIRE(cache_tv == tv + 1);
2!
3350
    }
2✔
3351

13✔
3352
    SECTION("notify() without a read transaction does not bump transaction version") {
26✔
3353
        auto tv = cache_tv;
4✔
3354

2✔
3355
        SECTION("non-schema change") {
4✔
3356
            external_write(config, [](auto& wt) {
2✔
3357
                wt.get_table("class_object")->create_object();
2✔
3358
            });
2✔
3359
        }
2✔
3360
        SECTION("schema change") {
4✔
3361
            external_write(config, [](auto& wt) {
2✔
3362
                wt.add_table("class_object 2");
2✔
3363
            });
2✔
3364
        }
2✔
3365

2✔
3366
        r->notify();
4✔
3367
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
4!
3368
        REQUIRE(cache_tv == tv);
4!
3369
        REQUIRE(cache_schema == schema);
4!
3370
    }
4✔
3371

13✔
3372
    SECTION("notify() with a read transaction bumps transaction version") {
26✔
3373
        r->read_group();
2✔
3374
        external_write(config, [](auto& wt) {
2✔
3375
            wt.get_table("class_object")->create_object();
2✔
3376
        });
2✔
3377

1✔
3378
        r->notify();
2✔
3379
        auto tv = cache_tv;
2✔
3380
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3381
        REQUIRE(cache_tv == tv + 1);
2!
3382
    }
2✔
3383

13✔
3384
    SECTION("notify() with a read transaction updates schema folloing external schema change") {
26✔
3385
        r->read_group();
2✔
3386
        external_write(config, [](auto& wt) {
2✔
3387
            wt.add_table("class_object 2");
2✔
3388
        });
2✔
3389

1✔
3390
        r->notify();
2✔
3391
        auto tv = cache_tv;
2✔
3392
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3393
        REQUIRE(cache_tv == tv + 1);
2!
3394
        REQUIRE(cache_schema.size() == 2);
2!
3395
        REQUIRE(cache_schema.find("object 2") != cache_schema.end());
2!
3396
    }
2✔
3397

13✔
3398
    SECTION("transaction version is bumped after refresh() following external non-schema write") {
26✔
3399
        external_write(config, [](auto& wt) {
2✔
3400
            wt.get_table("class_object")->create_object();
2✔
3401
        });
2✔
3402

1✔
3403
        r->refresh();
2✔
3404
        auto tv = cache_tv;
2✔
3405
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3406
        REQUIRE(cache_tv == tv + 1);
2!
3407
    }
2✔
3408

13✔
3409
    SECTION("schema is reread following refresh() over external schema change") {
26✔
3410
        external_write(config, [](auto& wt) {
2✔
3411
            wt.add_table("class_object 2");
2✔
3412
        });
2✔
3413

1✔
3414
        r->refresh();
2✔
3415
        auto tv = cache_tv;
2✔
3416
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3417
        REQUIRE(cache_tv == tv + 1);
2!
3418
        REQUIRE(cache_schema.size() == 2);
2!
3419
        REQUIRE(cache_schema.find("object 2") != cache_schema.end());
2!
3420
    }
2✔
3421

13✔
3422
    SECTION("update_schema() to version already on disk updates cache") {
26✔
3423
        r->read_group();
2✔
3424
        external_write(config, [](auto& wt) {
2✔
3425
            auto table = wt.add_table("class_object 2");
2✔
3426
            table->add_column(type_Int, "value");
2✔
3427
        });
2✔
3428

1✔
3429
        auto tv = cache_tv;
2✔
3430
        r->update_schema(schema2);
2✔
3431

1✔
3432
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3433
        REQUIRE(cache_tv == tv + 1); // only +1 because update_schema() did not perform a write
2!
3434
        REQUIRE(cache_schema.size() == 2);
2!
3435
        REQUIRE(cache_schema.find("object 2") != cache_schema.end());
2!
3436
    }
2✔
3437

13✔
3438
    SECTION("update_schema() to version already on disk updates cache") {
26✔
3439
        r->read_group();
2✔
3440
        external_write(config, [](auto& wt) {
2✔
3441
            auto table = wt.add_table("class_object 2");
2✔
3442
            table->add_column(type_Int, "value");
2✔
3443
        });
2✔
3444

1✔
3445
        auto tv = cache_tv;
2✔
3446
        r->update_schema(schema2);
2✔
3447

1✔
3448
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3449
        REQUIRE(cache_tv == tv + 1); // only +1 because update_schema() did not perform a write
2!
3450
        REQUIRE(cache_schema.size() == 2);
2!
3451
        REQUIRE(cache_schema.find("object 2") != cache_schema.end());
2!
3452
    }
2✔
3453

13✔
3454
    SECTION("update_schema() to version populated on disk while waiting for the write lock updates cache") {
26✔
3455
        r->read_group();
2✔
3456

1✔
3457
        // We want to commit the write while we're waiting on the write lock on
1✔
3458
        // this thread, which can't really be done in a properly synchronized manner
1✔
3459
        std::chrono::microseconds wait_time{5000};
2✔
3460
#if REALM_ANDROID
3461
        // When running on device or in an emulator we need to wait longer due
3462
        // to them being slow
3463
        wait_time *= 10;
3464
#endif
3465

1✔
3466
        bool did_run = false;
2✔
3467
        JoiningThread thread([&] {
2✔
3468
            ExternalWriter writer(config);
2✔
3469
            if (writer.wt.get_table("class_object 2"))
2✔
3470
                return;
×
3471
            did_run = true;
2✔
3472

1✔
3473
            auto table = writer.wt.add_table("class_object 2");
2✔
3474
            table->add_column(type_Int, "value");
2✔
3475
            std::this_thread::sleep_for(wait_time * 2);
2✔
3476
            writer.wt.commit();
2✔
3477
        });
2✔
3478
        std::this_thread::sleep_for(wait_time);
2✔
3479

1✔
3480
        auto tv = cache_tv;
2✔
3481
        r->update_schema(Schema{
2✔
3482
            {"object", {{"value", PropertyType::Int}}},
2✔
3483
            {"object 2", {{"value", PropertyType::Int}}},
2✔
3484
        });
2✔
3485

1✔
3486
        // just skip the test if the timing was wrong to avoid spurious failures
1✔
3487
        if (!did_run)
2✔
3488
            return;
×
3489

1✔
3490
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3491
        REQUIRE(cache_tv == tv + 1); // only +1 because update_schema()'s write was rolled back
2!
3492
        REQUIRE(cache_schema.size() == 2);
2!
3493
        REQUIRE(cache_schema.find("object 2") != cache_schema.end());
2!
3494
    }
2✔
3495
}
26✔
3496

3497
TEST_CASE("SharedRealm: dynamic schema mode doesn't invalidate object schema pointers when schema hasn't changed") {
2✔
3498
    TestFile config;
2✔
3499

1✔
3500
    // Prepopulate the Realm with the schema.
1✔
3501
    Realm::Config config_with_schema = config;
2✔
3502
    config_with_schema.schema_version = 1;
2✔
3503
    config_with_schema.schema_mode = SchemaMode::Automatic;
2✔
3504
    config_with_schema.schema =
2✔
3505
        Schema{{"object",
2✔
3506
                {
2✔
3507
                    {"value", PropertyType::Int, Property::IsPrimary{true}},
2✔
3508
                    {"value 2", PropertyType::Int, Property::IsPrimary{false}, Property::IsIndexed{true}},
2✔
3509
                }}};
2✔
3510
    auto r1 = Realm::get_shared_realm(config_with_schema);
2✔
3511

1✔
3512
    // Retrieve the object schema in dynamic mode.
1✔
3513
    auto r2 = Realm::get_shared_realm(config);
2✔
3514
    auto* object_schema = &*r2->schema().find("object");
2✔
3515

1✔
3516
    // Perform an empty write to create a new version, resulting in the other Realm needing to re-read the schema.
1✔
3517
    r1->begin_transaction();
2✔
3518
    r1->commit_transaction();
2✔
3519

1✔
3520
    // Advance to the latest version, and verify the object schema is at the same location in memory.
1✔
3521
    r2->read_group();
2✔
3522
    REQUIRE(object_schema == &*r2->schema().find("object"));
2!
3523
}
2✔
3524

3525
TEST_CASE("SharedRealm: declaring an object as embedded results in creating an embedded table") {
2✔
3526
    TestFile config;
2✔
3527

1✔
3528
    // Prepopulate the Realm with the schema.
1✔
3529
    config.schema = Schema{{"object1",
2✔
3530
                            ObjectSchema::ObjectType::Embedded,
2✔
3531
                            {
2✔
3532
                                {"value", PropertyType::Int},
2✔
3533
                            }},
2✔
3534
                           {"object2",
2✔
3535
                            {
2✔
3536
                                {"value", PropertyType::Object | PropertyType::Nullable, "object1"},
2✔
3537
                            }}};
2✔
3538
    auto r1 = Realm::get_shared_realm(config);
2✔
3539

1✔
3540
    Group& g = r1->read_group();
2✔
3541
    auto t = g.get_table("class_object1");
2✔
3542
    REQUIRE(t->is_embedded());
2!
3543
}
2✔
3544

3545
TEST_CASE("SharedRealm: SchemaChangedFunction") {
16✔
3546
    struct Context : BindingContext {
16✔
3547
        size_t* change_count;
16✔
3548
        Schema* schema;
16✔
3549
        Context(size_t* count_out, Schema* schema_out)
16✔
3550
            : change_count(count_out)
16✔
3551
            , schema(schema_out)
16✔
3552
        {
22✔
3553
        }
22✔
3554

8✔
3555
        void schema_did_change(Schema const& changed_schema) override
16✔
3556
        {
13✔
3557
            ++*change_count;
10✔
3558
            *schema = changed_schema;
10✔
3559
        }
10✔
3560
    };
16✔
3561

8✔
3562
    size_t schema_changed_called = 0;
16✔
3563
    Schema changed_fixed_schema;
16✔
3564
    TestFile config;
16✔
3565
    auto dynamic_config = config;
16✔
3566

8✔
3567
    config.schema = Schema{{"object1",
16✔
3568
                            {
16✔
3569
                                {"value", PropertyType::Int},
16✔
3570
                            }},
16✔
3571
                           {"object2",
16✔
3572
                            {
16✔
3573
                                {"value", PropertyType::Int},
16✔
3574
                            }}};
16✔
3575
    config.schema_version = 1;
16✔
3576
    auto r1 = Realm::get_shared_realm(config);
16✔
3577
    r1->read_group();
16✔
3578
    r1->m_binding_context.reset(new Context(&schema_changed_called, &changed_fixed_schema));
16✔
3579

8✔
3580
    SECTION("Fixed schema") {
16✔
3581
        SECTION("update_schema") {
10✔
3582
            auto new_schema = Schema{{"object3",
2✔
3583
                                      {
2✔
3584
                                          {"value", PropertyType::Int},
2✔
3585
                                      }}};
2✔
3586
            r1->update_schema(new_schema, 2);
2✔
3587
            REQUIRE(schema_changed_called == 1);
2!
3588
            REQUIRE(changed_fixed_schema.find("object3")->property_for_name("value")->column_key != ColKey{});
2!
3589
        }
2✔
3590

5✔
3591
        SECTION("Open a new Realm instance with same config won't trigger") {
10✔
3592
            auto r2 = Realm::get_shared_realm(config);
2✔
3593
            REQUIRE(schema_changed_called == 0);
2!
3594
        }
2✔
3595

5✔
3596
        SECTION("Non schema related transaction doesn't trigger") {
10✔
3597
            auto r2 = Realm::get_shared_realm(config);
2✔
3598
            r2->begin_transaction();
2✔
3599
            r2->commit_transaction();
2✔
3600
            r1->refresh();
2✔
3601
            REQUIRE(schema_changed_called == 0);
2!
3602
        }
2✔
3603

5✔
3604
        SECTION("Schema is changed by another Realm") {
10✔
3605
            auto r2 = Realm::get_shared_realm(config);
2✔
3606
            r2->begin_transaction();
2✔
3607
            r2->read_group().get_table("class_object1")->add_column(type_String, "new col");
2✔
3608
            r2->commit_transaction();
2✔
3609
            r1->refresh();
2✔
3610
            REQUIRE(schema_changed_called == 1);
2!
3611
            REQUIRE(changed_fixed_schema.find("object1")->property_for_name("value")->column_key != ColKey{});
2!
3612
        }
2✔
3613

5✔
3614
        // This is not a valid use case. m_schema won't be refreshed.
5✔
3615
        SECTION("Schema is changed by this Realm won't trigger") {
10✔
3616
            r1->begin_transaction();
2✔
3617
            r1->read_group().get_table("class_object1")->add_column(type_String, "new col");
2✔
3618
            r1->commit_transaction();
2✔
3619
            REQUIRE(schema_changed_called == 0);
2!
3620
        }
2✔
3621
    }
10✔
3622

8✔
3623
    SECTION("Dynamic schema") {
16✔
3624
        size_t dynamic_schema_changed_called = 0;
6✔
3625
        Schema changed_dynamic_schema;
6✔
3626
        auto r2 = Realm::get_shared_realm(dynamic_config);
6✔
3627
        r2->m_binding_context.reset(new Context(&dynamic_schema_changed_called, &changed_dynamic_schema));
6✔
3628

3✔
3629
        SECTION("set_schema_subset") {
6✔
3630
            auto new_schema = Schema{{"object1",
2✔
3631
                                      {
2✔
3632
                                          {"value", PropertyType::Int},
2✔
3633
                                      }}};
2✔
3634
            r2->set_schema_subset(new_schema);
2✔
3635
            REQUIRE(schema_changed_called == 0);
2!
3636
            REQUIRE(dynamic_schema_changed_called == 1);
2!
3637
            REQUIRE(changed_dynamic_schema.find("object1")->property_for_name("value")->column_key != ColKey{});
2!
3638
        }
2✔
3639

3✔
3640
        SECTION("Non schema related transaction will always trigger in dynamic mode") {
6✔
3641
            auto r1 = Realm::get_shared_realm(config);
2✔
3642
            // An empty transaction will trigger the schema changes always in dynamic mode.
1✔
3643
            r1->begin_transaction();
2✔
3644
            r1->commit_transaction();
2✔
3645
            r2->refresh();
2✔
3646
            REQUIRE(dynamic_schema_changed_called == 1);
2!
3647
            REQUIRE(changed_dynamic_schema.find("object1")->property_for_name("value")->column_key != ColKey{});
2!
3648
        }
2✔
3649

3✔
3650
        SECTION("Schema is changed by another Realm") {
6✔
3651
            r1->begin_transaction();
2✔
3652
            r1->read_group().get_table("class_object1")->add_column(type_String, "new col");
2✔
3653
            r1->commit_transaction();
2✔
3654
            r2->refresh();
2✔
3655
            REQUIRE(dynamic_schema_changed_called == 1);
2!
3656
            REQUIRE(changed_dynamic_schema.find("object1")->property_for_name("value")->column_key != ColKey{});
2!
3657
        }
2✔
3658
    }
6✔
3659
}
16✔
3660

3661
TEST_CASE("SharedRealm: compact on launch") {
4✔
3662
    // Make compactable Realm
2✔
3663
    TestFile config;
4✔
3664
    config.automatic_change_notifications = false;
4✔
3665
    int num_opens = 0;
4✔
3666
    config.should_compact_on_launch_function = [&](uint64_t total_bytes, uint64_t used_bytes) {
12✔
3667
        REQUIRE(total_bytes > used_bytes);
12!
3668
        num_opens++;
12✔
3669
        return num_opens != 2;
12✔
3670
    };
12✔
3671
    config.schema = Schema{
4✔
3672
        {"object", {{"value", PropertyType::String}}},
4✔
3673
    };
4✔
3674
    REQUIRE(num_opens == 0);
4!
3675
    auto r = Realm::get_shared_realm(config);
4✔
3676
    REQUIRE(num_opens == 1);
4!
3677
    r->begin_transaction();
4✔
3678
    auto table = r->read_group().get_table("class_object");
4✔
3679
    size_t count = 1000;
4✔
3680
    for (size_t i = 0; i < count; ++i)
4,004✔
3681
        table->create_object().set_all(util::format("Foo_%1", i % 10).c_str());
4,000✔
3682
    r->commit_transaction();
4✔
3683
    REQUIRE(table->size() == count);
4!
3684
    r->close();
4✔
3685

2✔
3686
    SECTION("compact reduces the file size") {
4✔
3687
#ifndef _WIN32
2✔
3688
        // Confirm expected sizes before and after opening the Realm
1✔
3689
        size_t size_before = size_t(util::File(config.path).get_size());
2✔
3690
        r = Realm::get_shared_realm(config);
2✔
3691
        REQUIRE(num_opens == 2);
2!
3692
        r->close();
2✔
3693
        REQUIRE(size_t(util::File(config.path).get_size()) == size_before); // File size after returning false
2!
3694
        r = Realm::get_shared_realm(config);
2✔
3695
        REQUIRE(num_opens == 3);
2!
3696
        REQUIRE(size_t(util::File(config.path).get_size()) < size_before); // File size after returning true
2!
3697

1✔
3698
        // Validate that the file still contains what it should
1✔
3699
        REQUIRE(r->read_group().get_table("class_object")->size() == count);
2!
3700

1✔
3701
        // Registering for a collection notification shouldn't crash when compact on launch is used.
1✔
3702
        Results results(r, r->read_group().get_table("class_object"));
2✔
3703
        results.add_notification_callback([](CollectionChangeSet const&) {});
1✔
3704
        r->close();
2✔
3705
#endif
2✔
3706
    }
2✔
3707

2✔
3708
    SECTION("compact function does not get invoked if realm is open on another thread") {
4✔
3709
        config.scheduler = util::Scheduler::make_frozen(VersionID());
2✔
3710
        r = Realm::get_shared_realm(config);
2✔
3711
        REQUIRE(num_opens == 2);
2!
3712
        std::thread([&] {
2✔
3713
            auto r2 = Realm::get_shared_realm(config);
2✔
3714
            REQUIRE(num_opens == 2);
2!
3715
        }).join();
2✔
3716
        r->close();
2✔
3717
        std::thread([&] {
2✔
3718
            auto r3 = Realm::get_shared_realm(config);
2✔
3719
            REQUIRE(num_opens == 3);
2!
3720
        }).join();
2✔
3721
    }
2✔
3722
}
4✔
3723

3724
struct ModeAutomatic {
3725
    static constexpr SchemaMode mode = SchemaMode::Automatic;
3726
    static constexpr bool should_call_init_on_version_bump = false;
3727
};
3728
struct ModeAdditive {
3729
    static constexpr SchemaMode mode = SchemaMode::AdditiveExplicit;
3730
    static constexpr bool should_call_init_on_version_bump = false;
3731
};
3732
struct ModeManual {
3733
    static constexpr SchemaMode mode = SchemaMode::Manual;
3734
    static constexpr bool should_call_init_on_version_bump = false;
3735
};
3736
struct ModeSoftResetFile {
3737
    static constexpr SchemaMode mode = SchemaMode::SoftResetFile;
3738
    static constexpr bool should_call_init_on_version_bump = true;
3739
};
3740
struct ModeHardResetFile {
3741
    static constexpr SchemaMode mode = SchemaMode::HardResetFile;
3742
    static constexpr bool should_call_init_on_version_bump = true;
3743
};
3744

3745
TEMPLATE_TEST_CASE("SharedRealm: update_schema with initialization_function", "[init][update schema]", ModeAutomatic,
3746
                   ModeAdditive, ModeManual, ModeSoftResetFile, ModeHardResetFile)
3747
{
30✔
3748
    TestFile config;
30✔
3749
    config.schema_mode = TestType::mode;
30✔
3750
    bool initialization_function_called = false;
30✔
3751
    uint64_t schema_version_in_callback = -1;
30✔
3752
    Schema schema_in_callback;
30✔
3753
    auto initialization_function = [&initialization_function_called, &schema_version_in_callback,
30✔
3754
                                    &schema_in_callback](auto shared_realm) {
27✔
3755
        REQUIRE(shared_realm->is_in_transaction());
24!
3756
        initialization_function_called = true;
24✔
3757
        schema_version_in_callback = shared_realm->schema_version();
24✔
3758
        schema_in_callback = shared_realm->schema();
24✔
3759
    };
24✔
3760

15✔
3761
    Schema schema{
30✔
3762
        {"object", {{"value", PropertyType::String}}},
30✔
3763
    };
30✔
3764

15✔
3765
    SECTION("call initialization function directly by update_schema") {
30✔
3766
        // Open in dynamic mode with no schema specified
5✔
3767
        auto realm = Realm::get_shared_realm(config);
10✔
3768
        REQUIRE_FALSE(initialization_function_called);
10!
3769

5✔
3770
        realm->update_schema(schema, 0, nullptr, initialization_function);
10✔
3771
        REQUIRE(initialization_function_called);
10!
3772
        REQUIRE(schema_version_in_callback == 0);
10!
3773
        REQUIRE(schema_in_callback.compare(schema).size() == 0);
10!
3774
    }
10✔
3775

15✔
3776
    config.schema_version = 0;
30✔
3777
    config.schema = schema;
30✔
3778

15✔
3779
    SECTION("initialization function should be called for unversioned realm") {
30✔
3780
        config.initialization_function = initialization_function;
10✔
3781
        Realm::get_shared_realm(config);
10✔
3782
        REQUIRE(initialization_function_called);
10!
3783
        REQUIRE(schema_version_in_callback == 0);
10!
3784
        REQUIRE(schema_in_callback.compare(schema).size() == 0);
10!
3785
    }
10✔
3786

15✔
3787
    SECTION("initialization function for versioned realm") {
30✔
3788
        // Initialize v0
5✔
3789
        Realm::get_shared_realm(config);
10✔
3790

5✔
3791
        config.schema_version = 1;
10✔
3792
        config.initialization_function = initialization_function;
10✔
3793
        Realm::get_shared_realm(config);
10✔
3794
        REQUIRE(initialization_function_called == TestType::should_call_init_on_version_bump);
10!
3795
        if (TestType::should_call_init_on_version_bump) {
10✔
3796
            REQUIRE(schema_version_in_callback == 1);
4!
3797
            REQUIRE(schema_in_callback.compare(schema).size() == 0);
4!
3798
        }
4✔
3799
    }
10✔
3800
}
30✔
3801

3802
TEST_CASE("BindingContext is notified about delivery of change notifications") {
16✔
3803
    _impl::RealmCoordinator::assert_no_open_realms();
16✔
3804
    InMemoryTestFile config;
16✔
3805
    config.automatic_change_notifications = false;
16✔
3806

8✔
3807
    auto r = Realm::get_shared_realm(config);
16✔
3808
    r->update_schema({
16✔
3809
        {"object", {{"value", PropertyType::Int}}},
16✔
3810
    });
16✔
3811

8✔
3812
    auto coordinator = _impl::RealmCoordinator::get_coordinator(config.path);
16✔
3813
    auto table = r->read_group().get_table("class_object");
16✔
3814

8✔
3815
    SECTION("BindingContext notified even if no callbacks are registered") {
16✔
3816
        static int binding_context_start_notify_calls = 0;
4✔
3817
        static int binding_context_end_notify_calls = 0;
4✔
3818
        struct Context : BindingContext {
4✔
3819
            void will_send_notifications() override
4✔
3820
            {
4✔
3821
                ++binding_context_start_notify_calls;
4✔
3822
            }
4✔
3823

2✔
3824
            void did_send_notifications() override
4✔
3825
            {
4✔
3826
                ++binding_context_end_notify_calls;
4✔
3827
            }
4✔
3828
        };
4✔
3829
        r->m_binding_context.reset(new Context());
4✔
3830

2✔
3831
        SECTION("local commit") {
4✔
3832
            binding_context_start_notify_calls = 0;
2✔
3833
            binding_context_end_notify_calls = 0;
2✔
3834
            coordinator->on_change();
2✔
3835
            r->begin_transaction();
2✔
3836
            REQUIRE(binding_context_start_notify_calls == 1);
2!
3837
            REQUIRE(binding_context_end_notify_calls == 1);
2!
3838
            r->cancel_transaction();
2✔
3839
        }
2✔
3840

2✔
3841
        SECTION("remote commit") {
4✔
3842
            binding_context_start_notify_calls = 0;
2✔
3843
            binding_context_end_notify_calls = 0;
2✔
3844
            JoiningThread([&] {
2✔
3845
                auto r2 = coordinator->get_realm(util::Scheduler::make_frozen(VersionID()));
2✔
3846
                r2->begin_transaction();
2✔
3847
                auto table2 = r2->read_group().get_table("class_object");
2✔
3848
                table2->create_object();
2✔
3849
                r2->commit_transaction();
2✔
3850
            });
2✔
3851
            advance_and_notify(*r);
2✔
3852
            REQUIRE(binding_context_start_notify_calls == 1);
2!
3853
            REQUIRE(binding_context_end_notify_calls == 1);
2!
3854
        }
2✔
3855
    }
4✔
3856

8✔
3857
    SECTION("notify BindingContext before and after sending notifications") {
16✔
3858
        static int binding_context_start_notify_calls = 0;
4✔
3859
        static int binding_context_end_notify_calls = 0;
4✔
3860
        static int notification_calls = 0;
4✔
3861

2✔
3862
        auto col = table->get_column_key("value");
4✔
3863
        Results results1(r, table->where().greater_equal(col, 0));
4✔
3864
        Results results2(r, table->where().less(col, 10));
4✔
3865

2✔
3866
        auto token1 = results1.add_notification_callback([&](CollectionChangeSet) {
4✔
3867
            ++notification_calls;
4✔
3868
        });
4✔
3869

2✔
3870
        auto token2 = results2.add_notification_callback([&](CollectionChangeSet) {
4✔
3871
            ++notification_calls;
4✔
3872
        });
4✔
3873

2✔
3874
        struct Context : BindingContext {
4✔
3875
            void will_send_notifications() override
4✔
3876
            {
4✔
3877
                REQUIRE(notification_calls == 0);
4!
3878
                REQUIRE(binding_context_end_notify_calls == 0);
4!
3879
                ++binding_context_start_notify_calls;
4✔
3880
            }
4✔
3881

2✔
3882
            void did_send_notifications() override
4✔
3883
            {
4✔
3884
                REQUIRE(notification_calls == 2);
4!
3885
                REQUIRE(binding_context_start_notify_calls == 1);
4!
3886
                ++binding_context_end_notify_calls;
4✔
3887
            }
4✔
3888
        };
4✔
3889
        r->m_binding_context.reset(new Context());
4✔
3890

2✔
3891
        SECTION("local commit") {
4✔
3892
            binding_context_start_notify_calls = 0;
2✔
3893
            binding_context_end_notify_calls = 0;
2✔
3894
            notification_calls = 0;
2✔
3895
            coordinator->on_change();
2✔
3896
            r->begin_transaction();
2✔
3897
            table->create_object();
2✔
3898
            r->commit_transaction();
2✔
3899
            REQUIRE(binding_context_start_notify_calls == 1);
2!
3900
            REQUIRE(binding_context_end_notify_calls == 1);
2!
3901
        }
2✔
3902

2✔
3903
        SECTION("remote commit") {
4✔
3904
            binding_context_start_notify_calls = 0;
2✔
3905
            binding_context_end_notify_calls = 0;
2✔
3906
            notification_calls = 0;
2✔
3907
            JoiningThread([&] {
2✔
3908
                auto r2 = coordinator->get_realm(util::Scheduler::make_frozen(VersionID()));
2✔
3909
                r2->begin_transaction();
2✔
3910
                auto table2 = r2->read_group().get_table("class_object");
2✔
3911
                table2->create_object();
2✔
3912
                r2->commit_transaction();
2✔
3913
            });
2✔
3914
            advance_and_notify(*r);
2✔
3915
            REQUIRE(binding_context_start_notify_calls == 1);
2!
3916
            REQUIRE(binding_context_end_notify_calls == 1);
2!
3917
        }
2✔
3918
    }
4✔
3919

8✔
3920
    SECTION("did_send() is skipped if the Realm is closed first") {
16✔
3921
        Results results(r, table->where());
8✔
3922
        bool do_close = true;
8✔
3923
        auto token = results.add_notification_callback([&](CollectionChangeSet) {
8✔
3924
            if (do_close)
8✔
3925
                r->close();
4✔
3926
        });
8✔
3927

4✔
3928
        struct FailOnDidSend : BindingContext {
8✔
3929
            void did_send_notifications() override
8✔
3930
            {
4✔
3931
                FAIL("did_send_notifications() should not have been called");
×
3932
            }
×
3933
        };
8✔
3934
        struct CloseOnWillChange : FailOnDidSend {
8✔
3935
            Realm& realm;
8✔
3936
            CloseOnWillChange(Realm& realm)
8✔
3937
                : realm(realm)
8✔
3938
            {
6✔
3939
            }
4✔
3940

4✔
3941
            void will_send_notifications() override
8✔
3942
            {
6✔
3943
                realm.close();
4✔
3944
            }
4✔
3945
        };
8✔
3946

4✔
3947
        SECTION("closed in notification callback for notify()") {
8✔
3948
            r->m_binding_context.reset(new FailOnDidSend);
2✔
3949
            coordinator->on_change();
2✔
3950
            r->notify();
2✔
3951
        }
2✔
3952

4✔
3953
        SECTION("closed in notification callback for refresh()") {
8✔
3954
            do_close = false;
2✔
3955
            coordinator->on_change();
2✔
3956
            r->notify();
2✔
3957
            do_close = true;
2✔
3958

1✔
3959
            JoiningThread([&] {
2✔
3960
                auto r = coordinator->get_realm(util::Scheduler::make_frozen(VersionID()));
2✔
3961
                r->begin_transaction();
2✔
3962
                r->read_group().get_table("class_object")->create_object();
2✔
3963
                r->commit_transaction();
2✔
3964
            });
2✔
3965

1✔
3966
            r->m_binding_context.reset(new FailOnDidSend);
2✔
3967
            coordinator->on_change();
2✔
3968
            r->refresh();
2✔
3969
        }
2✔
3970

4✔
3971
        SECTION("closed in will_send() for notify()") {
8✔
3972
            r->m_binding_context.reset(new CloseOnWillChange(*r));
2✔
3973
            coordinator->on_change();
2✔
3974
            r->notify();
2✔
3975
        }
2✔
3976

4✔
3977
        SECTION("closed in will_send() for refresh()") {
8✔
3978
            do_close = false;
2✔
3979
            coordinator->on_change();
2✔
3980
            r->notify();
2✔
3981
            do_close = true;
2✔
3982

1✔
3983
            JoiningThread([&] {
2✔
3984
                auto r = coordinator->get_realm(util::Scheduler::make_frozen(VersionID()));
2✔
3985
                r->begin_transaction();
2✔
3986
                r->read_group().get_table("class_object")->create_object();
2✔
3987
                r->commit_transaction();
2✔
3988
            });
2✔
3989

1✔
3990
            r->m_binding_context.reset(new CloseOnWillChange(*r));
2✔
3991
            coordinator->on_change();
2✔
3992
            r->refresh();
2✔
3993
        }
2✔
3994
    }
8✔
3995
#ifdef _WIN32
3996
    _impl::RealmCoordinator::clear_all_caches();
3997
#endif
3998
}
16✔
3999

4000
TEST_CASE("RealmCoordinator: get_unbound_realm()") {
8✔
4001
    TestFile config;
8✔
4002
    config.cache = true;
8✔
4003
    config.schema = Schema{
8✔
4004
        {"object", {{"value", PropertyType::Int}}},
8✔
4005
    };
8✔
4006

4✔
4007
    ThreadSafeReference ref;
8✔
4008
    std::thread([&] {
8✔
4009
        ref = _impl::RealmCoordinator::get_coordinator(config)->get_unbound_realm();
8✔
4010
    }).join();
8✔
4011

4✔
4012
    SECTION("checks thread after being resolved") {
8✔
4013
        auto realm = Realm::get_shared_realm(std::move(ref));
2✔
4014
        REQUIRE_NOTHROW(realm->verify_thread());
2✔
4015
        std::thread([&] {
2✔
4016
            REQUIRE_EXCEPTION(realm->verify_thread(), WrongThread, "Realm accessed from incorrect thread.");
2✔
4017
        }).join();
2✔
4018
    }
2✔
4019

4✔
4020
    SECTION("delivers notifications to the thread it is resolved on") {
8✔
4021
#ifndef _WIN32
2✔
4022
        if (!util::EventLoop::has_implementation())
2✔
4023
            return;
×
4024
        auto realm = Realm::get_shared_realm(std::move(ref));
2✔
4025
        Results results(realm, ObjectStore::table_for_object_type(realm->read_group(), "object")->where());
2✔
4026
        bool called = false;
2✔
4027
        auto token = results.add_notification_callback([&](CollectionChangeSet) {
2✔
4028
            called = true;
2✔
4029
        });
2✔
4030
        util::EventLoop::main().run_until([&] {
124✔
4031
            return called;
124✔
4032
        });
124✔
4033
#endif
2✔
4034
    }
2✔
4035

4✔
4036
    SECTION("resolves to existing cached Realm for the thread if caching is enabled") {
8✔
4037
        auto r1 = Realm::get_shared_realm(config);
2✔
4038
        auto r2 = Realm::get_shared_realm(std::move(ref));
2✔
4039
        REQUIRE(r1 == r2);
2!
4040
    }
2✔
4041

4✔
4042
    SECTION("resolves to a new Realm if caching is disabled") {
8✔
4043
        config.cache = false;
2✔
4044
        auto r1 = Realm::get_shared_realm(config);
2✔
4045
        auto r2 = Realm::get_shared_realm(std::move(ref));
2✔
4046
        REQUIRE(r1 != r2);
2!
4047

1✔
4048
        // New unbound with cache disabled
1✔
4049
        std::thread([&] {
2✔
4050
            ref = _impl::RealmCoordinator::get_coordinator(config)->get_unbound_realm();
2✔
4051
        }).join();
2✔
4052
        auto r3 = Realm::get_shared_realm(std::move(ref));
2✔
4053
        REQUIRE(r1 != r3);
2!
4054
        REQUIRE(r2 != r3);
2!
4055

1✔
4056
        // New local with cache enabled should grab the resolved unbound
1✔
4057
        config.cache = true;
2✔
4058
        auto r4 = Realm::get_shared_realm(config);
2✔
4059
        REQUIRE(r4 == r2);
2!
4060
    }
2✔
4061
}
8✔
4062

4063
TEST_CASE("Immutable Realms") {
40✔
4064
    TestFile config; // can't be in-memory because we have to write a file to open in immutable mode
40✔
4065
    config.schema_version = 1;
40✔
4066
    config.schema = Schema{{"object", {{"value", PropertyType::Int}}}};
40✔
4067

20✔
4068
    {
40✔
4069
        auto realm = Realm::get_shared_realm(config);
40✔
4070
        realm->begin_transaction();
40✔
4071
        realm->read_group().get_table("class_object")->create_object();
40✔
4072
        realm->commit_transaction();
40✔
4073
    }
40✔
4074

20✔
4075
    config.schema_mode = SchemaMode::Immutable;
40✔
4076
    auto realm = Realm::get_shared_realm(config);
40✔
4077
    realm->read_group();
40✔
4078

20✔
4079
    SECTION("unsupported functions") {
40✔
4080
        SECTION("update_schema()") {
10✔
4081
            REQUIRE_THROWS_AS(realm->compact(), WrongTransactionState);
2✔
4082
        }
2✔
4083
        SECTION("begin_transaction()") {
10✔
4084
            REQUIRE_THROWS_AS(realm->begin_transaction(), WrongTransactionState);
2✔
4085
        }
2✔
4086
        SECTION("async_begin_transaction()") {
10✔
4087
            REQUIRE_THROWS_AS(realm->async_begin_transaction(nullptr), WrongTransactionState);
2✔
4088
        }
2✔
4089
        SECTION("refresh()") {
10✔
4090
            REQUIRE_THROWS_AS(realm->refresh(), WrongTransactionState);
2✔
4091
        }
2✔
4092
        SECTION("compact()") {
10✔
4093
            REQUIRE_THROWS_AS(realm->compact(), WrongTransactionState);
2✔
4094
        }
2✔
4095
    }
10✔
4096

20✔
4097
    SECTION("supported functions") {
40✔
4098
        SECTION("is_in_transaction()") {
30✔
4099
            REQUIRE_FALSE(realm->is_in_transaction());
2!
4100
        }
2✔
4101
        SECTION("is_in_async_transaction()") {
30✔
4102
            REQUIRE_FALSE(realm->is_in_transaction());
2!
4103
        }
2✔
4104
        SECTION("freeze()") {
30✔
4105
            std::shared_ptr<Realm> frozen;
2✔
4106
            REQUIRE_NOTHROW(frozen = realm->freeze());
2✔
4107
            REQUIRE(frozen->read_group().get_table("class_object")->size() == 1);
2!
4108
            REQUIRE_NOTHROW(frozen = Realm::get_frozen_realm(config, realm->read_transaction_version()));
2✔
4109
            REQUIRE(frozen->read_group().get_table("class_object")->size() == 1);
2!
4110
        }
2✔
4111
        SECTION("notify()") {
30✔
4112
            REQUIRE_NOTHROW(realm->notify());
2✔
4113
        }
2✔
4114
        SECTION("is_in_read_transaction()") {
30✔
4115
            REQUIRE(realm->is_in_read_transaction());
2!
4116
        }
2✔
4117
        SECTION("last_seen_transaction_version()") {
30✔
4118
            REQUIRE(realm->last_seen_transaction_version() == 1);
2!
4119
        }
2✔
4120
        SECTION("get_number_of_versions()") {
30✔
4121
            REQUIRE(realm->get_number_of_versions() == 1);
2!
4122
        }
2✔
4123
        SECTION("read_transaction_version()") {
30✔
4124
            REQUIRE(realm->read_transaction_version() == VersionID{1, 0});
2!
4125
        }
2✔
4126
        SECTION("current_transaction_version()") {
30✔
4127
            REQUIRE(realm->current_transaction_version() == VersionID{1, 0});
2!
4128
        }
2✔
4129
        SECTION("latest_snapshot_version()") {
30✔
4130
            REQUIRE(realm->latest_snapshot_version() == 1);
2!
4131
        }
2✔
4132
        SECTION("duplicate()") {
30✔
4133
            auto duplicate = realm->duplicate();
2✔
4134
            REQUIRE(duplicate->get_table("class_object")->size() == 1);
2!
4135
        }
2✔
4136
        SECTION("invalidate()") {
30✔
4137
            REQUIRE_NOTHROW(realm->invalidate());
2✔
4138
            REQUIRE_FALSE(realm->is_in_read_transaction());
2!
4139
            REQUIRE(realm->read_group().get_table("class_object")->size() == 1);
2!
4140
        }
2✔
4141
        SECTION("close()") {
30✔
4142
            REQUIRE_NOTHROW(realm->close());
2✔
4143
            REQUIRE(realm->is_closed());
2!
4144
        }
2✔
4145
        SECTION("has_pending_async_work()") {
30✔
4146
            REQUIRE_FALSE(realm->has_pending_async_work());
2!
4147
        }
2✔
4148
        SECTION("wait_for_change()") {
30✔
4149
            REQUIRE_FALSE(realm->wait_for_change());
2!
4150
        }
2✔
4151
    }
30✔
4152
}
40✔
4153

4154
TEST_CASE("KeyPathMapping generation") {
2✔
4155
    TestFile config;
2✔
4156
    realm::query_parser::KeyPathMapping mapping;
2✔
4157

1✔
4158
    SECTION("class aliasing") {
2✔
4159
        Schema schema = {
2✔
4160
            {"PersistedName", {{"age", PropertyType::Int}}, {}, "AlternativeName"},
2✔
4161
            {"class_with_policy",
2✔
4162
             {{"value", PropertyType::Int},
2✔
4163
              {"child", PropertyType::Object | PropertyType::Nullable, "class_with_policy"}},
2✔
4164
             {{"parents", PropertyType::LinkingObjects | PropertyType::Array, "class_with_policy", "child"}},
2✔
4165
             "ClassWithPolicy"},
2✔
4166
        };
2✔
4167
        schema.validate();
2✔
4168
        config.schema = schema;
2✔
4169
        auto realm = Realm::get_shared_realm(config);
2✔
4170
        realm::populate_keypath_mapping(mapping, *realm);
2✔
4171
        REQUIRE(mapping.has_table_mapping("AlternativeName"));
2!
4172
        REQUIRE("class_PersistedName" == mapping.get_table_mapping("AlternativeName"));
2!
4173

1✔
4174
        auto table = realm->read_group().get_table("class_class_with_policy");
2✔
4175
        std::vector<Mixed> args{0};
2✔
4176
        auto q = table->query("parents.value = $0", args, mapping);
2✔
4177
        REQUIRE(q.count() == 0);
2!
4178
    }
2✔
4179
}
2✔
4180

4181
TEST_CASE("Concurrent operations") {
4✔
4182
    SECTION("Async commits together with online compaction") {
4✔
4183
        // This is a reproduction test for issue https://github.com/realm/realm-dart/issues/1396
1✔
4184
        // First create a relatively large realm, then delete the content and do some more
1✔
4185
        // commits using async commits. If a compaction is started when doing an async commit
1✔
4186
        // then the subsequent committing done in the helper thread will illegally COW the
1✔
4187
        // top array. When the next mutation is done, the top array will be reported as being
1✔
4188
        // already freed.
1✔
4189
        TestFile config;
2✔
4190
        config.schema_version = 1;
2✔
4191
        config.schema = Schema{{"object", {{"value", PropertyType::Int}}}};
2✔
4192

1✔
4193
        auto realm_1 = Realm::get_shared_realm(config);
2✔
4194
        Results res(realm_1, realm_1->read_group().get_table("class_object")->where());
2✔
4195
        auto realm_2 = Realm::get_shared_realm(config);
2✔
4196

1✔
4197
        {
2✔
4198
            // Create a lot of objects
1✔
4199
            realm_2->begin_transaction();
2✔
4200
            auto table = realm_2->read_group().get_table("class_object");
2✔
4201
            for (int i = 0; i < 400000; i++) {
800,002✔
4202
                table->create_object().set("value", i);
800,000✔
4203
            }
800,000✔
4204
            realm_2->commit_transaction();
2✔
4205
        }
2✔
4206

1✔
4207
        int commit_1 = 0;
2✔
4208
        int commit_2 = 0;
2✔
4209

1✔
4210
        for (int i = 0; i < 4; i++) {
10✔
4211
            realm_1->async_begin_transaction([&]() {
8✔
4212
                // Clearing the DB will reduce the need for space
4✔
4213
                // This will trigger an online compaction
4✔
4214
                // Before the fix, the probram would crash here next time around.
4✔
4215
                res.clear();
8✔
4216
                realm_1->async_commit_transaction([&](std::exception_ptr) {
8✔
4217
                    commit_1++;
8✔
4218
                });
8✔
4219
            });
8✔
4220
            realm_2->async_begin_transaction([&]() {
8✔
4221
                // Make sure we will continue to have something to delete
4✔
4222
                auto table = realm_2->read_group().get_table("class_object");
8✔
4223
                for (int i = 0; i < 100; i++) {
808✔
4224
                    table->create_object().set("value", i);
800✔
4225
                }
800✔
4226
                realm_2->async_commit_transaction([&](std::exception_ptr) {
8✔
4227
                    commit_2++;
8✔
4228
                });
8✔
4229
            });
8✔
4230
        }
8✔
4231

1✔
4232
        util::EventLoop::main().run_until([&] {
8,533✔
4233
            return commit_1 == 4 && commit_2 == 4;
8,533✔
4234
        });
8,533✔
4235
    }
2✔
4236

2✔
4237
    SECTION("No open realms") {
4✔
4238
        // This is just to check that the section above did not leave any realms open
1✔
4239
        _impl::RealmCoordinator::assert_no_open_realms();
2✔
4240
    }
2✔
4241
}
4✔
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