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

realm / realm-core / 2213

10 Apr 2024 11:21PM UTC coverage: 91.792% (-0.8%) from 92.623%
2213

push

Evergreen

web-flow
Add missing availability checks for SecCopyErrorMessageString (#7577)

This requires iOS 11.3 and we currently target iOS 11.

94842 of 175770 branches covered (53.96%)

7 of 22 new or added lines in 2 files covered. (31.82%)

1861 existing lines in 82 files now uncovered.

242866 of 264583 relevant lines covered (91.79%)

5593111.45 hits per line

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

98.97
/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/app_metadata.hpp>
51
#include <realm/object-store/sync/sync_session.hpp>
52

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

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

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

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

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

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

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

87
using namespace realm;
88

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

38✔
246

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

38✔
390
    SECTION("should sensibly handle opening an uninitialized file without a schema specified") {
76✔
391
        config.cache = GENERATE(false, true);
4✔
392

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

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

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

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

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

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

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

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

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

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

1✔
446
        realm1->refresh();
2✔
447
        realm2->refresh();
2✔
448

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

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

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

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

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

38✔
519
    protected:
76✔
520
        size_t m_id;
76✔
521
    };
76✔
522

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

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

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

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

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

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

1✔
571
        REQUIRE(frozen != realm);
2!
572
        REQUIRE(realm->read_transaction_version() == frozen->read_transaction_version());
2!
573

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

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

1✔
585
        REQUIRE(frozen1 != realm);
2!
586
        REQUIRE(realm->read_transaction_version() == frozen1->read_transaction_version());
2!
587

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

1✔
593
        REQUIRE(realm->read_transaction_version() > frozen1->read_transaction_version());
2!
594

1✔
595
        auto frozen2 = realm->freeze();
2✔
596
        frozen2->read_group();
2✔
597

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

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

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

1✔
614
        config.schema = full_schema;
2✔
615

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

1✔
619
        config.schema = subset_schema;
2✔
620

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

15✔
926
    TestSyncManager tsm;
30✔
927
    SyncTestFile config(tsm, "default");
30✔
928
    ObjectSchema object_schema = {"object",
30✔
929
                                  {
30✔
930
                                      {"_id", PropertyType::Int, Property::IsPrimary{true}},
30✔
931
                                      {"value", PropertyType::Int},
30✔
932
                                  }};
30✔
933
    config.schema = Schema{object_schema};
30✔
934
    SyncTestFile config2(tsm, "default");
30✔
935
    config2.schema = config.schema;
30✔
936

15✔
937
    std::mutex mutex;
30✔
938

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

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

15✔
962
    SECTION("can write a realm file without client file id") {
30✔
963
        ThreadSafeReference realm_ref;
2✔
964
        SyncTestFile config3(tsm, "default");
2✔
965
        config3.schema = config.schema;
2✔
966
        uint64_t client_file_id;
2✔
967

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

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

1✔
990
            realm->convert(config3);
2✔
991
        }
2✔
992

1✔
993
        // Create some more content on the server
1✔
994
        origin->begin_transaction();
2✔
995
        cls.create_object(7);
2✔
996
        origin->commit_transaction();
2✔
997
        wait_for_upload(*origin);
2✔
998

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

1✔
1005
        // Make sure we have got a new client file id
1✔
1006
        REQUIRE(realm->read_group().get_sync_file_id() != client_file_id);
2!
1007
        REQUIRE(cls.num_objects() == 3);
2!
1008

1✔
1009
        // Check that we can continue committing to this realm
1✔
1010
        realm->begin_transaction();
2✔
1011
        cls2.create_object(5);
2✔
1012
        realm->commit_transaction();
2✔
1013
        wait_for_upload(*realm);
2✔
1014

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

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

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

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

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

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

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

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

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

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

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

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

15✔
1138
    auto expired_token = encode_fake_jwt("", 123, 456);
30✔
1139

15✔
1140
    SECTION("can async open while waiting for a token refresh") {
30✔
1141
        struct User : TestUser {
2✔
1142
            using TestUser::TestUser;
2✔
1143
            CompletionHandler stored_completion;
2✔
1144
            void request_access_token(CompletionHandler&& completion) override
2✔
1145
            {
2✔
1146
                stored_completion = std::move(completion);
2✔
1147
            }
2✔
1148
            bool access_token_refresh_required() const override
2✔
1149
            {
2✔
1150
                return !stored_completion;
2✔
1151
            }
2✔
1152
        };
2✔
1153
        auto user = std::make_shared<User>("realm", tsm.sync_manager());
2✔
1154
        SyncTestFile config(user, "realm");
2✔
1155
        auto valid_token = user->access_token();
2✔
1156
        user->m_access_token = expired_token;
2✔
1157

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

1✔
1172
        util::EventLoop::main().run_until([&] {
12,865✔
1173
            return called.load();
12,865✔
1174
        });
12,865✔
1175
        std::lock_guard<std::mutex> lock(mutex);
2✔
1176
        REQUIRE(called);
2!
1177
    }
2✔
1178

15✔
1179
    SECTION("cancels download and reports an error on auth error") {
30✔
1180
        struct User : TestUser {
2✔
1181
            using TestUser::TestUser;
2✔
1182
            void request_access_token(CompletionHandler&& completion) override
2✔
1183
            {
2✔
1184
                completion(app::AppError(ErrorCodes::HTTPError, "403 error", "", 403));
2✔
1185
            }
2✔
1186
            bool access_token_refresh_required() const override
2✔
1187
            {
2✔
1188
                return true;
2✔
1189
            }
2✔
1190
        };
2✔
1191
        auto user = std::make_shared<User>("realm", tsm.sync_manager());
2✔
1192
        user->m_access_token = expired_token;
2✔
1193
        user->m_refresh_token = expired_token;
2✔
1194
        SyncTestFile config(user, "realm");
2✔
1195

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

52✔
1613
    auto wait_for_done = [&]() {
100✔
1614
        util::EventLoop::main().run_until([&] {
72,127✔
1615
            return done;
72,127✔
1616
        });
72,127✔
1617
        REQUIRE(done);
96!
1618
    };
96✔
1619

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

52✔
1647
    auto verify_persisted_count = [&](size_t expected) {
78✔
1648
        if (realm)
52✔
1649
            realm->close();
50✔
1650
        _impl::RealmCoordinator::assert_no_open_realms();
52✔
1651

26✔
1652
        auto new_realm = Realm::get_shared_realm(config);
52✔
1653
        auto table = new_realm->read_group().get_table("class_object");
52✔
1654
        REQUIRE(table->size() == expected);
52!
1655
    };
52✔
1656

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

2✔
1674
                    // Wait until the main thread is waiting for the lock.
2✔
1675
                    while (!db->other_writers_waiting_for_lock()) {
8✔
1676
                        millisleep(1);
4✔
1677
                    }
4✔
1678
                    write->close();
4✔
1679
                });
4✔
1680

2✔
1681
                // Wait for the background thread to have acquired the lock
2✔
1682
                sema.get_stone();
4✔
1683

2✔
1684
                auto scheduler = realm->scheduler();
4✔
1685
                realm->async_begin_transaction([&] {
2✔
1686
                    // We should never get here as the realm is closed
UNCOV
1687
                    FAIL();
×
UNCOV
1688
                });
×
1689

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

2✔
1693
                {
4✔
1694
                    // Verify that we released the write lock
2✔
1695
                    auto db = DB::create(make_in_realm_history(), config.path, options);
4✔
1696
                    REQUIRE(db->start_write(/* nonblocking */ true));
4!
1697
                }
4✔
1698

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

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

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

2✔
1840
                realm->async_begin_transaction([&] {
4✔
1841
                    table->create_object().set(col, 45);
4✔
1842
                    realm->async_commit_transaction([&](std::exception_ptr) {
4✔
1843
                        done = true;
4✔
1844
                    });
4✔
1845
                });
4✔
1846

2✔
1847
                wait_for_done();
4✔
1848
                verify_persisted_count(1);
4✔
1849
            }
4✔
1850
        }
40✔
1851
    }
208✔
1852

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

1✔
1907
        // Transaction should have been rolled back
1✔
1908
        REQUIRE_FALSE(realm->is_in_transaction());
2!
1909
        REQUIRE(table->size() == 0);
2!
1910
        REQUIRE(called);
2!
1911

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

1✔
1936
        // Transaction should have been rolled back
1✔
1937
        REQUIRE_FALSE(realm->is_in_transaction());
2!
1938
        REQUIRE(table->size() == 0);
2!
1939

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

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

52✔
1994
    if (_impl::SimulatedFailure::is_enabled()) {
104✔
1995
        SECTION("error in the synchronous part of async commit") {
104✔
1996
            realm->begin_transaction();
2✔
1997
            table->create_object();
2✔
1998

1✔
1999
            using sf = _impl::SimulatedFailure;
2✔
2000
            sf::OneShotPrimeGuard pg(sf::shared_group__grow_reader_mapping);
2✔
2001
            REQUIRE_THROWS_AS(realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2002
                FAIL("should not call completion");
2✔
2003
            }),
2✔
2004
                              _impl::SimulatedFailure);
2✔
2005
            REQUIRE_FALSE(realm->is_in_transaction());
2!
2006
        }
2✔
2007
        SECTION("error in the async part of async commit") {
104✔
2008
            realm->begin_transaction();
2✔
2009
            table->create_object();
2✔
2010

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

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

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

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

1✔
2187
        util::EventLoop::main().run_until([&] {
942✔
2188
            return !realm->is_in_async_transaction();
942✔
2189
        });
942✔
2190

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

1✔
2224
        util::EventLoop::main().run_until([&] {
3✔
2225
            return persisted;
3✔
2226
        });
3✔
2227
        verify_persisted_count(1);
2✔
2228
    }
2✔
2229
    SECTION("sync commit of async transaction with subsequent pending async transaction") {
104✔
2230
        realm->async_begin_transaction([&, realm]() {
2✔
2231
            table->create_object();
2✔
2232
            realm->commit_transaction();
2✔
2233
        });
2✔
2234
        realm->async_begin_transaction([&, realm]() {
2✔
2235
            table->create_object();
2✔
2236
            realm->commit_transaction();
2✔
2237
            done = true;
2✔
2238
        });
2✔
2239
        wait_for_done();
2✔
2240
        REQUIRE(table->size() == 2);
2!
2241
    }
2✔
2242
    SECTION("release reference to Realm after async begin") {
104✔
2243
        std::weak_ptr<Realm> weak_realm = realm;
2✔
2244
        realm->async_begin_transaction([&]() {
2✔
2245
            table->create_object().set(col, 45);
2✔
2246
            weak_realm.lock()->async_commit_transaction([&](std::exception_ptr) {
2✔
2247
                done = true;
2✔
2248
            });
2✔
2249
        });
2✔
2250
        realm = nullptr;
2✔
2251
        wait_for_done();
2✔
2252
        verify_persisted_count(1);
2✔
2253
    }
2✔
2254
    SECTION("object change information") {
104✔
2255
        realm->begin_transaction();
2✔
2256
        auto list_col = table->get_column_key("ints");
2✔
2257
        auto set_col = table->get_column_key("int set");
2✔
2258
        auto dict_col = table->get_column_key("int dictionary");
2✔
2259
        auto obj = table->create_object();
2✔
2260
        auto list = obj.get_list<Int>(list_col);
2✔
2261
        for (int i = 0; i < 3; ++i)
8✔
2262
            list.add(i);
6✔
2263
        auto set = obj.get_set<Int>(set_col);
2✔
2264
        set.insert(0);
2✔
2265
        auto dict = obj.get_dictionary(dict_col);
2✔
2266
        dict.insert("a", 0);
2✔
2267
        realm->commit_transaction();
2✔
2268

1✔
2269
        Observer observer(obj);
2✔
2270
        observer.realm = realm;
2✔
2271
        realm->m_binding_context.reset(&observer);
2✔
2272

1✔
2273
        realm->async_begin_transaction([&]() {
2✔
2274
            list.clear();
2✔
2275
            set.clear();
2✔
2276
            dict.clear();
2✔
2277
            done = true;
2✔
2278
        });
2✔
2279
        wait_for_done();
2✔
2280
        REQUIRE(observer.array_change(0, list_col) == IndexSet{0, 1, 2});
2!
2281
        REQUIRE(observer.array_change(0, set_col) == IndexSet{});
2!
2282
        REQUIRE(observer.array_change(0, dict_col) == IndexSet{});
2!
2283
        realm->m_binding_context.release();
2✔
2284
    }
2✔
2285

52✔
2286
    SECTION("begin_transaction() from within did_change()") {
104✔
2287
        struct Context : public BindingContext {
2✔
2288
            void did_change(std::vector<ObserverState> const&, std::vector<void*> const&, bool) override
2✔
2289
            {
4✔
2290
                auto r = realm.lock();
4✔
2291
                r->begin_transaction();
4✔
2292
                auto table = r->read_group().get_table("class_object");
4✔
2293
                table->create_object();
4✔
2294
                if (++change_count == 1) {
4✔
2295
                    r->commit_transaction();
2✔
2296
                }
2✔
2297
                else {
2✔
2298
                    r->cancel_transaction();
2✔
2299
                }
2✔
2300
            }
4✔
2301
            int change_count = 0;
2✔
2302
        };
2✔
2303

1✔
2304
        realm->m_binding_context.reset(new Context());
2✔
2305
        realm->m_binding_context->realm = realm;
2✔
2306

1✔
2307
        realm->begin_transaction();
2✔
2308
        auto table = realm->read_group().get_table("class_object");
2✔
2309
        table->create_object();
2✔
2310
        bool persisted = false;
2✔
2311
        realm->async_commit_transaction([&persisted](auto) {
2✔
2312
            persisted = true;
2✔
2313
        });
2✔
2314
        REQUIRE(table->size() == 2);
2!
2315
        REQUIRE(persisted);
2!
2316
    }
2✔
2317

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

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

52✔
2339
    SECTION("async write grouping with manual barriers") {
104✔
2340
        size_t completion_calls = 0;
2✔
2341
        for (size_t i = 0; i < 41; ++i) {
84✔
2342
            realm->async_begin_transaction([&, i, realm] {
82✔
2343
                // The top ref in the Realm file should only be updated once every 6 commits
41✔
2344
                CHECK(Group(config.path, config.encryption_key.data()).get_table("class_object")->size() ==
82!
2345
                      (i / 6) * 6);
82✔
2346

41✔
2347
                table->create_object();
82✔
2348
                realm->async_commit_transaction(
82✔
2349
                    [&](std::exception_ptr) {
82✔
2350
                        ++completion_calls;
82✔
2351
                    },
82✔
2352
                    (i + 1) % 6 != 0);
82✔
2353
            });
82✔
2354
        }
82✔
2355
        util::EventLoop::main().run_until([&] {
6,550✔
2356
            return completion_calls == 41;
6,550✔
2357
        });
6,550✔
2358
    }
2✔
2359

52✔
2360
    SECTION("async writes scheduled inside sync write") {
104✔
2361
        realm->begin_transaction();
2✔
2362
        realm->async_begin_transaction([&] {
2✔
2363
            REQUIRE(table->size() == 1);
2!
2364
            table->create_object();
2✔
2365
            realm->async_commit_transaction();
2✔
2366
        });
2✔
2367
        realm->async_begin_transaction([&] {
2✔
2368
            REQUIRE(table->size() == 2);
2!
2369
            table->create_object();
2✔
2370
            realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2371
                done = true;
2✔
2372
            });
2✔
2373
        });
2✔
2374
        REQUIRE(table->size() == 0);
2!
2375
        table->create_object();
2✔
2376
        realm->commit_transaction();
2✔
2377
        wait_for_done();
2✔
2378
        REQUIRE(table->size() == 3);
2!
2379
    }
2✔
2380

52✔
2381
    SECTION("async writes scheduled inside multiple sync write") {
104✔
2382
        realm->begin_transaction();
2✔
2383
        realm->async_begin_transaction([&] {
2✔
2384
            REQUIRE(table->size() == 2);
2!
2385
            table->create_object();
2✔
2386
            realm->async_commit_transaction();
2✔
2387
        });
2✔
2388
        realm->async_begin_transaction([&] {
2✔
2389
            REQUIRE(table->size() == 3);
2!
2390
            table->create_object();
2✔
2391
            realm->async_commit_transaction();
2✔
2392
        });
2✔
2393
        REQUIRE(table->size() == 0);
2!
2394
        table->create_object();
2✔
2395
        realm->commit_transaction();
2✔
2396

1✔
2397
        realm->begin_transaction();
2✔
2398
        realm->async_begin_transaction([&] {
2✔
2399
            REQUIRE(table->size() == 4);
2!
2400
            table->create_object();
2✔
2401
            realm->async_commit_transaction();
2✔
2402
        });
2✔
2403
        realm->async_begin_transaction([&] {
2✔
2404
            REQUIRE(table->size() == 5);
2!
2405
            table->create_object();
2✔
2406
            realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2407
                done = true;
2✔
2408
            });
2✔
2409
        });
2✔
2410
        REQUIRE(table->size() == 1);
2!
2411
        table->create_object();
2✔
2412
        realm->commit_transaction();
2✔
2413

1✔
2414

1✔
2415
        wait_for_done();
2✔
2416
        REQUIRE(table->size() == 6);
2!
2417
    }
2✔
2418

52✔
2419
    SECTION("async writes which would run inside sync writes are deferred") {
104✔
2420
        realm->async_begin_transaction([&] {
2✔
2421
            done = true;
2✔
2422
        });
2✔
2423

1✔
2424
        // Wait for the background thread to hold the write lock (without letting
1✔
2425
        // the event loop run so that the scheduled task isn't run)
1✔
2426
        DBOptions options;
2✔
2427
        options.encryption_key = config.encryption_key.data();
2✔
2428
        auto db = DB::create(make_in_realm_history(), config.path, options);
2✔
2429
        while (db->start_write(true))
2✔
UNCOV
2430
            millisleep(1);
×
2431

1✔
2432
        realm->begin_transaction();
2✔
2433

1✔
2434
        // Invoke the pending callback
1✔
2435
        util::EventLoop::main().run_pending();
2✔
2436
        // Should not have run the async write block
1✔
2437
        REQUIRE(done == false);
2!
2438

1✔
2439
        // Should run the async write block once the synchronous transaction is done
1✔
2440
        realm->cancel_transaction();
2✔
2441
        REQUIRE(done == false);
2!
2442
        util::EventLoop::main().run_pending();
2✔
2443
        REQUIRE(done == true);
2!
2444
    }
2✔
2445

52✔
2446
    util::EventLoop::main().run_until([&] {
162✔
2447
        return !realm || !realm->has_pending_async_work();
162✔
2448
    });
162✔
2449

52✔
2450
    _impl::RealmCoordinator::clear_all_caches();
104✔
2451
}
104✔
2452

2453
TEST_CASE("Call run_async_completions after realm has been closed") {
2✔
2454
    // This requires a special scheduler as we have to call Realm::close
1✔
2455
    // just after DB::AsyncCommitHelper has made a callback to the function
1✔
2456
    // that asks the scheduler to invoke run_async_completions()
1✔
2457

1✔
2458
    struct ManualScheduler : util::Scheduler {
2✔
2459
        std::mutex mutex;
2✔
2460
        std::condition_variable cv;
2✔
2461
        std::vector<util::UniqueFunction<void()>> callbacks;
2✔
2462

1✔
2463
        void invoke(util::UniqueFunction<void()>&& cb) override
2✔
2464
        {
2✔
2465
            {
2✔
2466
                std::lock_guard lock(mutex);
2✔
2467
                callbacks.push_back(std::move(cb));
2✔
2468
            }
2✔
2469
            cv.notify_all();
2✔
2470
        }
2✔
2471

1✔
2472
        bool is_on_thread() const noexcept override
2✔
2473
        {
4✔
2474
            return true;
4✔
2475
        }
4✔
2476
        bool is_same_as(const Scheduler*) const noexcept override
2✔
2477
        {
1✔
UNCOV
2478
            return false;
×
UNCOV
2479
        }
×
2480
        bool can_invoke() const noexcept override
2✔
2481
        {
1✔
UNCOV
2482
            return true;
×
UNCOV
2483
        }
×
2484
    };
2✔
2485

1✔
2486
    auto scheduler = std::make_shared<ManualScheduler>();
2✔
2487

1✔
2488
    TestFile config;
2✔
2489
    config.schema_version = 0;
2✔
2490
    config.schema = Schema{{"object", {{"value", PropertyType::Int}}}};
2✔
2491
    config.scheduler = scheduler;
2✔
2492
    config.automatic_change_notifications = false;
2✔
2493

1✔
2494
    auto realm = Realm::get_shared_realm(config);
2✔
2495

1✔
2496
    realm->begin_transaction();
2✔
2497
    realm->async_commit_transaction([](std::exception_ptr) {});
2✔
2498

1✔
2499
    std::vector<util::UniqueFunction<void()>> callbacks;
2✔
2500
    {
2✔
2501
        std::unique_lock lock(scheduler->mutex);
2✔
2502
        // Wait for scheduler to be invoked
1✔
2503
        scheduler->cv.wait(lock, [&] {
4✔
2504
            return !scheduler->callbacks.empty();
4✔
2505
        });
4✔
2506
        callbacks.swap(scheduler->callbacks);
2✔
2507
    }
2✔
2508
    realm->close();
2✔
2509
    // Call whatever functions that was added to scheduler.
1✔
2510
    for (auto& cb : callbacks)
2✔
2511
        cb();
2✔
2512
}
2✔
2513

2514
// Our libuv scheduler currently does not support background threads, so we can
2515
// only run this on apple platforms
2516
#if REALM_PLATFORM_APPLE
2517
TEST_CASE("SharedRealm: async writes on multiple threads") {
5✔
2518
    _impl::RealmCoordinator::assert_no_open_realms();
5✔
2519

2520
    TestFile config;
5✔
2521
    config.cache = true;
5✔
2522
    config.schema_version = 0;
5✔
2523
    config.schema = Schema{{"object", {{"value", PropertyType::Int}}}};
5✔
2524
    auto realm = Realm::get_shared_realm(config);
5✔
2525
    auto table_key = realm->read_group().get_table("class_object")->get_key();
5✔
2526
    realm->close();
5✔
2527

2528
    struct QueueState {
5✔
2529
        dispatch_queue_t queue;
5✔
2530
        Realm::Config config;
5✔
2531
    };
5✔
2532
    std::vector<QueueState> queues;
5✔
2533
    for (int i = 0; i < 10; ++i) {
55✔
2534
        auto queue = dispatch_queue_create(util::format("queue %1", i).c_str(), 0);
50✔
2535
        Realm::Config queue_config = config;
50✔
2536
        queue_config.scheduler = util::Scheduler::make_dispatch(static_cast<void*>(queue));
50✔
2537
        queues.push_back({queue, std::move(queue_config)});
50✔
2538
    }
50✔
2539

2540
    std::atomic<size_t> completions = 0;
5✔
2541
    // Capturing by reference when mixing lambda and blocks is weird, so capture
2542
    // a pointer instead
2543
    auto completions_ptr = &completions;
5✔
2544

2545
    auto async_write_and_async_commit = [=](const Realm::Config& config) {
124✔
2546
        Realm::get_shared_realm(config)->async_begin_transaction([=] {
124✔
2547
            auto realm = Realm::get_shared_realm(config);
124✔
2548
            realm->read_group().get_table(table_key)->create_object();
124✔
2549
            realm->async_commit_transaction([=](std::exception_ptr) {
124✔
2550
                ++*completions_ptr;
124✔
2551
            });
124✔
2552
        });
124✔
2553
    };
124✔
2554
    auto async_write_and_sync_commit = [=](const Realm::Config& config) {
123✔
2555
        Realm::get_shared_realm(config)->async_begin_transaction([=] {
124✔
2556
            auto realm = Realm::get_shared_realm(config);
124✔
2557
            realm->read_group().get_table(table_key)->create_object();
124✔
2558
            realm->commit_transaction();
124✔
2559
            ++*completions_ptr;
124✔
2560
        });
124✔
2561
    };
123✔
2562
    auto sync_write_and_async_commit = [=](const Realm::Config& config) {
124✔
2563
        auto realm = Realm::get_shared_realm(config);
124✔
2564
        realm->begin_transaction();
124✔
2565
        realm->read_group().get_table(table_key)->create_object();
124✔
2566
        realm->async_commit_transaction([=](std::exception_ptr) {
124✔
2567
            ++*completions_ptr;
124✔
2568
        });
124✔
2569
    };
124✔
2570
    auto sync_write_and_sync_commit = [=](const Realm::Config& config) {
124✔
2571
        auto realm = Realm::get_shared_realm(config);
124✔
2572
        realm->begin_transaction();
124✔
2573
        realm->read_group().get_table(table_key)->create_object();
124✔
2574
        realm->commit_transaction();
124✔
2575
        ++*completions_ptr;
124✔
2576
    };
124✔
2577

2578
    SECTION("async begin and async commit") {
5✔
2579
        for (auto& queue : queues) {
10✔
2580
            dispatch_async(queue.queue, ^{
10✔
2581
                for (int i = 0; i < 10; ++i) {
110✔
2582
                    async_write_and_async_commit(queue.config);
100✔
2583
                }
100✔
2584
            });
10✔
2585
        }
10✔
2586
        util::EventLoop::main().run_until([&] {
130✔
2587
            return completions == 100;
130✔
2588
        });
130✔
2589
    }
1✔
2590
    SECTION("async begin and sync commit") {
5✔
2591
        for (auto& queue : queues) {
10✔
2592
            dispatch_async(queue.queue, ^{
10✔
2593
                for (int i = 0; i < 10; ++i) {
109✔
2594
                    async_write_and_sync_commit(queue.config);
99✔
2595
                }
99✔
2596
            });
10✔
2597
        }
10✔
2598
        util::EventLoop::main().run_until([&] {
122✔
2599
            return completions == 100;
122✔
2600
        });
122✔
2601
    }
1✔
2602
    SECTION("sync begin and async commit") {
5✔
2603
        for (auto& queue : queues) {
10✔
2604
            dispatch_async(queue.queue, ^{
10✔
2605
                for (int i = 0; i < 10; ++i) {
110✔
2606
                    sync_write_and_async_commit(queue.config);
100✔
2607
                }
100✔
2608
            });
10✔
2609
        }
10✔
2610
        util::EventLoop::main().run_until([&] {
134✔
2611
            return completions == 100;
134✔
2612
        });
134✔
2613
    }
1✔
2614
    SECTION("sync begin and sync commit") {
5✔
2615
        for (auto& queue : queues) {
10✔
2616
            dispatch_async(queue.queue, ^{
10✔
2617
                for (int i = 0; i < 10; ++i) {
110✔
2618
                    sync_write_and_sync_commit(queue.config);
100✔
2619
                }
100✔
2620
            });
10✔
2621
        }
10✔
2622
        util::EventLoop::main().run_until([&] {
122✔
2623
            return completions == 100;
122✔
2624
        });
122✔
2625
    }
1✔
2626
    SECTION("mixed sync and async") {
5✔
2627
        // Test every permutation of each of the variants
2628
        struct IndexedOp {
1✔
2629
            int index;
1✔
2630
            std::function<void(const Realm::Config& config)> fn;
1✔
2631
        };
1✔
2632
        std::array<IndexedOp, 4> functions{{
1✔
2633
            {0, async_write_and_async_commit},
1✔
2634
            {1, sync_write_and_async_commit},
1✔
2635
            {2, async_write_and_sync_commit},
1✔
2636
            {3, sync_write_and_sync_commit},
1✔
2637
        }};
1✔
2638
        size_t i = 0;
1✔
2639
        size_t expected_completions = 0;
1✔
2640
        do {
24✔
2641
            auto& queue = queues[i++ % 10];
24✔
2642
            auto functions_copy = functions;
24✔
2643
            dispatch_async(queue.queue, ^{
24✔
2644
                for (auto& fn : functions_copy) {
96✔
2645
                    fn.fn(queue.config);
96✔
2646
                }
96✔
2647
            });
24✔
2648
            expected_completions += 4;
24✔
2649
        } while (std::next_permutation(functions.begin(), functions.end(), [](auto& a, auto& b) {
70✔
2650
            return a.index < b.index;
70✔
2651
        }));
70✔
2652

2653
        util::EventLoop::main().run_until([&] {
138✔
2654
            return completions == expected_completions;
138✔
2655
        });
138✔
2656
    }
1✔
2657

2658

2659
    realm = Realm::get_shared_realm(config);
5✔
2660
    REQUIRE(realm->read_group().get_table(table_key)->size() == completions);
5!
2661

2662
    for (auto& queue : queues) {
50✔
2663
        dispatch_sync(queue.queue, ^{
50✔
2664
                      });
50✔
2665
    }
50✔
2666
}
5✔
2667
#endif
2668

2669
class LooperDelegate {
2670
public:
2671
    LooperDelegate() {}
2✔
2672
    void run_once()
2673
    {
1,875✔
2674
        for (auto it = m_tasks.begin(); it != m_tasks.end(); ++it) {
2,744✔
2675
            if (it->may_run && *it->may_run) {
875✔
2676
                it->the_job();
6✔
2677
                m_tasks.erase(it);
6✔
2678
                return;
6✔
2679
            }
6✔
2680
        }
875✔
2681
    }
1,875✔
2682
    std::shared_ptr<bool> add_task(util::UniqueFunction<void()>&& the_job)
2683
    {
6✔
2684
        m_tasks.push_back(Task{std::make_shared<bool>(false), std::move(the_job)});
6✔
2685
        return m_tasks.back().may_run;
6✔
2686
    }
6✔
2687
    bool has_tasks()
UNCOV
2688
    {
×
UNCOV
2689
        return !m_tasks.empty();
×
UNCOV
2690
    }
×
2691

2692
private:
2693
    struct Task {
2694
        std::shared_ptr<bool> may_run;
2695
        util::UniqueFunction<void()> the_job;
2696
    };
2697
    std::vector<Task> m_tasks;
2698
};
2699

2700
#ifndef _WIN32
2701
TEST_CASE("SharedRealm: async_writes_2") {
2✔
2702
    _impl::RealmCoordinator::assert_no_open_realms();
2✔
2703
    if (!util::EventLoop::has_implementation())
2✔
UNCOV
2704
        return;
×
2705

1✔
2706
    TestFile config;
2✔
2707
    config.schema_version = 0;
2✔
2708
    config.schema = Schema{
2✔
2709
        {"object", {{"value", PropertyType::Int}}},
2✔
2710
    };
2✔
2711
    bool done = false;
2✔
2712
    auto realm = Realm::get_shared_realm(config);
2✔
2713
    int write_nr = 0;
2✔
2714
    int commit_nr = 0;
2✔
2715
    auto table = realm->read_group().get_table("class_object");
2✔
2716
    auto col = table->get_column_key("value");
2✔
2717
    LooperDelegate ld;
2✔
2718
    std::shared_ptr<bool> t1_rdy = ld.add_task([&, realm]() {
2✔
2719
        REQUIRE(write_nr == 0);
2!
2720
        ++write_nr;
2✔
2721
        table->create_object().set(col, 45);
2✔
2722
        realm->cancel_transaction();
2✔
2723
    });
2✔
2724
    std::shared_ptr<bool> t2_rdy = ld.add_task([&, realm]() {
2✔
2725
        REQUIRE(write_nr == 1);
2!
2726
        ++write_nr;
2✔
2727
        table->create_object().set(col, 45);
2✔
2728
        realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2729
            REQUIRE(commit_nr == 0);
2!
2730
            ++commit_nr;
2✔
2731
        });
2✔
2732
    });
2✔
2733
    std::shared_ptr<bool> t3_rdy = ld.add_task([&, realm]() {
2✔
2734
        ++write_nr;
2✔
2735
        auto o = table->get_object(0);
2✔
2736
        o.set(col, o.get<int64_t>(col) + 37);
2✔
2737
        realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2738
            ++commit_nr;
2✔
2739
            done = true;
2✔
2740
        });
2✔
2741
    });
2✔
2742

1✔
2743
    // Make some notify_only transactions
1✔
2744
    realm->async_begin_transaction(
2✔
2745
        [&]() {
2✔
2746
            *t1_rdy = true;
2✔
2747
        },
2✔
2748
        true);
2✔
2749
    realm->async_begin_transaction(
2✔
2750
        [&]() {
2✔
2751
            *t2_rdy = true;
2✔
2752
        },
2✔
2753
        true);
2✔
2754
    realm->async_begin_transaction(
2✔
2755
        [&]() {
2✔
2756
            *t3_rdy = true;
2✔
2757
        },
2✔
2758
        true);
2✔
2759

1✔
2760
    util::EventLoop::main().run_until([&, realm] {
1,875✔
2761
        ld.run_once();
1,875✔
2762
        return done;
1,875✔
2763
    });
1,875✔
2764
    REQUIRE(done);
2!
2765
}
2✔
2766
#endif
2767

2768
TEST_CASE("SharedRealm: notifications") {
14✔
2769
    if (!util::EventLoop::has_implementation())
14✔
UNCOV
2770
        return;
×
2771

7✔
2772
    TestFile config;
14✔
2773
    config.schema_version = 0;
14✔
2774
    config.schema = Schema{
14✔
2775
        {"object", {{"value", PropertyType::Int}}},
14✔
2776
    };
14✔
2777

7✔
2778
    struct Context : BindingContext {
14✔
2779
        size_t* change_count;
14✔
2780
        util::UniqueFunction<void()> did_change_fn;
14✔
2781
        util::UniqueFunction<void()> changes_available_fn;
14✔
2782

7✔
2783
        Context(size_t* out)
14✔
2784
            : change_count(out)
14✔
2785
        {
14✔
2786
        }
14✔
2787

7✔
2788
        void did_change(std::vector<ObserverState> const&, std::vector<void*> const&, bool) override
14✔
2789
        {
24✔
2790
            ++*change_count;
24✔
2791
            if (did_change_fn)
24✔
2792
                did_change_fn();
12✔
2793
        }
24✔
2794

7✔
2795
        void changes_available() override
14✔
2796
        {
12✔
2797
            if (changes_available_fn)
10✔
2798
                changes_available_fn();
2✔
2799
        }
10✔
2800
    };
14✔
2801

7✔
2802
    size_t change_count = 0;
14✔
2803
    auto realm = Realm::get_shared_realm(config);
14✔
2804
    realm->read_group();
14✔
2805
    auto context = new Context{&change_count};
14✔
2806
    realm->m_binding_context.reset(context);
14✔
2807
    realm->m_binding_context->realm = realm;
14✔
2808

7✔
2809
    SECTION("local notifications are sent synchronously") {
14✔
2810
        realm->begin_transaction();
2✔
2811
        REQUIRE(change_count == 0);
2!
2812
        realm->commit_transaction();
2✔
2813
        REQUIRE(change_count == 1);
2!
2814
    }
2✔
2815
#ifndef _WIN32
14✔
2816
    SECTION("remote notifications are sent asynchronously") {
14✔
2817
        auto r2 = Realm::get_shared_realm(config);
2✔
2818
        r2->begin_transaction();
2✔
2819
        r2->commit_transaction();
2✔
2820
        REQUIRE(change_count == 0);
2!
2821
        util::EventLoop::main().run_until([&] {
9✔
2822
            return change_count > 0;
9✔
2823
        });
9✔
2824
        REQUIRE(change_count == 1);
2!
2825
    }
2✔
2826

7✔
2827
    SECTION("notifications created in async transaction are sent synchronously") {
14✔
2828
        realm->async_begin_transaction([&] {
2✔
2829
            REQUIRE(change_count == 0);
2!
2830
            realm->async_commit_transaction();
2✔
2831
            REQUIRE(change_count == 1);
2!
2832
        });
2✔
2833
        REQUIRE(change_count == 0);
2!
2834
        util::EventLoop::main().run_until([&] {
14✔
2835
            return change_count > 0;
14✔
2836
        });
14✔
2837
        REQUIRE(change_count == 1);
2!
2838
        util::EventLoop::main().run_until([&] {
824✔
2839
            return !realm->has_pending_async_work();
824✔
2840
        });
824✔
2841
    }
2✔
2842
#endif
14✔
2843
    SECTION("refresh() from within changes_available() refreshes") {
14✔
2844
        context->changes_available_fn = [&] {
2✔
2845
            REQUIRE(realm->refresh());
2!
2846
        };
2✔
2847
        realm->set_auto_refresh(false);
2✔
2848

1✔
2849
        auto r2 = Realm::get_shared_realm(config);
2✔
2850
        r2->begin_transaction();
2✔
2851
        r2->commit_transaction();
2✔
2852
        realm->notify();
2✔
2853
        // Should return false as the realm was already advanced
1✔
2854
        REQUIRE_FALSE(realm->refresh());
2!
2855
    }
2✔
2856

7✔
2857
    SECTION("refresh() from within did_change() is a no-op") {
14✔
2858
        context->did_change_fn = [&] {
4✔
2859
            if (change_count > 1)
4✔
2860
                return;
2✔
2861

1✔
2862
            // Create another version so that refresh() advances the version
1✔
2863
            auto r2 = Realm::get_shared_realm(realm->config());
2✔
2864
            r2->begin_transaction();
2✔
2865
            r2->commit_transaction();
2✔
2866

1✔
2867
            REQUIRE_FALSE(realm->refresh());
2!
2868
        };
2✔
2869

1✔
2870
        auto r2 = Realm::get_shared_realm(config);
2✔
2871
        r2->begin_transaction();
2✔
2872
        r2->commit_transaction();
2✔
2873

1✔
2874
        REQUIRE(realm->refresh());
2!
2875
        REQUIRE(change_count == 1);
2!
2876

1✔
2877
        REQUIRE(realm->refresh());
2!
2878
        REQUIRE(change_count == 2);
2!
2879
        REQUIRE_FALSE(realm->refresh());
2!
2880
    }
2✔
2881

7✔
2882
    SECTION("begin_write() from within did_change() produces recursive notifications") {
14✔
2883
        context->did_change_fn = [&] {
8✔
2884
            if (realm->is_in_transaction())
8✔
2885
                realm->cancel_transaction();
6✔
2886
            if (change_count > 3)
8✔
2887
                return;
2✔
2888

3✔
2889
            // Create another version so that begin_write() advances the version
3✔
2890
            auto r2 = Realm::get_shared_realm(realm->config());
6✔
2891
            r2->begin_transaction();
6✔
2892
            r2->commit_transaction();
6✔
2893

3✔
2894
            realm->begin_transaction();
6✔
2895
            REQUIRE(change_count == 4);
6!
2896
        };
6✔
2897

1✔
2898
        auto r2 = Realm::get_shared_realm(config);
2✔
2899
        r2->begin_transaction();
2✔
2900
        r2->commit_transaction();
2✔
2901
        REQUIRE(realm->refresh());
2!
2902
        REQUIRE(change_count == 4);
2!
2903
        REQUIRE_FALSE(realm->refresh());
2!
2904
    }
2✔
2905

7✔
2906
#if REALM_ENABLE_SYNC
14✔
2907
    SECTION("SubscriptionStore writes produce notifications") {
14✔
2908
        auto subscription_store = sync::SubscriptionStore::create(TestHelper::get_db(realm));
2✔
2909
        REQUIRE(change_count == 0);
2!
2910
        util::EventLoop::main().run_until([&] {
11✔
2911
            return change_count > 0;
11✔
2912
        });
11✔
2913
        REQUIRE(change_count == 1);
2!
2914

1✔
2915
        subscription_store->get_active().make_mutable_copy().commit();
2✔
2916
        REQUIRE(change_count == 1);
2!
2917
        util::EventLoop::main().run_until([&] {
11✔
2918
            return change_count > 1;
11✔
2919
        });
11✔
2920
        REQUIRE(change_count == 2);
2!
2921
    }
2✔
2922
#endif
14✔
2923
}
14✔
2924

2925
TEST_CASE("SharedRealm: schema updating from external changes") {
14✔
2926
    TestFile config;
14✔
2927
    config.schema_version = 0;
14✔
2928
    config.schema_mode = SchemaMode::AdditiveExplicit;
14✔
2929
    config.schema = Schema{
14✔
2930
        {"object",
14✔
2931
         {
14✔
2932
             {"value", PropertyType::Int, Property::IsPrimary{true}},
14✔
2933
             {"value 2", PropertyType::Int, Property::IsPrimary{false}, Property::IsIndexed{true}},
14✔
2934
         }},
14✔
2935
    };
14✔
2936

7✔
2937
    SECTION("newly added columns update table columns but are not added to properties") {
14✔
2938
        // Does this test add any value when column keys are stable?
2✔
2939
        auto r1 = Realm::get_shared_realm(config);
4✔
2940
        auto r2 = Realm::get_shared_realm(config);
4✔
2941
        auto test = [&] {
4✔
2942
            r2->begin_transaction();
4✔
2943
            r2->read_group().get_table("class_object")->add_column(type_String, "new col");
4✔
2944
            r2->commit_transaction();
4✔
2945

2✔
2946
            auto& object_schema = *r1->schema().find("object");
4✔
2947
            REQUIRE(object_schema.persisted_properties.size() == 2);
4!
2948
            ColKey col = object_schema.persisted_properties[0].column_key;
4✔
2949
            r1->refresh();
4✔
2950
            REQUIRE(object_schema.persisted_properties[0].column_key == col);
4!
2951
        };
4✔
2952
        SECTION("with an active read transaction") {
4✔
2953
            r1->read_group();
2✔
2954
            test();
2✔
2955
        }
2✔
2956
        SECTION("without an active read transaction") {
4✔
2957
            r1->invalidate();
2✔
2958
            test();
2✔
2959
        }
2✔
2960
    }
4✔
2961

7✔
2962
    SECTION("beginning a read transaction checks for incompatible changes") {
14✔
2963
        auto r = Realm::get_shared_realm(config);
10✔
2964
        r->invalidate();
10✔
2965

5✔
2966
        auto& db = TestHelper::get_db(r);
10✔
2967
        WriteTransaction wt(db);
10✔
2968
        auto& table = *wt.get_table("class_object");
10✔
2969

5✔
2970
        SECTION("removing a property") {
10✔
2971
            table.remove_column(table.get_column_key("value"));
2✔
2972
            wt.commit();
2✔
2973
            REQUIRE_THROWS_CONTAINING(r->refresh(), "Property 'object.value' has been removed.");
2✔
2974
        }
2✔
2975

5✔
2976
        SECTION("change property type") {
10✔
2977
            table.remove_column(table.get_column_key("value 2"));
2✔
2978
            table.add_column(type_Float, "value 2");
2✔
2979
            wt.commit();
2✔
2980
            REQUIRE_THROWS_CONTAINING(r->refresh(),
2✔
2981
                                      "Property 'object.value 2' has been changed from 'int' to 'float'");
2✔
2982
        }
2✔
2983

5✔
2984
        SECTION("make property optional") {
10✔
2985
            table.remove_column(table.get_column_key("value 2"));
2✔
2986
            table.add_column(type_Int, "value 2", true);
2✔
2987
            wt.commit();
2✔
2988
            REQUIRE_THROWS_CONTAINING(r->refresh(), "Property 'object.value 2' has been made optional");
2✔
2989
        }
2✔
2990

5✔
2991
        SECTION("recreate column with no changes") {
10✔
2992
            table.remove_column(table.get_column_key("value 2"));
2✔
2993
            table.add_column(type_Int, "value 2");
2✔
2994
            wt.commit();
2✔
2995
            REQUIRE_NOTHROW(r->refresh());
2✔
2996
        }
2✔
2997

5✔
2998
        SECTION("remove index from non-PK") {
10✔
2999
            table.remove_search_index(table.get_column_key("value 2"));
2✔
3000
            wt.commit();
2✔
3001
            REQUIRE_NOTHROW(r->refresh());
2✔
3002
        }
2✔
3003
    }
10✔
3004
}
14✔
3005

3006
TEST_CASE("SharedRealm: close()") {
4✔
3007
    TestFile config;
4✔
3008
    config.schema_version = 1;
4✔
3009
    config.schema = Schema{
4✔
3010
        {"object", {{"value", PropertyType::Int}}},
4✔
3011
        {"list", {{"list", PropertyType::Object | PropertyType::Array, "object"}}},
4✔
3012
    };
4✔
3013

2✔
3014
    auto realm = Realm::get_shared_realm(config);
4✔
3015

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

1✔
3019
        realm->close();
2✔
3020
        REQUIRE(realm->is_closed());
2!
3021
        REQUIRE_EXCEPTION(realm->verify_open(), ClosedRealm, msg);
2✔
3022

1✔
3023
        REQUIRE_EXCEPTION(realm->update_schema(Schema{}), ClosedRealm, msg);
2✔
3024
        REQUIRE_EXCEPTION(realm->rename_property(Schema{}, "", "", ""), ClosedRealm, msg);
2✔
3025
        REQUIRE_EXCEPTION(realm->set_schema_subset(Schema{}), ClosedRealm, msg);
2✔
3026

1✔
3027
        REQUIRE_EXCEPTION(realm->begin_transaction(), ClosedRealm, msg);
2✔
3028
        REQUIRE_EXCEPTION(realm->commit_transaction(), ClosedRealm, msg);
2✔
3029
        REQUIRE_EXCEPTION(realm->cancel_transaction(), ClosedRealm, msg);
2✔
3030
        REQUIRE(!realm->is_in_transaction());
2!
3031

1✔
3032
        REQUIRE_EXCEPTION(realm->async_begin_transaction(nullptr), ClosedRealm, msg);
2✔
3033
        REQUIRE_EXCEPTION(realm->async_commit_transaction(nullptr), ClosedRealm, msg);
2✔
3034
        REQUIRE_EXCEPTION(realm->async_cancel_transaction(0), ClosedRealm, msg);
2✔
3035
        REQUIRE_FALSE(realm->is_in_async_transaction());
2!
3036

1✔
3037
        REQUIRE_EXCEPTION(realm->freeze(), ClosedRealm, msg);
2✔
3038
        REQUIRE_FALSE(realm->is_frozen());
2!
3039
        REQUIRE_EXCEPTION(realm->get_number_of_versions(), ClosedRealm, msg);
2✔
3040
        REQUIRE_EXCEPTION(realm->read_transaction_version(), ClosedRealm, msg);
2✔
3041
        REQUIRE_EXCEPTION(realm->duplicate(), ClosedRealm, msg);
2✔
3042

1✔
3043
        REQUIRE_EXCEPTION(realm->enable_wait_for_change(), ClosedRealm, msg);
2✔
3044
        REQUIRE_EXCEPTION(realm->wait_for_change(), ClosedRealm, msg);
2✔
3045
        REQUIRE_EXCEPTION(realm->wait_for_change_release(), ClosedRealm, msg);
2✔
3046

1✔
3047
        REQUIRE_NOTHROW(realm->notify());
2✔
3048
        REQUIRE_EXCEPTION(realm->refresh(), ClosedRealm, msg);
2✔
3049
        REQUIRE_EXCEPTION(realm->invalidate(), ClosedRealm, msg);
2✔
3050
        REQUIRE_EXCEPTION(realm->compact(), ClosedRealm, msg);
2✔
3051
        REQUIRE_EXCEPTION(realm->convert(realm->config()), ClosedRealm, msg);
2✔
3052
        REQUIRE_EXCEPTION(realm->write_copy(), ClosedRealm, msg);
2✔
3053

1✔
3054
#if REALM_ENABLE_SYNC
2✔
3055
        REQUIRE_FALSE(realm->sync_session());
2!
3056
        msg = "Flexible sync is not enabled";
2✔
3057
        REQUIRE_EXCEPTION(realm->get_latest_subscription_set(), IllegalOperation, msg);
2✔
3058
        REQUIRE_EXCEPTION(realm->get_active_subscription_set(), IllegalOperation, msg);
2✔
3059
#endif
2✔
3060
    }
2✔
3061

2✔
3062
    SECTION("fully closes database file even with live notifiers") {
4✔
3063
        auto& group = realm->read_group();
2✔
3064
        realm->begin_transaction();
2✔
3065
        auto obj = ObjectStore::table_for_object_type(group, "list")->create_object();
2✔
3066
        realm->commit_transaction();
2✔
3067

1✔
3068
        Results results(realm, ObjectStore::table_for_object_type(group, "object"));
2✔
3069
        List list(realm, obj.get_linklist("list"));
2✔
3070
        Object object(realm, obj);
2✔
3071

1✔
3072
        auto obj_token = object.add_notification_callback([](CollectionChangeSet) {});
2✔
3073
        auto list_token = list.add_notification_callback([](CollectionChangeSet) {});
2✔
3074
        auto results_token = results.add_notification_callback([](CollectionChangeSet) {});
2✔
3075

1✔
3076
        // Perform a dummy transaction to ensure the notifiers actually acquire
1✔
3077
        // resources that need to be closed
1✔
3078
        realm->begin_transaction();
2✔
3079
        realm->commit_transaction();
2✔
3080

1✔
3081
        realm->close();
2✔
3082

1✔
3083
        // Verify that we're able to acquire an exclusive lock
1✔
3084
        REQUIRE(DB::call_with_lock(config.path, [](auto) {}));
2!
3085
    }
2✔
3086
}
4✔
3087

3088
TEST_CASE("Realm::delete_files()") {
12✔
3089
    TestFile config;
12✔
3090
    config.schema_version = 1;
12✔
3091
    config.schema = Schema{{"object", {{"value", PropertyType::Int}}}};
12✔
3092
    auto realm = Realm::get_shared_realm(config);
12✔
3093
    auto path = config.path;
12✔
3094

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

6✔
3100
    SECTION("Deleting files of a closed Realm succeeds.") {
12✔
3101
        realm->close();
2✔
3102
        bool did_delete = false;
2✔
3103
        Realm::delete_files(path, &did_delete);
2✔
3104
        REQUIRE(did_delete);
2!
3105
        REQUIRE_FALSE(util::File::exists(path));
2!
3106
        REQUIRE_FALSE(util::File::exists(path + ".management"));
2!
3107
        REQUIRE_FALSE(util::File::exists(path + ".note"));
2!
3108
        REQUIRE_FALSE(util::File::exists(path + ".log"));
2!
3109

1✔
3110
        // Deleting the .lock file is not safe. It must still exist.
1✔
3111
        REQUIRE(util::File::exists(path + ".lock"));
2!
3112
    }
2✔
3113

6✔
3114
    SECTION("Trying to delete files of an open Realm fails.") {
12✔
3115
        REQUIRE_EXCEPTION(Realm::delete_files(path), ErrorCodes::DeleteOnOpenRealm,
2✔
3116
                          util::format("Cannot delete files of an open Realm: '%1' is still in use.", path));
2✔
3117
        REQUIRE(util::File::exists(path + ".lock"));
2!
3118
        REQUIRE(util::File::exists(path));
2!
3119
        REQUIRE(util::File::exists(path + ".management"));
2!
3120
#ifndef _WIN32
2✔
3121
        REQUIRE(util::File::exists(path + ".note"));
2!
3122
#endif
2✔
3123
        REQUIRE(util::File::exists(path + ".log"));
2!
3124
    }
2✔
3125

6✔
3126
    SECTION("Deleting the same Realm multiple times.") {
12✔
3127
        realm->close();
2✔
3128
        Realm::delete_files(path);
2✔
3129
        Realm::delete_files(path);
2✔
3130
        Realm::delete_files(path);
2✔
3131
    }
2✔
3132

6✔
3133
    SECTION("Calling delete on a folder that does not exist.") {
12✔
3134
        auto fake_path = "/tmp/doesNotExist/realm.424242";
2✔
3135
        bool did_delete = false;
2✔
3136
        Realm::delete_files(fake_path, &did_delete);
2✔
3137
        REQUIRE_FALSE(did_delete);
2!
3138
    }
2✔
3139

6✔
3140
    SECTION("passing did_delete is optional") {
12✔
3141
        realm->close();
2✔
3142
        Realm::delete_files(path, nullptr);
2✔
3143
    }
2✔
3144

6✔
3145
    SECTION("Deleting a Realm which does not exist does not set did_delete") {
12✔
3146
        TestFile new_config;
2✔
3147
        bool did_delete = false;
2✔
3148
        Realm::delete_files(new_config.path, &did_delete);
2✔
3149
        REQUIRE_FALSE(did_delete);
2!
3150
    }
2✔
3151
}
12✔
3152

3153
TEST_CASE("ShareRealm: in-memory mode from buffer") {
2✔
3154
    TestFile config;
2✔
3155
    config.schema_version = 1;
2✔
3156
    config.schema = Schema{
2✔
3157
        {"object", {{"value", PropertyType::Int}}},
2✔
3158
    };
2✔
3159

1✔
3160
    SECTION("Save and open Realm from in-memory buffer") {
2✔
3161
        // Write in-memory copy of Realm to a buffer
1✔
3162
        auto realm = Realm::get_shared_realm(config);
2✔
3163
        OwnedBinaryData realm_buffer = realm->write_copy();
2✔
3164

1✔
3165
        // Open the buffer as a new (immutable in-memory) Realm
1✔
3166
        realm::Realm::Config config2;
2✔
3167
        config2.in_memory = true;
2✔
3168
        config2.schema_mode = SchemaMode::Immutable;
2✔
3169
        config2.realm_data = realm_buffer.get();
2✔
3170

1✔
3171
        auto realm2 = Realm::get_shared_realm(config2);
2✔
3172

1✔
3173
        // Verify that it can read the schema and that it is the same
1✔
3174
        REQUIRE(realm->schema().size() == 1);
2!
3175
        auto it = realm->schema().find("object");
2✔
3176
        auto table = realm->read_group().get_table("class_object");
2✔
3177
        REQUIRE(it != realm->schema().end());
2!
3178
        REQUIRE(it->table_key == table->get_key());
2!
3179
        REQUIRE(it->persisted_properties.size() == 1);
2!
3180
        REQUIRE(it->persisted_properties[0].name == "value");
2!
3181
        REQUIRE(it->persisted_properties[0].column_key == table->get_column_key("value"));
2!
3182

1✔
3183
        // Test invalid configs
1✔
3184
        realm::Realm::Config config3;
2✔
3185
        config3.realm_data = realm_buffer.get();
2✔
3186
        REQUIRE_EXCEPTION(Realm::get_shared_realm(config3), IllegalCombination,
2✔
3187
                          "In-memory realms initialized from memory buffers can only be opened in read-only mode");
2✔
3188

1✔
3189
        config3.in_memory = true;
2✔
3190
        config3.schema_mode = SchemaMode::Immutable;
2✔
3191
        config3.path = "path";
2✔
3192
        REQUIRE_EXCEPTION(Realm::get_shared_realm(config3), IllegalCombination,
2✔
3193
                          "Specifying both memory buffer and path is invalid");
2✔
3194

1✔
3195
        config3.path = "";
2✔
3196
        config3.encryption_key = std::vector<char>(64, 'a');
2✔
3197
        REQUIRE_EXCEPTION(Realm::get_shared_realm(config3), IllegalCombination,
2✔
3198
                          "Memory buffers do not support encryption");
2✔
3199
    }
2✔
3200
}
2✔
3201

3202
TEST_CASE("ShareRealm: realm closed in did_change callback") {
6✔
3203
    TestFile config;
6✔
3204
    config.schema_version = 1;
6✔
3205
    config.schema = Schema{
6✔
3206
        {"object", {{"value", PropertyType::Int}}},
6✔
3207
    };
6✔
3208
    config.automatic_change_notifications = false;
6✔
3209
    auto r1 = Realm::get_shared_realm(config);
6✔
3210

3✔
3211
    r1->begin_transaction();
6✔
3212
    auto table = r1->read_group().get_table("class_object");
6✔
3213
    table->create_object();
6✔
3214
    r1->commit_transaction();
6✔
3215

3✔
3216
    struct Context : public BindingContext {
6✔
3217
        Context(std::shared_ptr<Realm>& realm)
6✔
3218
            : realm(&realm)
6✔
3219
        {
6✔
3220
        }
6✔
3221
        std::shared_ptr<Realm>* realm;
6✔
3222
        void did_change(std::vector<ObserverState> const&, std::vector<void*> const&, bool) override
6✔
3223
        {
6✔
3224
            auto realm = this->realm; // close() will delete `this`
6✔
3225
            (*realm)->close();
6✔
3226
            realm->reset();
6✔
3227
        }
6✔
3228
    };
6✔
3229

3✔
3230
    SECTION("did_change") {
6✔
3231
        r1->m_binding_context.reset(new Context(r1));
2✔
3232
        r1->invalidate();
2✔
3233

1✔
3234
        auto r2 = Realm::get_shared_realm(config);
2✔
3235
        r2->begin_transaction();
2✔
3236
        r2->read_group().get_table("class_object")->create_object();
2✔
3237
        r2->commit_transaction();
2✔
3238
        r2.reset();
2✔
3239

1✔
3240
        r1->notify();
2✔
3241
    }
2✔
3242

3✔
3243
    SECTION("did_change with async results") {
6✔
3244
        r1->m_binding_context.reset(new Context(r1));
2✔
3245
        Results results(r1, table->where());
2✔
3246
        auto token = results.add_notification_callback([&](CollectionChangeSet) {
1✔
3247
            // Should not be called.
UNCOV
3248
            REQUIRE(false);
×
UNCOV
3249
        });
×
3250

1✔
3251
        auto r2 = Realm::get_shared_realm(config);
2✔
3252
        r2->begin_transaction();
2✔
3253
        r2->read_group().get_table("class_object")->create_object();
2✔
3254
        r2->commit_transaction();
2✔
3255
        r2.reset();
2✔
3256

1✔
3257
        auto coordinator = _impl::RealmCoordinator::get_coordinator(config.path);
2✔
3258
        coordinator->on_change();
2✔
3259

1✔
3260
        r1->notify();
2✔
3261
    }
2✔
3262

3✔
3263
    SECTION("refresh") {
6✔
3264
        r1->m_binding_context.reset(new Context(r1));
2✔
3265

1✔
3266
        auto r2 = Realm::get_shared_realm(config);
2✔
3267
        r2->begin_transaction();
2✔
3268
        r2->read_group().get_table("class_object")->create_object();
2✔
3269
        r2->commit_transaction();
2✔
3270
        r2.reset();
2✔
3271

1✔
3272
        REQUIRE_FALSE(r1->refresh());
2!
3273
    }
2✔
3274
}
6✔
3275

3276
TEST_CASE("RealmCoordinator: schema cache") {
16✔
3277
    TestFile config;
16✔
3278
    auto coordinator = _impl::RealmCoordinator::get_coordinator(config.path);
16✔
3279

8✔
3280
    Schema cache_schema;
16✔
3281
    uint64_t cache_sv = -1, cache_tv = -1;
16✔
3282

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

8✔
3297
    SECTION("valid initial schema sets cache") {
16✔
3298
        coordinator->cache_schema(schema, 5, 10);
2✔
3299
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3300
        REQUIRE(cache_schema == schema);
2!
3301
        REQUIRE(cache_sv == 5);
2!
3302
        REQUIRE(cache_tv == 10);
2!
3303
    }
2✔
3304

8✔
3305
    SECTION("cache can be updated with newer schema") {
16✔
3306
        coordinator->cache_schema(schema, 5, 10);
2✔
3307
        coordinator->cache_schema(schema2, 6, 11);
2✔
3308
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3309
        REQUIRE(cache_schema == schema2);
2!
3310
        REQUIRE(cache_sv == 6);
2!
3311
        REQUIRE(cache_tv == 11);
2!
3312
    }
2✔
3313

8✔
3314
    SECTION("empty schema is ignored") {
16✔
3315
        coordinator->cache_schema(Schema{}, 5, 10);
2✔
3316
        REQUIRE_FALSE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3317

1✔
3318
        coordinator->cache_schema(schema, 5, 10);
2✔
3319
        coordinator->cache_schema(Schema{}, 5, 10);
2✔
3320
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3321
        REQUIRE(cache_schema == schema);
2!
3322
        REQUIRE(cache_sv == 5);
2!
3323
        REQUIRE(cache_tv == 10);
2!
3324
    }
2✔
3325

8✔
3326
    SECTION("schema for older transaction is ignored") {
16✔
3327
        coordinator->cache_schema(schema, 5, 10);
2✔
3328
        coordinator->cache_schema(schema2, 4, 8);
2✔
3329

1✔
3330
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3331
        REQUIRE(cache_schema == schema);
2!
3332
        REQUIRE(cache_sv == 5);
2!
3333
        REQUIRE(cache_tv == 10);
2!
3334

1✔
3335
        coordinator->advance_schema_cache(10, 20);
2✔
3336
        coordinator->cache_schema(schema, 6, 15);
2✔
3337
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3338
        REQUIRE(cache_tv == 20); // should not have dropped to 15
2!
3339
    }
2✔
3340

8✔
3341
    SECTION("advance_schema() from transaction version bumps transaction version") {
16✔
3342
        coordinator->cache_schema(schema, 5, 10);
2✔
3343
        coordinator->advance_schema_cache(10, 12);
2✔
3344
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3345
        REQUIRE(cache_schema == schema);
2!
3346
        REQUIRE(cache_sv == 5);
2!
3347
        REQUIRE(cache_tv == 12);
2!
3348
    }
2✔
3349

8✔
3350
    SECTION("advance_schema() ending before transaction version does nothing") {
16✔
3351
        coordinator->cache_schema(schema, 5, 10);
2✔
3352
        coordinator->advance_schema_cache(8, 9);
2✔
3353
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3354
        REQUIRE(cache_schema == schema);
2!
3355
        REQUIRE(cache_sv == 5);
2!
3356
        REQUIRE(cache_tv == 10);
2!
3357
    }
2✔
3358

8✔
3359
    SECTION("advance_schema() extending over transaction version bumps version") {
16✔
3360
        coordinator->cache_schema(schema, 5, 10);
2✔
3361
        coordinator->advance_schema_cache(3, 15);
2✔
3362
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3363
        REQUIRE(cache_schema == schema);
2!
3364
        REQUIRE(cache_sv == 5);
2!
3365
        REQUIRE(cache_tv == 15);
2!
3366
    }
2✔
3367

8✔
3368
    SECTION("advance_schema() with no cahced schema does nothing") {
16✔
3369
        coordinator->advance_schema_cache(3, 15);
2✔
3370
        REQUIRE_FALSE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3371
    }
2✔
3372
}
16✔
3373

3374
TEST_CASE("SharedRealm: coordinator schema cache") {
26✔
3375
    TestFile config;
26✔
3376
    auto r = Realm::get_shared_realm(config);
26✔
3377
    auto coordinator = _impl::RealmCoordinator::get_coordinator(config.path);
26✔
3378

13✔
3379
    Schema cache_schema;
26✔
3380
    uint64_t cache_sv = -1, cache_tv = -1;
26✔
3381

13✔
3382
    Schema schema{
26✔
3383
        {"object", {{"value", PropertyType::Int}}},
26✔
3384
    };
26✔
3385
    Schema schema2{
26✔
3386
        {"object",
26✔
3387
         {
26✔
3388
             {"value", PropertyType::Int},
26✔
3389
         }},
26✔
3390
        {"object 2",
26✔
3391
         {
26✔
3392
             {"value", PropertyType::Int},
26✔
3393
         }},
26✔
3394
    };
26✔
3395

13✔
3396
    class ExternalWriter {
26✔
3397
    private:
26✔
3398
        std::shared_ptr<Realm> m_realm;
26✔
3399

13✔
3400
    public:
26✔
3401
        WriteTransaction wt;
26✔
3402
        ExternalWriter(Realm::Config const& config)
26✔
3403
            : m_realm([&] {
22✔
3404
                auto c = config;
18✔
3405
                c.scheduler = util::Scheduler::make_frozen(VersionID());
18✔
3406
                return _impl::RealmCoordinator::get_coordinator(c.path)->get_realm(c, util::none);
18✔
3407
            }())
18✔
3408
            , wt(TestHelper::get_db(m_realm))
26✔
3409
        {
22✔
3410
        }
18✔
3411
    };
26✔
3412

13✔
3413
    auto external_write = [&](Realm::Config const& config, auto&& fn) {
21✔
3414
        ExternalWriter wt(config);
16✔
3415
        fn(wt.wt);
16✔
3416
        wt.wt.commit();
16✔
3417
    };
16✔
3418

13✔
3419
    SECTION("is initially empty for uninitialized file") {
26✔
3420
        REQUIRE_FALSE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3421
    }
2✔
3422
    r->update_schema(schema);
26✔
3423

13✔
3424
    SECTION("is populated after calling update_schema()") {
26✔
3425
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3426
        REQUIRE(cache_sv == 0);
2!
3427
        REQUIRE(cache_schema == schema);
2!
3428
        REQUIRE(cache_schema.begin()->persisted_properties[0].column_key != ColKey{});
2!
3429
    }
2✔
3430

13✔
3431
    coordinator = nullptr;
26✔
3432
    r = nullptr;
26✔
3433
    r = Realm::get_shared_realm(config);
26✔
3434
    coordinator = _impl::RealmCoordinator::get_coordinator(config.path);
26✔
3435
    REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
26!
3436

13✔
3437
    SECTION("is populated after opening an initialized file") {
26✔
3438
        REQUIRE(cache_sv == 0);
2!
3439
        REQUIRE(cache_tv == 2); // with in-realm history the version doesn't reset
2!
3440
        REQUIRE(cache_schema == schema);
2!
3441
        REQUIRE(cache_schema.begin()->persisted_properties[0].column_key != ColKey{});
2!
3442
    }
2✔
3443

13✔
3444
    SECTION("transaction version is bumped after a local write") {
26✔
3445
        auto tv = cache_tv;
2✔
3446
        r->begin_transaction();
2✔
3447
        r->commit_transaction();
2✔
3448
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3449
        REQUIRE(cache_tv == tv + 1);
2!
3450
    }
2✔
3451

13✔
3452
    SECTION("notify() without a read transaction does not bump transaction version") {
26✔
3453
        auto tv = cache_tv;
4✔
3454

2✔
3455
        SECTION("non-schema change") {
4✔
3456
            external_write(config, [](auto& wt) {
2✔
3457
                wt.get_table("class_object")->create_object();
2✔
3458
            });
2✔
3459
        }
2✔
3460
        SECTION("schema change") {
4✔
3461
            external_write(config, [](auto& wt) {
2✔
3462
                wt.add_table("class_object 2");
2✔
3463
            });
2✔
3464
        }
2✔
3465

2✔
3466
        r->notify();
4✔
3467
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
4!
3468
        REQUIRE(cache_tv == tv);
4!
3469
        REQUIRE(cache_schema == schema);
4!
3470
    }
4✔
3471

13✔
3472
    SECTION("notify() with a read transaction bumps transaction version") {
26✔
3473
        r->read_group();
2✔
3474
        external_write(config, [](auto& wt) {
2✔
3475
            wt.get_table("class_object")->create_object();
2✔
3476
        });
2✔
3477

1✔
3478
        r->notify();
2✔
3479
        auto tv = cache_tv;
2✔
3480
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3481
        REQUIRE(cache_tv == tv + 1);
2!
3482
    }
2✔
3483

13✔
3484
    SECTION("notify() with a read transaction updates schema folloing external schema change") {
26✔
3485
        r->read_group();
2✔
3486
        external_write(config, [](auto& wt) {
2✔
3487
            wt.add_table("class_object 2");
2✔
3488
        });
2✔
3489

1✔
3490
        r->notify();
2✔
3491
        auto tv = cache_tv;
2✔
3492
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3493
        REQUIRE(cache_tv == tv + 1);
2!
3494
        REQUIRE(cache_schema.size() == 2);
2!
3495
        REQUIRE(cache_schema.find("object 2") != cache_schema.end());
2!
3496
    }
2✔
3497

13✔
3498
    SECTION("transaction version is bumped after refresh() following external non-schema write") {
26✔
3499
        external_write(config, [](auto& wt) {
2✔
3500
            wt.get_table("class_object")->create_object();
2✔
3501
        });
2✔
3502

1✔
3503
        r->refresh();
2✔
3504
        auto tv = cache_tv;
2✔
3505
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3506
        REQUIRE(cache_tv == tv + 1);
2!
3507
    }
2✔
3508

13✔
3509
    SECTION("schema is reread following refresh() over external schema change") {
26✔
3510
        external_write(config, [](auto& wt) {
2✔
3511
            wt.add_table("class_object 2");
2✔
3512
        });
2✔
3513

1✔
3514
        r->refresh();
2✔
3515
        auto tv = cache_tv;
2✔
3516
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3517
        REQUIRE(cache_tv == tv + 1);
2!
3518
        REQUIRE(cache_schema.size() == 2);
2!
3519
        REQUIRE(cache_schema.find("object 2") != cache_schema.end());
2!
3520
    }
2✔
3521

13✔
3522
    SECTION("update_schema() to version already on disk updates cache") {
26✔
3523
        r->read_group();
2✔
3524
        external_write(config, [](auto& wt) {
2✔
3525
            auto table = wt.add_table("class_object 2");
2✔
3526
            table->add_column(type_Int, "value");
2✔
3527
        });
2✔
3528

1✔
3529
        auto tv = cache_tv;
2✔
3530
        r->update_schema(schema2);
2✔
3531

1✔
3532
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3533
        REQUIRE(cache_tv == tv + 1); // only +1 because update_schema() did not perform a write
2!
3534
        REQUIRE(cache_schema.size() == 2);
2!
3535
        REQUIRE(cache_schema.find("object 2") != cache_schema.end());
2!
3536
    }
2✔
3537

13✔
3538
    SECTION("update_schema() to version already on disk updates cache") {
26✔
3539
        r->read_group();
2✔
3540
        external_write(config, [](auto& wt) {
2✔
3541
            auto table = wt.add_table("class_object 2");
2✔
3542
            table->add_column(type_Int, "value");
2✔
3543
        });
2✔
3544

1✔
3545
        auto tv = cache_tv;
2✔
3546
        r->update_schema(schema2);
2✔
3547

1✔
3548
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3549
        REQUIRE(cache_tv == tv + 1); // only +1 because update_schema() did not perform a write
2!
3550
        REQUIRE(cache_schema.size() == 2);
2!
3551
        REQUIRE(cache_schema.find("object 2") != cache_schema.end());
2!
3552
    }
2✔
3553

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

1✔
3557
        // We want to commit the write while we're waiting on the write lock on
1✔
3558
        // this thread, which can't really be done in a properly synchronized manner
1✔
3559
        std::chrono::microseconds wait_time{5000};
2✔
3560
#if REALM_ANDROID
3561
        // When running on device or in an emulator we need to wait longer due
3562
        // to them being slow
3563
        wait_time *= 10;
3564
#endif
3565

1✔
3566
        bool did_run = false;
2✔
3567
        JoiningThread thread([&] {
2✔
3568
            ExternalWriter writer(config);
2✔
3569
            if (writer.wt.get_table("class_object 2"))
2✔
UNCOV
3570
                return;
×
3571
            did_run = true;
2✔
3572

1✔
3573
            auto table = writer.wt.add_table("class_object 2");
2✔
3574
            table->add_column(type_Int, "value");
2✔
3575
            std::this_thread::sleep_for(wait_time * 2);
2✔
3576
            writer.wt.commit();
2✔
3577
        });
2✔
3578
        std::this_thread::sleep_for(wait_time);
2✔
3579

1✔
3580
        auto tv = cache_tv;
2✔
3581
        r->update_schema(Schema{
2✔
3582
            {"object", {{"value", PropertyType::Int}}},
2✔
3583
            {"object 2", {{"value", PropertyType::Int}}},
2✔
3584
        });
2✔
3585

1✔
3586
        // just skip the test if the timing was wrong to avoid spurious failures
1✔
3587
        if (!did_run)
2✔
UNCOV
3588
            return;
×
3589

1✔
3590
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3591
        REQUIRE(cache_tv == tv + 1); // only +1 because update_schema()'s write was rolled back
2!
3592
        REQUIRE(cache_schema.size() == 2);
2!
3593
        REQUIRE(cache_schema.find("object 2") != cache_schema.end());
2!
3594
    }
2✔
3595
}
26✔
3596

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

1✔
3600
    // Prepopulate the Realm with the schema.
1✔
3601
    Realm::Config config_with_schema = config;
2✔
3602
    config_with_schema.schema_version = 1;
2✔
3603
    config_with_schema.schema_mode = SchemaMode::Automatic;
2✔
3604
    config_with_schema.schema =
2✔
3605
        Schema{{"object",
2✔
3606
                {
2✔
3607
                    {"value", PropertyType::Int, Property::IsPrimary{true}},
2✔
3608
                    {"value 2", PropertyType::Int, Property::IsPrimary{false}, Property::IsIndexed{true}},
2✔
3609
                }}};
2✔
3610
    auto r1 = Realm::get_shared_realm(config_with_schema);
2✔
3611

1✔
3612
    // Retrieve the object schema in dynamic mode.
1✔
3613
    auto r2 = Realm::get_shared_realm(config);
2✔
3614
    auto* object_schema = &*r2->schema().find("object");
2✔
3615

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

1✔
3620
    // Advance to the latest version, and verify the object schema is at the same location in memory.
1✔
3621
    r2->read_group();
2✔
3622
    REQUIRE(object_schema == &*r2->schema().find("object"));
2!
3623
}
2✔
3624

3625
TEST_CASE("SharedRealm: declaring an object as embedded results in creating an embedded table") {
2✔
3626
    TestFile config;
2✔
3627

1✔
3628
    // Prepopulate the Realm with the schema.
1✔
3629
    config.schema = Schema{{"object1",
2✔
3630
                            ObjectSchema::ObjectType::Embedded,
2✔
3631
                            {
2✔
3632
                                {"value", PropertyType::Int},
2✔
3633
                            }},
2✔
3634
                           {"object2",
2✔
3635
                            {
2✔
3636
                                {"value", PropertyType::Object | PropertyType::Nullable, "object1"},
2✔
3637
                            }}};
2✔
3638
    auto r1 = Realm::get_shared_realm(config);
2✔
3639

1✔
3640
    Group& g = r1->read_group();
2✔
3641
    auto t = g.get_table("class_object1");
2✔
3642
    REQUIRE(t->is_embedded());
2!
3643
}
2✔
3644

3645
TEST_CASE("SharedRealm: SchemaChangedFunction") {
16✔
3646
    struct Context : BindingContext {
16✔
3647
        size_t* change_count;
16✔
3648
        Schema* schema;
16✔
3649
        Context(size_t* count_out, Schema* schema_out)
16✔
3650
            : change_count(count_out)
16✔
3651
            , schema(schema_out)
16✔
3652
        {
22✔
3653
        }
22✔
3654

8✔
3655
        void schema_did_change(Schema const& changed_schema) override
16✔
3656
        {
13✔
3657
            ++*change_count;
10✔
3658
            *schema = changed_schema;
10✔
3659
        }
10✔
3660
    };
16✔
3661

8✔
3662
    size_t schema_changed_called = 0;
16✔
3663
    Schema changed_fixed_schema;
16✔
3664
    TestFile config;
16✔
3665
    RealmConfig dynamic_config = config;
16✔
3666

8✔
3667
    config.schema = Schema{{"object1",
16✔
3668
                            {
16✔
3669
                                {"value", PropertyType::Int},
16✔
3670
                            }},
16✔
3671
                           {"object2",
16✔
3672
                            {
16✔
3673
                                {"value", PropertyType::Int},
16✔
3674
                            }}};
16✔
3675
    config.schema_version = 1;
16✔
3676
    auto r1 = Realm::get_shared_realm(config);
16✔
3677
    r1->read_group();
16✔
3678
    r1->m_binding_context.reset(new Context(&schema_changed_called, &changed_fixed_schema));
16✔
3679

8✔
3680
    SECTION("Fixed schema") {
16✔
3681
        SECTION("update_schema") {
10✔
3682
            auto new_schema = Schema{{"object3",
2✔
3683
                                      {
2✔
3684
                                          {"value", PropertyType::Int},
2✔
3685
                                      }}};
2✔
3686
            r1->update_schema(new_schema, 2);
2✔
3687
            REQUIRE(schema_changed_called == 1);
2!
3688
            REQUIRE(changed_fixed_schema.find("object3")->property_for_name("value")->column_key != ColKey{});
2!
3689
        }
2✔
3690

5✔
3691
        SECTION("Open a new Realm instance with same config won't trigger") {
10✔
3692
            auto r2 = Realm::get_shared_realm(config);
2✔
3693
            REQUIRE(schema_changed_called == 0);
2!
3694
        }
2✔
3695

5✔
3696
        SECTION("Non schema related transaction doesn't trigger") {
10✔
3697
            auto r2 = Realm::get_shared_realm(config);
2✔
3698
            r2->begin_transaction();
2✔
3699
            r2->commit_transaction();
2✔
3700
            r1->refresh();
2✔
3701
            REQUIRE(schema_changed_called == 0);
2!
3702
        }
2✔
3703

5✔
3704
        SECTION("Schema is changed by another Realm") {
10✔
3705
            auto r2 = Realm::get_shared_realm(config);
2✔
3706
            r2->begin_transaction();
2✔
3707
            r2->read_group().get_table("class_object1")->add_column(type_String, "new col");
2✔
3708
            r2->commit_transaction();
2✔
3709
            r1->refresh();
2✔
3710
            REQUIRE(schema_changed_called == 1);
2!
3711
            REQUIRE(changed_fixed_schema.find("object1")->property_for_name("value")->column_key != ColKey{});
2!
3712
        }
2✔
3713

5✔
3714
        // This is not a valid use case. m_schema won't be refreshed.
5✔
3715
        SECTION("Schema is changed by this Realm won't trigger") {
10✔
3716
            r1->begin_transaction();
2✔
3717
            r1->read_group().get_table("class_object1")->add_column(type_String, "new col");
2✔
3718
            r1->commit_transaction();
2✔
3719
            REQUIRE(schema_changed_called == 0);
2!
3720
        }
2✔
3721
    }
10✔
3722

8✔
3723
    SECTION("Dynamic schema") {
16✔
3724
        size_t dynamic_schema_changed_called = 0;
6✔
3725
        Schema changed_dynamic_schema;
6✔
3726
        auto r2 = Realm::get_shared_realm(dynamic_config);
6✔
3727
        r2->m_binding_context.reset(new Context(&dynamic_schema_changed_called, &changed_dynamic_schema));
6✔
3728

3✔
3729
        SECTION("set_schema_subset") {
6✔
3730
            auto new_schema = Schema{{"object1",
2✔
3731
                                      {
2✔
3732
                                          {"value", PropertyType::Int},
2✔
3733
                                      }}};
2✔
3734
            r2->set_schema_subset(new_schema);
2✔
3735
            REQUIRE(schema_changed_called == 0);
2!
3736
            REQUIRE(dynamic_schema_changed_called == 1);
2!
3737
            REQUIRE(changed_dynamic_schema.find("object1")->property_for_name("value")->column_key != ColKey{});
2!
3738
        }
2✔
3739

3✔
3740
        SECTION("Non schema related transaction will always trigger in dynamic mode") {
6✔
3741
            auto r1 = Realm::get_shared_realm(config);
2✔
3742
            // An empty transaction will trigger the schema changes always in dynamic mode.
1✔
3743
            r1->begin_transaction();
2✔
3744
            r1->commit_transaction();
2✔
3745
            r2->refresh();
2✔
3746
            REQUIRE(dynamic_schema_changed_called == 1);
2!
3747
            REQUIRE(changed_dynamic_schema.find("object1")->property_for_name("value")->column_key != ColKey{});
2!
3748
        }
2✔
3749

3✔
3750
        SECTION("Schema is changed by another Realm") {
6✔
3751
            r1->begin_transaction();
2✔
3752
            r1->read_group().get_table("class_object1")->add_column(type_String, "new col");
2✔
3753
            r1->commit_transaction();
2✔
3754
            r2->refresh();
2✔
3755
            REQUIRE(dynamic_schema_changed_called == 1);
2!
3756
            REQUIRE(changed_dynamic_schema.find("object1")->property_for_name("value")->column_key != ColKey{});
2!
3757
        }
2✔
3758
    }
6✔
3759
}
16✔
3760

3761
TEST_CASE("SharedRealm: compact on launch") {
4✔
3762
    // Make compactable Realm
2✔
3763
    TestFile config;
4✔
3764
    config.automatic_change_notifications = false;
4✔
3765
    int num_opens = 0;
4✔
3766
    config.should_compact_on_launch_function = [&](uint64_t total_bytes, uint64_t used_bytes) {
12✔
3767
        REQUIRE(total_bytes > used_bytes);
12!
3768
        num_opens++;
12✔
3769
        return num_opens != 2;
12✔
3770
    };
12✔
3771
    config.schema = Schema{
4✔
3772
        {"object", {{"value", PropertyType::String}}},
4✔
3773
    };
4✔
3774
    REQUIRE(num_opens == 0);
4!
3775
    auto r = Realm::get_shared_realm(config);
4✔
3776
    REQUIRE(num_opens == 1);
4!
3777
    r->begin_transaction();
4✔
3778
    auto table = r->read_group().get_table("class_object");
4✔
3779
    size_t count = 1000;
4✔
3780
    for (size_t i = 0; i < count; ++i)
4,004✔
3781
        table->create_object().set_all(util::format("Foo_%1", i % 10).c_str());
4,000✔
3782
    r->commit_transaction();
4✔
3783
    REQUIRE(table->size() == count);
4!
3784
    r->close();
4✔
3785

2✔
3786
    SECTION("compact reduces the file size") {
4✔
3787
#ifndef _WIN32
2✔
3788
        // Confirm expected sizes before and after opening the Realm
1✔
3789
        size_t size_before = size_t(util::File(config.path).get_size());
2✔
3790
        r = Realm::get_shared_realm(config);
2✔
3791
        REQUIRE(num_opens == 2);
2!
3792
        r->close();
2✔
3793
        REQUIRE(size_t(util::File(config.path).get_size()) == size_before); // File size after returning false
2!
3794
        r = Realm::get_shared_realm(config);
2✔
3795
        REQUIRE(num_opens == 3);
2!
3796
        REQUIRE(size_t(util::File(config.path).get_size()) < size_before); // File size after returning true
2!
3797

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

1✔
3801
        // Registering for a collection notification shouldn't crash when compact on launch is used.
1✔
3802
        Results results(r, r->read_group().get_table("class_object"));
2✔
3803
        results.add_notification_callback([](CollectionChangeSet const&) {});
1✔
3804
        r->close();
2✔
3805
#endif
2✔
3806
    }
2✔
3807

2✔
3808
    SECTION("compact function does not get invoked if realm is open on another thread") {
4✔
3809
        config.scheduler = util::Scheduler::make_frozen(VersionID());
2✔
3810
        r = Realm::get_shared_realm(config);
2✔
3811
        REQUIRE(num_opens == 2);
2!
3812
        std::thread([&] {
2✔
3813
            auto r2 = Realm::get_shared_realm(config);
2✔
3814
            REQUIRE(num_opens == 2);
2!
3815
        }).join();
2✔
3816
        r->close();
2✔
3817
        std::thread([&] {
2✔
3818
            auto r3 = Realm::get_shared_realm(config);
2✔
3819
            REQUIRE(num_opens == 3);
2!
3820
        }).join();
2✔
3821
    }
2✔
3822
}
4✔
3823

3824
struct ModeAutomatic {
3825
    static constexpr SchemaMode mode = SchemaMode::Automatic;
3826
    static constexpr bool should_call_init_on_version_bump = false;
3827
};
3828
struct ModeAdditive {
3829
    static constexpr SchemaMode mode = SchemaMode::AdditiveExplicit;
3830
    static constexpr bool should_call_init_on_version_bump = false;
3831
};
3832
struct ModeManual {
3833
    static constexpr SchemaMode mode = SchemaMode::Manual;
3834
    static constexpr bool should_call_init_on_version_bump = false;
3835
};
3836
struct ModeSoftResetFile {
3837
    static constexpr SchemaMode mode = SchemaMode::SoftResetFile;
3838
    static constexpr bool should_call_init_on_version_bump = true;
3839
};
3840
struct ModeHardResetFile {
3841
    static constexpr SchemaMode mode = SchemaMode::HardResetFile;
3842
    static constexpr bool should_call_init_on_version_bump = true;
3843
};
3844

3845
TEMPLATE_TEST_CASE("SharedRealm: update_schema with initialization_function", "[init][update schema]", ModeAutomatic,
3846
                   ModeAdditive, ModeManual, ModeSoftResetFile, ModeHardResetFile)
3847
{
30✔
3848
    TestFile config;
30✔
3849
    config.schema_mode = TestType::mode;
30✔
3850
    bool initialization_function_called = false;
30✔
3851
    uint64_t schema_version_in_callback = -1;
30✔
3852
    Schema schema_in_callback;
30✔
3853
    auto initialization_function = [&initialization_function_called, &schema_version_in_callback,
30✔
3854
                                    &schema_in_callback](auto shared_realm) {
27✔
3855
        REQUIRE(shared_realm->is_in_transaction());
24!
3856
        initialization_function_called = true;
24✔
3857
        schema_version_in_callback = shared_realm->schema_version();
24✔
3858
        schema_in_callback = shared_realm->schema();
24✔
3859
    };
24✔
3860

15✔
3861
    Schema schema{
30✔
3862
        {"object", {{"value", PropertyType::String}}},
30✔
3863
    };
30✔
3864

15✔
3865
    SECTION("call initialization function directly by update_schema") {
30✔
3866
        // Open in dynamic mode with no schema specified
5✔
3867
        auto realm = Realm::get_shared_realm(config);
10✔
3868
        REQUIRE_FALSE(initialization_function_called);
10!
3869

5✔
3870
        realm->update_schema(schema, 0, nullptr, initialization_function);
10✔
3871
        REQUIRE(initialization_function_called);
10!
3872
        REQUIRE(schema_version_in_callback == 0);
10!
3873
        REQUIRE(schema_in_callback.compare(schema).size() == 0);
10!
3874
    }
10✔
3875

15✔
3876
    config.schema_version = 0;
30✔
3877
    config.schema = schema;
30✔
3878

15✔
3879
    SECTION("initialization function should be called for unversioned realm") {
30✔
3880
        config.initialization_function = initialization_function;
10✔
3881
        Realm::get_shared_realm(config);
10✔
3882
        REQUIRE(initialization_function_called);
10!
3883
        REQUIRE(schema_version_in_callback == 0);
10!
3884
        REQUIRE(schema_in_callback.compare(schema).size() == 0);
10!
3885
    }
10✔
3886

15✔
3887
    SECTION("initialization function for versioned realm") {
30✔
3888
        // Initialize v0
5✔
3889
        Realm::get_shared_realm(config);
10✔
3890

5✔
3891
        config.schema_version = 1;
10✔
3892
        config.initialization_function = initialization_function;
10✔
3893
        Realm::get_shared_realm(config);
10✔
3894
        REQUIRE(initialization_function_called == TestType::should_call_init_on_version_bump);
10!
3895
        if (TestType::should_call_init_on_version_bump) {
10✔
3896
            REQUIRE(schema_version_in_callback == 1);
4!
3897
            REQUIRE(schema_in_callback.compare(schema).size() == 0);
4!
3898
        }
4✔
3899
    }
10✔
3900
}
30✔
3901

3902
TEST_CASE("BindingContext is notified about delivery of change notifications") {
16✔
3903
    _impl::RealmCoordinator::assert_no_open_realms();
16✔
3904
    InMemoryTestFile config;
16✔
3905
    config.automatic_change_notifications = false;
16✔
3906

8✔
3907
    auto r = Realm::get_shared_realm(config);
16✔
3908
    r->update_schema({
16✔
3909
        {"object", {{"value", PropertyType::Int}}},
16✔
3910
    });
16✔
3911

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

8✔
3915
    SECTION("BindingContext notified even if no callbacks are registered") {
16✔
3916
        static int binding_context_start_notify_calls = 0;
4✔
3917
        static int binding_context_end_notify_calls = 0;
4✔
3918
        struct Context : BindingContext {
4✔
3919
            void will_send_notifications() override
4✔
3920
            {
4✔
3921
                ++binding_context_start_notify_calls;
4✔
3922
            }
4✔
3923

2✔
3924
            void did_send_notifications() override
4✔
3925
            {
4✔
3926
                ++binding_context_end_notify_calls;
4✔
3927
            }
4✔
3928
        };
4✔
3929
        r->m_binding_context.reset(new Context());
4✔
3930

2✔
3931
        SECTION("local commit") {
4✔
3932
            binding_context_start_notify_calls = 0;
2✔
3933
            binding_context_end_notify_calls = 0;
2✔
3934
            coordinator->on_change();
2✔
3935
            r->begin_transaction();
2✔
3936
            REQUIRE(binding_context_start_notify_calls == 1);
2!
3937
            REQUIRE(binding_context_end_notify_calls == 1);
2!
3938
            r->cancel_transaction();
2✔
3939
        }
2✔
3940

2✔
3941
        SECTION("remote commit") {
4✔
3942
            binding_context_start_notify_calls = 0;
2✔
3943
            binding_context_end_notify_calls = 0;
2✔
3944
            JoiningThread([&] {
2✔
3945
                auto r2 = coordinator->get_realm(util::Scheduler::make_frozen(VersionID()));
2✔
3946
                r2->begin_transaction();
2✔
3947
                auto table2 = r2->read_group().get_table("class_object");
2✔
3948
                table2->create_object();
2✔
3949
                r2->commit_transaction();
2✔
3950
            });
2✔
3951
            advance_and_notify(*r);
2✔
3952
            REQUIRE(binding_context_start_notify_calls == 1);
2!
3953
            REQUIRE(binding_context_end_notify_calls == 1);
2!
3954
        }
2✔
3955
    }
4✔
3956

8✔
3957
    SECTION("notify BindingContext before and after sending notifications") {
16✔
3958
        static int binding_context_start_notify_calls = 0;
4✔
3959
        static int binding_context_end_notify_calls = 0;
4✔
3960
        static int notification_calls = 0;
4✔
3961

2✔
3962
        auto col = table->get_column_key("value");
4✔
3963
        Results results1(r, table->where().greater_equal(col, 0));
4✔
3964
        Results results2(r, table->where().less(col, 10));
4✔
3965

2✔
3966
        auto token1 = results1.add_notification_callback([&](CollectionChangeSet) {
4✔
3967
            ++notification_calls;
4✔
3968
        });
4✔
3969

2✔
3970
        auto token2 = results2.add_notification_callback([&](CollectionChangeSet) {
4✔
3971
            ++notification_calls;
4✔
3972
        });
4✔
3973

2✔
3974
        struct Context : BindingContext {
4✔
3975
            void will_send_notifications() override
4✔
3976
            {
4✔
3977
                REQUIRE(notification_calls == 0);
4!
3978
                REQUIRE(binding_context_end_notify_calls == 0);
4!
3979
                ++binding_context_start_notify_calls;
4✔
3980
            }
4✔
3981

2✔
3982
            void did_send_notifications() override
4✔
3983
            {
4✔
3984
                REQUIRE(notification_calls == 2);
4!
3985
                REQUIRE(binding_context_start_notify_calls == 1);
4!
3986
                ++binding_context_end_notify_calls;
4✔
3987
            }
4✔
3988
        };
4✔
3989
        r->m_binding_context.reset(new Context());
4✔
3990

2✔
3991
        SECTION("local commit") {
4✔
3992
            binding_context_start_notify_calls = 0;
2✔
3993
            binding_context_end_notify_calls = 0;
2✔
3994
            notification_calls = 0;
2✔
3995
            coordinator->on_change();
2✔
3996
            r->begin_transaction();
2✔
3997
            table->create_object();
2✔
3998
            r->commit_transaction();
2✔
3999
            REQUIRE(binding_context_start_notify_calls == 1);
2!
4000
            REQUIRE(binding_context_end_notify_calls == 1);
2!
4001
        }
2✔
4002

2✔
4003
        SECTION("remote commit") {
4✔
4004
            binding_context_start_notify_calls = 0;
2✔
4005
            binding_context_end_notify_calls = 0;
2✔
4006
            notification_calls = 0;
2✔
4007
            JoiningThread([&] {
2✔
4008
                auto r2 = coordinator->get_realm(util::Scheduler::make_frozen(VersionID()));
2✔
4009
                r2->begin_transaction();
2✔
4010
                auto table2 = r2->read_group().get_table("class_object");
2✔
4011
                table2->create_object();
2✔
4012
                r2->commit_transaction();
2✔
4013
            });
2✔
4014
            advance_and_notify(*r);
2✔
4015
            REQUIRE(binding_context_start_notify_calls == 1);
2!
4016
            REQUIRE(binding_context_end_notify_calls == 1);
2!
4017
        }
2✔
4018
    }
4✔
4019

8✔
4020
    SECTION("did_send() is skipped if the Realm is closed first") {
16✔
4021
        Results results(r, table->where());
8✔
4022
        bool do_close = true;
8✔
4023
        auto token = results.add_notification_callback([&](CollectionChangeSet) {
8✔
4024
            if (do_close)
8✔
4025
                r->close();
4✔
4026
        });
8✔
4027

4✔
4028
        struct FailOnDidSend : BindingContext {
8✔
4029
            void did_send_notifications() override
8✔
4030
            {
4✔
UNCOV
4031
                FAIL("did_send_notifications() should not have been called");
×
UNCOV
4032
            }
×
4033
        };
8✔
4034
        struct CloseOnWillChange : FailOnDidSend {
8✔
4035
            Realm& realm;
8✔
4036
            CloseOnWillChange(Realm& realm)
8✔
4037
                : realm(realm)
8✔
4038
            {
6✔
4039
            }
4✔
4040

4✔
4041
            void will_send_notifications() override
8✔
4042
            {
6✔
4043
                realm.close();
4✔
4044
            }
4✔
4045
        };
8✔
4046

4✔
4047
        SECTION("closed in notification callback for notify()") {
8✔
4048
            r->m_binding_context.reset(new FailOnDidSend);
2✔
4049
            coordinator->on_change();
2✔
4050
            r->notify();
2✔
4051
        }
2✔
4052

4✔
4053
        SECTION("closed in notification callback for refresh()") {
8✔
4054
            do_close = false;
2✔
4055
            coordinator->on_change();
2✔
4056
            r->notify();
2✔
4057
            do_close = true;
2✔
4058

1✔
4059
            JoiningThread([&] {
2✔
4060
                auto r = coordinator->get_realm(util::Scheduler::make_frozen(VersionID()));
2✔
4061
                r->begin_transaction();
2✔
4062
                r->read_group().get_table("class_object")->create_object();
2✔
4063
                r->commit_transaction();
2✔
4064
            });
2✔
4065

1✔
4066
            r->m_binding_context.reset(new FailOnDidSend);
2✔
4067
            coordinator->on_change();
2✔
4068
            r->refresh();
2✔
4069
        }
2✔
4070

4✔
4071
        SECTION("closed in will_send() for notify()") {
8✔
4072
            r->m_binding_context.reset(new CloseOnWillChange(*r));
2✔
4073
            coordinator->on_change();
2✔
4074
            r->notify();
2✔
4075
        }
2✔
4076

4✔
4077
        SECTION("closed in will_send() for refresh()") {
8✔
4078
            do_close = false;
2✔
4079
            coordinator->on_change();
2✔
4080
            r->notify();
2✔
4081
            do_close = true;
2✔
4082

1✔
4083
            JoiningThread([&] {
2✔
4084
                auto r = coordinator->get_realm(util::Scheduler::make_frozen(VersionID()));
2✔
4085
                r->begin_transaction();
2✔
4086
                r->read_group().get_table("class_object")->create_object();
2✔
4087
                r->commit_transaction();
2✔
4088
            });
2✔
4089

1✔
4090
            r->m_binding_context.reset(new CloseOnWillChange(*r));
2✔
4091
            coordinator->on_change();
2✔
4092
            r->refresh();
2✔
4093
        }
2✔
4094
    }
8✔
4095
#ifdef _WIN32
4096
    _impl::RealmCoordinator::clear_all_caches();
4097
#endif
4098
}
16✔
4099

4100
TEST_CASE("RealmCoordinator: get_unbound_realm()") {
8✔
4101
    TestFile config;
8✔
4102
    config.cache = true;
8✔
4103
    config.schema = Schema{
8✔
4104
        {"object", {{"value", PropertyType::Int}}},
8✔
4105
    };
8✔
4106

4✔
4107
    ThreadSafeReference ref;
8✔
4108
    std::thread([&] {
8✔
4109
        ref = _impl::RealmCoordinator::get_coordinator(config)->get_unbound_realm();
8✔
4110
    }).join();
8✔
4111

4✔
4112
    SECTION("checks thread after being resolved") {
8✔
4113
        auto realm = Realm::get_shared_realm(std::move(ref));
2✔
4114
        REQUIRE_NOTHROW(realm->verify_thread());
2✔
4115
        std::thread([&] {
2✔
4116
            REQUIRE_EXCEPTION(realm->verify_thread(), WrongThread, "Realm accessed from incorrect thread.");
2✔
4117
        }).join();
2✔
4118
    }
2✔
4119

4✔
4120
    SECTION("delivers notifications to the thread it is resolved on") {
8✔
4121
#ifndef _WIN32
2✔
4122
        if (!util::EventLoop::has_implementation())
2✔
UNCOV
4123
            return;
×
4124
        auto realm = Realm::get_shared_realm(std::move(ref));
2✔
4125
        Results results(realm, ObjectStore::table_for_object_type(realm->read_group(), "object")->where());
2✔
4126
        bool called = false;
2✔
4127
        auto token = results.add_notification_callback([&](CollectionChangeSet) {
2✔
4128
            called = true;
2✔
4129
        });
2✔
4130
        util::EventLoop::main().run_until([&] {
56✔
4131
            return called;
56✔
4132
        });
56✔
4133
#endif
2✔
4134
    }
2✔
4135

4✔
4136
    SECTION("resolves to existing cached Realm for the thread if caching is enabled") {
8✔
4137
        auto r1 = Realm::get_shared_realm(config);
2✔
4138
        auto r2 = Realm::get_shared_realm(std::move(ref));
2✔
4139
        REQUIRE(r1 == r2);
2!
4140
    }
2✔
4141

4✔
4142
    SECTION("resolves to a new Realm if caching is disabled") {
8✔
4143
        config.cache = false;
2✔
4144
        auto r1 = Realm::get_shared_realm(config);
2✔
4145
        auto r2 = Realm::get_shared_realm(std::move(ref));
2✔
4146
        REQUIRE(r1 != r2);
2!
4147

1✔
4148
        // New unbound with cache disabled
1✔
4149
        std::thread([&] {
2✔
4150
            ref = _impl::RealmCoordinator::get_coordinator(config)->get_unbound_realm();
2✔
4151
        }).join();
2✔
4152
        auto r3 = Realm::get_shared_realm(std::move(ref));
2✔
4153
        REQUIRE(r1 != r3);
2!
4154
        REQUIRE(r2 != r3);
2!
4155

1✔
4156
        // New local with cache enabled should grab the resolved unbound
1✔
4157
        config.cache = true;
2✔
4158
        auto r4 = Realm::get_shared_realm(config);
2✔
4159
        REQUIRE(r4 == r2);
2!
4160
    }
2✔
4161
}
8✔
4162

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

20✔
4168
    {
40✔
4169
        auto realm = Realm::get_shared_realm(config);
40✔
4170
        realm->begin_transaction();
40✔
4171
        realm->read_group().get_table("class_object")->create_object();
40✔
4172
        realm->commit_transaction();
40✔
4173
    }
40✔
4174

20✔
4175
    config.schema_mode = SchemaMode::Immutable;
40✔
4176
    auto realm = Realm::get_shared_realm(config);
40✔
4177
    realm->read_group();
40✔
4178

20✔
4179
    SECTION("unsupported functions") {
40✔
4180
        SECTION("update_schema()") {
10✔
4181
            REQUIRE_THROWS_AS(realm->compact(), WrongTransactionState);
2✔
4182
        }
2✔
4183
        SECTION("begin_transaction()") {
10✔
4184
            REQUIRE_THROWS_AS(realm->begin_transaction(), WrongTransactionState);
2✔
4185
        }
2✔
4186
        SECTION("async_begin_transaction()") {
10✔
4187
            REQUIRE_THROWS_AS(realm->async_begin_transaction(nullptr), WrongTransactionState);
2✔
4188
        }
2✔
4189
        SECTION("refresh()") {
10✔
4190
            REQUIRE_THROWS_AS(realm->refresh(), WrongTransactionState);
2✔
4191
        }
2✔
4192
        SECTION("compact()") {
10✔
4193
            REQUIRE_THROWS_AS(realm->compact(), WrongTransactionState);
2✔
4194
        }
2✔
4195
    }
10✔
4196

20✔
4197
    SECTION("supported functions") {
40✔
4198
        SECTION("is_in_transaction()") {
30✔
4199
            REQUIRE_FALSE(realm->is_in_transaction());
2!
4200
        }
2✔
4201
        SECTION("is_in_async_transaction()") {
30✔
4202
            REQUIRE_FALSE(realm->is_in_transaction());
2!
4203
        }
2✔
4204
        SECTION("freeze()") {
30✔
4205
            std::shared_ptr<Realm> frozen;
2✔
4206
            REQUIRE_NOTHROW(frozen = realm->freeze());
2✔
4207
            REQUIRE(frozen->read_group().get_table("class_object")->size() == 1);
2!
4208
            REQUIRE_NOTHROW(frozen = Realm::get_frozen_realm(config, realm->read_transaction_version()));
2✔
4209
            REQUIRE(frozen->read_group().get_table("class_object")->size() == 1);
2!
4210
        }
2✔
4211
        SECTION("notify()") {
30✔
4212
            REQUIRE_NOTHROW(realm->notify());
2✔
4213
        }
2✔
4214
        SECTION("is_in_read_transaction()") {
30✔
4215
            REQUIRE(realm->is_in_read_transaction());
2!
4216
        }
2✔
4217
        SECTION("last_seen_transaction_version()") {
30✔
4218
            REQUIRE(realm->last_seen_transaction_version() == 1);
2!
4219
        }
2✔
4220
        SECTION("get_number_of_versions()") {
30✔
4221
            REQUIRE(realm->get_number_of_versions() == 1);
2!
4222
        }
2✔
4223
        SECTION("read_transaction_version()") {
30✔
4224
            REQUIRE(realm->read_transaction_version() == VersionID{1, 0});
2!
4225
        }
2✔
4226
        SECTION("current_transaction_version()") {
30✔
4227
            REQUIRE(realm->current_transaction_version() == VersionID{1, 0});
2!
4228
        }
2✔
4229
        SECTION("latest_snapshot_version()") {
30✔
4230
            REQUIRE(realm->latest_snapshot_version() == 1);
2!
4231
        }
2✔
4232
        SECTION("duplicate()") {
30✔
4233
            auto duplicate = realm->duplicate();
2✔
4234
            REQUIRE(duplicate->get_table("class_object")->size() == 1);
2!
4235
        }
2✔
4236
        SECTION("invalidate()") {
30✔
4237
            REQUIRE_NOTHROW(realm->invalidate());
2✔
4238
            REQUIRE_FALSE(realm->is_in_read_transaction());
2!
4239
            REQUIRE(realm->read_group().get_table("class_object")->size() == 1);
2!
4240
        }
2✔
4241
        SECTION("close()") {
30✔
4242
            REQUIRE_NOTHROW(realm->close());
2✔
4243
            REQUIRE(realm->is_closed());
2!
4244
        }
2✔
4245
        SECTION("has_pending_async_work()") {
30✔
4246
            REQUIRE_FALSE(realm->has_pending_async_work());
2!
4247
        }
2✔
4248
        SECTION("wait_for_change()") {
30✔
4249
            REQUIRE_FALSE(realm->wait_for_change());
2!
4250
        }
2✔
4251
    }
30✔
4252
}
40✔
4253

4254
TEST_CASE("KeyPathMapping generation") {
2✔
4255
    TestFile config;
2✔
4256
    realm::query_parser::KeyPathMapping mapping;
2✔
4257

1✔
4258
    SECTION("class aliasing") {
2✔
4259
        Schema schema = {
2✔
4260
            {"PersistedName", {{"age", PropertyType::Int}}, {}, "AlternativeName"},
2✔
4261
            {"class_with_policy",
2✔
4262
             {{"value", PropertyType::Int},
2✔
4263
              {"child", PropertyType::Object | PropertyType::Nullable, "class_with_policy"}},
2✔
4264
             {{"parents", PropertyType::LinkingObjects | PropertyType::Array, "class_with_policy", "child"}},
2✔
4265
             "ClassWithPolicy"},
2✔
4266
        };
2✔
4267
        schema.validate();
2✔
4268
        config.schema = schema;
2✔
4269
        auto realm = Realm::get_shared_realm(config);
2✔
4270
        realm::populate_keypath_mapping(mapping, *realm);
2✔
4271
        REQUIRE(mapping.has_table_mapping("AlternativeName"));
2!
4272
        REQUIRE("class_PersistedName" == mapping.get_table_mapping("AlternativeName"));
2!
4273

1✔
4274
        auto table = realm->read_group().get_table("class_class_with_policy");
2✔
4275
        std::vector<Mixed> args{0};
2✔
4276
        auto q = table->query("parents.value = $0", args, mapping);
2✔
4277
        REQUIRE(q.count() == 0);
2!
4278
    }
2✔
4279
}
2✔
4280

4281
TEST_CASE("Concurrent operations") {
4✔
4282
    SECTION("Async commits together with online compaction") {
4✔
4283
        // This is a reproduction test for issue https://github.com/realm/realm-dart/issues/1396
1✔
4284
        // First create a relatively large realm, then delete the content and do some more
1✔
4285
        // commits using async commits. If a compaction is started when doing an async commit
1✔
4286
        // then the subsequent committing done in the helper thread will illegally COW the
1✔
4287
        // top array. When the next mutation is done, the top array will be reported as being
1✔
4288
        // already freed.
1✔
4289
        TestFile config;
2✔
4290
        config.schema_version = 1;
2✔
4291
        config.schema = Schema{{"object", {{"value", PropertyType::Int}}}};
2✔
4292

1✔
4293
        auto realm_1 = Realm::get_shared_realm(config);
2✔
4294
        Results res(realm_1, realm_1->read_group().get_table("class_object")->where());
2✔
4295
        auto realm_2 = Realm::get_shared_realm(config);
2✔
4296

1✔
4297
        {
2✔
4298
            // Create a lot of objects
1✔
4299
            realm_2->begin_transaction();
2✔
4300
            auto table = realm_2->read_group().get_table("class_object");
2✔
4301
            for (int i = 0; i < 400000; i++) {
800,002✔
4302
                table->create_object().set("value", i);
800,000✔
4303
            }
800,000✔
4304
            realm_2->commit_transaction();
2✔
4305
        }
2✔
4306

1✔
4307
        int commit_1 = 0;
2✔
4308
        int commit_2 = 0;
2✔
4309

1✔
4310
        for (int i = 0; i < 4; i++) {
10✔
4311
            realm_1->async_begin_transaction([&]() {
8✔
4312
                // Clearing the DB will reduce the need for space
4✔
4313
                // This will trigger an online compaction
4✔
4314
                // Before the fix, the probram would crash here next time around.
4✔
4315
                res.clear();
8✔
4316
                realm_1->async_commit_transaction([&](std::exception_ptr) {
8✔
4317
                    commit_1++;
8✔
4318
                });
8✔
4319
            });
8✔
4320
            realm_2->async_begin_transaction([&]() {
8✔
4321
                // Make sure we will continue to have something to delete
4✔
4322
                auto table = realm_2->read_group().get_table("class_object");
8✔
4323
                for (int i = 0; i < 100; i++) {
808✔
4324
                    table->create_object().set("value", i);
800✔
4325
                }
800✔
4326
                realm_2->async_commit_transaction([&](std::exception_ptr) {
8✔
4327
                    commit_2++;
8✔
4328
                });
8✔
4329
            });
8✔
4330
        }
8✔
4331

1✔
4332
        util::EventLoop::main().run_until([&] {
7,391✔
4333
            return commit_1 == 4 && commit_2 == 4;
7,391✔
4334
        });
7,391✔
4335
    }
2✔
4336

2✔
4337
    SECTION("No open realms") {
4✔
4338
        // This is just to check that the section above did not leave any realms open
1✔
4339
        _impl::RealmCoordinator::assert_no_open_realms();
2✔
4340
    }
2✔
4341
}
4✔
4342

4343
TEST_CASE("Notification logging") {
2✔
4344
    using namespace std::chrono_literals;
2✔
4345
    TestFile config;
2✔
4346
    // util::LogCategory::realm.set_default_level_threshold(util::Logger::Level::all);
1✔
4347
    config.schema_version = 1;
2✔
4348
    config.schema = Schema{{"object", {{"value", PropertyType::Int}}}};
2✔
4349

1✔
4350
    auto realm = Realm::get_shared_realm(config);
2✔
4351
    auto table = realm->read_group().get_table("class_object");
2✔
4352
    int changed = 0;
2✔
4353
    Results res(realm, table->query("value == 5"));
2✔
4354
    auto token = res.add_notification_callback([&changed](CollectionChangeSet const&) {
24✔
4355
        changed++;
24✔
4356
    });
24✔
4357

1✔
4358
    int commit_nr = 0;
2✔
4359
    util::EventLoop::main().run_until([&] {
22✔
4360
        for (int64_t i = 0; i < 10; i++) {
242✔
4361
            realm->begin_transaction();
220✔
4362
            table->create_object().set("value", i);
220✔
4363
            realm->commit_transaction();
220✔
4364
            std::this_thread::sleep_for(2ms);
220✔
4365
        }
220✔
4366
        return ++commit_nr == 10;
22✔
4367
    });
22✔
4368
}
2✔
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