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

realm / realm-core / 1787

28 Oct 2023 12:35PM UTC coverage: 91.591% (+0.009%) from 91.582%
1787

push

Evergreen

web-flow
Improve configurations for sanitized builds (#6911)

* Refactor sanitizer flags for different build types:

** Enable address sanitizer for msvc
** Allow to build with sanitizer for diffent optimized build (also Debug)
** Make RelASAN, RelTSAN, RelUSAN, RelUSAN just shortcuts for half-optimized builds

* Fix usage of moved object for fuzz tester
* Check asan/tsan on macos x64/arm64
* Check asan with msvc 2019
* Remove Jenkins sanitized builders replaced by evergreen configs
* Work-around stack-use-after-scope with msvc2019 and mpark
* Fix crash on check with staled ColKeys
* fix a buffer overrun in a test
* fix a race in async_open_realm test util
* Add some logger related test fixes
* Work around catch2 limmitation with not thread safe asserts and TSAN races
* Run multiprocesses tests under sanitizers
* add assert for an error reported by undefined sanitizer
* Workaround uv scheduler main thread only constraint for callbacks called from non main thread and requesting a realm

---------

Co-authored-by: James Stone <james.stone@mongodb.com>

94356 of 173648 branches covered (0.0%)

54 of 63 new or added lines in 15 files covered. (85.71%)

2202 existing lines in 52 files now uncovered.

230692 of 251872 relevant lines covered (91.59%)

7167285.55 hits per line

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

99.04
/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/class.hpp>
37
#include <realm/object-store/thread_safe_reference.hpp>
38
#include <realm/object-store/impl/realm_coordinator.hpp>
39
#include <realm/object-store/util/event_loop_dispatcher.hpp>
40
#include <realm/object-store/util/scheduler.hpp>
41

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

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

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

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

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

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

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

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

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

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

86
using namespace realm;
87

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

38✔
245

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1✔
972
        // Create some content
1✔
973
        auto origin = Realm::get_shared_realm(config);
2✔
974
        origin->begin_transaction();
2✔
975
        Class cls = origin->get_class("object");
2✔
976
        cls.create_object(0);
2✔
977
        origin->commit_transaction();
2✔
978
        wait_for_upload(*origin);
2✔
979

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

1✔
994
            realm->convert(config3);
2✔
995
        }
2✔
996

1✔
997
        // Create some more content on the server
1✔
998
        origin->begin_transaction();
2✔
999
        cls.create_object(7);
2✔
1000
        origin->commit_transaction();
2✔
1001
        wait_for_upload(*origin);
2✔
1002

1✔
1003
        // Now open a realm based on the realm file created above
1✔
1004
        auto realm = Realm::get_shared_realm(config3);
2✔
1005
        Class cls2 = realm->get_class("object");
2✔
1006
        wait_for_download(*realm);
2✔
1007
        wait_for_upload(*realm);
2✔
1008

1✔
1009
        // Make sure we have got a new client file id
1✔
1010
        REQUIRE(realm->read_group().get_sync_file_id() != client_file_id);
2!
1011
        REQUIRE(cls.num_objects() == 3);
2!
1012

1✔
1013
        // Check that we can continue committing to this realm
1✔
1014
        realm->begin_transaction();
2✔
1015
        cls2.create_object(5);
2✔
1016
        realm->commit_transaction();
2✔
1017
        wait_for_upload(*realm);
2✔
1018

1✔
1019
        // Check that this change is now in the original realm
1✔
1020
        wait_for_download(*origin);
2✔
1021
        origin->refresh();
2✔
1022
        REQUIRE(cls.num_objects() == 4);
2!
1023
    }
2✔
1024

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

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

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

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

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

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

1✔
1104
        {
2✔
1105
            auto realm = Realm::get_shared_realm(config2);
2✔
1106
            realm->begin_transaction();
2✔
1107
            realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1108
            realm->commit_transaction();
2✔
1109
            wait_for_upload(*realm);
2✔
1110
        }
2✔
1111

1✔
1112
        auto [ref, error] = async_open_realm(config);
2✔
1113
        REQUIRE(ref);
2!
1114
        REQUIRE_FALSE(error);
2!
1115
        REQUIRE(Realm::get_shared_realm(std::move(ref))->read_group().get_table("class_object")->size() == 1);
2!
1116
    }
2✔
1117

15✔
1118
    SECTION("can download multiple Realms at a time") {
30✔
1119
        SyncTestFile config1(init_sync_manager.app(), "realm1");
2✔
1120
        SyncTestFile config2(init_sync_manager.app(), "realm2");
2✔
1121
        SyncTestFile config3(init_sync_manager.app(), "realm3");
2✔
1122
        SyncTestFile config4(init_sync_manager.app(), "realm4");
2✔
1123

1✔
1124
        std::vector<std::shared_ptr<AsyncOpenTask>> tasks = {
2✔
1125
            Realm::get_synchronized_realm(config1),
2✔
1126
            Realm::get_synchronized_realm(config2),
2✔
1127
            Realm::get_synchronized_realm(config3),
2✔
1128
            Realm::get_synchronized_realm(config4),
2✔
1129
        };
2✔
1130

1✔
1131
        std::atomic<int> completed{0};
2✔
1132
        for (auto& task : tasks) {
8✔
1133
            task->start([&](auto, auto) {
8✔
1134
                ++completed;
8✔
1135
            });
8✔
1136
        }
8✔
1137
        util::EventLoop::main().run_until([&] {
40,516✔
1138
            return completed == 4;
40,516✔
1139
        });
40,516✔
1140
    }
2✔
1141

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

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

15✔
1155
    SECTION("can async open while waiting for a token refresh") {
30✔
1156
        SyncTestFile config(init_sync_manager.app(), "realm");
2✔
1157
        auto valid_token = config.sync_config->user->access_token();
2✔
1158
        config.sync_config->user->update_access_token(std::move(invalid_token));
2✔
1159

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

1✔
1169
        auto body = nlohmann::json({{"access_token", valid_token}}).dump();
2✔
1170
        init_sync_manager.network_callback(app::Response{200, 0, {}, body});
2✔
1171
        util::EventLoop::main().run_until([&] {
13,763✔
1172
            return called.load();
13,763✔
1173
        });
13,763✔
1174
        std::lock_guard<std::mutex> lock(mutex);
2✔
1175
        REQUIRE(called);
2!
1176
    }
2✔
1177

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

1✔
1191
        SyncTestFile config(tsm.app(), "realm");
2✔
1192
        config.sync_config->user->log_in(invalid_token, invalid_token);
2✔
1193

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

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

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

15✔
1231
    Schema with_added_object = Schema{object_schema,
30✔
1232
                                      {"added",
30✔
1233
                                       {
30✔
1234
                                           {"_id", PropertyType::Int, Property::IsPrimary{true}},
30✔
1235
                                       }}};
30✔
1236

15✔
1237
    SECTION("read-only mode applies remote schema changes") {
30✔
1238
        // Create the local file without "added"
1✔
1239
        Realm::get_shared_realm(config2);
2✔
1240

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

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

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

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

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

1✔
1278
        config.schema_mode = SchemaMode::ReadOnly;
2✔
1279
        config.schema = Schema{{"object",
2✔
1280
                                {
2✔
1281
                                    {"_id", PropertyType::Int, Property::IsPrimary{true}},
2✔
1282
                                    {"value", PropertyType::Int},
2✔
1283
                                    {"value2", PropertyType::Int},
2✔
1284
                                }}};
2✔
1285

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

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

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

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

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

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

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

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

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

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

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

4✔
1359
    SyncTestFile sync_config1(tsm.app(), "default");
8✔
1360
    sync_config1.schema = schema;
8✔
1361
    TestFile local_config1;
8✔
1362
    local_config1.schema = schema;
8✔
1363
    local_config1.schema_version = sync_config1.schema_version;
8✔
1364

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

1✔
1373
        // Copy to a new sync config
1✔
1374
        SyncTestFile sync_config2(tsm.app(), "default");
2✔
1375
        sync_config2.schema = schema;
2✔
1376

1✔
1377
        sync_realm1->convert(sync_config2);
2✔
1378

1✔
1379
        auto sync_realm2 = Realm::get_shared_realm(sync_config2);
2✔
1380

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

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

1✔
1392
        sync_realm1->refresh();
2✔
1393
        REQUIRE(sync_realm1->read_group().get_table("class_object")->size() == 2);
2!
1394
    }
2✔
1395

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

1✔
1404
        sync_realm->convert(local_config1);
2✔
1405

1✔
1406
        auto local_realm = Realm::get_shared_realm(local_config1);
2✔
1407

1✔
1408
        // Check that the data also exists in the new realm
1✔
1409
        REQUIRE(local_realm->read_group().get_table("class_object")->size() == 1);
2!
1410
    }
2✔
1411

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

1✔
1418
        // Copy to a new sync config
1✔
1419
        local_realm->convert(sync_config1);
2✔
1420

1✔
1421
        auto sync_realm = Realm::get_shared_realm(sync_config1);
2✔
1422

1✔
1423
        // Check that the data also exists in the new realm
1✔
1424
        REQUIRE(sync_realm->read_group().get_table("class_object")->size() == 1);
2!
1425
    }
2✔
1426

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

1✔
1433
        // Copy to a new local config
1✔
1434
        TestFile local_config2;
2✔
1435
        local_config2.schema = schema;
2✔
1436
        local_config2.schema_version = local_config1.schema_version;
2✔
1437
        local_realm1->convert(local_config2);
2✔
1438

1✔
1439
        auto local_realm2 = Realm::get_shared_realm(local_config2);
2✔
1440

1✔
1441
        // Check that the data also exists in the new realm
1✔
1442
        REQUIRE(local_realm2->read_group().get_table("class_object")->size() == 1);
2!
1443
    }
2✔
1444
}
8✔
1445

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

8✔
1461
    SyncTestFile sync_config1(tsm.app(), "default");
16✔
1462
    sync_config1.schema = schema;
16✔
1463
    TestFile local_config1;
16✔
1464
    local_config1.schema = schema;
16✔
1465
    local_config1.schema_version = sync_config1.schema_version;
16✔
1466

8✔
1467
    SECTION("can copy a synced realm to a synced realm") {
16✔
1468
        auto sync_realm1 = Realm::get_shared_realm(sync_config1);
4✔
1469
        sync_realm1->begin_transaction();
4✔
1470

2✔
1471
        SECTION("null embedded object") {
4✔
1472
            sync_realm1->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1473
        }
2✔
1474

2✔
1475
        SECTION("embedded object") {
4✔
1476
            auto obj = sync_realm1->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1477
            auto col_key = sync_realm1->read_group().get_table("class_object")->get_column_key("embedded_link");
2✔
1478
            obj.create_and_set_linked_object(col_key);
2✔
1479
        }
2✔
1480

2✔
1481
        sync_realm1->commit_transaction();
4✔
1482
        wait_for_upload(*sync_realm1);
4✔
1483
        wait_for_download(*sync_realm1);
4✔
1484

2✔
1485
        // Copy to a new sync config
2✔
1486
        SyncTestFile sync_config2(tsm.app(), "default");
4✔
1487
        sync_config2.schema = schema;
4✔
1488

2✔
1489
        sync_realm1->convert(sync_config2);
4✔
1490

2✔
1491
        auto sync_realm2 = Realm::get_shared_realm(sync_config2);
4✔
1492

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

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

2✔
1504
        sync_realm1->refresh();
4✔
1505
        REQUIRE(sync_realm1->read_group().get_table("class_object")->size() == 2);
4!
1506
    }
4✔
1507

8✔
1508
    SECTION("can convert a synced realm to a local realm") {
16✔
1509
        auto sync_realm = Realm::get_shared_realm(sync_config1);
4✔
1510
        sync_realm->begin_transaction();
4✔
1511

2✔
1512
        SECTION("null embedded object") {
4✔
1513
            sync_realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1514
        }
2✔
1515

2✔
1516
        SECTION("embedded object") {
4✔
1517
            auto obj = sync_realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1518
            auto col_key = sync_realm->read_group().get_table("class_object")->get_column_key("embedded_link");
2✔
1519
            obj.create_and_set_linked_object(col_key);
2✔
1520
        }
2✔
1521

2✔
1522
        sync_realm->commit_transaction();
4✔
1523
        wait_for_upload(*sync_realm);
4✔
1524
        wait_for_download(*sync_realm);
4✔
1525

2✔
1526
        sync_realm->convert(local_config1);
4✔
1527

2✔
1528
        auto local_realm = Realm::get_shared_realm(local_config1);
4✔
1529

2✔
1530
        // Check that the data also exists in the new realm
2✔
1531
        REQUIRE(local_realm->read_group().get_table("class_object")->size() == 1);
4!
1532
    }
4✔
1533

8✔
1534
    SECTION("can convert a local realm to a synced realm") {
16✔
1535
        auto local_realm = Realm::get_shared_realm(local_config1);
4✔
1536
        local_realm->begin_transaction();
4✔
1537

2✔
1538
        SECTION("null embedded object") {
4✔
1539
            local_realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1540
        }
2✔
1541

2✔
1542
        SECTION("embedded object") {
4✔
1543
            auto obj = local_realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1544
            auto col_key = local_realm->read_group().get_table("class_object")->get_column_key("embedded_link");
2✔
1545
            obj.create_and_set_linked_object(col_key);
2✔
1546
        }
2✔
1547

2✔
1548
        local_realm->commit_transaction();
4✔
1549

2✔
1550
        // Copy to a new sync config
2✔
1551
        local_realm->convert(sync_config1);
4✔
1552

2✔
1553
        auto sync_realm = Realm::get_shared_realm(sync_config1);
4✔
1554

2✔
1555
        // Check that the data also exists in the new realm
2✔
1556
        REQUIRE(sync_realm->read_group().get_table("class_object")->size() == 1);
4!
1557
    }
4✔
1558

8✔
1559
    SECTION("can copy a local realm to a local realm") {
16✔
1560
        auto local_realm1 = Realm::get_shared_realm(local_config1);
4✔
1561
        local_realm1->begin_transaction();
4✔
1562

2✔
1563
        SECTION("null embedded object") {
4✔
1564
            local_realm1->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1565
        }
2✔
1566

2✔
1567
        SECTION("embedded object") {
4✔
1568
            auto obj = local_realm1->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1569
            auto col_key = local_realm1->read_group().get_table("class_object")->get_column_key("embedded_link");
2✔
1570
            obj.create_and_set_linked_object(col_key);
2✔
1571
        }
2✔
1572

2✔
1573
        local_realm1->commit_transaction();
4✔
1574

2✔
1575
        // Copy to a new local config
2✔
1576
        TestFile local_config2;
4✔
1577
        local_config2.schema = schema;
4✔
1578
        local_config2.schema_version = local_config1.schema_version;
4✔
1579
        local_realm1->convert(local_config2);
4✔
1580

2✔
1581
        auto local_realm2 = Realm::get_shared_realm(local_config2);
4✔
1582

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

1589
TEST_CASE("SharedRealm: async writes") {
104✔
1590
    _impl::RealmCoordinator::assert_no_open_realms();
104✔
1591
    if (!util::EventLoop::has_implementation())
104✔
1592
        return;
×
1593

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

52✔
1606
    auto wait_for_done = [&]() {
100✔
1607
        util::EventLoop::main().run_until([&] {
89,454✔
1608
            return done;
89,454✔
1609
        });
89,454✔
1610
        REQUIRE(done);
96!
1611
    };
96✔
1612

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

52✔
1640
    auto verify_persisted_count = [&](size_t expected) {
78✔
1641
        if (realm)
52✔
1642
            realm->close();
50✔
1643
        _impl::RealmCoordinator::assert_no_open_realms();
52✔
1644

26✔
1645
        auto new_realm = Realm::get_shared_realm(config);
52✔
1646
        auto table = new_realm->read_group().get_table("class_object");
52✔
1647
        REQUIRE(table->size() == expected);
52!
1648
    };
52✔
1649

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

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

2✔
1674
                // Wait for the background thread to have acquired the lock
2✔
1675
                sema.get_stone();
4✔
1676

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

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

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

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

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

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

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

2✔
1840
                wait_for_done();
4✔
1841
                verify_persisted_count(1);
4✔
1842
            }
4✔
1843
        }
40✔
1844
    }
208✔
1845

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

1✔
1900
        // Transaction should have been rolled back
1✔
1901
        REQUIRE_FALSE(realm->is_in_transaction());
2!
1902
        REQUIRE(table->size() == 0);
2!
1903
        REQUIRE(called);
2!
1904

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

1✔
1929
        // Transaction should have been rolled back
1✔
1930
        REQUIRE_FALSE(realm->is_in_transaction());
2!
1931
        REQUIRE(table->size() == 0);
2!
1932

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

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

52✔
1987
    if (_impl::SimulatedFailure::is_enabled()) {
104✔
1988
        SECTION("error in the synchronous part of async commit") {
104✔
1989
            realm->begin_transaction();
2✔
1990
            table->create_object();
2✔
1991

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

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

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

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

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

1✔
2180
        util::EventLoop::main().run_until([&] {
828✔
2181
            return !realm->is_in_async_transaction();
828✔
2182
        });
828✔
2183

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

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

1✔
2256
        Observer observer(obj);
2✔
2257
        observer.realm = realm;
2✔
2258
        realm->m_binding_context.reset(&observer);
2✔
2259

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

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

1✔
2287
        realm->m_binding_context.reset(new Context());
2✔
2288
        realm->m_binding_context->realm = realm;
2✔
2289

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

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

41✔
2309
                table->create_object();
82✔
2310
                realm->async_commit_transaction(
82✔
2311
                    [&](std::exception_ptr) {
82✔
2312
                        ++completion_calls;
82✔
2313
                    },
82✔
2314
                    true);
82✔
2315
            });
82✔
2316
        }
82✔
2317
        util::EventLoop::main().run_until([&] {
3,104✔
2318
            return completion_calls == 41;
3,104✔
2319
        });
3,104✔
2320
    }
2✔
2321

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

41✔
2330
                table->create_object();
82✔
2331
                realm->async_commit_transaction(
82✔
2332
                    [&](std::exception_ptr) {
82✔
2333
                        ++completion_calls;
82✔
2334
                    },
82✔
2335
                    (i + 1) % 6 != 0);
82✔
2336
            });
82✔
2337
        }
82✔
2338
        util::EventLoop::main().run_until([&] {
11,441✔
2339
            return completion_calls == 41;
11,441✔
2340
        });
11,441✔
2341
    }
2✔
2342

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

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

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

1✔
2397

1✔
2398
        wait_for_done();
2✔
2399
        REQUIRE(table->size() == 6);
2!
2400
    }
2✔
2401

52✔
2402
    SECTION("async writes which would run inside sync writes are deferred") {
104✔
2403
        realm->async_begin_transaction([&] {
2✔
2404
            done = true;
2✔
2405
        });
2✔
2406

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

1✔
2415
        realm->begin_transaction();
2✔
2416

1✔
2417
        // Invoke the pending callback
1✔
2418
        util::EventLoop::main().run_pending();
2✔
2419
        // Should not have run the async write block
1✔
2420
        REQUIRE(done == false);
2!
2421

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

52✔
2429
    util::EventLoop::main().run_until([&] {
162✔
2430
        return !realm || !realm->has_pending_async_work();
162✔
2431
    });
162✔
2432

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

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

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

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

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

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

2574
        util::EventLoop::main().run_until([&] {
138✔
2575
            return completions == expected_completions;
138✔
2576
        });
138✔
2577
    }
1✔
2578

2579

2580
    realm = Realm::get_shared_realm(config);
5✔
2581
    REQUIRE(realm->read_group().get_table(table_key)->size() == completions);
5!
2582

2583
    for (auto& queue : queues) {
50✔
2584
        dispatch_sync(queue.queue, ^{
50✔
2585
                      });
50✔
2586
    }
50✔
2587
}
5✔
2588
#endif
2589

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

2613
private:
2614
    struct Task {
2615
        std::shared_ptr<bool> may_run;
2616
        util::UniqueFunction<void()> the_job;
2617
    };
2618
    std::vector<Task> m_tasks;
2619
};
2620

2621
#ifndef _WIN32
2622
TEST_CASE("SharedRealm: async_writes_2") {
2✔
2623
    _impl::RealmCoordinator::assert_no_open_realms();
2✔
2624
    if (!util::EventLoop::has_implementation())
2✔
2625
        return;
×
2626

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

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

1✔
2682
    util::EventLoop::main().run_until([&, realm] {
1,739✔
2683
        ld.run_once();
1,739✔
2684
        return done;
1,739✔
2685
    });
1,739✔
2686
    REQUIRE(done);
2!
2687
}
2✔
2688
#endif
2689

2690
TEST_CASE("SharedRealm: notifications") {
14✔
2691
    if (!util::EventLoop::has_implementation())
14✔
2692
        return;
×
2693

7✔
2694
    TestFile config;
14✔
2695
    config.schema_version = 0;
14✔
2696
    config.schema = Schema{
14✔
2697
        {"object", {{"value", PropertyType::Int}}},
14✔
2698
    };
14✔
2699

7✔
2700
    struct Context : BindingContext {
14✔
2701
        size_t* change_count;
14✔
2702
        util::UniqueFunction<void()> did_change_fn;
14✔
2703
        util::UniqueFunction<void()> changes_available_fn;
14✔
2704

7✔
2705
        Context(size_t* out)
14✔
2706
            : change_count(out)
14✔
2707
        {
14✔
2708
        }
14✔
2709

7✔
2710
        void did_change(std::vector<ObserverState> const&, std::vector<void*> const&, bool) override
14✔
2711
        {
24✔
2712
            ++*change_count;
24✔
2713
            if (did_change_fn)
24✔
2714
                did_change_fn();
12✔
2715
        }
24✔
2716

7✔
2717
        void changes_available() override
14✔
2718
        {
12✔
2719
            if (changes_available_fn)
10✔
2720
                changes_available_fn();
2✔
2721
        }
10✔
2722
    };
14✔
2723

7✔
2724
    size_t change_count = 0;
14✔
2725
    auto realm = Realm::get_shared_realm(config);
14✔
2726
    realm->read_group();
14✔
2727
    auto context = new Context{&change_count};
14✔
2728
    realm->m_binding_context.reset(context);
14✔
2729
    realm->m_binding_context->realm = realm;
14✔
2730

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

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

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

7✔
2779
    SECTION("refresh() from within did_change() is a no-op") {
14✔
2780
        context->did_change_fn = [&] {
4✔
2781
            if (change_count > 1)
4✔
2782
                return;
2✔
2783

1✔
2784
            // Create another version so that refresh() advances the version
1✔
2785
            auto r2 = Realm::get_shared_realm(realm->config());
2✔
2786
            r2->begin_transaction();
2✔
2787
            r2->commit_transaction();
2✔
2788

1✔
2789
            REQUIRE_FALSE(realm->refresh());
2!
2790
        };
2✔
2791

1✔
2792
        auto r2 = Realm::get_shared_realm(config);
2✔
2793
        r2->begin_transaction();
2✔
2794
        r2->commit_transaction();
2✔
2795

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

1✔
2799
        REQUIRE(realm->refresh());
2!
2800
        REQUIRE(change_count == 2);
2!
2801
        REQUIRE_FALSE(realm->refresh());
2!
2802
    }
2✔
2803

7✔
2804
    SECTION("begin_write() from within did_change() produces recursive notifications") {
14✔
2805
        context->did_change_fn = [&] {
8✔
2806
            if (realm->is_in_transaction())
8✔
2807
                realm->cancel_transaction();
6✔
2808
            if (change_count > 3)
8✔
2809
                return;
2✔
2810

3✔
2811
            // Create another version so that begin_write() advances the version
3✔
2812
            auto r2 = Realm::get_shared_realm(realm->config());
6✔
2813
            r2->begin_transaction();
6✔
2814
            r2->commit_transaction();
6✔
2815

3✔
2816
            realm->begin_transaction();
6✔
2817
            REQUIRE(change_count == 4);
6!
2818
        };
6✔
2819

1✔
2820
        auto r2 = Realm::get_shared_realm(config);
2✔
2821
        r2->begin_transaction();
2✔
2822
        r2->commit_transaction();
2✔
2823
        REQUIRE(realm->refresh());
2!
2824
        REQUIRE(change_count == 4);
2!
2825
        REQUIRE_FALSE(realm->refresh());
2!
2826
    }
2✔
2827

7✔
2828
#if REALM_ENABLE_SYNC
14✔
2829
    SECTION("SubscriptionStore writes produce notifications") {
14✔
2830
        auto subscription_store = sync::SubscriptionStore::create(TestHelper::get_db(realm));
2✔
2831
        REQUIRE(change_count == 0);
2!
2832
        util::EventLoop::main().run_until([&] {
11✔
2833
            return change_count > 0;
11✔
2834
        });
11✔
2835
        REQUIRE(change_count == 1);
2!
2836

1✔
2837
        subscription_store->get_active().make_mutable_copy().commit();
2✔
2838
        REQUIRE(change_count == 1);
2!
2839
        util::EventLoop::main().run_until([&] {
11✔
2840
            return change_count > 1;
11✔
2841
        });
11✔
2842
        REQUIRE(change_count == 2);
2!
2843
    }
2✔
2844
#endif
14✔
2845
}
14✔
2846

2847
TEST_CASE("SharedRealm: schema updating from external changes") {
14✔
2848
    TestFile config;
14✔
2849
    config.schema_version = 0;
14✔
2850
    config.schema_mode = SchemaMode::AdditiveExplicit;
14✔
2851
    config.schema = Schema{
14✔
2852
        {"object",
14✔
2853
         {
14✔
2854
             {"value", PropertyType::Int, Property::IsPrimary{true}},
14✔
2855
             {"value 2", PropertyType::Int, Property::IsPrimary{false}, Property::IsIndexed{true}},
14✔
2856
         }},
14✔
2857
    };
14✔
2858

7✔
2859
    SECTION("newly added columns update table columns but are not added to properties") {
14✔
2860
        // Does this test add any value when column keys are stable?
2✔
2861
        auto r1 = Realm::get_shared_realm(config);
4✔
2862
        auto r2 = Realm::get_shared_realm(config);
4✔
2863
        auto test = [&] {
4✔
2864
            r2->begin_transaction();
4✔
2865
            r2->read_group().get_table("class_object")->add_column(type_String, "new col");
4✔
2866
            r2->commit_transaction();
4✔
2867

2✔
2868
            auto& object_schema = *r1->schema().find("object");
4✔
2869
            REQUIRE(object_schema.persisted_properties.size() == 2);
4!
2870
            ColKey col = object_schema.persisted_properties[0].column_key;
4✔
2871
            r1->refresh();
4✔
2872
            REQUIRE(object_schema.persisted_properties[0].column_key == col);
4!
2873
        };
4✔
2874
        SECTION("with an active read transaction") {
4✔
2875
            r1->read_group();
2✔
2876
            test();
2✔
2877
        }
2✔
2878
        SECTION("without an active read transaction") {
4✔
2879
            r1->invalidate();
2✔
2880
            test();
2✔
2881
        }
2✔
2882
    }
4✔
2883

7✔
2884
    SECTION("beginning a read transaction checks for incompatible changes") {
14✔
2885
        auto r = Realm::get_shared_realm(config);
10✔
2886
        r->invalidate();
10✔
2887

5✔
2888
        auto& db = TestHelper::get_db(r);
10✔
2889
        WriteTransaction wt(db);
10✔
2890
        auto& table = *wt.get_table("class_object");
10✔
2891

5✔
2892
        SECTION("removing a property") {
10✔
2893
            table.remove_column(table.get_column_key("value"));
2✔
2894
            wt.commit();
2✔
2895
            REQUIRE_THROWS_CONTAINING(r->refresh(), "Property 'object.value' has been removed.");
2✔
2896
        }
2✔
2897

5✔
2898
        SECTION("change property type") {
10✔
2899
            table.remove_column(table.get_column_key("value 2"));
2✔
2900
            table.add_column(type_Float, "value 2");
2✔
2901
            wt.commit();
2✔
2902
            REQUIRE_THROWS_CONTAINING(r->refresh(),
2✔
2903
                                      "Property 'object.value 2' has been changed from 'int' to 'float'");
2✔
2904
        }
2✔
2905

5✔
2906
        SECTION("make property optional") {
10✔
2907
            table.remove_column(table.get_column_key("value 2"));
2✔
2908
            table.add_column(type_Int, "value 2", true);
2✔
2909
            wt.commit();
2✔
2910
            REQUIRE_THROWS_CONTAINING(r->refresh(), "Property 'object.value 2' has been made optional");
2✔
2911
        }
2✔
2912

5✔
2913
        SECTION("recreate column with no changes") {
10✔
2914
            table.remove_column(table.get_column_key("value 2"));
2✔
2915
            table.add_column(type_Int, "value 2");
2✔
2916
            wt.commit();
2✔
2917
            REQUIRE_NOTHROW(r->refresh());
2✔
2918
        }
2✔
2919

5✔
2920
        SECTION("remove index from non-PK") {
10✔
2921
            table.remove_search_index(table.get_column_key("value 2"));
2✔
2922
            wt.commit();
2✔
2923
            REQUIRE_NOTHROW(r->refresh());
2✔
2924
        }
2✔
2925
    }
10✔
2926
}
14✔
2927

2928
TEST_CASE("SharedRealm: close()") {
4✔
2929
    TestFile config;
4✔
2930
    config.schema_version = 1;
4✔
2931
    config.schema = Schema{
4✔
2932
        {"object", {{"value", PropertyType::Int}}},
4✔
2933
        {"list", {{"list", PropertyType::Object | PropertyType::Array, "object"}}},
4✔
2934
    };
4✔
2935

2✔
2936
    auto realm = Realm::get_shared_realm(config);
4✔
2937

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

1✔
2941
        realm->close();
2✔
2942
        REQUIRE(realm->is_closed());
2!
2943
        REQUIRE_EXCEPTION(realm->verify_open(), ClosedRealm, msg);
2✔
2944

1✔
2945
        REQUIRE_EXCEPTION(realm->update_schema(Schema{}), ClosedRealm, msg);
2✔
2946
        REQUIRE_EXCEPTION(realm->rename_property(Schema{}, "", "", ""), ClosedRealm, msg);
2✔
2947
        REQUIRE_EXCEPTION(realm->set_schema_subset(Schema{}), ClosedRealm, msg);
2✔
2948

1✔
2949
        REQUIRE_EXCEPTION(realm->begin_transaction(), ClosedRealm, msg);
2✔
2950
        REQUIRE_EXCEPTION(realm->commit_transaction(), ClosedRealm, msg);
2✔
2951
        REQUIRE_EXCEPTION(realm->cancel_transaction(), ClosedRealm, msg);
2✔
2952
        REQUIRE(!realm->is_in_transaction());
2!
2953

1✔
2954
        REQUIRE_EXCEPTION(realm->async_begin_transaction(nullptr), ClosedRealm, msg);
2✔
2955
        REQUIRE_EXCEPTION(realm->async_commit_transaction(nullptr), ClosedRealm, msg);
2✔
2956
        REQUIRE_EXCEPTION(realm->async_cancel_transaction(0), ClosedRealm, msg);
2✔
2957
        REQUIRE_FALSE(realm->is_in_async_transaction());
2!
2958

1✔
2959
        REQUIRE_EXCEPTION(realm->freeze(), ClosedRealm, msg);
2✔
2960
        REQUIRE_FALSE(realm->is_frozen());
2!
2961
        REQUIRE_EXCEPTION(realm->get_number_of_versions(), ClosedRealm, msg);
2✔
2962
        REQUIRE_EXCEPTION(realm->read_transaction_version(), ClosedRealm, msg);
2✔
2963
        REQUIRE_EXCEPTION(realm->duplicate(), ClosedRealm, msg);
2✔
2964

1✔
2965
        REQUIRE_EXCEPTION(realm->enable_wait_for_change(), ClosedRealm, msg);
2✔
2966
        REQUIRE_EXCEPTION(realm->wait_for_change(), ClosedRealm, msg);
2✔
2967
        REQUIRE_EXCEPTION(realm->wait_for_change_release(), ClosedRealm, msg);
2✔
2968

1✔
2969
        REQUIRE_NOTHROW(realm->notify());
2✔
2970
        REQUIRE_EXCEPTION(realm->refresh(), ClosedRealm, msg);
2✔
2971
        REQUIRE_EXCEPTION(realm->invalidate(), ClosedRealm, msg);
2✔
2972
        REQUIRE_EXCEPTION(realm->compact(), ClosedRealm, msg);
2✔
2973
        REQUIRE_EXCEPTION(realm->convert(realm->config()), ClosedRealm, msg);
2✔
2974
        REQUIRE_EXCEPTION(realm->write_copy(), ClosedRealm, msg);
2✔
2975

1✔
2976
#if REALM_ENABLE_SYNC
2✔
2977
        REQUIRE_FALSE(realm->sync_session());
2!
2978
        msg = "Flexible sync is not enabled";
2✔
2979
        REQUIRE_EXCEPTION(realm->get_latest_subscription_set(), IllegalOperation, msg);
2✔
2980
        REQUIRE_EXCEPTION(realm->get_active_subscription_set(), IllegalOperation, msg);
2✔
2981
#endif
2✔
2982
    }
2✔
2983

2✔
2984
    SECTION("fully closes database file even with live notifiers") {
4✔
2985
        auto& group = realm->read_group();
2✔
2986
        realm->begin_transaction();
2✔
2987
        auto obj = ObjectStore::table_for_object_type(group, "list")->create_object();
2✔
2988
        realm->commit_transaction();
2✔
2989

1✔
2990
        Results results(realm, ObjectStore::table_for_object_type(group, "object"));
2✔
2991
        List list(realm, obj.get_linklist("list"));
2✔
2992
        Object object(realm, obj);
2✔
2993

1✔
2994
        auto obj_token = object.add_notification_callback([](CollectionChangeSet) {});
2✔
2995
        auto list_token = list.add_notification_callback([](CollectionChangeSet) {});
2✔
2996
        auto results_token = results.add_notification_callback([](CollectionChangeSet) {});
2✔
2997

1✔
2998
        // Perform a dummy transaction to ensure the notifiers actually acquire
1✔
2999
        // resources that need to be closed
1✔
3000
        realm->begin_transaction();
2✔
3001
        realm->commit_transaction();
2✔
3002

1✔
3003
        realm->close();
2✔
3004

1✔
3005
        // Verify that we're able to acquire an exclusive lock
1✔
3006
        REQUIRE(DB::call_with_lock(config.path, [](auto) {}));
2!
3007
    }
2✔
3008
}
4✔
3009

3010
TEST_CASE("Realm::delete_files()") {
12✔
3011
    TestFile config;
12✔
3012
    config.schema_version = 1;
12✔
3013
    config.schema = Schema{{"object", {{"value", PropertyType::Int}}}};
12✔
3014
    auto realm = Realm::get_shared_realm(config);
12✔
3015
    auto path = config.path;
12✔
3016

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

6✔
3022
    SECTION("Deleting files of a closed Realm succeeds.") {
12✔
3023
        realm->close();
2✔
3024
        bool did_delete = false;
2✔
3025
        Realm::delete_files(path, &did_delete);
2✔
3026
        REQUIRE(did_delete);
2!
3027
        REQUIRE_FALSE(util::File::exists(path));
2!
3028
        REQUIRE_FALSE(util::File::exists(path + ".management"));
2!
3029
        REQUIRE_FALSE(util::File::exists(path + ".note"));
2!
3030
        REQUIRE_FALSE(util::File::exists(path + ".log"));
2!
3031

1✔
3032
        // Deleting the .lock file is not safe. It must still exist.
1✔
3033
        REQUIRE(util::File::exists(path + ".lock"));
2!
3034
    }
2✔
3035

6✔
3036
    SECTION("Trying to delete files of an open Realm fails.") {
12✔
3037
        REQUIRE_EXCEPTION(Realm::delete_files(path), ErrorCodes::DeleteOnOpenRealm,
2✔
3038
                          util::format("Cannot delete files of an open Realm: '%1' is still in use.", path));
2✔
3039
        REQUIRE(util::File::exists(path + ".lock"));
2!
3040
        REQUIRE(util::File::exists(path));
2!
3041
        REQUIRE(util::File::exists(path + ".management"));
2!
3042
#ifndef _WIN32
2✔
3043
        REQUIRE(util::File::exists(path + ".note"));
2!
3044
#endif
2✔
3045
        REQUIRE(util::File::exists(path + ".log"));
2!
3046
    }
2✔
3047

6✔
3048
    SECTION("Deleting the same Realm multiple times.") {
12✔
3049
        realm->close();
2✔
3050
        Realm::delete_files(path);
2✔
3051
        Realm::delete_files(path);
2✔
3052
        Realm::delete_files(path);
2✔
3053
    }
2✔
3054

6✔
3055
    SECTION("Calling delete on a folder that does not exist.") {
12✔
3056
        auto fake_path = "/tmp/doesNotExist/realm.424242";
2✔
3057
        bool did_delete = false;
2✔
3058
        Realm::delete_files(fake_path, &did_delete);
2✔
3059
        REQUIRE_FALSE(did_delete);
2!
3060
    }
2✔
3061

6✔
3062
    SECTION("passing did_delete is optional") {
12✔
3063
        realm->close();
2✔
3064
        Realm::delete_files(path, nullptr);
2✔
3065
    }
2✔
3066

6✔
3067
    SECTION("Deleting a Realm which does not exist does not set did_delete") {
12✔
3068
        TestFile new_config;
2✔
3069
        bool did_delete = false;
2✔
3070
        Realm::delete_files(new_config.path, &did_delete);
2✔
3071
        REQUIRE_FALSE(did_delete);
2!
3072
    }
2✔
3073
}
12✔
3074

3075
TEST_CASE("ShareRealm: in-memory mode from buffer") {
2✔
3076
    TestFile config;
2✔
3077
    config.schema_version = 1;
2✔
3078
    config.schema = Schema{
2✔
3079
        {"object", {{"value", PropertyType::Int}}},
2✔
3080
    };
2✔
3081

1✔
3082
    SECTION("Save and open Realm from in-memory buffer") {
2✔
3083
        // Write in-memory copy of Realm to a buffer
1✔
3084
        auto realm = Realm::get_shared_realm(config);
2✔
3085
        OwnedBinaryData realm_buffer = realm->write_copy();
2✔
3086

1✔
3087
        // Open the buffer as a new (immutable in-memory) Realm
1✔
3088
        realm::Realm::Config config2;
2✔
3089
        config2.in_memory = true;
2✔
3090
        config2.schema_mode = SchemaMode::Immutable;
2✔
3091
        config2.realm_data = realm_buffer.get();
2✔
3092

1✔
3093
        auto realm2 = Realm::get_shared_realm(config2);
2✔
3094

1✔
3095
        // Verify that it can read the schema and that it is the same
1✔
3096
        REQUIRE(realm->schema().size() == 1);
2!
3097
        auto it = realm->schema().find("object");
2✔
3098
        auto table = realm->read_group().get_table("class_object");
2✔
3099
        REQUIRE(it != realm->schema().end());
2!
3100
        REQUIRE(it->table_key == table->get_key());
2!
3101
        REQUIRE(it->persisted_properties.size() == 1);
2!
3102
        REQUIRE(it->persisted_properties[0].name == "value");
2!
3103
        REQUIRE(it->persisted_properties[0].column_key == table->get_column_key("value"));
2!
3104

1✔
3105
        // Test invalid configs
1✔
3106
        realm::Realm::Config config3;
2✔
3107
        config3.realm_data = realm_buffer.get();
2✔
3108
        REQUIRE_EXCEPTION(Realm::get_shared_realm(config3), IllegalCombination,
2✔
3109
                          "In-memory realms initialized from memory buffers can only be opened in read-only mode");
2✔
3110

1✔
3111
        config3.in_memory = true;
2✔
3112
        config3.schema_mode = SchemaMode::Immutable;
2✔
3113
        config3.path = "path";
2✔
3114
        REQUIRE_EXCEPTION(Realm::get_shared_realm(config3), IllegalCombination,
2✔
3115
                          "Specifying both memory buffer and path is invalid");
2✔
3116

1✔
3117
        config3.path = "";
2✔
3118
        config3.encryption_key = std::vector<char>(64, 'a');
2✔
3119
        REQUIRE_EXCEPTION(Realm::get_shared_realm(config3), IllegalCombination,
2✔
3120
                          "Memory buffers do not support encryption");
2✔
3121
    }
2✔
3122
}
2✔
3123

3124
TEST_CASE("ShareRealm: realm closed in did_change callback") {
6✔
3125
    TestFile config;
6✔
3126
    config.schema_version = 1;
6✔
3127
    config.schema = Schema{
6✔
3128
        {"object", {{"value", PropertyType::Int}}},
6✔
3129
    };
6✔
3130
    config.automatic_change_notifications = false;
6✔
3131
    auto r1 = Realm::get_shared_realm(config);
6✔
3132

3✔
3133
    r1->begin_transaction();
6✔
3134
    auto table = r1->read_group().get_table("class_object");
6✔
3135
    table->create_object();
6✔
3136
    r1->commit_transaction();
6✔
3137

3✔
3138
    struct Context : public BindingContext {
6✔
3139
        Context(std::shared_ptr<Realm>& realm)
6✔
3140
            : realm(&realm)
6✔
3141
        {
6✔
3142
        }
6✔
3143
        std::shared_ptr<Realm>* realm;
6✔
3144
        void did_change(std::vector<ObserverState> const&, std::vector<void*> const&, bool) override
6✔
3145
        {
6✔
3146
            auto realm = this->realm; // close() will delete `this`
6✔
3147
            (*realm)->close();
6✔
3148
            realm->reset();
6✔
3149
        }
6✔
3150
    };
6✔
3151

3✔
3152
    SECTION("did_change") {
6✔
3153
        r1->m_binding_context.reset(new Context(r1));
2✔
3154
        r1->invalidate();
2✔
3155

1✔
3156
        auto r2 = Realm::get_shared_realm(config);
2✔
3157
        r2->begin_transaction();
2✔
3158
        r2->read_group().get_table("class_object")->create_object();
2✔
3159
        r2->commit_transaction();
2✔
3160
        r2.reset();
2✔
3161

1✔
3162
        r1->notify();
2✔
3163
    }
2✔
3164

3✔
3165
    SECTION("did_change with async results") {
6✔
3166
        r1->m_binding_context.reset(new Context(r1));
2✔
3167
        Results results(r1, table->where());
2✔
3168
        auto token = results.add_notification_callback([&](CollectionChangeSet) {
1✔
3169
            // Should not be called.
3170
            REQUIRE(false);
×
3171
        });
×
3172

1✔
3173
        auto r2 = Realm::get_shared_realm(config);
2✔
3174
        r2->begin_transaction();
2✔
3175
        r2->read_group().get_table("class_object")->create_object();
2✔
3176
        r2->commit_transaction();
2✔
3177
        r2.reset();
2✔
3178

1✔
3179
        auto coordinator = _impl::RealmCoordinator::get_coordinator(config.path);
2✔
3180
        coordinator->on_change();
2✔
3181

1✔
3182
        r1->notify();
2✔
3183
    }
2✔
3184

3✔
3185
    SECTION("refresh") {
6✔
3186
        r1->m_binding_context.reset(new Context(r1));
2✔
3187

1✔
3188
        auto r2 = Realm::get_shared_realm(config);
2✔
3189
        r2->begin_transaction();
2✔
3190
        r2->read_group().get_table("class_object")->create_object();
2✔
3191
        r2->commit_transaction();
2✔
3192
        r2.reset();
2✔
3193

1✔
3194
        REQUIRE_FALSE(r1->refresh());
2!
3195
    }
2✔
3196
}
6✔
3197

3198
TEST_CASE("RealmCoordinator: schema cache") {
16✔
3199
    TestFile config;
16✔
3200
    auto coordinator = _impl::RealmCoordinator::get_coordinator(config.path);
16✔
3201

8✔
3202
    Schema cache_schema;
16✔
3203
    uint64_t cache_sv = -1, cache_tv = -1;
16✔
3204

8✔
3205
    Schema schema{
16✔
3206
        {"object", {{"value", PropertyType::Int}}},
16✔
3207
    };
16✔
3208
    Schema schema2{
16✔
3209
        {"object",
16✔
3210
         {
16✔
3211
             {"value", PropertyType::Int},
16✔
3212
         }},
16✔
3213
        {"object 2",
16✔
3214
         {
16✔
3215
             {"value", PropertyType::Int},
16✔
3216
         }},
16✔
3217
    };
16✔
3218

8✔
3219
    SECTION("valid initial schema sets cache") {
16✔
3220
        coordinator->cache_schema(schema, 5, 10);
2✔
3221
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3222
        REQUIRE(cache_schema == schema);
2!
3223
        REQUIRE(cache_sv == 5);
2!
3224
        REQUIRE(cache_tv == 10);
2!
3225
    }
2✔
3226

8✔
3227
    SECTION("cache can be updated with newer schema") {
16✔
3228
        coordinator->cache_schema(schema, 5, 10);
2✔
3229
        coordinator->cache_schema(schema2, 6, 11);
2✔
3230
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3231
        REQUIRE(cache_schema == schema2);
2!
3232
        REQUIRE(cache_sv == 6);
2!
3233
        REQUIRE(cache_tv == 11);
2!
3234
    }
2✔
3235

8✔
3236
    SECTION("empty schema is ignored") {
16✔
3237
        coordinator->cache_schema(Schema{}, 5, 10);
2✔
3238
        REQUIRE_FALSE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3239

1✔
3240
        coordinator->cache_schema(schema, 5, 10);
2✔
3241
        coordinator->cache_schema(Schema{}, 5, 10);
2✔
3242
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3243
        REQUIRE(cache_schema == schema);
2!
3244
        REQUIRE(cache_sv == 5);
2!
3245
        REQUIRE(cache_tv == 10);
2!
3246
    }
2✔
3247

8✔
3248
    SECTION("schema for older transaction is ignored") {
16✔
3249
        coordinator->cache_schema(schema, 5, 10);
2✔
3250
        coordinator->cache_schema(schema2, 4, 8);
2✔
3251

1✔
3252
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3253
        REQUIRE(cache_schema == schema);
2!
3254
        REQUIRE(cache_sv == 5);
2!
3255
        REQUIRE(cache_tv == 10);
2!
3256

1✔
3257
        coordinator->advance_schema_cache(10, 20);
2✔
3258
        coordinator->cache_schema(schema, 6, 15);
2✔
3259
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3260
        REQUIRE(cache_tv == 20); // should not have dropped to 15
2!
3261
    }
2✔
3262

8✔
3263
    SECTION("advance_schema() from transaction version bumps transaction version") {
16✔
3264
        coordinator->cache_schema(schema, 5, 10);
2✔
3265
        coordinator->advance_schema_cache(10, 12);
2✔
3266
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3267
        REQUIRE(cache_schema == schema);
2!
3268
        REQUIRE(cache_sv == 5);
2!
3269
        REQUIRE(cache_tv == 12);
2!
3270
    }
2✔
3271

8✔
3272
    SECTION("advance_schema() ending before transaction version does nothing") {
16✔
3273
        coordinator->cache_schema(schema, 5, 10);
2✔
3274
        coordinator->advance_schema_cache(8, 9);
2✔
3275
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3276
        REQUIRE(cache_schema == schema);
2!
3277
        REQUIRE(cache_sv == 5);
2!
3278
        REQUIRE(cache_tv == 10);
2!
3279
    }
2✔
3280

8✔
3281
    SECTION("advance_schema() extending over transaction version bumps version") {
16✔
3282
        coordinator->cache_schema(schema, 5, 10);
2✔
3283
        coordinator->advance_schema_cache(3, 15);
2✔
3284
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3285
        REQUIRE(cache_schema == schema);
2!
3286
        REQUIRE(cache_sv == 5);
2!
3287
        REQUIRE(cache_tv == 15);
2!
3288
    }
2✔
3289

8✔
3290
    SECTION("advance_schema() with no cahced schema does nothing") {
16✔
3291
        coordinator->advance_schema_cache(3, 15);
2✔
3292
        REQUIRE_FALSE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3293
    }
2✔
3294
}
16✔
3295

3296
TEST_CASE("SharedRealm: coordinator schema cache") {
26✔
3297
    TestFile config;
26✔
3298
    auto r = Realm::get_shared_realm(config);
26✔
3299
    auto coordinator = _impl::RealmCoordinator::get_coordinator(config.path);
26✔
3300

13✔
3301
    Schema cache_schema;
26✔
3302
    uint64_t cache_sv = -1, cache_tv = -1;
26✔
3303

13✔
3304
    Schema schema{
26✔
3305
        {"object", {{"value", PropertyType::Int}}},
26✔
3306
    };
26✔
3307
    Schema schema2{
26✔
3308
        {"object",
26✔
3309
         {
26✔
3310
             {"value", PropertyType::Int},
26✔
3311
         }},
26✔
3312
        {"object 2",
26✔
3313
         {
26✔
3314
             {"value", PropertyType::Int},
26✔
3315
         }},
26✔
3316
    };
26✔
3317

13✔
3318
    class ExternalWriter {
26✔
3319
    private:
26✔
3320
        std::shared_ptr<Realm> m_realm;
26✔
3321

13✔
3322
    public:
26✔
3323
        WriteTransaction wt;
26✔
3324
        ExternalWriter(Realm::Config const& config)
26✔
3325
            : m_realm([&] {
22✔
3326
                auto c = config;
18✔
3327
                c.scheduler = util::Scheduler::make_frozen(VersionID());
18✔
3328
                return _impl::RealmCoordinator::get_coordinator(c.path)->get_realm(c, util::none);
18✔
3329
            }())
18✔
3330
            , wt(TestHelper::get_db(m_realm))
26✔
3331
        {
22✔
3332
        }
18✔
3333
    };
26✔
3334

13✔
3335
    auto external_write = [&](Realm::Config const& config, auto&& fn) {
21✔
3336
        ExternalWriter wt(config);
16✔
3337
        fn(wt.wt);
16✔
3338
        wt.wt.commit();
16✔
3339
    };
16✔
3340

13✔
3341
    SECTION("is initially empty for uninitialized file") {
26✔
3342
        REQUIRE_FALSE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3343
    }
2✔
3344
    r->update_schema(schema);
26✔
3345

13✔
3346
    SECTION("is populated after calling update_schema()") {
26✔
3347
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3348
        REQUIRE(cache_sv == 0);
2!
3349
        REQUIRE(cache_schema == schema);
2!
3350
        REQUIRE(cache_schema.begin()->persisted_properties[0].column_key != ColKey{});
2!
3351
    }
2✔
3352

13✔
3353
    coordinator = nullptr;
26✔
3354
    r = nullptr;
26✔
3355
    r = Realm::get_shared_realm(config);
26✔
3356
    coordinator = _impl::RealmCoordinator::get_coordinator(config.path);
26✔
3357
    REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
26!
3358

13✔
3359
    SECTION("is populated after opening an initialized file") {
26✔
3360
        REQUIRE(cache_sv == 0);
2!
3361
        REQUIRE(cache_tv == 2); // with in-realm history the version doesn't reset
2!
3362
        REQUIRE(cache_schema == schema);
2!
3363
        REQUIRE(cache_schema.begin()->persisted_properties[0].column_key != ColKey{});
2!
3364
    }
2✔
3365

13✔
3366
    SECTION("transaction version is bumped after a local write") {
26✔
3367
        auto tv = cache_tv;
2✔
3368
        r->begin_transaction();
2✔
3369
        r->commit_transaction();
2✔
3370
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3371
        REQUIRE(cache_tv == tv + 1);
2!
3372
    }
2✔
3373

13✔
3374
    SECTION("notify() without a read transaction does not bump transaction version") {
26✔
3375
        auto tv = cache_tv;
4✔
3376

2✔
3377
        SECTION("non-schema change") {
4✔
3378
            external_write(config, [](auto& wt) {
2✔
3379
                wt.get_table("class_object")->create_object();
2✔
3380
            });
2✔
3381
        }
2✔
3382
        SECTION("schema change") {
4✔
3383
            external_write(config, [](auto& wt) {
2✔
3384
                wt.add_table("class_object 2");
2✔
3385
            });
2✔
3386
        }
2✔
3387

2✔
3388
        r->notify();
4✔
3389
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
4!
3390
        REQUIRE(cache_tv == tv);
4!
3391
        REQUIRE(cache_schema == schema);
4!
3392
    }
4✔
3393

13✔
3394
    SECTION("notify() with a read transaction bumps transaction version") {
26✔
3395
        r->read_group();
2✔
3396
        external_write(config, [](auto& wt) {
2✔
3397
            wt.get_table("class_object")->create_object();
2✔
3398
        });
2✔
3399

1✔
3400
        r->notify();
2✔
3401
        auto tv = cache_tv;
2✔
3402
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3403
        REQUIRE(cache_tv == tv + 1);
2!
3404
    }
2✔
3405

13✔
3406
    SECTION("notify() with a read transaction updates schema folloing external schema change") {
26✔
3407
        r->read_group();
2✔
3408
        external_write(config, [](auto& wt) {
2✔
3409
            wt.add_table("class_object 2");
2✔
3410
        });
2✔
3411

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

13✔
3420
    SECTION("transaction version is bumped after refresh() following external non-schema write") {
26✔
3421
        external_write(config, [](auto& wt) {
2✔
3422
            wt.get_table("class_object")->create_object();
2✔
3423
        });
2✔
3424

1✔
3425
        r->refresh();
2✔
3426
        auto tv = cache_tv;
2✔
3427
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3428
        REQUIRE(cache_tv == tv + 1);
2!
3429
    }
2✔
3430

13✔
3431
    SECTION("schema is reread following refresh() over external schema change") {
26✔
3432
        external_write(config, [](auto& wt) {
2✔
3433
            wt.add_table("class_object 2");
2✔
3434
        });
2✔
3435

1✔
3436
        r->refresh();
2✔
3437
        auto tv = cache_tv;
2✔
3438
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3439
        REQUIRE(cache_tv == tv + 1);
2!
3440
        REQUIRE(cache_schema.size() == 2);
2!
3441
        REQUIRE(cache_schema.find("object 2") != cache_schema.end());
2!
3442
    }
2✔
3443

13✔
3444
    SECTION("update_schema() to version already on disk updates cache") {
26✔
3445
        r->read_group();
2✔
3446
        external_write(config, [](auto& wt) {
2✔
3447
            auto table = wt.add_table("class_object 2");
2✔
3448
            table->add_column(type_Int, "value");
2✔
3449
        });
2✔
3450

1✔
3451
        auto tv = cache_tv;
2✔
3452
        r->update_schema(schema2);
2✔
3453

1✔
3454
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3455
        REQUIRE(cache_tv == tv + 1); // only +1 because update_schema() did not perform a write
2!
3456
        REQUIRE(cache_schema.size() == 2);
2!
3457
        REQUIRE(cache_schema.find("object 2") != cache_schema.end());
2!
3458
    }
2✔
3459

13✔
3460
    SECTION("update_schema() to version already on disk updates cache") {
26✔
3461
        r->read_group();
2✔
3462
        external_write(config, [](auto& wt) {
2✔
3463
            auto table = wt.add_table("class_object 2");
2✔
3464
            table->add_column(type_Int, "value");
2✔
3465
        });
2✔
3466

1✔
3467
        auto tv = cache_tv;
2✔
3468
        r->update_schema(schema2);
2✔
3469

1✔
3470
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3471
        REQUIRE(cache_tv == tv + 1); // only +1 because update_schema() did not perform a write
2!
3472
        REQUIRE(cache_schema.size() == 2);
2!
3473
        REQUIRE(cache_schema.find("object 2") != cache_schema.end());
2!
3474
    }
2✔
3475

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

1✔
3479
        // We want to commit the write while we're waiting on the write lock on
1✔
3480
        // this thread, which can't really be done in a properly synchronized manner
1✔
3481
        std::chrono::microseconds wait_time{5000};
2✔
3482
#if REALM_ANDROID
3483
        // When running on device or in an emulator we need to wait longer due
3484
        // to them being slow
3485
        wait_time *= 10;
3486
#endif
3487

1✔
3488
        bool did_run = false;
2✔
3489
        JoiningThread thread([&] {
2✔
3490
            ExternalWriter writer(config);
2✔
3491
            if (writer.wt.get_table("class_object 2"))
2✔
3492
                return;
×
3493
            did_run = true;
2✔
3494

1✔
3495
            auto table = writer.wt.add_table("class_object 2");
2✔
3496
            table->add_column(type_Int, "value");
2✔
3497
            std::this_thread::sleep_for(wait_time * 2);
2✔
3498
            writer.wt.commit();
2✔
3499
        });
2✔
3500
        std::this_thread::sleep_for(wait_time);
2✔
3501

1✔
3502
        auto tv = cache_tv;
2✔
3503
        r->update_schema(Schema{
2✔
3504
            {"object", {{"value", PropertyType::Int}}},
2✔
3505
            {"object 2", {{"value", PropertyType::Int}}},
2✔
3506
        });
2✔
3507

1✔
3508
        // just skip the test if the timing was wrong to avoid spurious failures
1✔
3509
        if (!did_run)
2✔
3510
            return;
×
3511

1✔
3512
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3513
        REQUIRE(cache_tv == tv + 1); // only +1 because update_schema()'s write was rolled back
2!
3514
        REQUIRE(cache_schema.size() == 2);
2!
3515
        REQUIRE(cache_schema.find("object 2") != cache_schema.end());
2!
3516
    }
2✔
3517
}
26✔
3518

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

1✔
3522
    // Prepopulate the Realm with the schema.
1✔
3523
    Realm::Config config_with_schema = config;
2✔
3524
    config_with_schema.schema_version = 1;
2✔
3525
    config_with_schema.schema_mode = SchemaMode::Automatic;
2✔
3526
    config_with_schema.schema =
2✔
3527
        Schema{{"object",
2✔
3528
                {
2✔
3529
                    {"value", PropertyType::Int, Property::IsPrimary{true}},
2✔
3530
                    {"value 2", PropertyType::Int, Property::IsPrimary{false}, Property::IsIndexed{true}},
2✔
3531
                }}};
2✔
3532
    auto r1 = Realm::get_shared_realm(config_with_schema);
2✔
3533

1✔
3534
    // Retrieve the object schema in dynamic mode.
1✔
3535
    auto r2 = Realm::get_shared_realm(config);
2✔
3536
    auto* object_schema = &*r2->schema().find("object");
2✔
3537

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

1✔
3542
    // Advance to the latest version, and verify the object schema is at the same location in memory.
1✔
3543
    r2->read_group();
2✔
3544
    REQUIRE(object_schema == &*r2->schema().find("object"));
2!
3545
}
2✔
3546

3547
TEST_CASE("SharedRealm: declaring an object as embedded results in creating an embedded table") {
2✔
3548
    TestFile config;
2✔
3549

1✔
3550
    // Prepopulate the Realm with the schema.
1✔
3551
    config.schema = Schema{{"object1",
2✔
3552
                            ObjectSchema::ObjectType::Embedded,
2✔
3553
                            {
2✔
3554
                                {"value", PropertyType::Int},
2✔
3555
                            }},
2✔
3556
                           {"object2",
2✔
3557
                            {
2✔
3558
                                {"value", PropertyType::Object | PropertyType::Nullable, "object1"},
2✔
3559
                            }}};
2✔
3560
    auto r1 = Realm::get_shared_realm(config);
2✔
3561

1✔
3562
    Group& g = r1->read_group();
2✔
3563
    auto t = g.get_table("class_object1");
2✔
3564
    REQUIRE(t->is_embedded());
2!
3565
}
2✔
3566

3567
TEST_CASE("SharedRealm: SchemaChangedFunction") {
16✔
3568
    struct Context : BindingContext {
16✔
3569
        size_t* change_count;
16✔
3570
        Schema* schema;
16✔
3571
        Context(size_t* count_out, Schema* schema_out)
16✔
3572
            : change_count(count_out)
16✔
3573
            , schema(schema_out)
16✔
3574
        {
22✔
3575
        }
22✔
3576

8✔
3577
        void schema_did_change(Schema const& changed_schema) override
16✔
3578
        {
13✔
3579
            ++*change_count;
10✔
3580
            *schema = changed_schema;
10✔
3581
        }
10✔
3582
    };
16✔
3583

8✔
3584
    size_t schema_changed_called = 0;
16✔
3585
    Schema changed_fixed_schema;
16✔
3586
    TestFile config;
16✔
3587
    RealmConfig dynamic_config = config;
16✔
3588

8✔
3589
    config.schema = Schema{{"object1",
16✔
3590
                            {
16✔
3591
                                {"value", PropertyType::Int},
16✔
3592
                            }},
16✔
3593
                           {"object2",
16✔
3594
                            {
16✔
3595
                                {"value", PropertyType::Int},
16✔
3596
                            }}};
16✔
3597
    config.schema_version = 1;
16✔
3598
    auto r1 = Realm::get_shared_realm(config);
16✔
3599
    r1->read_group();
16✔
3600
    r1->m_binding_context.reset(new Context(&schema_changed_called, &changed_fixed_schema));
16✔
3601

8✔
3602
    SECTION("Fixed schema") {
16✔
3603
        SECTION("update_schema") {
10✔
3604
            auto new_schema = Schema{{"object3",
2✔
3605
                                      {
2✔
3606
                                          {"value", PropertyType::Int},
2✔
3607
                                      }}};
2✔
3608
            r1->update_schema(new_schema, 2);
2✔
3609
            REQUIRE(schema_changed_called == 1);
2!
3610
            REQUIRE(changed_fixed_schema.find("object3")->property_for_name("value")->column_key != ColKey{});
2!
3611
        }
2✔
3612

5✔
3613
        SECTION("Open a new Realm instance with same config won't trigger") {
10✔
3614
            auto r2 = Realm::get_shared_realm(config);
2✔
3615
            REQUIRE(schema_changed_called == 0);
2!
3616
        }
2✔
3617

5✔
3618
        SECTION("Non schema related transaction doesn't trigger") {
10✔
3619
            auto r2 = Realm::get_shared_realm(config);
2✔
3620
            r2->begin_transaction();
2✔
3621
            r2->commit_transaction();
2✔
3622
            r1->refresh();
2✔
3623
            REQUIRE(schema_changed_called == 0);
2!
3624
        }
2✔
3625

5✔
3626
        SECTION("Schema is changed by another Realm") {
10✔
3627
            auto r2 = Realm::get_shared_realm(config);
2✔
3628
            r2->begin_transaction();
2✔
3629
            r2->read_group().get_table("class_object1")->add_column(type_String, "new col");
2✔
3630
            r2->commit_transaction();
2✔
3631
            r1->refresh();
2✔
3632
            REQUIRE(schema_changed_called == 1);
2!
3633
            REQUIRE(changed_fixed_schema.find("object1")->property_for_name("value")->column_key != ColKey{});
2!
3634
        }
2✔
3635

5✔
3636
        // This is not a valid use case. m_schema won't be refreshed.
5✔
3637
        SECTION("Schema is changed by this Realm won't trigger") {
10✔
3638
            r1->begin_transaction();
2✔
3639
            r1->read_group().get_table("class_object1")->add_column(type_String, "new col");
2✔
3640
            r1->commit_transaction();
2✔
3641
            REQUIRE(schema_changed_called == 0);
2!
3642
        }
2✔
3643
    }
10✔
3644

8✔
3645
    SECTION("Dynamic schema") {
16✔
3646
        size_t dynamic_schema_changed_called = 0;
6✔
3647
        Schema changed_dynamic_schema;
6✔
3648
        auto r2 = Realm::get_shared_realm(dynamic_config);
6✔
3649
        r2->m_binding_context.reset(new Context(&dynamic_schema_changed_called, &changed_dynamic_schema));
6✔
3650

3✔
3651
        SECTION("set_schema_subset") {
6✔
3652
            auto new_schema = Schema{{"object1",
2✔
3653
                                      {
2✔
3654
                                          {"value", PropertyType::Int},
2✔
3655
                                      }}};
2✔
3656
            r2->set_schema_subset(new_schema);
2✔
3657
            REQUIRE(schema_changed_called == 0);
2!
3658
            REQUIRE(dynamic_schema_changed_called == 1);
2!
3659
            REQUIRE(changed_dynamic_schema.find("object1")->property_for_name("value")->column_key != ColKey{});
2!
3660
        }
2✔
3661

3✔
3662
        SECTION("Non schema related transaction will always trigger in dynamic mode") {
6✔
3663
            auto r1 = Realm::get_shared_realm(config);
2✔
3664
            // An empty transaction will trigger the schema changes always in dynamic mode.
1✔
3665
            r1->begin_transaction();
2✔
3666
            r1->commit_transaction();
2✔
3667
            r2->refresh();
2✔
3668
            REQUIRE(dynamic_schema_changed_called == 1);
2!
3669
            REQUIRE(changed_dynamic_schema.find("object1")->property_for_name("value")->column_key != ColKey{});
2!
3670
        }
2✔
3671

3✔
3672
        SECTION("Schema is changed by another Realm") {
6✔
3673
            r1->begin_transaction();
2✔
3674
            r1->read_group().get_table("class_object1")->add_column(type_String, "new col");
2✔
3675
            r1->commit_transaction();
2✔
3676
            r2->refresh();
2✔
3677
            REQUIRE(dynamic_schema_changed_called == 1);
2!
3678
            REQUIRE(changed_dynamic_schema.find("object1")->property_for_name("value")->column_key != ColKey{});
2!
3679
        }
2✔
3680
    }
6✔
3681
}
16✔
3682

3683
TEST_CASE("SharedRealm: compact on launch") {
4✔
3684
    // Make compactable Realm
2✔
3685
    TestFile config;
4✔
3686
    config.automatic_change_notifications = false;
4✔
3687
    int num_opens = 0;
4✔
3688
    config.should_compact_on_launch_function = [&](uint64_t total_bytes, uint64_t used_bytes) {
12✔
3689
        REQUIRE(total_bytes > used_bytes);
12!
3690
        num_opens++;
12✔
3691
        return num_opens != 2;
12✔
3692
    };
12✔
3693
    config.schema = Schema{
4✔
3694
        {"object", {{"value", PropertyType::String}}},
4✔
3695
    };
4✔
3696
    REQUIRE(num_opens == 0);
4!
3697
    auto r = Realm::get_shared_realm(config);
4✔
3698
    REQUIRE(num_opens == 1);
4!
3699
    r->begin_transaction();
4✔
3700
    auto table = r->read_group().get_table("class_object");
4✔
3701
    size_t count = 1000;
4✔
3702
    for (size_t i = 0; i < count; ++i)
4,004✔
3703
        table->create_object().set_all(util::format("Foo_%1", i % 10).c_str());
4,000✔
3704
    r->commit_transaction();
4✔
3705
    REQUIRE(table->size() == count);
4!
3706
    r->close();
4✔
3707

2✔
3708
    SECTION("compact reduces the file size") {
4✔
3709
#ifndef _WIN32
2✔
3710
        // Confirm expected sizes before and after opening the Realm
1✔
3711
        size_t size_before = size_t(util::File(config.path).get_size());
2✔
3712
        r = Realm::get_shared_realm(config);
2✔
3713
        REQUIRE(num_opens == 2);
2!
3714
        r->close();
2✔
3715
        REQUIRE(size_t(util::File(config.path).get_size()) == size_before); // File size after returning false
2!
3716
        r = Realm::get_shared_realm(config);
2✔
3717
        REQUIRE(num_opens == 3);
2!
3718
        REQUIRE(size_t(util::File(config.path).get_size()) < size_before); // File size after returning true
2!
3719

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

1✔
3723
        // Registering for a collection notification shouldn't crash when compact on launch is used.
1✔
3724
        Results results(r, r->read_group().get_table("class_object"));
2✔
3725
        results.add_notification_callback([](CollectionChangeSet const&) {});
1✔
3726
        r->close();
2✔
3727
#endif
2✔
3728
    }
2✔
3729

2✔
3730
    SECTION("compact function does not get invoked if realm is open on another thread") {
4✔
3731
        config.scheduler = util::Scheduler::make_frozen(VersionID());
2✔
3732
        r = Realm::get_shared_realm(config);
2✔
3733
        REQUIRE(num_opens == 2);
2!
3734
        std::thread([&] {
2✔
3735
            auto r2 = Realm::get_shared_realm(config);
2✔
3736
            REQUIRE(num_opens == 2);
2!
3737
        }).join();
2✔
3738
        r->close();
2✔
3739
        std::thread([&] {
2✔
3740
            auto r3 = Realm::get_shared_realm(config);
2✔
3741
            REQUIRE(num_opens == 3);
2!
3742
        }).join();
2✔
3743
    }
2✔
3744
}
4✔
3745

3746
struct ModeAutomatic {
3747
    static constexpr SchemaMode mode = SchemaMode::Automatic;
3748
    static constexpr bool should_call_init_on_version_bump = false;
3749
};
3750
struct ModeAdditive {
3751
    static constexpr SchemaMode mode = SchemaMode::AdditiveExplicit;
3752
    static constexpr bool should_call_init_on_version_bump = false;
3753
};
3754
struct ModeManual {
3755
    static constexpr SchemaMode mode = SchemaMode::Manual;
3756
    static constexpr bool should_call_init_on_version_bump = false;
3757
};
3758
struct ModeSoftResetFile {
3759
    static constexpr SchemaMode mode = SchemaMode::SoftResetFile;
3760
    static constexpr bool should_call_init_on_version_bump = true;
3761
};
3762
struct ModeHardResetFile {
3763
    static constexpr SchemaMode mode = SchemaMode::HardResetFile;
3764
    static constexpr bool should_call_init_on_version_bump = true;
3765
};
3766

3767
TEMPLATE_TEST_CASE("SharedRealm: update_schema with initialization_function", "[init][update schema]", ModeAutomatic,
3768
                   ModeAdditive, ModeManual, ModeSoftResetFile, ModeHardResetFile)
3769
{
30✔
3770
    TestFile config;
30✔
3771
    config.schema_mode = TestType::mode;
30✔
3772
    bool initialization_function_called = false;
30✔
3773
    uint64_t schema_version_in_callback = -1;
30✔
3774
    Schema schema_in_callback;
30✔
3775
    auto initialization_function = [&initialization_function_called, &schema_version_in_callback,
30✔
3776
                                    &schema_in_callback](auto shared_realm) {
27✔
3777
        REQUIRE(shared_realm->is_in_transaction());
24!
3778
        initialization_function_called = true;
24✔
3779
        schema_version_in_callback = shared_realm->schema_version();
24✔
3780
        schema_in_callback = shared_realm->schema();
24✔
3781
    };
24✔
3782

15✔
3783
    Schema schema{
30✔
3784
        {"object", {{"value", PropertyType::String}}},
30✔
3785
    };
30✔
3786

15✔
3787
    SECTION("call initialization function directly by update_schema") {
30✔
3788
        // Open in dynamic mode with no schema specified
5✔
3789
        auto realm = Realm::get_shared_realm(config);
10✔
3790
        REQUIRE_FALSE(initialization_function_called);
10!
3791

5✔
3792
        realm->update_schema(schema, 0, nullptr, initialization_function);
10✔
3793
        REQUIRE(initialization_function_called);
10!
3794
        REQUIRE(schema_version_in_callback == 0);
10!
3795
        REQUIRE(schema_in_callback.compare(schema).size() == 0);
10!
3796
    }
10✔
3797

15✔
3798
    config.schema_version = 0;
30✔
3799
    config.schema = schema;
30✔
3800

15✔
3801
    SECTION("initialization function should be called for unversioned realm") {
30✔
3802
        config.initialization_function = initialization_function;
10✔
3803
        Realm::get_shared_realm(config);
10✔
3804
        REQUIRE(initialization_function_called);
10!
3805
        REQUIRE(schema_version_in_callback == 0);
10!
3806
        REQUIRE(schema_in_callback.compare(schema).size() == 0);
10!
3807
    }
10✔
3808

15✔
3809
    SECTION("initialization function for versioned realm") {
30✔
3810
        // Initialize v0
5✔
3811
        Realm::get_shared_realm(config);
10✔
3812

5✔
3813
        config.schema_version = 1;
10✔
3814
        config.initialization_function = initialization_function;
10✔
3815
        Realm::get_shared_realm(config);
10✔
3816
        REQUIRE(initialization_function_called == TestType::should_call_init_on_version_bump);
10!
3817
        if (TestType::should_call_init_on_version_bump) {
10✔
3818
            REQUIRE(schema_version_in_callback == 1);
4!
3819
            REQUIRE(schema_in_callback.compare(schema).size() == 0);
4!
3820
        }
4✔
3821
    }
10✔
3822
}
30✔
3823

3824
TEST_CASE("BindingContext is notified about delivery of change notifications") {
16✔
3825
    _impl::RealmCoordinator::assert_no_open_realms();
16✔
3826
    InMemoryTestFile config;
16✔
3827
    config.automatic_change_notifications = false;
16✔
3828

8✔
3829
    auto r = Realm::get_shared_realm(config);
16✔
3830
    r->update_schema({
16✔
3831
        {"object", {{"value", PropertyType::Int}}},
16✔
3832
    });
16✔
3833

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

8✔
3837
    SECTION("BindingContext notified even if no callbacks are registered") {
16✔
3838
        static int binding_context_start_notify_calls = 0;
4✔
3839
        static int binding_context_end_notify_calls = 0;
4✔
3840
        struct Context : BindingContext {
4✔
3841
            void will_send_notifications() override
4✔
3842
            {
4✔
3843
                ++binding_context_start_notify_calls;
4✔
3844
            }
4✔
3845

2✔
3846
            void did_send_notifications() override
4✔
3847
            {
4✔
3848
                ++binding_context_end_notify_calls;
4✔
3849
            }
4✔
3850
        };
4✔
3851
        r->m_binding_context.reset(new Context());
4✔
3852

2✔
3853
        SECTION("local commit") {
4✔
3854
            binding_context_start_notify_calls = 0;
2✔
3855
            binding_context_end_notify_calls = 0;
2✔
3856
            coordinator->on_change();
2✔
3857
            r->begin_transaction();
2✔
3858
            REQUIRE(binding_context_start_notify_calls == 1);
2!
3859
            REQUIRE(binding_context_end_notify_calls == 1);
2!
3860
            r->cancel_transaction();
2✔
3861
        }
2✔
3862

2✔
3863
        SECTION("remote commit") {
4✔
3864
            binding_context_start_notify_calls = 0;
2✔
3865
            binding_context_end_notify_calls = 0;
2✔
3866
            JoiningThread([&] {
2✔
3867
                auto r2 = coordinator->get_realm(util::Scheduler::make_frozen(VersionID()));
2✔
3868
                r2->begin_transaction();
2✔
3869
                auto table2 = r2->read_group().get_table("class_object");
2✔
3870
                table2->create_object();
2✔
3871
                r2->commit_transaction();
2✔
3872
            });
2✔
3873
            advance_and_notify(*r);
2✔
3874
            REQUIRE(binding_context_start_notify_calls == 1);
2!
3875
            REQUIRE(binding_context_end_notify_calls == 1);
2!
3876
        }
2✔
3877
    }
4✔
3878

8✔
3879
    SECTION("notify BindingContext before and after sending notifications") {
16✔
3880
        static int binding_context_start_notify_calls = 0;
4✔
3881
        static int binding_context_end_notify_calls = 0;
4✔
3882
        static int notification_calls = 0;
4✔
3883

2✔
3884
        auto col = table->get_column_key("value");
4✔
3885
        Results results1(r, table->where().greater_equal(col, 0));
4✔
3886
        Results results2(r, table->where().less(col, 10));
4✔
3887

2✔
3888
        auto token1 = results1.add_notification_callback([&](CollectionChangeSet) {
4✔
3889
            ++notification_calls;
4✔
3890
        });
4✔
3891

2✔
3892
        auto token2 = results2.add_notification_callback([&](CollectionChangeSet) {
4✔
3893
            ++notification_calls;
4✔
3894
        });
4✔
3895

2✔
3896
        struct Context : BindingContext {
4✔
3897
            void will_send_notifications() override
4✔
3898
            {
4✔
3899
                REQUIRE(notification_calls == 0);
4!
3900
                REQUIRE(binding_context_end_notify_calls == 0);
4!
3901
                ++binding_context_start_notify_calls;
4✔
3902
            }
4✔
3903

2✔
3904
            void did_send_notifications() override
4✔
3905
            {
4✔
3906
                REQUIRE(notification_calls == 2);
4!
3907
                REQUIRE(binding_context_start_notify_calls == 1);
4!
3908
                ++binding_context_end_notify_calls;
4✔
3909
            }
4✔
3910
        };
4✔
3911
        r->m_binding_context.reset(new Context());
4✔
3912

2✔
3913
        SECTION("local commit") {
4✔
3914
            binding_context_start_notify_calls = 0;
2✔
3915
            binding_context_end_notify_calls = 0;
2✔
3916
            notification_calls = 0;
2✔
3917
            coordinator->on_change();
2✔
3918
            r->begin_transaction();
2✔
3919
            table->create_object();
2✔
3920
            r->commit_transaction();
2✔
3921
            REQUIRE(binding_context_start_notify_calls == 1);
2!
3922
            REQUIRE(binding_context_end_notify_calls == 1);
2!
3923
        }
2✔
3924

2✔
3925
        SECTION("remote commit") {
4✔
3926
            binding_context_start_notify_calls = 0;
2✔
3927
            binding_context_end_notify_calls = 0;
2✔
3928
            notification_calls = 0;
2✔
3929
            JoiningThread([&] {
2✔
3930
                auto r2 = coordinator->get_realm(util::Scheduler::make_frozen(VersionID()));
2✔
3931
                r2->begin_transaction();
2✔
3932
                auto table2 = r2->read_group().get_table("class_object");
2✔
3933
                table2->create_object();
2✔
3934
                r2->commit_transaction();
2✔
3935
            });
2✔
3936
            advance_and_notify(*r);
2✔
3937
            REQUIRE(binding_context_start_notify_calls == 1);
2!
3938
            REQUIRE(binding_context_end_notify_calls == 1);
2!
3939
        }
2✔
3940
    }
4✔
3941

8✔
3942
    SECTION("did_send() is skipped if the Realm is closed first") {
16✔
3943
        Results results(r, table->where());
8✔
3944
        bool do_close = true;
8✔
3945
        auto token = results.add_notification_callback([&](CollectionChangeSet) {
8✔
3946
            if (do_close)
8✔
3947
                r->close();
4✔
3948
        });
8✔
3949

4✔
3950
        struct FailOnDidSend : BindingContext {
8✔
3951
            void did_send_notifications() override
8✔
3952
            {
4✔
3953
                FAIL("did_send_notifications() should not have been called");
×
3954
            }
×
3955
        };
8✔
3956
        struct CloseOnWillChange : FailOnDidSend {
8✔
3957
            Realm& realm;
8✔
3958
            CloseOnWillChange(Realm& realm)
8✔
3959
                : realm(realm)
8✔
3960
            {
6✔
3961
            }
4✔
3962

4✔
3963
            void will_send_notifications() override
8✔
3964
            {
6✔
3965
                realm.close();
4✔
3966
            }
4✔
3967
        };
8✔
3968

4✔
3969
        SECTION("closed in notification callback for notify()") {
8✔
3970
            r->m_binding_context.reset(new FailOnDidSend);
2✔
3971
            coordinator->on_change();
2✔
3972
            r->notify();
2✔
3973
        }
2✔
3974

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

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

1✔
3988
            r->m_binding_context.reset(new FailOnDidSend);
2✔
3989
            coordinator->on_change();
2✔
3990
            r->refresh();
2✔
3991
        }
2✔
3992

4✔
3993
        SECTION("closed in will_send() for notify()") {
8✔
3994
            r->m_binding_context.reset(new CloseOnWillChange(*r));
2✔
3995
            coordinator->on_change();
2✔
3996
            r->notify();
2✔
3997
        }
2✔
3998

4✔
3999
        SECTION("closed in will_send() for refresh()") {
8✔
4000
            do_close = false;
2✔
4001
            coordinator->on_change();
2✔
4002
            r->notify();
2✔
4003
            do_close = true;
2✔
4004

1✔
4005
            JoiningThread([&] {
2✔
4006
                auto r = coordinator->get_realm(util::Scheduler::make_frozen(VersionID()));
2✔
4007
                r->begin_transaction();
2✔
4008
                r->read_group().get_table("class_object")->create_object();
2✔
4009
                r->commit_transaction();
2✔
4010
            });
2✔
4011

1✔
4012
            r->m_binding_context.reset(new CloseOnWillChange(*r));
2✔
4013
            coordinator->on_change();
2✔
4014
            r->refresh();
2✔
4015
        }
2✔
4016
    }
8✔
4017
#ifdef _WIN32
4018
    _impl::RealmCoordinator::clear_all_caches();
4019
#endif
4020
}
16✔
4021

4022
TEST_CASE("RealmCoordinator: get_unbound_realm()") {
8✔
4023
    TestFile config;
8✔
4024
    config.cache = true;
8✔
4025
    config.schema = Schema{
8✔
4026
        {"object", {{"value", PropertyType::Int}}},
8✔
4027
    };
8✔
4028

4✔
4029
    ThreadSafeReference ref;
8✔
4030
    std::thread([&] {
8✔
4031
        ref = _impl::RealmCoordinator::get_coordinator(config)->get_unbound_realm();
8✔
4032
    }).join();
8✔
4033

4✔
4034
    SECTION("checks thread after being resolved") {
8✔
4035
        auto realm = Realm::get_shared_realm(std::move(ref));
2✔
4036
        REQUIRE_NOTHROW(realm->verify_thread());
2✔
4037
        std::thread([&] {
2✔
4038
            REQUIRE_EXCEPTION(realm->verify_thread(), WrongThread, "Realm accessed from incorrect thread.");
2✔
4039
        }).join();
2✔
4040
    }
2✔
4041

4✔
4042
    SECTION("delivers notifications to the thread it is resolved on") {
8✔
4043
#ifndef _WIN32
2✔
4044
        if (!util::EventLoop::has_implementation())
2✔
4045
            return;
×
4046
        auto realm = Realm::get_shared_realm(std::move(ref));
2✔
4047
        Results results(realm, ObjectStore::table_for_object_type(realm->read_group(), "object")->where());
2✔
4048
        bool called = false;
2✔
4049
        auto token = results.add_notification_callback([&](CollectionChangeSet) {
2✔
4050
            called = true;
2✔
4051
        });
2✔
4052
        util::EventLoop::main().run_until([&] {
111✔
4053
            return called;
111✔
4054
        });
111✔
4055
#endif
2✔
4056
    }
2✔
4057

4✔
4058
    SECTION("resolves to existing cached Realm for the thread if caching is enabled") {
8✔
4059
        auto r1 = Realm::get_shared_realm(config);
2✔
4060
        auto r2 = Realm::get_shared_realm(std::move(ref));
2✔
4061
        REQUIRE(r1 == r2);
2!
4062
    }
2✔
4063

4✔
4064
    SECTION("resolves to a new Realm if caching is disabled") {
8✔
4065
        config.cache = false;
2✔
4066
        auto r1 = Realm::get_shared_realm(config);
2✔
4067
        auto r2 = Realm::get_shared_realm(std::move(ref));
2✔
4068
        REQUIRE(r1 != r2);
2!
4069

1✔
4070
        // New unbound with cache disabled
1✔
4071
        std::thread([&] {
2✔
4072
            ref = _impl::RealmCoordinator::get_coordinator(config)->get_unbound_realm();
2✔
4073
        }).join();
2✔
4074
        auto r3 = Realm::get_shared_realm(std::move(ref));
2✔
4075
        REQUIRE(r1 != r3);
2!
4076
        REQUIRE(r2 != r3);
2!
4077

1✔
4078
        // New local with cache enabled should grab the resolved unbound
1✔
4079
        config.cache = true;
2✔
4080
        auto r4 = Realm::get_shared_realm(config);
2✔
4081
        REQUIRE(r4 == r2);
2!
4082
    }
2✔
4083
}
8✔
4084

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

20✔
4090
    {
40✔
4091
        auto realm = Realm::get_shared_realm(config);
40✔
4092
        realm->begin_transaction();
40✔
4093
        realm->read_group().get_table("class_object")->create_object();
40✔
4094
        realm->commit_transaction();
40✔
4095
    }
40✔
4096

20✔
4097
    config.schema_mode = SchemaMode::Immutable;
40✔
4098
    auto realm = Realm::get_shared_realm(config);
40✔
4099
    realm->read_group();
40✔
4100

20✔
4101
    SECTION("unsupported functions") {
40✔
4102
        SECTION("update_schema()") {
10✔
4103
            REQUIRE_THROWS_AS(realm->compact(), WrongTransactionState);
2✔
4104
        }
2✔
4105
        SECTION("begin_transaction()") {
10✔
4106
            REQUIRE_THROWS_AS(realm->begin_transaction(), WrongTransactionState);
2✔
4107
        }
2✔
4108
        SECTION("async_begin_transaction()") {
10✔
4109
            REQUIRE_THROWS_AS(realm->async_begin_transaction(nullptr), WrongTransactionState);
2✔
4110
        }
2✔
4111
        SECTION("refresh()") {
10✔
4112
            REQUIRE_THROWS_AS(realm->refresh(), WrongTransactionState);
2✔
4113
        }
2✔
4114
        SECTION("compact()") {
10✔
4115
            REQUIRE_THROWS_AS(realm->compact(), WrongTransactionState);
2✔
4116
        }
2✔
4117
    }
10✔
4118

20✔
4119
    SECTION("supported functions") {
40✔
4120
        SECTION("is_in_transaction()") {
30✔
4121
            REQUIRE_FALSE(realm->is_in_transaction());
2!
4122
        }
2✔
4123
        SECTION("is_in_async_transaction()") {
30✔
4124
            REQUIRE_FALSE(realm->is_in_transaction());
2!
4125
        }
2✔
4126
        SECTION("freeze()") {
30✔
4127
            std::shared_ptr<Realm> frozen;
2✔
4128
            REQUIRE_NOTHROW(frozen = realm->freeze());
2✔
4129
            REQUIRE(frozen->read_group().get_table("class_object")->size() == 1);
2!
4130
            REQUIRE_NOTHROW(frozen = Realm::get_frozen_realm(config, realm->read_transaction_version()));
2✔
4131
            REQUIRE(frozen->read_group().get_table("class_object")->size() == 1);
2!
4132
        }
2✔
4133
        SECTION("notify()") {
30✔
4134
            REQUIRE_NOTHROW(realm->notify());
2✔
4135
        }
2✔
4136
        SECTION("is_in_read_transaction()") {
30✔
4137
            REQUIRE(realm->is_in_read_transaction());
2!
4138
        }
2✔
4139
        SECTION("last_seen_transaction_version()") {
30✔
4140
            REQUIRE(realm->last_seen_transaction_version() == 1);
2!
4141
        }
2✔
4142
        SECTION("get_number_of_versions()") {
30✔
4143
            REQUIRE(realm->get_number_of_versions() == 1);
2!
4144
        }
2✔
4145
        SECTION("read_transaction_version()") {
30✔
4146
            REQUIRE(realm->read_transaction_version() == VersionID{1, 0});
2!
4147
        }
2✔
4148
        SECTION("current_transaction_version()") {
30✔
4149
            REQUIRE(realm->current_transaction_version() == VersionID{1, 0});
2!
4150
        }
2✔
4151
        SECTION("latest_snapshot_version()") {
30✔
4152
            REQUIRE(realm->latest_snapshot_version() == 1);
2!
4153
        }
2✔
4154
        SECTION("duplicate()") {
30✔
4155
            auto duplicate = realm->duplicate();
2✔
4156
            REQUIRE(duplicate->get_table("class_object")->size() == 1);
2!
4157
        }
2✔
4158
        SECTION("invalidate()") {
30✔
4159
            REQUIRE_NOTHROW(realm->invalidate());
2✔
4160
            REQUIRE_FALSE(realm->is_in_read_transaction());
2!
4161
            REQUIRE(realm->read_group().get_table("class_object")->size() == 1);
2!
4162
        }
2✔
4163
        SECTION("close()") {
30✔
4164
            REQUIRE_NOTHROW(realm->close());
2✔
4165
            REQUIRE(realm->is_closed());
2!
4166
        }
2✔
4167
        SECTION("has_pending_async_work()") {
30✔
4168
            REQUIRE_FALSE(realm->has_pending_async_work());
2!
4169
        }
2✔
4170
        SECTION("wait_for_change()") {
30✔
4171
            REQUIRE_FALSE(realm->wait_for_change());
2!
4172
        }
2✔
4173
    }
30✔
4174
}
40✔
4175

4176
TEST_CASE("KeyPathMapping generation") {
2✔
4177
    TestFile config;
2✔
4178
    realm::query_parser::KeyPathMapping mapping;
2✔
4179

1✔
4180
    SECTION("class aliasing") {
2✔
4181
        Schema schema = {
2✔
4182
            {"PersistedName", {{"age", PropertyType::Int}}, {}, "AlternativeName"},
2✔
4183
            {"class_with_policy",
2✔
4184
             {{"value", PropertyType::Int},
2✔
4185
              {"child", PropertyType::Object | PropertyType::Nullable, "class_with_policy"}},
2✔
4186
             {{"parents", PropertyType::LinkingObjects | PropertyType::Array, "class_with_policy", "child"}},
2✔
4187
             "ClassWithPolicy"},
2✔
4188
        };
2✔
4189
        schema.validate();
2✔
4190
        config.schema = schema;
2✔
4191
        auto realm = Realm::get_shared_realm(config);
2✔
4192
        realm::populate_keypath_mapping(mapping, *realm);
2✔
4193
        REQUIRE(mapping.has_table_mapping("AlternativeName"));
2!
4194
        REQUIRE("class_PersistedName" == mapping.get_table_mapping("AlternativeName"));
2!
4195

1✔
4196
        auto table = realm->read_group().get_table("class_class_with_policy");
2✔
4197
        std::vector<Mixed> args{0};
2✔
4198
        auto q = table->query("parents.value = $0", args, mapping);
2✔
4199
        REQUIRE(q.count() == 0);
2!
4200
    }
2✔
4201
}
2✔
4202

4203
TEST_CASE("Concurrent operations") {
4✔
4204
    SECTION("Async commits together with online compaction") {
4✔
4205
        // This is a reproduction test for issue https://github.com/realm/realm-dart/issues/1396
1✔
4206
        // First create a relatively large realm, then delete the content and do some more
1✔
4207
        // commits using async commits. If a compaction is started when doing an async commit
1✔
4208
        // then the subsequent committing done in the helper thread will illegally COW the
1✔
4209
        // top array. When the next mutation is done, the top array will be reported as being
1✔
4210
        // already freed.
1✔
4211
        TestFile config;
2✔
4212
        config.schema_version = 1;
2✔
4213
        config.schema = Schema{{"object", {{"value", PropertyType::Int}}}};
2✔
4214

1✔
4215
        auto realm_1 = Realm::get_shared_realm(config);
2✔
4216
        Results res(realm_1, realm_1->read_group().get_table("class_object")->where());
2✔
4217
        auto realm_2 = Realm::get_shared_realm(config);
2✔
4218

1✔
4219
        {
2✔
4220
            // Create a lot of objects
1✔
4221
            realm_2->begin_transaction();
2✔
4222
            auto table = realm_2->read_group().get_table("class_object");
2✔
4223
            for (int i = 0; i < 400000; i++) {
800,002✔
4224
                table->create_object().set("value", i);
800,000✔
4225
            }
800,000✔
4226
            realm_2->commit_transaction();
2✔
4227
        }
2✔
4228

1✔
4229
        int commit_1 = 0;
2✔
4230
        int commit_2 = 0;
2✔
4231

1✔
4232
        for (int i = 0; i < 4; i++) {
10✔
4233
            realm_1->async_begin_transaction([&]() {
8✔
4234
                // Clearing the DB will reduce the need for space
4✔
4235
                // This will trigger an online compaction
4✔
4236
                // Before the fix, the probram would crash here next time around.
4✔
4237
                res.clear();
8✔
4238
                realm_1->async_commit_transaction([&](std::exception_ptr) {
8✔
4239
                    commit_1++;
8✔
4240
                });
8✔
4241
            });
8✔
4242
            realm_2->async_begin_transaction([&]() {
8✔
4243
                // Make sure we will continue to have something to delete
4✔
4244
                auto table = realm_2->read_group().get_table("class_object");
8✔
4245
                for (int i = 0; i < 100; i++) {
808✔
4246
                    table->create_object().set("value", i);
800✔
4247
                }
800✔
4248
                realm_2->async_commit_transaction([&](std::exception_ptr) {
8✔
4249
                    commit_2++;
8✔
4250
                });
8✔
4251
            });
8✔
4252
        }
8✔
4253

1✔
4254
        util::EventLoop::main().run_until([&] {
8,642✔
4255
            return commit_1 == 4 && commit_2 == 4;
8,642✔
4256
        });
8,642✔
4257
    }
2✔
4258

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