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

realm / realm-core / 2267

27 Apr 2024 02:45AM UTC coverage: 90.734% (-0.02%) from 90.756%
2267

push

Evergreen

web-flow
Merge pull request #7639 from realm/release/14.6.0-again

merge release 14.6.0

101938 of 180236 branches covered (56.56%)

211 of 254 new or added lines in 5 files covered. (83.07%)

94 existing lines in 16 files now uncovered.

212406 of 234097 relevant lines covered (90.73%)

5753046.69 hits per line

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

98.1
/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
#include <util/sync/sync_test_utils.hpp>
49
#include <util/test_file.hpp>
50
#ifdef REALM_ENABLE_AUTH_TESTS
51
#include <util/sync/baas_admin_api.hpp>
52
#endif // REALM_ENABLE_AUTH_TESTS
53

54
#include <realm/object-store/sync/async_open_task.hpp>
55
#include <realm/object-store/sync/impl/app_metadata.hpp>
56
#include <realm/object-store/sync/sync_session.hpp>
57

58
#include <realm/sync/noinst/client_history_impl.hpp>
59
#include <realm/sync/subscriptions.hpp>
60
#endif // REALM_ENABLE_SYNC
61

62
#include <catch2/catch_all.hpp>
63
#include <catch2/matchers/catch_matchers_string.hpp>
64

65
#include <external/json/json.hpp>
66

67
#include <array>
68
#if REALM_HAVE_UV
69
#include <uv.h>
70
#endif // REALM_HAVE_UV
71

72
namespace realm {
73
class TestHelper {
74
public:
75
    static DBRef& get_db(SharedRealm const& shared_realm)
76
    {
7,168✔
77
        return Realm::Internal::get_db(*shared_realm);
7,168✔
78
    }
7,168✔
79

80
    static void begin_read(SharedRealm const& shared_realm, VersionID version)
81
    {
2✔
82
        Realm::Internal::begin_read(*shared_realm, version);
2✔
83
    }
2✔
84
};
85

86
static bool operator==(IndexSet const& a, IndexSet const& b)
87
{
6✔
88
    return std::equal(a.as_indexes().begin(), a.as_indexes().end(), b.as_indexes().begin(), b.as_indexes().end());
6✔
89
}
6✔
90
} // namespace realm
91

92
using namespace realm;
93

94
namespace {
95
class Observer : public BindingContext {
96
public:
97
    Observer(Obj& obj)
98
    {
2✔
99
        m_result.push_back(ObserverState{obj.get_table()->get_key(), obj.get_key(), nullptr});
2✔
100
    }
2✔
101

102
    IndexSet array_change(size_t index, ColKey col_key) const noexcept
103
    {
6✔
104
        auto& changes = m_result[index].changes;
6✔
105
        auto col = changes.find(col_key.value);
6✔
106
        return col == changes.end() ? IndexSet{} : col->second.indices;
6✔
107
    }
6✔
108

109
private:
110
    std::vector<ObserverState> m_result;
111
    std::vector<void*> m_invalidated;
112

113
    std::vector<ObserverState> get_observed_rows() override
114
    {
4✔
115
        return m_result;
4✔
116
    }
4✔
117

118
    void did_change(std::vector<ObserverState> const& observers, std::vector<void*> const& invalidated, bool) override
119
    {
4✔
120
        m_invalidated = invalidated;
4✔
121
        m_result = observers;
4✔
122
    }
4✔
123
};
124
} // namespace
125

126
TEST_CASE("SharedRealm: get_shared_realm()") {
76✔
127
    TestFile config;
76✔
128
    config.schema_version = 1;
76✔
129
    config.schema = Schema{
76✔
130
        {"object", {{"value", PropertyType::Int}}},
76✔
131
    };
76✔
132

133
    SECTION("should return the same instance when caching is enabled") {
76✔
134
        config.cache = true;
2✔
135
        auto realm1 = Realm::get_shared_realm(config);
2✔
136
        auto realm2 = Realm::get_shared_realm(config);
2✔
137
        REQUIRE(realm1.get() == realm2.get());
2!
138
    }
2✔
139

140
    SECTION("should return different instances when caching is disabled") {
76✔
141
        config.cache = false;
2✔
142
        auto realm1 = Realm::get_shared_realm(config);
2✔
143
        auto realm2 = Realm::get_shared_realm(config);
2✔
144
        REQUIRE(realm1.get() != realm2.get());
2!
145
    }
2✔
146

147
    SECTION("should validate that the config is sensible") {
76✔
148
        SECTION("bad encryption key") {
18✔
149
            config.encryption_key = std::vector<char>(2, 0);
2✔
150
            REQUIRE_EXCEPTION(Realm::get_shared_realm(config), InvalidEncryptionKey,
2✔
151
                              "Encryption key must be 64 bytes.");
2✔
152
        }
2✔
153

154
        SECTION("schema without schema version") {
18✔
155
            config.schema_version = ObjectStore::NotVersioned;
2✔
156
            REQUIRE_EXCEPTION(Realm::get_shared_realm(config), IllegalCombination,
2✔
157
                              "A schema version must be specified when the schema is specified");
2✔
158
        }
2✔
159

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

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

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

181
        SECTION("migration function for additive explicit") {
18✔
182
            config.schema_mode = SchemaMode::AdditiveExplicit;
2✔
183
            config.migration_function = [](auto, auto, auto) {};
2✔
184
            REQUIRE_EXCEPTION(Realm::get_shared_realm(config), IllegalCombination,
2✔
185
                              "Realms opened in Additive-only schema mode do not use a migration function");
2✔
186
        }
2✔
187

188
        SECTION("initialization function for immutable") {
18✔
189
            config.schema_mode = SchemaMode::Immutable;
2✔
190
            config.initialization_function = [](auto) {};
2✔
191
            REQUIRE_EXCEPTION(Realm::get_shared_realm(config), IllegalCombination,
2✔
192
                              "Realms opened in immutable mode do not use an initialization function");
2✔
193
        }
2✔
194

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

209
    SECTION("should reject mismatched config") {
76✔
210
        config.encryption_key.clear(); // may be set already when encrypting all
8✔
211

212
        SECTION("schema version") {
8✔
213
            auto realm = Realm::get_shared_realm(config);
2✔
214
            config.schema_version = 2;
2✔
215
            REQUIRE_EXCEPTION(
2✔
216
                Realm::get_shared_realm(config), MismatchedConfig,
2✔
217
                Catch::Matchers::Matches("Realm at path '.*' already opened with different schema version."));
2✔
218

219
            config.schema = util::none;
2✔
220
            config.schema_version = ObjectStore::NotVersioned;
2✔
221
            REQUIRE_NOTHROW(Realm::get_shared_realm(config));
2✔
222
        }
2✔
223

224
        SECTION("schema mode") {
8✔
225
            auto realm = Realm::get_shared_realm(config);
2✔
226
            config.schema_mode = SchemaMode::Manual;
2✔
227
            REQUIRE_EXCEPTION(
2✔
228
                Realm::get_shared_realm(config), MismatchedConfig,
2✔
229
                Catch::Matchers::Matches("Realm at path '.*' already opened with a different schema mode."));
2✔
230
        }
2✔
231

232
        SECTION("durability") {
8✔
233
            auto realm = Realm::get_shared_realm(config);
2✔
234
            config.in_memory = true;
2✔
235
            REQUIRE_EXCEPTION(
2✔
236
                Realm::get_shared_realm(config), MismatchedConfig,
2✔
237
                Catch::Matchers::Matches("Realm at path '.*' already opened with different inMemory settings."));
2✔
238
        }
2✔
239

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

251

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

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

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

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

293
    SECTION("should verify that the schema is valid") {
76✔
294
        config.schema =
2✔
295
            Schema{{"object",
2✔
296
                    {{"value", PropertyType::Int}},
2✔
297
                    {{"invalid backlink", PropertyType::LinkingObjects | PropertyType::Array, "object", "value"}}}};
2✔
298
        REQUIRE_THROWS_CONTAINING(Realm::get_shared_realm(config), "origin of linking objects property");
2✔
299
    }
2✔
300

301
    SECTION("should apply the schema if one is supplied") {
76✔
302
        Realm::get_shared_realm(config);
2✔
303

304
        {
2✔
305
            Group g(config.path, config.encryption_key.data());
2✔
306
            auto table = ObjectStore::table_for_object_type(g, "object");
2✔
307
            REQUIRE(table);
2!
308
            REQUIRE(table->get_column_count() == 1);
2!
309
            REQUIRE(table->get_column_name(*table->get_column_keys().begin()) == "value");
2!
310
        }
2✔
311

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

327
    SECTION("should properly roll back from migration errors") {
76✔
328
        Realm::get_shared_realm(config);
2✔
329

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

349
    SECTION("should read the schema from the file if none is supplied") {
76✔
350
        Realm::get_shared_realm(config);
2✔
351

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

364
    SECTION("should read the proper schema from the file if a custom version is supplied") {
76✔
365
        Realm::get_shared_realm(config);
2✔
366

367
        config.schema = util::none;
2✔
368
        config.schema_mode = SchemaMode::AdditiveExplicit;
2✔
369
        config.schema_version = 0;
2✔
370

371
        auto realm = Realm::get_shared_realm(config);
2✔
372
        REQUIRE(realm->schema().size() == 1);
2!
373

374
        auto& db = TestHelper::get_db(realm);
2✔
375
        auto rt = db->start_read();
2✔
376
        VersionID old_version = rt->get_version_of_current_transaction();
2✔
377
        realm->close();
2✔
378

379
        config.schema = Schema{
2✔
380
            {"object", {{"value", PropertyType::Int}}},
2✔
381
            {"object1", {{"value", PropertyType::Int}}},
2✔
382
        };
2✔
383
        config.schema_version = 1;
2✔
384
        realm = Realm::get_shared_realm(config);
2✔
385
        REQUIRE(realm->schema().size() == 2);
2!
386

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

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

398
        // create an empty file
399
        util::File(config.path, util::File::mode_Write);
4✔
400

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

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

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

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

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

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

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

443
        config.schema = util::none;
2✔
444
        auto realm3 = Realm::get_shared_realm(config);
2✔
445

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

451
        realm1->refresh();
2✔
452
        realm2->refresh();
2✔
453

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

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

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

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

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

524
    protected:
76✔
525
        size_t m_id;
76✔
526
    };
76✔
527

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

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

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

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

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

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

576
        REQUIRE(frozen != realm);
2!
577
        REQUIRE(realm->read_transaction_version() == frozen->read_transaction_version());
2!
578

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

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

590
        REQUIRE(frozen1 != realm);
2!
591
        REQUIRE(realm->read_transaction_version() == frozen1->read_transaction_version());
2!
592

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

598
        REQUIRE(realm->read_transaction_version() > frozen1->read_transaction_version());
2!
599

600
        auto frozen2 = realm->freeze();
2✔
601
        frozen2->read_group();
2✔
602

603
        REQUIRE(frozen2 != frozen1);
2!
604
        REQUIRE(frozen2 != realm);
2!
605
        REQUIRE(realm->read_transaction_version() == frozen2->read_transaction_version());
2!
606
        REQUIRE(frozen2->read_transaction_version() > frozen1->read_transaction_version());
2!
607
    }
2✔
608

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

615
        auto subset_schema = Schema{
2✔
616
            {"object1", {{"value", PropertyType::Int}}},
2✔
617
        };
2✔
618

619
        config.schema = full_schema;
2✔
620

621
        auto realm = Realm::get_shared_realm(config);
2✔
622
        realm->close();
2✔
623

624
        config.schema = subset_schema;
2✔
625

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

631
        REQUIRE(full_schema != subset_schema);
2!
632
        REQUIRE(realm->schema() == subset_schema);
2!
633
        REQUIRE(frozen_schema == subset_schema);
2!
634
    }
2✔
635

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

642
        auto subset_schema = Schema{
2✔
643
            {"object", {{"value1", PropertyType::Int}}},
2✔
644
        };
2✔
645

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

931
    TestSyncManager tsm;
48✔
932
    SyncTestFile config(tsm, "default");
48✔
933
    ObjectSchema object_schema = {"object",
48✔
934
                                  {
48✔
935
                                      {"_id", PropertyType::Int, Property::IsPrimary{true}},
48✔
936
                                      {"value", PropertyType::Int},
48✔
937
                                  }};
48✔
938
    config.schema = Schema{object_schema};
48✔
939
    SyncTestFile config2(tsm, "default");
48✔
940
    config2.schema = config.schema;
48✔
941

942
    std::mutex mutex;
48✔
943

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1119
    SECTION("can download multiple Realms at a time") {
48✔
1120
        SyncTestFile config1(tsm, "realm1");
2✔
1121
        SyncTestFile config2(tsm, "realm2");
2✔
1122
        SyncTestFile config3(tsm, "realm3");
2✔
1123
        SyncTestFile config4(tsm, "realm4");
2✔
1124

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

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

1143
    auto expired_token = encode_fake_jwt("", 123, 456);
48✔
1144

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

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

1177
        util::EventLoop::main().run_until([&] {
185,507✔
1178
            return called.load();
185,507✔
1179
        });
185,507✔
1180
        std::lock_guard<std::mutex> lock(mutex);
2✔
1181
        REQUIRE(called);
2!
1182
    }
2✔
1183

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

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

1223
#if REALM_APP_SERVICES
48✔
1224

1225
    SECTION("waiters are cancelled if cancel_waits_on_nonfatal_error") {
48✔
1226
        auto logger = util::Logger::get_default_logger();
18✔
1227
        auto transport = std::make_shared<HookedTransport<UnitTestTransport>>();
18✔
1228
        auto socket_provider = std::make_shared<HookedSocketProvider>(logger, "some user agent");
18✔
1229
        enum TestMode { expired_at_start, expired_by_websocket, websocket_fails };
18✔
1230
        enum FailureMode { location_fails, token_fails, token_not_authorized };
18✔
1231
        auto txt_test_mode = [](TestMode mode) {
36✔
1232
            switch (mode) {
36✔
1233
                case TestMode::expired_at_start:
12✔
1234
                    return "access token expired when realm is opened";
12✔
1235
                case TestMode::expired_by_websocket:
12✔
1236
                    return "access token expired by websocket";
12✔
1237
                case TestMode::websocket_fails:
12✔
1238
                    return "websocket returns connection failed";
12✔
NEW
1239
                default:
✔
NEW
1240
                    return "Unknown TestMode";
×
1241
            }
36✔
1242
        };
36✔
1243
        auto txt_failure_mode = [](FailureMode mode) {
36✔
1244
            switch (mode) {
36✔
1245
                case FailureMode::location_fails:
12✔
1246
                    return "location update fails";
12✔
1247
                case FailureMode::token_fails:
12✔
1248
                    return "access token refresh fails";
12✔
1249
                case FailureMode::token_not_authorized:
12✔
1250
                    return "websocket connect not authorized";
12✔
NEW
1251
                default:
✔
NEW
1252
                    return "Unknown FailureMode";
×
1253
            }
36✔
1254
        };
36✔
1255

1256
        app::AppConfig app_config;
18✔
1257
        set_app_config_defaults(app_config, transport);
18✔
1258
        app_config.sync_client_config.socket_provider = socket_provider;
18✔
1259
        app_config.base_file_path = util::make_temp_dir();
18✔
1260
        app_config.metadata_mode = app::AppConfig::MetadataMode::NoEncryption;
18✔
1261

1262
        auto the_app = app::App::get_app(app::App::CacheMode::Disabled, app_config);
18✔
1263
        create_user_and_log_in(the_app);
18✔
1264
        auto user = the_app->current_user();
18✔
1265
        // User should be logged in at this point
1266
        REQUIRE(user->is_logged_in());
18!
1267

1268
        bool not_authorized = false;
18✔
1269
        bool token_refresh_called = false;
18✔
1270
        bool location_refresh_called = false;
18✔
1271

1272
        TestMode test_mode = GENERATE(expired_at_start, expired_by_websocket, websocket_fails);
18✔
1273
        FailureMode failure = GENERATE(location_fails, token_fails, token_not_authorized);
18✔
1274

1275
        DYNAMIC_SECTION(txt_test_mode(test_mode) << " - " << txt_failure_mode(failure)) {
18✔
1276
            logger->info("TEST: %1 - %2", txt_test_mode(test_mode), txt_failure_mode(failure));
18✔
1277
            if (test_mode == TestMode::expired_at_start) {
18✔
1278
                // invalidate the user's cached access token
1279
                auto app_user = the_app->current_user();
6✔
1280
                app_user->update_data_for_testing([&](app::UserData& data) {
6✔
1281
                    data.access_token = RealmJWT(expired_token);
6✔
1282
                });
6✔
1283
            }
6✔
1284
            else if (test_mode == TestMode::expired_by_websocket) {
12✔
1285
                // tell websocket to return not authorized to refresh access token
1286
                not_authorized = true;
6✔
1287
            }
6✔
1288
        }
18✔
1289

1290
        the_app.reset();
18✔
1291

1292
        auto err_handler = [](std::shared_ptr<SyncSession> session, SyncError error) {
18✔
1293
            auto logger = util::Logger::get_default_logger();
6✔
1294
            logger->debug("The sync error handler caught an error: '%1' for '%2'", error.status, session->path());
6✔
1295
            // Ignore connection failed non-fatal errors and check for access token refresh unauthorized fatal errors
1296
            if (error.status.code() == ErrorCodes::SyncConnectFailed) {
6✔
NEW
1297
                REQUIRE_FALSE(error.is_fatal);
×
NEW
1298
                return;
×
NEW
1299
            }
×
1300
            // If it's not SyncConnectFailed, then it should be AuthError
1301
            REQUIRE(error.status.code() == ErrorCodes::AuthError);
6!
1302
            REQUIRE(error.is_fatal);
6!
1303
        };
6✔
1304

1305
        transport->request_hook = [&](const app::Request& req) -> std::optional<app::Response> {
30✔
1306
            static constexpr int CURLE_OPERATION_TIMEDOUT = 28;
30✔
1307
            std::lock_guard<std::mutex> lock(mutex);
30✔
1308
            if (req.url.find("/auth/session") != std::string::npos) {
30✔
1309
                token_refresh_called = true;
12✔
1310
                if (failure == FailureMode::token_not_authorized) {
12✔
1311
                    return app::Response{403, 0, {}, "403 not authorized"};
6✔
1312
                }
6✔
1313
                if (failure == FailureMode::token_fails) {
6✔
1314
                    return app::Response{0, CURLE_OPERATION_TIMEDOUT, {}, "Operation timed out"};
6✔
1315
                }
6✔
1316
            }
6✔
1317
            else if (req.url.find("/location") != std::string::npos) {
18✔
1318
                location_refresh_called = true;
18✔
1319
                if (failure == FailureMode::location_fails) {
18✔
1320
                    // Fake "offline/request timed out" custom error response
1321
                    return app::Response{0, CURLE_OPERATION_TIMEDOUT, {}, "Operation timed out"};
6✔
1322
                }
6✔
1323
            }
18✔
1324
            return std::nullopt;
12✔
1325
        };
30✔
1326

1327
        socket_provider->websocket_connect_func = [&]() -> std::optional<SocketProviderError> {
18✔
1328
            if (not_authorized) {
18✔
1329
                not_authorized = false; // one shot
6✔
1330
                return SocketProviderError(sync::websocket::WebSocketError::websocket_unauthorized,
6✔
1331
                                           "403 not authorized");
6✔
1332
            }
6✔
1333
            return SocketProviderError(sync::websocket::WebSocketError::websocket_connection_failed,
12✔
1334
                                       "Operation timed out");
12✔
1335
        };
18✔
1336

1337
        the_app = app::App::get_app(app::App::CacheMode::Disabled, app_config);
18✔
1338
        SyncTestFile config(the_app->current_user(), "realm");
18✔
1339
        config.sync_config->cancel_waits_on_nonfatal_error = true;
18✔
1340
        config.sync_config->error_handler = err_handler;
18✔
1341

1342
        // User should be logged in at this point
1343
        REQUIRE(config.sync_config->user->is_logged_in());
18!
1344

1345
        auto task = Realm::get_synchronized_realm(config);
18✔
1346
        auto pf = util::make_promise_future<std::exception_ptr>();
18✔
1347
        task->start([&pf](auto ref, auto error) mutable {
18✔
1348
            REQUIRE(!ref);
18!
1349
            REQUIRE(error);
18!
1350
            pf.promise.emplace_value(error);
18✔
1351
        });
18✔
1352

1353
        auto result = pf.future.get_no_throw();
18✔
1354
        REQUIRE(result.is_ok());
18!
1355
        REQUIRE(result.get_value());
18!
1356
        std::lock_guard<std::mutex> lock(mutex);
18✔
1357
        REQUIRE(location_refresh_called);
18!
1358
        if (failure != FailureMode::location_fails) {
18✔
1359
            REQUIRE(token_refresh_called);
12!
1360
        }
12✔
1361
    }
18✔
1362

1363
#endif // REALM_APP_SERVICES
48✔
1364

1365
    SECTION("read-only mode sets the schema version") {
48✔
1366
        {
2✔
1367
            SharedRealm realm = Realm::get_shared_realm(config);
2✔
1368
            wait_for_upload(*realm);
2✔
1369
            realm->close();
2✔
1370
        }
2✔
1371

1372
        config2.schema_mode = SchemaMode::ReadOnly;
2✔
1373
        auto [ref, error] = async_open_realm(config2);
2✔
1374
        REQUIRE(ref);
2!
1375
        REQUIRE_FALSE(error);
2!
1376
        REQUIRE(Realm::get_shared_realm(std::move(ref))->schema_version() == 1);
2!
1377
    }
2✔
1378

1379
    Schema with_added_object = Schema{object_schema,
48✔
1380
                                      {"added",
48✔
1381
                                       {
48✔
1382
                                           {"_id", PropertyType::Int, Property::IsPrimary{true}},
48✔
1383
                                       }}};
48✔
1384

1385
    SECTION("read-only mode applies remote schema changes") {
48✔
1386
        // Create the local file without "added"
1387
        Realm::get_shared_realm(config2);
2✔
1388

1389
        // Add the table server-side
1390
        config.schema = with_added_object;
2✔
1391
        config2.schema = with_added_object;
2✔
1392
        {
2✔
1393
            SharedRealm realm = Realm::get_shared_realm(config);
2✔
1394
            wait_for_upload(*realm);
2✔
1395
            realm->close();
2✔
1396
        }
2✔
1397

1398
        // Verify that the table gets added when reopening
1399
        config2.schema_mode = SchemaMode::ReadOnly;
2✔
1400
        auto [ref, error] = async_open_realm(config2);
2✔
1401
        REQUIRE(ref);
2!
1402
        REQUIRE_FALSE(error);
2!
1403
        auto realm = Realm::get_shared_realm(std::move(ref));
2✔
1404
        REQUIRE(realm->schema().find("added") != realm->schema().end());
2!
1405
        REQUIRE(realm->read_group().get_table("class_added"));
2!
1406
    }
2✔
1407

1408
    SECTION("read-only mode does not create tables not present on the server") {
48✔
1409
        // Create the local file without "added"
1410
        Realm::get_shared_realm(config2);
2✔
1411

1412
        config2.schema = with_added_object;
2✔
1413
        config2.schema_mode = SchemaMode::ReadOnly;
2✔
1414
        auto [ref, error] = async_open_realm(config2);
2✔
1415
        REQUIRE(ref);
2!
1416
        REQUIRE_FALSE(error);
2!
1417
        auto realm = Realm::get_shared_realm(std::move(ref));
2✔
1418
        REQUIRE(realm->schema().find("added") != realm->schema().end());
2!
1419
        REQUIRE_FALSE(realm->read_group().get_table("class_added"));
2!
1420
    }
2✔
1421

1422
    SECTION("adding a property to a newly downloaded read-only Realm reports an error") {
48✔
1423
        // Create the Realm on the server
1424
        wait_for_upload(*Realm::get_shared_realm(config2));
2✔
1425

1426
        config.schema_mode = SchemaMode::ReadOnly;
2✔
1427
        config.schema = Schema{{"object",
2✔
1428
                                {
2✔
1429
                                    {"_id", PropertyType::Int, Property::IsPrimary{true}},
2✔
1430
                                    {"value", PropertyType::Int},
2✔
1431
                                    {"value2", PropertyType::Int},
2✔
1432
                                }}};
2✔
1433

1434
        auto [ref, error] = async_open_realm(config);
2✔
1435
        REQUIRE_FALSE(ref);
2!
1436
        REQUIRE(error);
2!
1437
        REQUIRE_THROWS_CONTAINING(std::rethrow_exception(error), "Property 'object.value2' has been added.");
2✔
1438
    }
2✔
1439

1440
    SECTION("adding a property to an existing read-only Realm reports an error") {
48✔
1441
        Realm::get_shared_realm(config);
2✔
1442

1443
        config.schema_mode = SchemaMode::ReadOnly;
2✔
1444
        config.schema = Schema{{"object",
2✔
1445
                                {
2✔
1446
                                    {"_id", PropertyType::Int, Property::IsPrimary{true}},
2✔
1447
                                    {"value", PropertyType::Int},
2✔
1448
                                    {"value2", PropertyType::Int},
2✔
1449
                                }}};
2✔
1450
        REQUIRE_THROWS_CONTAINING(Realm::get_shared_realm(config), "Property 'object.value2' has been added.");
2✔
1451

1452
        auto [ref, error] = async_open_realm(config);
2✔
1453
        REQUIRE_FALSE(ref);
2!
1454
        REQUIRE(error);
2!
1455
        REQUIRE_THROWS_CONTAINING(std::rethrow_exception(error), "Property 'object.value2' has been added.");
2✔
1456
    }
2✔
1457

1458
    SECTION("removing a property from a newly downloaded read-only Realm leaves the column in place") {
48✔
1459
        // Create the Realm on the server
1460
        wait_for_upload(*Realm::get_shared_realm(config2));
2✔
1461

1462
        // Remove the "value" property from the schema
1463
        config.schema_mode = SchemaMode::ReadOnly;
2✔
1464
        config.schema = Schema{{"object",
2✔
1465
                                {
2✔
1466
                                    {"_id", PropertyType::Int, Property::IsPrimary{true}},
2✔
1467
                                }}};
2✔
1468

1469
        auto [ref, error] = async_open_realm(config);
2✔
1470
        REQUIRE(ref);
2!
1471
        REQUIRE_FALSE(error);
2!
1472
        REQUIRE(Realm::get_shared_realm(std::move(ref))
2!
1473
                    ->read_group()
2✔
1474
                    .get_table("class_object")
2✔
1475
                    ->get_column_key("value") != ColKey{});
2✔
1476
    }
2✔
1477

1478
    SECTION("removing a property from a existing read-only Realm leaves the column in place") {
48✔
1479
        Realm::get_shared_realm(config);
2✔
1480

1481
        // Remove the "value" property from the schema
1482
        config.schema_mode = SchemaMode::ReadOnly;
2✔
1483
        config.schema = Schema{{"object",
2✔
1484
                                {
2✔
1485
                                    {"_id", PropertyType::Int, Property::IsPrimary{true}},
2✔
1486
                                }}};
2✔
1487

1488
        auto [ref, error] = async_open_realm(config);
2✔
1489
        REQUIRE(ref);
2!
1490
        REQUIRE_FALSE(error);
2!
1491
        REQUIRE(Realm::get_shared_realm(std::move(ref))
2!
1492
                    ->read_group()
2✔
1493
                    .get_table("class_object")
2✔
1494
                    ->get_column_key("value") != ColKey{});
2✔
1495
    }
2✔
1496
}
48✔
1497

1498
#if REALM_ENABLE_AUTH_TESTS
1499

1500
TEST_CASE("Syhcnronized realm: AutoOpen", "[sync][baas][pbs][async open]") {
2✔
1501
    const auto partition = random_string(100);
2✔
1502
    auto schema = get_default_schema();
2✔
1503
    enum TestMode { expired_at_start, expired_by_websocket, websocket_fails };
2✔
1504
    enum FailureMode { location_fails, token_fails, token_not_authorized };
2✔
1505

1506
    auto logger = util::Logger::get_default_logger();
2✔
1507
    auto transport = std::make_shared<HookedTransport<>>();
2✔
1508
    auto socket_provider = std::make_shared<HookedSocketProvider>(logger, "some user agent");
2✔
1509
    std::mutex mutex;
2✔
1510

1511
    // Create the app session and get the logged in user identity
1512
    auto server_app_config = minimal_app_config("autoopen-realm", schema);
2✔
1513
    TestAppSession session(create_app(server_app_config), transport, DeleteApp{true}, realm::ReconnectMode::normal,
2✔
1514
                           socket_provider);
2✔
1515
    auto user = session.app()->current_user();
2✔
1516
    std::string identity = user->user_id();
2✔
1517
    REQUIRE(user->is_logged_in());
2!
1518
    REQUIRE(!identity.empty());
2!
1519
    // Reopen the App instance and retrieve the cached user
1520
    session.reopen(false);
2✔
1521
    user = session.app()->get_existing_logged_in_user(identity);
2✔
1522

1523
    SyncTestFile config(user, partition, schema);
2✔
1524
    config.sync_config->cancel_waits_on_nonfatal_error = true;
2✔
1525
    config.sync_config->error_handler = [&logger](std::shared_ptr<SyncSession> session, SyncError error) {
2✔
NEW
1526
        logger->debug("The sync error handler caught an error: '%1' for '%2'", error.status, session->path());
×
1527
        // Ignore connection failed non-fatal errors and check for access token refresh unauthorized fatal errors
NEW
1528
        if (error.status.code() == ErrorCodes::SyncConnectFailed) {
×
NEW
1529
            REQUIRE_FALSE(error.is_fatal);
×
NEW
1530
            return;
×
NEW
1531
        }
×
1532
        // If it's not SyncConnectFailed, then it should be AuthError
NEW
1533
        REQUIRE(error.status.code() == ErrorCodes::AuthError);
×
NEW
1534
        REQUIRE(error.is_fatal);
×
NEW
1535
    };
×
1536

1537
    bool not_authorized = false;
2✔
1538
    bool token_refresh_called = false;
2✔
1539
    bool location_refresh_called = false;
2✔
1540

1541
    FailureMode failure = FailureMode::location_fails;
2✔
1542

1543
    transport->request_hook = [&](const app::Request& req) -> std::optional<app::Response> {
2✔
1544
        static constexpr int CURLE_OPERATION_TIMEDOUT = 28;
2✔
1545
        std::lock_guard<std::mutex> lock(mutex);
2✔
1546
        if (req.url.find("/auth/session") != std::string::npos) {
2✔
NEW
1547
            token_refresh_called = true;
×
NEW
1548
            if (failure == FailureMode::token_not_authorized) {
×
NEW
1549
                return app::Response{403, 0, {}, "403 not authorized"};
×
NEW
1550
            }
×
NEW
1551
            if (failure == FailureMode::token_fails) {
×
NEW
1552
                return app::Response{0, CURLE_OPERATION_TIMEDOUT, {}, "Operation timed out"};
×
NEW
1553
            }
×
NEW
1554
        }
×
1555
        else if (req.url.find("/location") != std::string::npos) {
2✔
1556
            location_refresh_called = true;
2✔
1557
            if (failure == FailureMode::location_fails) {
2✔
1558
                // Fake "offline/request timed out" custom error response
1559
                return app::Response{0, CURLE_OPERATION_TIMEDOUT, {}, "Operation timed out"};
2✔
1560
            }
2✔
1561
        }
2✔
NEW
1562
        return std::nullopt;
×
1563
    };
2✔
1564

1565
    socket_provider->websocket_connect_func = [&]() -> std::optional<SocketProviderError> {
2✔
1566
        if (not_authorized) {
2✔
NEW
1567
            not_authorized = false; // one shot
×
NEW
1568
            return SocketProviderError(sync::websocket::WebSocketError::websocket_unauthorized, "403 not authorized");
×
NEW
1569
        }
×
1570
        return SocketProviderError(sync::websocket::WebSocketError::websocket_connection_failed,
2✔
1571
                                   "Operation timed out");
2✔
1572
    };
2✔
1573

1574
    auto task = Realm::get_synchronized_realm(config);
2✔
1575
    auto pf = util::make_promise_future<std::exception_ptr>();
2✔
1576
    task->start([&pf](auto ref, auto error) mutable {
2✔
1577
        REQUIRE(!ref);
2!
1578
        REQUIRE(error);
2!
1579
        pf.promise.emplace_value(error);
2✔
1580
    });
2✔
1581

1582
    auto result = pf.future.get_no_throw();
2✔
1583
    REQUIRE(result.is_ok());
2!
1584
    REQUIRE(result.get_value());
2!
1585
    {
2✔
1586
        std::lock_guard<std::mutex> lock(mutex);
2✔
1587
        REQUIRE(location_refresh_called);
2!
1588
        if (failure != FailureMode::location_fails) {
2✔
NEW
1589
            REQUIRE(token_refresh_called);
×
NEW
1590
        }
×
1591
    }
2✔
1592

1593
    transport->request_hook = nullptr;
2✔
1594
    socket_provider->websocket_connect_func = nullptr;
2✔
1595
    auto r = Realm::get_shared_realm(config);
2✔
1596
    wait_for_download(*r);
2✔
1597
}
2✔
1598

1599
#endif // REALM_ENABLE_AUTH_TESTS
1600

1601
TEST_CASE("SharedRealm: convert", "[sync][pbs][convert]") {
8✔
1602
    TestSyncManager tsm;
8✔
1603
    ObjectSchema object_schema = {"object",
8✔
1604
                                  {
8✔
1605
                                      {"_id", PropertyType::Int, Property::IsPrimary{true}},
8✔
1606
                                      {"value", PropertyType::Int},
8✔
1607
                                  }};
8✔
1608
    Schema schema{object_schema};
8✔
1609

1610
    SyncTestFile sync_config1(tsm, "default");
8✔
1611
    sync_config1.schema = schema;
8✔
1612
    TestFile local_config1;
8✔
1613
    local_config1.schema = schema;
8✔
1614
    local_config1.schema_version = sync_config1.schema_version;
8✔
1615

1616
    SECTION("can copy a synced realm to a synced realm") {
8✔
1617
        auto sync_realm1 = Realm::get_shared_realm(sync_config1);
2✔
1618
        sync_realm1->begin_transaction();
2✔
1619
        sync_realm1->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1620
        sync_realm1->commit_transaction();
2✔
1621
        wait_for_upload(*sync_realm1);
2✔
1622
        wait_for_download(*sync_realm1);
2✔
1623

1624
        // Copy to a new sync config
1625
        SyncTestFile sync_config2(tsm, "default");
2✔
1626
        sync_config2.schema = schema;
2✔
1627

1628
        sync_realm1->convert(sync_config2);
2✔
1629

1630
        auto sync_realm2 = Realm::get_shared_realm(sync_config2);
2✔
1631

1632
        // Check that the data also exists in the new realm
1633
        REQUIRE(sync_realm2->read_group().get_table("class_object")->size() == 1);
2!
1634

1635
        // Verify that sync works and objects created in the new copy will get
1636
        // synchronized to the old copy
1637
        sync_realm2->begin_transaction();
2✔
1638
        sync_realm2->read_group().get_table("class_object")->create_object_with_primary_key(1);
2✔
1639
        sync_realm2->commit_transaction();
2✔
1640
        wait_for_upload(*sync_realm2);
2✔
1641
        wait_for_download(*sync_realm1);
2✔
1642

1643
        sync_realm1->refresh();
2✔
1644
        REQUIRE(sync_realm1->read_group().get_table("class_object")->size() == 2);
2!
1645
    }
2✔
1646

1647
    SECTION("can convert a synced realm to a local realm") {
8✔
1648
        auto sync_realm = Realm::get_shared_realm(sync_config1);
2✔
1649
        sync_realm->begin_transaction();
2✔
1650
        sync_realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1651
        sync_realm->commit_transaction();
2✔
1652
        wait_for_upload(*sync_realm);
2✔
1653
        wait_for_download(*sync_realm);
2✔
1654

1655
        sync_realm->convert(local_config1);
2✔
1656

1657
        auto local_realm = Realm::get_shared_realm(local_config1);
2✔
1658

1659
        // Check that the data also exists in the new realm
1660
        REQUIRE(local_realm->read_group().get_table("class_object")->size() == 1);
2!
1661
    }
2✔
1662

1663
    SECTION("can convert a local realm to a synced realm") {
8✔
1664
        auto local_realm = Realm::get_shared_realm(local_config1);
2✔
1665
        local_realm->begin_transaction();
2✔
1666
        local_realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1667
        local_realm->commit_transaction();
2✔
1668

1669
        // Copy to a new sync config
1670
        local_realm->convert(sync_config1);
2✔
1671

1672
        auto sync_realm = Realm::get_shared_realm(sync_config1);
2✔
1673

1674
        // Check that the data also exists in the new realm
1675
        REQUIRE(sync_realm->read_group().get_table("class_object")->size() == 1);
2!
1676
    }
2✔
1677

1678
    SECTION("can copy a local realm to a local realm") {
8✔
1679
        auto local_realm1 = Realm::get_shared_realm(local_config1);
2✔
1680
        local_realm1->begin_transaction();
2✔
1681
        local_realm1->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1682
        local_realm1->commit_transaction();
2✔
1683

1684
        // Copy to a new local config
1685
        TestFile local_config2;
2✔
1686
        local_config2.schema = schema;
2✔
1687
        local_config2.schema_version = local_config1.schema_version;
2✔
1688
        local_realm1->convert(local_config2);
2✔
1689

1690
        auto local_realm2 = Realm::get_shared_realm(local_config2);
2✔
1691

1692
        // Check that the data also exists in the new realm
1693
        REQUIRE(local_realm2->read_group().get_table("class_object")->size() == 1);
2!
1694
    }
2✔
1695
}
8✔
1696

1697
TEST_CASE("SharedRealm: convert - embedded objects", "[sync][pbs][convert][embedded objects]") {
16✔
1698
    TestSyncManager tsm;
16✔
1699
    ObjectSchema object_schema = {"object",
16✔
1700
                                  {
16✔
1701
                                      {"_id", PropertyType::Int, Property::IsPrimary{true}},
16✔
1702
                                      {"value", PropertyType::Int},
16✔
1703
                                      {"embedded_link", PropertyType::Object | PropertyType::Nullable, "embedded"},
16✔
1704
                                  }};
16✔
1705
    ObjectSchema embedded_schema = {"embedded",
16✔
1706
                                    ObjectSchema::ObjectType::Embedded,
16✔
1707
                                    {
16✔
1708
                                        {"name", PropertyType::String | PropertyType::Nullable},
16✔
1709
                                    }};
16✔
1710
    Schema schema{object_schema, embedded_schema};
16✔
1711

1712
    SyncTestFile sync_config1(tsm, "default");
16✔
1713
    sync_config1.schema = schema;
16✔
1714
    TestFile local_config1;
16✔
1715
    local_config1.schema = schema;
16✔
1716
    local_config1.schema_version = sync_config1.schema_version;
16✔
1717

1718
    SECTION("can copy a synced realm to a synced realm") {
16✔
1719
        auto sync_realm1 = Realm::get_shared_realm(sync_config1);
4✔
1720
        sync_realm1->begin_transaction();
4✔
1721

1722
        SECTION("null embedded object") {
4✔
1723
            sync_realm1->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1724
        }
2✔
1725

1726
        SECTION("embedded object") {
4✔
1727
            auto obj = sync_realm1->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1728
            auto col_key = sync_realm1->read_group().get_table("class_object")->get_column_key("embedded_link");
2✔
1729
            obj.create_and_set_linked_object(col_key);
2✔
1730
        }
2✔
1731

1732
        sync_realm1->commit_transaction();
4✔
1733
        wait_for_upload(*sync_realm1);
4✔
1734
        wait_for_download(*sync_realm1);
4✔
1735

1736
        // Copy to a new sync config
1737
        SyncTestFile sync_config2(tsm, "default");
4✔
1738
        sync_config2.schema = schema;
4✔
1739

1740
        sync_realm1->convert(sync_config2);
4✔
1741

1742
        auto sync_realm2 = Realm::get_shared_realm(sync_config2);
4✔
1743

1744
        // Check that the data also exists in the new realm
1745
        REQUIRE(sync_realm2->read_group().get_table("class_object")->size() == 1);
4!
1746

1747
        // Verify that sync works and objects created in the new copy will get
1748
        // synchronized to the old copy
1749
        sync_realm2->begin_transaction();
4✔
1750
        sync_realm2->read_group().get_table("class_object")->create_object_with_primary_key(1);
4✔
1751
        sync_realm2->commit_transaction();
4✔
1752
        wait_for_upload(*sync_realm2);
4✔
1753
        wait_for_download(*sync_realm1);
4✔
1754

1755
        sync_realm1->refresh();
4✔
1756
        REQUIRE(sync_realm1->read_group().get_table("class_object")->size() == 2);
4!
1757
    }
4✔
1758

1759
    SECTION("can convert a synced realm to a local realm") {
16✔
1760
        auto sync_realm = Realm::get_shared_realm(sync_config1);
4✔
1761
        sync_realm->begin_transaction();
4✔
1762

1763
        SECTION("null embedded object") {
4✔
1764
            sync_realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1765
        }
2✔
1766

1767
        SECTION("embedded object") {
4✔
1768
            auto obj = sync_realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1769
            auto col_key = sync_realm->read_group().get_table("class_object")->get_column_key("embedded_link");
2✔
1770
            obj.create_and_set_linked_object(col_key);
2✔
1771
        }
2✔
1772

1773
        sync_realm->commit_transaction();
4✔
1774
        wait_for_upload(*sync_realm);
4✔
1775
        wait_for_download(*sync_realm);
4✔
1776

1777
        sync_realm->convert(local_config1);
4✔
1778

1779
        auto local_realm = Realm::get_shared_realm(local_config1);
4✔
1780

1781
        // Check that the data also exists in the new realm
1782
        REQUIRE(local_realm->read_group().get_table("class_object")->size() == 1);
4!
1783
    }
4✔
1784

1785
    SECTION("can convert a local realm to a synced realm") {
16✔
1786
        auto local_realm = Realm::get_shared_realm(local_config1);
4✔
1787
        local_realm->begin_transaction();
4✔
1788

1789
        SECTION("null embedded object") {
4✔
1790
            local_realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1791
        }
2✔
1792

1793
        SECTION("embedded object") {
4✔
1794
            auto obj = local_realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1795
            auto col_key = local_realm->read_group().get_table("class_object")->get_column_key("embedded_link");
2✔
1796
            obj.create_and_set_linked_object(col_key);
2✔
1797
        }
2✔
1798

1799
        local_realm->commit_transaction();
4✔
1800

1801
        // Copy to a new sync config
1802
        local_realm->convert(sync_config1);
4✔
1803

1804
        auto sync_realm = Realm::get_shared_realm(sync_config1);
4✔
1805

1806
        // Check that the data also exists in the new realm
1807
        REQUIRE(sync_realm->read_group().get_table("class_object")->size() == 1);
4!
1808
    }
4✔
1809

1810
    SECTION("can copy a local realm to a local realm") {
16✔
1811
        auto local_realm1 = Realm::get_shared_realm(local_config1);
4✔
1812
        local_realm1->begin_transaction();
4✔
1813

1814
        SECTION("null embedded object") {
4✔
1815
            local_realm1->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1816
        }
2✔
1817

1818
        SECTION("embedded object") {
4✔
1819
            auto obj = local_realm1->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1820
            auto col_key = local_realm1->read_group().get_table("class_object")->get_column_key("embedded_link");
2✔
1821
            obj.create_and_set_linked_object(col_key);
2✔
1822
        }
2✔
1823

1824
        local_realm1->commit_transaction();
4✔
1825

1826
        // Copy to a new local config
1827
        TestFile local_config2;
4✔
1828
        local_config2.schema = schema;
4✔
1829
        local_config2.schema_version = local_config1.schema_version;
4✔
1830
        local_realm1->convert(local_config2);
4✔
1831

1832
        auto local_realm2 = Realm::get_shared_realm(local_config2);
4✔
1833

1834
        // Check that the data also exists in the new realm
1835
        REQUIRE(local_realm2->read_group().get_table("class_object")->size() == 1);
4!
1836
    }
4✔
1837
}
16✔
1838
#endif // REALM_ENABLE_SYNC
1839

1840
TEST_CASE("SharedRealm: async writes") {
104✔
1841
    _impl::RealmCoordinator::assert_no_open_realms();
104✔
1842
    if (!util::EventLoop::has_implementation())
104✔
1843
        return;
×
1844

1845
    TestFile config;
104✔
1846
    config.schema_version = 0;
104✔
1847
    config.schema = Schema{
104✔
1848
        {"object",
104✔
1849
         {
104✔
1850
             {"value", PropertyType::Int},
104✔
1851
             {"ints", PropertyType::Array | PropertyType::Int},
104✔
1852
             {"int set", PropertyType::Set | PropertyType::Int},
104✔
1853
             {"int dictionary", PropertyType::Dictionary | PropertyType::Int},
104✔
1854
         }},
104✔
1855
    };
104✔
1856
    bool done = false;
104✔
1857
    auto realm = Realm::get_shared_realm(config);
104✔
1858
    auto table = realm->read_group().get_table("class_object");
104✔
1859
    auto col = table->get_column_key("value");
104✔
1860
    int write_nr = 0;
104✔
1861
    int commit_nr = 0;
104✔
1862

1863
    auto wait_for_done = [&]() {
104✔
1864
        util::EventLoop::main().run_until([&] {
247,834✔
1865
            return done;
247,834✔
1866
        });
247,834✔
1867
        REQUIRE(done);
96!
1868
    };
96✔
1869

1870
    SECTION("async commit transaction") {
104✔
1871
        realm->async_begin_transaction([&]() {
2✔
1872
            REQUIRE(write_nr == 0);
2!
1873
            ++write_nr;
2✔
1874
            table->create_object().set(col, 45);
2✔
1875
            realm->async_commit_transaction([&](std::exception_ptr) {
2✔
1876
                REQUIRE(commit_nr == 0);
2!
1877
                ++commit_nr;
2✔
1878
            });
2✔
1879
        });
2✔
1880
        for (int expected = 1; expected < 1000; ++expected) {
2,000✔
1881
            realm->async_begin_transaction([&, expected]() {
1,998✔
1882
                REQUIRE(write_nr == expected);
1,998!
1883
                ++write_nr;
1,998✔
1884
                auto o = table->get_object(0);
1,998✔
1885
                o.set(col, o.get<int64_t>(col) + 37);
1,998✔
1886
                realm->async_commit_transaction(
1,998✔
1887
                    [&](auto) {
1,998✔
1888
                        ++commit_nr;
1,998✔
1889
                        done = commit_nr == 1000;
1,998✔
1890
                    },
1,998✔
1891
                    true);
1,998✔
1892
            });
1,998✔
1893
        }
1,998✔
1894
        wait_for_done();
2✔
1895
    }
2✔
1896

1897
    auto verify_persisted_count = [&](size_t expected) {
104✔
1898
        if (realm)
52✔
1899
            realm->close();
50✔
1900
        _impl::RealmCoordinator::assert_no_open_realms();
52✔
1901

1902
        auto new_realm = Realm::get_shared_realm(config);
52✔
1903
        auto table = new_realm->read_group().get_table("class_object");
52✔
1904
        REQUIRE(table->size() == expected);
52!
1905
    };
52✔
1906

1907
    using RealmCloseFunction = void (Realm::*)();
104✔
1908
    static RealmCloseFunction close_functions[] = {&Realm::close, &Realm::invalidate};
104✔
1909
    static const char* close_function_names[] = {"close()", "invalidate()"};
104✔
1910
    for (int i = 0; i < 2; ++i) {
312✔
1911
        SECTION(close_function_names[i]) {
208✔
1912
            bool persisted = false;
40✔
1913
            SECTION("before write lock is acquired") {
40✔
1914
                DBOptions options;
4✔
1915
                options.encryption_key = config.encryption_key.data();
4✔
1916
                // Acquire the write lock with a different DB instance so that we'll
1917
                // be stuck in the Requesting stage
1918
                realm::test_util::BowlOfStonesSemaphore sema;
4✔
1919
                JoiningThread thread([&] {
4✔
1920
                    auto db = DB::create(make_in_realm_history(), config.path, options);
4✔
1921
                    auto write = db->start_write();
4✔
1922
                    sema.add_stone();
4✔
1923

1924
                    // Wait until the main thread is waiting for the lock.
1925
                    while (!db->other_writers_waiting_for_lock()) {
8✔
1926
                        millisleep(1);
4✔
1927
                    }
4✔
1928
                    write->close();
4✔
1929
                });
4✔
1930

1931
                // Wait for the background thread to have acquired the lock
1932
                sema.get_stone();
4✔
1933

1934
                auto scheduler = realm->scheduler();
4✔
1935
                realm->async_begin_transaction([&] {
4✔
1936
                    // We should never get here as the realm is closed
1937
                    FAIL();
×
1938
                });
×
1939

1940
                // close() should block until we can acquire the write lock
1941
                std::invoke(close_functions[i], *realm);
4✔
1942

1943
                {
4✔
1944
                    // Verify that we released the write lock
1945
                    auto db = DB::create(make_in_realm_history(), config.path, options);
4✔
1946
                    REQUIRE(db->start_write(/* nonblocking */ true));
4!
1947
                }
4✔
1948

1949
                // Verify that the transaction callback never got enqueued
1950
                scheduler->invoke([&] {
4✔
1951
                    done = true;
4✔
1952
                });
4✔
1953
                wait_for_done();
4✔
1954
            }
4✔
1955
            SECTION("before async_begin_transaction() callback") {
40✔
1956
                auto scheduler = realm->scheduler();
4✔
1957
                realm->async_begin_transaction([&] {
4✔
1958
                    // We should never get here as the realm is closed
1959
                    FAIL();
×
1960
                });
×
1961
                std::invoke(close_functions[i], *realm);
4✔
1962
                scheduler->invoke([&] {
4✔
1963
                    done = true;
4✔
1964
                });
4✔
1965
                wait_for_done();
4✔
1966
                verify_persisted_count(0);
4✔
1967
            }
4✔
1968
            SECTION("inside async_begin_transaction() callback before commit") {
40✔
1969
                realm->async_begin_transaction([&] {
4✔
1970
                    table->create_object().set(col, 45);
4✔
1971
                    std::invoke(close_functions[i], *realm);
4✔
1972
                    done = true;
4✔
1973
                });
4✔
1974
                wait_for_done();
4✔
1975
                verify_persisted_count(0);
4✔
1976
            }
4✔
1977
            SECTION("inside async_begin_transaction() callback after sync commit") {
40✔
1978
                realm->async_begin_transaction([&] {
4✔
1979
                    table->create_object().set(col, 45);
4✔
1980
                    realm->commit_transaction();
4✔
1981
                    std::invoke(close_functions[i], *realm);
4✔
1982
                    done = true;
4✔
1983
                });
4✔
1984
                wait_for_done();
4✔
1985
                verify_persisted_count(1);
4✔
1986
            }
4✔
1987
            SECTION("inside async_begin_transaction() callback after async commit") {
40✔
1988
                realm->async_begin_transaction([&] {
4✔
1989
                    table->create_object().set(col, 45);
4✔
1990
                    realm->async_commit_transaction([&](std::exception_ptr) {
4✔
1991
                        persisted = true;
4✔
1992
                    });
4✔
1993
                    std::invoke(close_functions[i], *realm);
4✔
1994
                    REQUIRE(persisted);
4!
1995
                    done = true;
4✔
1996
                });
4✔
1997
                wait_for_done();
4✔
1998
                verify_persisted_count(1);
4✔
1999
            }
4✔
2000
            SECTION("inside async commit completion") {
40✔
2001
                realm->async_begin_transaction([&] {
4✔
2002
                    table->create_object().set(col, 45);
4✔
2003
                    realm->async_commit_transaction([&](std::exception_ptr) {
4✔
2004
                        done = true;
4✔
2005
                        std::invoke(close_functions[i], *realm);
4✔
2006
                    });
4✔
2007
                });
4✔
2008
                wait_for_done();
4✔
2009
                verify_persisted_count(1);
4✔
2010
            }
4✔
2011
            SECTION("between commit and sync") {
40✔
2012
                realm->async_begin_transaction([&] {
4✔
2013
                    table->create_object().set(col, 45);
4✔
2014
                    realm->async_commit_transaction([&](std::exception_ptr) {
4✔
2015
                        persisted = true;
4✔
2016
                    });
4✔
2017
                    done = true;
4✔
2018
                });
4✔
2019
                wait_for_done();
4✔
2020
                std::invoke(close_functions[i], *realm);
4✔
2021
                REQUIRE(persisted);
4!
2022
                verify_persisted_count(1);
4✔
2023
            }
4✔
2024
            SECTION("with multiple pending commits") {
40✔
2025
                int complete_count = 0;
4✔
2026
                realm->async_begin_transaction([&] {
4✔
2027
                    table->create_object().set(col, 45);
4✔
2028
                    realm->async_commit_transaction([&](std::exception_ptr) {
4✔
2029
                        ++complete_count;
4✔
2030
                    });
4✔
2031
                });
4✔
2032
                realm->async_begin_transaction([&] {
4✔
2033
                    table->create_object().set(col, 45);
4✔
2034
                    realm->async_commit_transaction(
4✔
2035
                        [&](auto) {
4✔
2036
                            ++complete_count;
4✔
2037
                        },
4✔
2038
                        true);
4✔
2039
                });
4✔
2040
                realm->async_begin_transaction([&] {
4✔
2041
                    table->create_object().set(col, 45);
4✔
2042
                    realm->async_commit_transaction(
4✔
2043
                        [&](auto) {
4✔
2044
                            ++complete_count;
4✔
2045
                        },
4✔
2046
                        true);
4✔
2047
                    done = true;
4✔
2048
                });
4✔
2049

2050
                wait_for_done();
4✔
2051
                std::invoke(close_functions[i], *realm);
4✔
2052
                REQUIRE(complete_count == 3);
4!
2053
                verify_persisted_count(3);
4✔
2054
            }
4✔
2055
            SECTION("inside async_begin_transaction() with pending commits") {
40✔
2056
                int complete_count = 0;
4✔
2057
                realm->async_begin_transaction([&] {
4✔
2058
                    table->create_object().set(col, 45);
4✔
2059
                    realm->async_commit_transaction([&](std::exception_ptr) {
4✔
2060
                        ++complete_count;
4✔
2061
                    });
4✔
2062
                });
4✔
2063
                realm->async_begin_transaction([&] {
4✔
2064
                    // This create should be discarded
2065
                    table->create_object().set(col, 45);
4✔
2066
                    std::invoke(close_functions[i], *realm);
4✔
2067
                    done = true;
4✔
2068
                });
4✔
2069

2070
                wait_for_done();
4✔
2071
                std::invoke(close_functions[i], *realm);
4✔
2072
                REQUIRE(complete_count == 1);
4!
2073
                verify_persisted_count(1);
4✔
2074
            }
4✔
2075
            SECTION("within did_change()") {
40✔
2076
                struct Context : public BindingContext {
4✔
2077
                    int i;
4✔
2078
                    Context(int i)
4✔
2079
                        : i(i)
4✔
2080
                    {
4✔
2081
                    }
4✔
2082
                    void did_change(std::vector<ObserverState> const&, std::vector<void*> const&, bool) override
4✔
2083
                    {
6✔
2084
                        std::invoke(close_functions[i], *realm.lock());
6✔
2085
                    }
6✔
2086
                };
4✔
2087
                realm->m_binding_context.reset(new Context(i));
4✔
2088
                realm->m_binding_context->realm = realm;
4✔
2089

2090
                realm->async_begin_transaction([&] {
4✔
2091
                    table->create_object().set(col, 45);
4✔
2092
                    realm->async_commit_transaction([&](std::exception_ptr) {
4✔
2093
                        done = true;
4✔
2094
                    });
4✔
2095
                });
4✔
2096

2097
                wait_for_done();
4✔
2098
                verify_persisted_count(1);
4✔
2099
            }
4✔
2100
        }
40✔
2101
    }
208✔
2102

2103
    SECTION("notify only with no further actions") {
104✔
2104
        realm->async_begin_transaction(
2✔
2105
            [&] {
2✔
2106
                done = true;
2✔
2107
            },
2✔
2108
            true);
2✔
2109
        wait_for_done();
2✔
2110
        realm->cancel_transaction();
2✔
2111
    }
2✔
2112
    SECTION("notify only with synchronous commit") {
104✔
2113
        realm->async_begin_transaction(
2✔
2114
            [&] {
2✔
2115
                done = true;
2✔
2116
            },
2✔
2117
            true);
2✔
2118
        wait_for_done();
2✔
2119
        table->create_object();
2✔
2120
        realm->commit_transaction();
2✔
2121
    }
2✔
2122
    SECTION("schedule async commits after notify only") {
104✔
2123
        realm->async_begin_transaction(
2✔
2124
            [&] {
2✔
2125
                done = true;
2✔
2126
            },
2✔
2127
            true);
2✔
2128
        wait_for_done();
2✔
2129
        done = false;
2✔
2130
        realm->async_begin_transaction([&] {
2✔
2131
            table->create_object();
2✔
2132
            done = true;
2✔
2133
            realm->commit_transaction();
2✔
2134
        });
2✔
2135
        table->create_object();
2✔
2136
        realm->commit_transaction();
2✔
2137
        REQUIRE(table->size() == 1);
2!
2138
        wait_for_done();
2✔
2139
        REQUIRE(table->size() == 2);
2!
2140
    }
2✔
2141
    SECTION("exception thrown during transaction with error handler") {
104✔
2142
        Realm::AsyncHandle h = 7;
2✔
2143
        bool called = false;
2✔
2144
        realm->set_async_error_handler([&](Realm::AsyncHandle handle, std::exception_ptr error) {
2✔
2145
            REQUIRE(error);
2!
2146
            REQUIRE_THROWS_CONTAINING(std::rethrow_exception(error), "an error");
2✔
2147
            CHECK(handle == h);
2!
2148
            called = true;
2✔
2149
        });
2✔
2150
        h = realm->async_begin_transaction([&] {
2✔
2151
            table->create_object();
2✔
2152
            done = true;
2✔
2153
            throw std::runtime_error("an error");
2✔
2154
        });
2✔
2155
        wait_for_done();
2✔
2156

2157
        // Transaction should have been rolled back
2158
        REQUIRE_FALSE(realm->is_in_transaction());
2!
2159
        REQUIRE(table->size() == 0);
2!
2160
        REQUIRE(called);
2!
2161

2162
        // Should be able to perform another write afterwards
2163
        done = false;
2✔
2164
        called = false;
2✔
2165
        h = realm->async_begin_transaction([&] {
2✔
2166
            table->create_object();
2✔
2167
            realm->commit_transaction();
2✔
2168
            done = true;
2✔
2169
        });
2✔
2170
        wait_for_done();
2✔
2171
        REQUIRE(table->size() == 1);
2!
2172
        REQUIRE_FALSE(called);
2!
2173
    }
2✔
2174
#ifndef _WIN32
104✔
2175
    SECTION("exception thrown during transaction without error handler") {
104✔
2176
        realm->set_async_error_handler(nullptr);
2✔
2177
        realm->async_begin_transaction([&] {
2✔
2178
            table->create_object();
2✔
2179
            throw std::runtime_error("an error");
2✔
2180
        });
2✔
2181
        REQUIRE_THROWS_CONTAINING(util::EventLoop::main().run_until([&] {
2✔
2182
            return false;
2✔
2183
        }),
2✔
2184
                                  "an error");
2✔
2185

2186
        // Transaction should have been rolled back
2187
        REQUIRE_FALSE(realm->is_in_transaction());
2!
2188
        REQUIRE(table->size() == 0);
2!
2189

2190
        // Should be able to perform another write afterwards
2191
        realm->async_begin_transaction([&, realm] {
2✔
2192
            table->create_object();
2✔
2193
            realm->commit_transaction();
2✔
2194
            done = true;
2✔
2195
        });
2✔
2196
        wait_for_done();
2✔
2197
        REQUIRE(table->size() == 1);
2!
2198
    }
2✔
2199
    SECTION("exception thrown during transaction without error handler after closing Realm") {
104✔
2200
        realm->set_async_error_handler(nullptr);
2✔
2201
        realm->async_begin_transaction([&] {
2✔
2202
            realm->close();
2✔
2203
            throw std::runtime_error("an error");
2✔
2204
        });
2✔
2205
        REQUIRE_THROWS_CONTAINING(util::EventLoop::main().run_until([&] {
2✔
2206
            return false;
2✔
2207
        }),
2✔
2208
                                  "an error");
2✔
2209
        REQUIRE(realm->is_closed());
2!
2210
    }
2✔
2211
#endif
104✔
2212
    SECTION("exception thrown from async commit completion callback with error handler") {
104✔
2213
        Realm::AsyncHandle h;
2✔
2214
        realm->set_async_error_handler([&](Realm::AsyncHandle handle, std::exception_ptr error) {
2✔
2215
            REQUIRE(error);
2!
2216
            REQUIRE_THROWS_CONTAINING(std::rethrow_exception(error), "an error");
2✔
2217
            CHECK(handle == h);
2!
2218
            done = true;
2✔
2219
        });
2✔
2220

2221
        realm->begin_transaction();
2✔
2222
        table->create_object();
2✔
2223
        h = realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2224
            throw std::runtime_error("an error");
2✔
2225
        });
2✔
2226
        wait_for_done();
2✔
2227
        verify_persisted_count(1);
2✔
2228
    }
2✔
2229
#ifndef _WIN32
104✔
2230
    SECTION("exception thrown from async commit completion callback without error handler") {
104✔
2231
        realm->begin_transaction();
2✔
2232
        table->create_object();
2✔
2233
        realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2234
            throw std::runtime_error("an error");
2✔
2235
        });
2✔
2236
        REQUIRE_THROWS_CONTAINING(util::EventLoop::main().run_until([&] {
2✔
2237
            return false;
2✔
2238
        }),
2✔
2239
                                  "an error");
2✔
2240
        REQUIRE(table->size() == 1);
2!
2241
    }
2✔
2242
#endif
104✔
2243

2244
    if (_impl::SimulatedFailure::is_enabled()) {
104✔
2245
        SECTION("error in the synchronous part of async commit") {
104✔
2246
            realm->begin_transaction();
2✔
2247
            table->create_object();
2✔
2248

2249
            using sf = _impl::SimulatedFailure;
2✔
2250
            sf::OneShotPrimeGuard pg(sf::shared_group__grow_reader_mapping);
2✔
2251
            REQUIRE_THROWS_AS(realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2252
                FAIL("should not call completion");
2✔
2253
            }),
2✔
2254
                              _impl::SimulatedFailure);
2✔
2255
            REQUIRE_FALSE(realm->is_in_transaction());
2!
2256
        }
2✔
2257
        SECTION("error in the async part of async commit") {
104✔
2258
            realm->begin_transaction();
2✔
2259
            table->create_object();
2✔
2260

2261
            using sf = _impl::SimulatedFailure;
2✔
2262
            sf::set_thread_local(false);
2✔
2263
            sf::OneShotPrimeGuard pg(sf::group_writer__commit);
2✔
2264
            realm->async_commit_transaction([&](std::exception_ptr e) {
2✔
2265
                REQUIRE(e);
2!
2266
                REQUIRE_THROWS_AS(std::rethrow_exception(e), _impl::SimulatedFailure);
2✔
2267
                done = true;
2✔
2268
            });
2✔
2269
            wait_for_done();
2✔
2270
            sf::set_thread_local(true);
2✔
2271
        }
2✔
2272
    }
104✔
2273
    SECTION("throw exception from did_change()") {
104✔
2274
        struct Context : public BindingContext {
2✔
2275
            void did_change(std::vector<ObserverState> const&, std::vector<void*> const&, bool) override
2✔
2276
            {
2✔
2277
                throw std::runtime_error("expected error");
2✔
2278
            }
2✔
2279
        };
2✔
2280
        realm->m_binding_context.reset(new Context);
2✔
2281

2282
        realm->begin_transaction();
2✔
2283
        auto table = realm->read_group().get_table("class_object");
2✔
2284
        table->create_object();
2✔
2285
        REQUIRE_THROWS_WITH(realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2286
            done = true;
2✔
2287
        }),
2✔
2288
                            "expected error");
2✔
2289
        wait_for_done();
2✔
2290
    }
2✔
2291
    SECTION("cancel scheduled async transaction") {
104✔
2292
        auto handle = realm->async_begin_transaction([&, realm]() {
2✔
2293
            table->create_object().set(col, 45);
×
2294
            realm->async_commit_transaction(
×
2295
                [&](auto) {
×
2296
                    done = true;
×
2297
                },
×
2298
                true);
×
2299
        });
×
2300
        realm->async_begin_transaction([&, realm]() {
2✔
2301
            table->create_object().set(col, 90);
2✔
2302
            realm->async_commit_transaction(
2✔
2303
                [&](auto) {
2✔
2304
                    done = true;
2✔
2305
                },
2✔
2306
                true);
2✔
2307
        });
2✔
2308
        realm->async_cancel_transaction(handle);
2✔
2309
        wait_for_done();
2✔
2310
        auto table = realm->read_group().get_table("class_object");
2✔
2311
        REQUIRE(table->size() == 1);
2!
2312
        REQUIRE(table->begin()->get<Int>("value") == 90);
2!
2313
    }
2✔
2314
    SECTION("synchronous cancel inside async transaction") {
104✔
2315
        realm->async_begin_transaction([&, realm]() {
2✔
2316
            REQUIRE(table->size() == 0);
2!
2317
            table->create_object().set(col, 45);
2✔
2318
            REQUIRE(table->size() == 1);
2!
2319
            realm->cancel_transaction();
2✔
2320
            REQUIRE(table->size() == 0);
2!
2321
            done = true;
2✔
2322
        });
2✔
2323
        wait_for_done();
2✔
2324
    }
2✔
2325
    SECTION("synchronous commit of async transaction after async commit which allows grouping") {
104✔
2326
        realm->async_begin_transaction([&, realm]() {
2✔
2327
            table->create_object().set(col, 45);
2✔
2328
            realm->async_commit_transaction(
2✔
2329
                [&](auto) {
2✔
2330
                    done = true;
2✔
2331
                },
2✔
2332
                true);
2✔
2333
        });
2✔
2334
        realm->async_begin_transaction([&, realm]() {
2✔
2335
            table->create_object().set(col, 45);
2✔
2336
            realm->commit_transaction();
2✔
2337
        });
2✔
2338
        wait_for_done();
2✔
2339
        auto table = realm->read_group().get_table("class_object");
2✔
2340
        REQUIRE(table->size() == 2);
2!
2341
    }
2✔
2342
    SECTION("synchronous transaction after async transaction with no commit") {
104✔
2343
        realm->async_begin_transaction([&]() {
2✔
2344
            table->create_object().set(col, 80);
2✔
2345
            done = true;
2✔
2346
        });
2✔
2347
        wait_for_done();
2✔
2348
        realm->begin_transaction();
2✔
2349
        table->create_object().set(col, 90);
2✔
2350
        realm->commit_transaction();
2✔
2351
        verify_persisted_count(1);
2✔
2352
    }
2✔
2353
    SECTION("synchronous transaction with scheduled async transaction with no commit") {
104✔
2354
        realm->async_begin_transaction([&]() {
2✔
2355
            table->create_object().set(col, 80);
2✔
2356
            done = true;
2✔
2357
        });
2✔
2358
        realm->begin_transaction();
2✔
2359
        table->create_object().set(col, 90);
2✔
2360
        realm->commit_transaction();
2✔
2361
        wait_for_done();
2✔
2362
        verify_persisted_count(1);
2✔
2363
    }
2✔
2364
    SECTION("synchronous transaction with scheduled async transaction") {
104✔
2365
        realm->async_begin_transaction([&, realm]() {
2✔
2366
            table->create_object().set(col, 80);
2✔
2367
            realm->commit_transaction();
2✔
2368
            done = true;
2✔
2369
        });
2✔
2370
        realm->begin_transaction();
2✔
2371
        table->create_object().set(col, 90);
2✔
2372
        realm->commit_transaction();
2✔
2373
        wait_for_done();
2✔
2374
        REQUIRE(table->size() == 2);
2!
2375
        REQUIRE(table->get_object(0).get<Int>(col) == 90);
2!
2376
        REQUIRE(table->get_object(1).get<Int>(col) == 80);
2!
2377
    }
2✔
2378
    SECTION("synchronous transaction with async write") {
104✔
2379
        realm->begin_transaction();
2✔
2380
        table->create_object().set(col, 45);
2✔
2381
        realm->async_commit_transaction();
2✔
2382

2383
        realm->begin_transaction();
2✔
2384
        table->create_object().set(col, 90);
2✔
2385
        realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2386
            done = true;
2✔
2387
        });
2✔
2388
        wait_for_done();
2✔
2389
        verify_persisted_count(2);
2✔
2390
    }
2✔
2391
    SECTION("synchronous transaction mixed with async transactions") {
104✔
2392
        realm->async_begin_transaction([&, realm]() {
2✔
2393
            table->create_object().set(col, 45);
2✔
2394
            done = true;
2✔
2395
            realm->async_commit_transaction();
2✔
2396
        });
2✔
2397
        realm->async_begin_transaction([&, realm]() {
2✔
2398
            table->create_object().set(col, 45);
2✔
2399
            realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2400
                done = true;
2✔
2401
            });
2✔
2402
        });
2✔
2403
        wait_for_done();
2✔
2404
        realm->begin_transaction(); // Here syncing of first async tr has not completed
2✔
2405
        REQUIRE(table->size() == 1);
2!
2406
        table->create_object().set(col, 90);
2✔
2407
        realm->commit_transaction(); // Will re-initiate async writes
2✔
2408

2409
        done = false;
2✔
2410
        wait_for_done();
2✔
2411
        verify_persisted_count(3);
2✔
2412
    }
2✔
2413
    SECTION("asynchronous transaction mixed with sync transaction that is cancelled") {
104✔
2414
        bool persisted = false;
2✔
2415
        realm->async_begin_transaction([&, realm]() {
2✔
2416
            table->create_object().set(col, 45);
2✔
2417
            done = true;
2✔
2418
            realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2419
                persisted = true;
2✔
2420
            });
2✔
2421
        });
2✔
2422
        realm->async_begin_transaction([&, realm]() {
2✔
2423
            table->create_object().set(col, 45);
2✔
2424
            auto handle = realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2425
                FAIL();
×
2426
            });
×
2427
            realm->async_cancel_transaction(handle);
2✔
2428
        });
2✔
2429
        wait_for_done();
2✔
2430
        realm->begin_transaction();
2✔
2431
        CHECK(persisted);
2!
2432
        persisted = false;
2✔
2433
        REQUIRE(table->size() == 1);
2!
2434
        table->create_object().set(col, 90);
2✔
2435
        realm->cancel_transaction();
2✔
2436

2437
        util::EventLoop::main().run_until([&] {
2,516✔
2438
            return !realm->is_in_async_transaction();
2,516✔
2439
        });
2,516✔
2440

2441
        REQUIRE(table->size() == 2);
2!
2442
        REQUIRE(!table->find_first_int(col, 90));
2!
2443
    }
2✔
2444
    SECTION("cancelled sync transaction with pending async transaction") {
104✔
2445
        realm->async_begin_transaction([&, realm]() {
2✔
2446
            table->create_object().set(col, 45);
2✔
2447
            realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2448
                done = true;
2✔
2449
            });
2✔
2450
        });
2✔
2451
        realm->begin_transaction();
2✔
2452
        REQUIRE(table->size() == 0);
2!
2453
        table->create_object();
2✔
2454
        realm->cancel_transaction();
2✔
2455
        REQUIRE(table->size() == 0);
2!
2456
        wait_for_done();
2✔
2457
        verify_persisted_count(1);
2✔
2458
    }
2✔
2459
    SECTION("cancelled sync transaction with pending async commit") {
104✔
2460
        bool persisted = false;
2✔
2461
        realm->async_begin_transaction([&, realm]() {
2✔
2462
            table->create_object().set(col, 45);
2✔
2463
            done = true;
2✔
2464
            realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2465
                persisted = true;
2✔
2466
            });
2✔
2467
        });
2✔
2468
        wait_for_done();
2✔
2469
        realm->begin_transaction();
2✔
2470
        REQUIRE(table->size() == 1);
2!
2471
        table->create_object();
2✔
2472
        realm->cancel_transaction();
2✔
2473

2474
        util::EventLoop::main().run_until([&] {
3✔
2475
            return persisted;
3✔
2476
        });
3✔
2477
        verify_persisted_count(1);
2✔
2478
    }
2✔
2479
    SECTION("sync commit of async transaction with subsequent pending async transaction") {
104✔
2480
        realm->async_begin_transaction([&, realm]() {
2✔
2481
            table->create_object();
2✔
2482
            realm->commit_transaction();
2✔
2483
        });
2✔
2484
        realm->async_begin_transaction([&, realm]() {
2✔
2485
            table->create_object();
2✔
2486
            realm->commit_transaction();
2✔
2487
            done = true;
2✔
2488
        });
2✔
2489
        wait_for_done();
2✔
2490
        REQUIRE(table->size() == 2);
2!
2491
    }
2✔
2492
    SECTION("release reference to Realm after async begin") {
104✔
2493
        std::weak_ptr<Realm> weak_realm = realm;
2✔
2494
        realm->async_begin_transaction([&]() {
2✔
2495
            table->create_object().set(col, 45);
2✔
2496
            weak_realm.lock()->async_commit_transaction([&](std::exception_ptr) {
2✔
2497
                done = true;
2✔
2498
            });
2✔
2499
        });
2✔
2500
        realm = nullptr;
2✔
2501
        wait_for_done();
2✔
2502
        verify_persisted_count(1);
2✔
2503
    }
2✔
2504
    SECTION("object change information") {
104✔
2505
        realm->begin_transaction();
2✔
2506
        auto list_col = table->get_column_key("ints");
2✔
2507
        auto set_col = table->get_column_key("int set");
2✔
2508
        auto dict_col = table->get_column_key("int dictionary");
2✔
2509
        auto obj = table->create_object();
2✔
2510
        auto list = obj.get_list<Int>(list_col);
2✔
2511
        for (int i = 0; i < 3; ++i)
8✔
2512
            list.add(i);
6✔
2513
        auto set = obj.get_set<Int>(set_col);
2✔
2514
        set.insert(0);
2✔
2515
        auto dict = obj.get_dictionary(dict_col);
2✔
2516
        dict.insert("a", 0);
2✔
2517
        realm->commit_transaction();
2✔
2518

2519
        Observer observer(obj);
2✔
2520
        observer.realm = realm;
2✔
2521
        realm->m_binding_context.reset(&observer);
2✔
2522

2523
        realm->async_begin_transaction([&]() {
2✔
2524
            list.clear();
2✔
2525
            set.clear();
2✔
2526
            dict.clear();
2✔
2527
            done = true;
2✔
2528
        });
2✔
2529
        wait_for_done();
2✔
2530
        REQUIRE(observer.array_change(0, list_col) == IndexSet{0, 1, 2});
2!
2531
        REQUIRE(observer.array_change(0, set_col) == IndexSet{});
2!
2532
        REQUIRE(observer.array_change(0, dict_col) == IndexSet{});
2!
2533
        realm->m_binding_context.release();
2✔
2534
    }
2✔
2535

2536
    SECTION("begin_transaction() from within did_change()") {
104✔
2537
        struct Context : public BindingContext {
2✔
2538
            void did_change(std::vector<ObserverState> const&, std::vector<void*> const&, bool) override
2✔
2539
            {
4✔
2540
                auto r = realm.lock();
4✔
2541
                r->begin_transaction();
4✔
2542
                auto table = r->read_group().get_table("class_object");
4✔
2543
                table->create_object();
4✔
2544
                if (++change_count == 1) {
4✔
2545
                    r->commit_transaction();
2✔
2546
                }
2✔
2547
                else {
2✔
2548
                    r->cancel_transaction();
2✔
2549
                }
2✔
2550
            }
4✔
2551
            int change_count = 0;
2✔
2552
        };
2✔
2553

2554
        realm->m_binding_context.reset(new Context());
2✔
2555
        realm->m_binding_context->realm = realm;
2✔
2556

2557
        realm->begin_transaction();
2✔
2558
        auto table = realm->read_group().get_table("class_object");
2✔
2559
        table->create_object();
2✔
2560
        bool persisted = false;
2✔
2561
        realm->async_commit_transaction([&persisted](auto) {
2✔
2562
            persisted = true;
2✔
2563
        });
2✔
2564
        REQUIRE(table->size() == 2);
2!
2565
        REQUIRE(persisted);
2!
2566
    }
2✔
2567

2568
    SECTION("async write grouping") {
104✔
2569
        size_t completion_calls = 0;
2✔
2570
        for (size_t i = 0; i < 41; ++i) {
84✔
2571
            realm->async_begin_transaction([&, i, realm] {
82✔
2572
                // The top ref in the Realm file should only be updated once every 20 commits
2573
                CHECK(Group(config.path, config.encryption_key.data()).get_table("class_object")->size() ==
82!
2574
                      (i / 20) * 20);
82✔
2575

2576
                table->create_object();
82✔
2577
                realm->async_commit_transaction(
82✔
2578
                    [&](std::exception_ptr) {
82✔
2579
                        ++completion_calls;
82✔
2580
                    },
82✔
2581
                    true);
82✔
2582
            });
82✔
2583
        }
82✔
2584
        util::EventLoop::main().run_until([&] {
10,322✔
2585
            return completion_calls == 41;
10,322✔
2586
        });
10,322✔
2587
    }
2✔
2588

2589
    SECTION("async write grouping with manual barriers") {
104✔
2590
        size_t completion_calls = 0;
2✔
2591
        for (size_t i = 0; i < 41; ++i) {
84✔
2592
            realm->async_begin_transaction([&, i, realm] {
82✔
2593
                // The top ref in the Realm file should only be updated once every 6 commits
2594
                CHECK(Group(config.path, config.encryption_key.data()).get_table("class_object")->size() ==
82!
2595
                      (i / 6) * 6);
82✔
2596

2597
                table->create_object();
82✔
2598
                realm->async_commit_transaction(
82✔
2599
                    [&](std::exception_ptr) {
82✔
2600
                        ++completion_calls;
82✔
2601
                    },
82✔
2602
                    (i + 1) % 6 != 0);
82✔
2603
            });
82✔
2604
        }
82✔
2605
        util::EventLoop::main().run_until([&] {
24,343✔
2606
            return completion_calls == 41;
24,343✔
2607
        });
24,343✔
2608
    }
2✔
2609

2610
    SECTION("async writes scheduled inside sync write") {
104✔
2611
        realm->begin_transaction();
2✔
2612
        realm->async_begin_transaction([&] {
2✔
2613
            REQUIRE(table->size() == 1);
2!
2614
            table->create_object();
2✔
2615
            realm->async_commit_transaction();
2✔
2616
        });
2✔
2617
        realm->async_begin_transaction([&] {
2✔
2618
            REQUIRE(table->size() == 2);
2!
2619
            table->create_object();
2✔
2620
            realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2621
                done = true;
2✔
2622
            });
2✔
2623
        });
2✔
2624
        REQUIRE(table->size() == 0);
2!
2625
        table->create_object();
2✔
2626
        realm->commit_transaction();
2✔
2627
        wait_for_done();
2✔
2628
        REQUIRE(table->size() == 3);
2!
2629
    }
2✔
2630

2631
    SECTION("async writes scheduled inside multiple sync write") {
104✔
2632
        realm->begin_transaction();
2✔
2633
        realm->async_begin_transaction([&] {
2✔
2634
            REQUIRE(table->size() == 2);
2!
2635
            table->create_object();
2✔
2636
            realm->async_commit_transaction();
2✔
2637
        });
2✔
2638
        realm->async_begin_transaction([&] {
2✔
2639
            REQUIRE(table->size() == 3);
2!
2640
            table->create_object();
2✔
2641
            realm->async_commit_transaction();
2✔
2642
        });
2✔
2643
        REQUIRE(table->size() == 0);
2!
2644
        table->create_object();
2✔
2645
        realm->commit_transaction();
2✔
2646

2647
        realm->begin_transaction();
2✔
2648
        realm->async_begin_transaction([&] {
2✔
2649
            REQUIRE(table->size() == 4);
2!
2650
            table->create_object();
2✔
2651
            realm->async_commit_transaction();
2✔
2652
        });
2✔
2653
        realm->async_begin_transaction([&] {
2✔
2654
            REQUIRE(table->size() == 5);
2!
2655
            table->create_object();
2✔
2656
            realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2657
                done = true;
2✔
2658
            });
2✔
2659
        });
2✔
2660
        REQUIRE(table->size() == 1);
2!
2661
        table->create_object();
2✔
2662
        realm->commit_transaction();
2✔
2663

2664

2665
        wait_for_done();
2✔
2666
        REQUIRE(table->size() == 6);
2!
2667
    }
2✔
2668

2669
    SECTION("async writes which would run inside sync writes are deferred") {
104✔
2670
        realm->async_begin_transaction([&] {
2✔
2671
            done = true;
2✔
2672
        });
2✔
2673

2674
        // Wait for the background thread to hold the write lock (without letting
2675
        // the event loop run so that the scheduled task isn't run)
2676
        DBOptions options;
2✔
2677
        options.encryption_key = config.encryption_key.data();
2✔
2678
        auto db = DB::create(make_in_realm_history(), config.path, options);
2✔
2679
        while (db->start_write(true))
2✔
2680
            millisleep(1);
×
2681

2682
        realm->begin_transaction();
2✔
2683

2684
        // Invoke the pending callback
2685
        util::EventLoop::main().run_pending();
2✔
2686
        // Should not have run the async write block
2687
        REQUIRE(done == false);
2!
2688

2689
        // Should run the async write block once the synchronous transaction is done
2690
        realm->cancel_transaction();
2✔
2691
        REQUIRE(done == false);
2!
2692
        util::EventLoop::main().run_pending();
2✔
2693
        REQUIRE(done == true);
2!
2694
    }
2✔
2695

2696
    util::EventLoop::main().run_until([&] {
162✔
2697
        return !realm || !realm->has_pending_async_work();
162✔
2698
    });
162✔
2699

2700
    _impl::RealmCoordinator::clear_all_caches();
104✔
2701
}
104✔
2702

2703
TEST_CASE("Call run_async_completions after realm has been closed") {
2✔
2704
    // This requires a special scheduler as we have to call Realm::close
2705
    // just after DB::AsyncCommitHelper has made a callback to the function
2706
    // that asks the scheduler to invoke run_async_completions()
2707

2708
    struct ManualScheduler : util::Scheduler {
2✔
2709
        std::mutex mutex;
2✔
2710
        std::condition_variable cv;
2✔
2711
        std::vector<util::UniqueFunction<void()>> callbacks;
2✔
2712

2713
        void invoke(util::UniqueFunction<void()>&& cb) override
2✔
2714
        {
2✔
2715
            {
2✔
2716
                std::lock_guard lock(mutex);
2✔
2717
                callbacks.push_back(std::move(cb));
2✔
2718
            }
2✔
2719
            cv.notify_all();
2✔
2720
        }
2✔
2721

2722
        bool is_on_thread() const noexcept override
2✔
2723
        {
4✔
2724
            return true;
4✔
2725
        }
4✔
2726
        bool is_same_as(const Scheduler*) const noexcept override
2✔
2727
        {
2✔
2728
            return false;
×
2729
        }
×
2730
        bool can_invoke() const noexcept override
2✔
2731
        {
2✔
2732
            return true;
×
2733
        }
×
2734
    };
2✔
2735

2736
    auto scheduler = std::make_shared<ManualScheduler>();
2✔
2737

2738
    TestFile config;
2✔
2739
    config.schema_version = 0;
2✔
2740
    config.schema = Schema{{"object", {{"value", PropertyType::Int}}}};
2✔
2741
    config.scheduler = scheduler;
2✔
2742
    config.automatic_change_notifications = false;
2✔
2743

2744
    auto realm = Realm::get_shared_realm(config);
2✔
2745

2746
    realm->begin_transaction();
2✔
2747
    realm->async_commit_transaction([](std::exception_ptr) {});
2✔
2748

2749
    std::vector<util::UniqueFunction<void()>> callbacks;
2✔
2750
    {
2✔
2751
        std::unique_lock lock(scheduler->mutex);
2✔
2752
        // Wait for scheduler to be invoked
2753
        scheduler->cv.wait(lock, [&] {
4✔
2754
            return !scheduler->callbacks.empty();
4✔
2755
        });
4✔
2756
        callbacks.swap(scheduler->callbacks);
2✔
2757
    }
2✔
2758
    realm->close();
2✔
2759
    // Call whatever functions that was added to scheduler.
2760
    for (auto& cb : callbacks)
2✔
2761
        cb();
2✔
2762
}
2✔
2763

2764
// Our libuv scheduler currently does not support background threads, so we can
2765
// only run this on apple platforms
2766
#if REALM_PLATFORM_APPLE
2767
TEST_CASE("SharedRealm: async writes on multiple threads") {
5✔
2768
    _impl::RealmCoordinator::assert_no_open_realms();
5✔
2769

2770
    TestFile config;
5✔
2771
    config.cache = true;
5✔
2772
    config.schema_version = 0;
5✔
2773
    config.schema = Schema{{"object", {{"value", PropertyType::Int}}}};
5✔
2774
    auto realm = Realm::get_shared_realm(config);
5✔
2775
    auto table_key = realm->read_group().get_table("class_object")->get_key();
5✔
2776
    realm->close();
5✔
2777

2778
    struct QueueState {
5✔
2779
        dispatch_queue_t queue;
5✔
2780
        Realm::Config config;
5✔
2781
    };
5✔
2782
    std::vector<QueueState> queues;
5✔
2783
    for (int i = 0; i < 10; ++i) {
55✔
2784
        auto queue = dispatch_queue_create(util::format("queue %1", i).c_str(), 0);
50✔
2785
        Realm::Config queue_config = config;
50✔
2786
        queue_config.scheduler = util::Scheduler::make_dispatch(static_cast<void*>(queue));
50✔
2787
        queues.push_back({queue, std::move(queue_config)});
50✔
2788
    }
50✔
2789

2790
    std::atomic<size_t> completions = 0;
5✔
2791
    // Capturing by reference when mixing lambda and blocks is weird, so capture
2792
    // a pointer instead
2793
    auto completions_ptr = &completions;
5✔
2794

2795
    auto async_write_and_async_commit = [=](const Realm::Config& config) {
124✔
2796
        Realm::get_shared_realm(config)->async_begin_transaction([=] {
124✔
2797
            auto realm = Realm::get_shared_realm(config);
124✔
2798
            realm->read_group().get_table(table_key)->create_object();
124✔
2799
            realm->async_commit_transaction([=](std::exception_ptr) {
124✔
2800
                ++*completions_ptr;
124✔
2801
            });
124✔
2802
        });
124✔
2803
    };
124✔
2804
    auto async_write_and_sync_commit = [=](const Realm::Config& config) {
124✔
2805
        Realm::get_shared_realm(config)->async_begin_transaction([=] {
124✔
2806
            auto realm = Realm::get_shared_realm(config);
124✔
2807
            realm->read_group().get_table(table_key)->create_object();
124✔
2808
            realm->commit_transaction();
124✔
2809
            ++*completions_ptr;
124✔
2810
        });
124✔
2811
    };
124✔
2812
    auto sync_write_and_async_commit = [=](const Realm::Config& config) {
124✔
2813
        auto realm = Realm::get_shared_realm(config);
124✔
2814
        realm->begin_transaction();
124✔
2815
        realm->read_group().get_table(table_key)->create_object();
124✔
2816
        realm->async_commit_transaction([=](std::exception_ptr) {
124✔
2817
            ++*completions_ptr;
124✔
2818
        });
124✔
2819
    };
124✔
2820
    auto sync_write_and_sync_commit = [=](const Realm::Config& config) {
124✔
2821
        auto realm = Realm::get_shared_realm(config);
124✔
2822
        realm->begin_transaction();
124✔
2823
        realm->read_group().get_table(table_key)->create_object();
124✔
2824
        realm->commit_transaction();
124✔
2825
        ++*completions_ptr;
124✔
2826
    };
124✔
2827

2828
    SECTION("async begin and async commit") {
5✔
2829
        for (auto& queue : queues) {
10✔
2830
            dispatch_async(queue.queue, ^{
10✔
2831
                for (int i = 0; i < 10; ++i) {
110✔
2832
                    async_write_and_async_commit(queue.config);
100✔
2833
                }
100✔
2834
            });
10✔
2835
        }
10✔
2836
        util::EventLoop::main().run_until([&] {
2,038✔
2837
            return completions == 100;
2,038✔
2838
        });
2,038✔
2839
    }
1✔
2840
    SECTION("async begin and sync commit") {
5✔
2841
        for (auto& queue : queues) {
10✔
2842
            dispatch_async(queue.queue, ^{
10✔
2843
                for (int i = 0; i < 10; ++i) {
110✔
2844
                    async_write_and_sync_commit(queue.config);
100✔
2845
                }
100✔
2846
            });
10✔
2847
        }
10✔
2848
        util::EventLoop::main().run_until([&] {
1,726✔
2849
            return completions == 100;
1,726✔
2850
        });
1,726✔
2851
    }
1✔
2852
    SECTION("sync begin and async commit") {
5✔
2853
        for (auto& queue : queues) {
10✔
2854
            dispatch_async(queue.queue, ^{
10✔
2855
                for (int i = 0; i < 10; ++i) {
110✔
2856
                    sync_write_and_async_commit(queue.config);
100✔
2857
                }
100✔
2858
            });
10✔
2859
        }
10✔
2860
        util::EventLoop::main().run_until([&] {
1,598✔
2861
            return completions == 100;
1,598✔
2862
        });
1,598✔
2863
    }
1✔
2864
    SECTION("sync begin and sync commit") {
5✔
2865
        for (auto& queue : queues) {
10✔
2866
            dispatch_async(queue.queue, ^{
10✔
2867
                for (int i = 0; i < 10; ++i) {
110✔
2868
                    sync_write_and_sync_commit(queue.config);
100✔
2869
                }
100✔
2870
            });
10✔
2871
        }
10✔
2872
        util::EventLoop::main().run_until([&] {
1,546✔
2873
            return completions == 100;
1,546✔
2874
        });
1,546✔
2875
    }
1✔
2876
    SECTION("mixed sync and async") {
5✔
2877
        // Test every permutation of each of the variants
2878
        struct IndexedOp {
1✔
2879
            int index;
1✔
2880
            std::function<void(const Realm::Config& config)> fn;
1✔
2881
        };
1✔
2882
        std::array<IndexedOp, 4> functions{{
1✔
2883
            {0, async_write_and_async_commit},
1✔
2884
            {1, sync_write_and_async_commit},
1✔
2885
            {2, async_write_and_sync_commit},
1✔
2886
            {3, sync_write_and_sync_commit},
1✔
2887
        }};
1✔
2888
        size_t i = 0;
1✔
2889
        size_t expected_completions = 0;
1✔
2890
        do {
24✔
2891
            auto& queue = queues[i++ % 10];
24✔
2892
            auto functions_copy = functions;
24✔
2893
            dispatch_async(queue.queue, ^{
24✔
2894
                for (auto& fn : functions_copy) {
96✔
2895
                    fn.fn(queue.config);
96✔
2896
                }
96✔
2897
            });
24✔
2898
            expected_completions += 4;
24✔
2899
        } while (std::next_permutation(functions.begin(), functions.end(), [](auto& a, auto& b) {
70✔
2900
            return a.index < b.index;
70✔
2901
        }));
70✔
2902

2903
        util::EventLoop::main().run_until([&] {
1,546✔
2904
            return completions == expected_completions;
1,546✔
2905
        });
1,546✔
2906
    }
1✔
2907

2908

2909
    realm = Realm::get_shared_realm(config);
5✔
2910
    REQUIRE(realm->read_group().get_table(table_key)->size() == completions);
5!
2911

2912
    for (auto& queue : queues) {
50✔
2913
        dispatch_sync(queue.queue, ^{
50✔
2914
                      });
50✔
2915
    }
50✔
2916
}
5✔
2917
#endif
2918

2919
class LooperDelegate {
2920
public:
2921
    LooperDelegate() {}
2✔
2922
    void run_once()
2923
    {
6,495✔
2924
        for (auto it = m_tasks.begin(); it != m_tasks.end(); ++it) {
9,402✔
2925
            if (it->may_run && *it->may_run) {
2,913✔
2926
                it->the_job();
6✔
2927
                m_tasks.erase(it);
6✔
2928
                return;
6✔
2929
            }
6✔
2930
        }
2,913✔
2931
    }
6,495✔
2932
    std::shared_ptr<bool> add_task(util::UniqueFunction<void()>&& the_job)
2933
    {
6✔
2934
        m_tasks.push_back(Task{std::make_shared<bool>(false), std::move(the_job)});
6✔
2935
        return m_tasks.back().may_run;
6✔
2936
    }
6✔
2937
    bool has_tasks()
2938
    {
×
2939
        return !m_tasks.empty();
×
2940
    }
×
2941

2942
private:
2943
    struct Task {
2944
        std::shared_ptr<bool> may_run;
2945
        util::UniqueFunction<void()> the_job;
2946
    };
2947
    std::vector<Task> m_tasks;
2948
};
2949

2950
#ifndef _WIN32
2951
TEST_CASE("SharedRealm: async_writes_2") {
2✔
2952
    _impl::RealmCoordinator::assert_no_open_realms();
2✔
2953
    if (!util::EventLoop::has_implementation())
2✔
2954
        return;
×
2955

2956
    TestFile config;
2✔
2957
    config.schema_version = 0;
2✔
2958
    config.schema = Schema{
2✔
2959
        {"object", {{"value", PropertyType::Int}}},
2✔
2960
    };
2✔
2961
    bool done = false;
2✔
2962
    auto realm = Realm::get_shared_realm(config);
2✔
2963
    int write_nr = 0;
2✔
2964
    int commit_nr = 0;
2✔
2965
    auto table = realm->read_group().get_table("class_object");
2✔
2966
    auto col = table->get_column_key("value");
2✔
2967
    LooperDelegate ld;
2✔
2968
    std::shared_ptr<bool> t1_rdy = ld.add_task([&, realm]() {
2✔
2969
        REQUIRE(write_nr == 0);
2!
2970
        ++write_nr;
2✔
2971
        table->create_object().set(col, 45);
2✔
2972
        realm->cancel_transaction();
2✔
2973
    });
2✔
2974
    std::shared_ptr<bool> t2_rdy = ld.add_task([&, realm]() {
2✔
2975
        REQUIRE(write_nr == 1);
2!
2976
        ++write_nr;
2✔
2977
        table->create_object().set(col, 45);
2✔
2978
        realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2979
            REQUIRE(commit_nr == 0);
2!
2980
            ++commit_nr;
2✔
2981
        });
2✔
2982
    });
2✔
2983
    std::shared_ptr<bool> t3_rdy = ld.add_task([&, realm]() {
2✔
2984
        ++write_nr;
2✔
2985
        auto o = table->get_object(0);
2✔
2986
        o.set(col, o.get<int64_t>(col) + 37);
2✔
2987
        realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2988
            ++commit_nr;
2✔
2989
            done = true;
2✔
2990
        });
2✔
2991
    });
2✔
2992

2993
    // Make some notify_only transactions
2994
    realm->async_begin_transaction(
2✔
2995
        [&]() {
2✔
2996
            *t1_rdy = true;
2✔
2997
        },
2✔
2998
        true);
2✔
2999
    realm->async_begin_transaction(
2✔
3000
        [&]() {
2✔
3001
            *t2_rdy = true;
2✔
3002
        },
2✔
3003
        true);
2✔
3004
    realm->async_begin_transaction(
2✔
3005
        [&]() {
2✔
3006
            *t3_rdy = true;
2✔
3007
        },
2✔
3008
        true);
2✔
3009

3010
    util::EventLoop::main().run_until([&, realm] {
6,495✔
3011
        ld.run_once();
6,495✔
3012
        return done;
6,495✔
3013
    });
6,495✔
3014
    REQUIRE(done);
2!
3015
}
2✔
3016
#endif
3017

3018
TEST_CASE("SharedRealm: notifications") {
14✔
3019
    if (!util::EventLoop::has_implementation())
14✔
3020
        return;
×
3021

3022
    TestFile config;
14✔
3023
    config.schema_version = 0;
14✔
3024
    config.schema = Schema{
14✔
3025
        {"object", {{"value", PropertyType::Int}}},
14✔
3026
    };
14✔
3027

3028
    struct Context : BindingContext {
14✔
3029
        size_t* change_count;
14✔
3030
        util::UniqueFunction<void()> did_change_fn;
14✔
3031
        util::UniqueFunction<void()> changes_available_fn;
14✔
3032

3033
        Context(size_t* out)
14✔
3034
            : change_count(out)
14✔
3035
        {
14✔
3036
        }
14✔
3037

3038
        void did_change(std::vector<ObserverState> const&, std::vector<void*> const&, bool) override
14✔
3039
        {
24✔
3040
            ++*change_count;
24✔
3041
            if (did_change_fn)
24✔
3042
                did_change_fn();
12✔
3043
        }
24✔
3044

3045
        void changes_available() override
14✔
3046
        {
14✔
3047
            if (changes_available_fn)
10✔
3048
                changes_available_fn();
2✔
3049
        }
10✔
3050
    };
14✔
3051

3052
    size_t change_count = 0;
14✔
3053
    auto realm = Realm::get_shared_realm(config);
14✔
3054
    realm->read_group();
14✔
3055
    auto context = new Context{&change_count};
14✔
3056
    realm->m_binding_context.reset(context);
14✔
3057
    realm->m_binding_context->realm = realm;
14✔
3058

3059
    SECTION("local notifications are sent synchronously") {
14✔
3060
        realm->begin_transaction();
2✔
3061
        REQUIRE(change_count == 0);
2!
3062
        realm->commit_transaction();
2✔
3063
        REQUIRE(change_count == 1);
2!
3064
    }
2✔
3065
#ifndef _WIN32
14✔
3066
    SECTION("remote notifications are sent asynchronously") {
14✔
3067
        auto r2 = Realm::get_shared_realm(config);
2✔
3068
        r2->begin_transaction();
2✔
3069
        r2->commit_transaction();
2✔
3070
        REQUIRE(change_count == 0);
2!
3071
        util::EventLoop::main().run_until([&] {
9✔
3072
            return change_count > 0;
9✔
3073
        });
9✔
3074
        REQUIRE(change_count == 1);
2!
3075
    }
2✔
3076

3077
    SECTION("notifications created in async transaction are sent synchronously") {
14✔
3078
        realm->async_begin_transaction([&] {
2✔
3079
            REQUIRE(change_count == 0);
2!
3080
            realm->async_commit_transaction();
2✔
3081
            REQUIRE(change_count == 1);
2!
3082
        });
2✔
3083
        REQUIRE(change_count == 0);
2!
3084
        util::EventLoop::main().run_until([&] {
22✔
3085
            return change_count > 0;
22✔
3086
        });
22✔
3087
        REQUIRE(change_count == 1);
2!
3088
        util::EventLoop::main().run_until([&] {
3,475✔
3089
            return !realm->has_pending_async_work();
3,475✔
3090
        });
3,475✔
3091
    }
2✔
3092
#endif
14✔
3093
    SECTION("refresh() from within changes_available() refreshes") {
14✔
3094
        context->changes_available_fn = [&] {
2✔
3095
            REQUIRE(realm->refresh());
2!
3096
        };
2✔
3097
        realm->set_auto_refresh(false);
2✔
3098

3099
        auto r2 = Realm::get_shared_realm(config);
2✔
3100
        r2->begin_transaction();
2✔
3101
        r2->commit_transaction();
2✔
3102
        realm->notify();
2✔
3103
        // Should return false as the realm was already advanced
3104
        REQUIRE_FALSE(realm->refresh());
2!
3105
    }
2✔
3106

3107
    SECTION("refresh() from within did_change() is a no-op") {
14✔
3108
        context->did_change_fn = [&] {
4✔
3109
            if (change_count > 1)
4✔
3110
                return;
2✔
3111

3112
            // Create another version so that refresh() advances the version
3113
            auto r2 = Realm::get_shared_realm(realm->config());
2✔
3114
            r2->begin_transaction();
2✔
3115
            r2->commit_transaction();
2✔
3116

3117
            REQUIRE_FALSE(realm->refresh());
2!
3118
        };
2✔
3119

3120
        auto r2 = Realm::get_shared_realm(config);
2✔
3121
        r2->begin_transaction();
2✔
3122
        r2->commit_transaction();
2✔
3123

3124
        REQUIRE(realm->refresh());
2!
3125
        REQUIRE(change_count == 1);
2!
3126

3127
        REQUIRE(realm->refresh());
2!
3128
        REQUIRE(change_count == 2);
2!
3129
        REQUIRE_FALSE(realm->refresh());
2!
3130
    }
2✔
3131

3132
    SECTION("begin_write() from within did_change() produces recursive notifications") {
14✔
3133
        context->did_change_fn = [&] {
8✔
3134
            if (realm->is_in_transaction())
8✔
3135
                realm->cancel_transaction();
6✔
3136
            if (change_count > 3)
8✔
3137
                return;
2✔
3138

3139
            // Create another version so that begin_write() advances the version
3140
            auto r2 = Realm::get_shared_realm(realm->config());
6✔
3141
            r2->begin_transaction();
6✔
3142
            r2->commit_transaction();
6✔
3143

3144
            realm->begin_transaction();
6✔
3145
            REQUIRE(change_count == 4);
6!
3146
        };
6✔
3147

3148
        auto r2 = Realm::get_shared_realm(config);
2✔
3149
        r2->begin_transaction();
2✔
3150
        r2->commit_transaction();
2✔
3151
        REQUIRE(realm->refresh());
2!
3152
        REQUIRE(change_count == 4);
2!
3153
        REQUIRE_FALSE(realm->refresh());
2!
3154
    }
2✔
3155

3156
#if REALM_ENABLE_SYNC
14✔
3157
    SECTION("SubscriptionStore writes produce notifications") {
14✔
3158
        auto subscription_store = sync::SubscriptionStore::create(TestHelper::get_db(realm));
2✔
3159
        REQUIRE(change_count == 0);
2!
3160
        util::EventLoop::main().run_until([&] {
11✔
3161
            return change_count > 0;
11✔
3162
        });
11✔
3163
        REQUIRE(change_count == 1);
2!
3164

3165
        subscription_store->get_active().make_mutable_copy().commit();
2✔
3166
        REQUIRE(change_count == 1);
2!
3167
        util::EventLoop::main().run_until([&] {
11✔
3168
            return change_count > 1;
11✔
3169
        });
11✔
3170
        REQUIRE(change_count == 2);
2!
3171
    }
2✔
3172
#endif
14✔
3173
}
14✔
3174

3175
TEST_CASE("SharedRealm: schema updating from external changes") {
14✔
3176
    TestFile config;
14✔
3177
    config.schema_version = 0;
14✔
3178
    config.schema_mode = SchemaMode::AdditiveExplicit;
14✔
3179
    config.schema = Schema{
14✔
3180
        {"object",
14✔
3181
         {
14✔
3182
             {"value", PropertyType::Int, Property::IsPrimary{true}},
14✔
3183
             {"value 2", PropertyType::Int, Property::IsPrimary{false}, Property::IsIndexed{true}},
14✔
3184
         }},
14✔
3185
    };
14✔
3186

3187
    SECTION("newly added columns update table columns but are not added to properties") {
14✔
3188
        // Does this test add any value when column keys are stable?
3189
        auto r1 = Realm::get_shared_realm(config);
4✔
3190
        auto r2 = Realm::get_shared_realm(config);
4✔
3191
        auto test = [&] {
4✔
3192
            r2->begin_transaction();
4✔
3193
            r2->read_group().get_table("class_object")->add_column(type_String, "new col");
4✔
3194
            r2->commit_transaction();
4✔
3195

3196
            auto& object_schema = *r1->schema().find("object");
4✔
3197
            REQUIRE(object_schema.persisted_properties.size() == 2);
4!
3198
            ColKey col = object_schema.persisted_properties[0].column_key;
4✔
3199
            r1->refresh();
4✔
3200
            REQUIRE(object_schema.persisted_properties[0].column_key == col);
4!
3201
        };
4✔
3202
        SECTION("with an active read transaction") {
4✔
3203
            r1->read_group();
2✔
3204
            test();
2✔
3205
        }
2✔
3206
        SECTION("without an active read transaction") {
4✔
3207
            r1->invalidate();
2✔
3208
            test();
2✔
3209
        }
2✔
3210
    }
4✔
3211

3212
    SECTION("beginning a read transaction checks for incompatible changes") {
14✔
3213
        auto r = Realm::get_shared_realm(config);
10✔
3214
        r->invalidate();
10✔
3215

3216
        auto& db = TestHelper::get_db(r);
10✔
3217
        WriteTransaction wt(db);
10✔
3218
        auto& table = *wt.get_table("class_object");
10✔
3219

3220
        SECTION("removing a property") {
10✔
3221
            table.remove_column(table.get_column_key("value"));
2✔
3222
            wt.commit();
2✔
3223
            REQUIRE_THROWS_CONTAINING(r->refresh(), "Property 'object.value' has been removed.");
2✔
3224
        }
2✔
3225

3226
        SECTION("change property type") {
10✔
3227
            table.remove_column(table.get_column_key("value 2"));
2✔
3228
            table.add_column(type_Float, "value 2");
2✔
3229
            wt.commit();
2✔
3230
            REQUIRE_THROWS_CONTAINING(r->refresh(),
2✔
3231
                                      "Property 'object.value 2' has been changed from 'int' to 'float'");
2✔
3232
        }
2✔
3233

3234
        SECTION("make property optional") {
10✔
3235
            table.remove_column(table.get_column_key("value 2"));
2✔
3236
            table.add_column(type_Int, "value 2", true);
2✔
3237
            wt.commit();
2✔
3238
            REQUIRE_THROWS_CONTAINING(r->refresh(), "Property 'object.value 2' has been made optional");
2✔
3239
        }
2✔
3240

3241
        SECTION("recreate column with no changes") {
10✔
3242
            table.remove_column(table.get_column_key("value 2"));
2✔
3243
            table.add_column(type_Int, "value 2");
2✔
3244
            wt.commit();
2✔
3245
            REQUIRE_NOTHROW(r->refresh());
2✔
3246
        }
2✔
3247

3248
        SECTION("remove index from non-PK") {
10✔
3249
            table.remove_search_index(table.get_column_key("value 2"));
2✔
3250
            wt.commit();
2✔
3251
            REQUIRE_NOTHROW(r->refresh());
2✔
3252
        }
2✔
3253
    }
10✔
3254
}
14✔
3255

3256
TEST_CASE("SharedRealm: close()") {
4✔
3257
    TestFile config;
4✔
3258
    config.schema_version = 1;
4✔
3259
    config.schema = Schema{
4✔
3260
        {"object", {{"value", PropertyType::Int}}},
4✔
3261
        {"list", {{"list", PropertyType::Object | PropertyType::Array, "object"}}},
4✔
3262
    };
4✔
3263

3264
    auto realm = Realm::get_shared_realm(config);
4✔
3265

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

3269
        realm->close();
2✔
3270
        REQUIRE(realm->is_closed());
2!
3271
        REQUIRE_EXCEPTION(realm->verify_open(), ClosedRealm, msg);
2✔
3272

3273
        REQUIRE_EXCEPTION(realm->update_schema(Schema{}), ClosedRealm, msg);
2✔
3274
        REQUIRE_EXCEPTION(realm->rename_property(Schema{}, "", "", ""), ClosedRealm, msg);
2✔
3275
        REQUIRE_EXCEPTION(realm->set_schema_subset(Schema{}), ClosedRealm, msg);
2✔
3276

3277
        REQUIRE_EXCEPTION(realm->begin_transaction(), ClosedRealm, msg);
2✔
3278
        REQUIRE_EXCEPTION(realm->commit_transaction(), ClosedRealm, msg);
2✔
3279
        REQUIRE_EXCEPTION(realm->cancel_transaction(), ClosedRealm, msg);
2✔
3280
        REQUIRE(!realm->is_in_transaction());
2!
3281

3282
        REQUIRE_EXCEPTION(realm->async_begin_transaction(nullptr), ClosedRealm, msg);
2✔
3283
        REQUIRE_EXCEPTION(realm->async_commit_transaction(nullptr), ClosedRealm, msg);
2✔
3284
        REQUIRE_EXCEPTION(realm->async_cancel_transaction(0), ClosedRealm, msg);
2✔
3285
        REQUIRE_FALSE(realm->is_in_async_transaction());
2!
3286

3287
        REQUIRE_EXCEPTION(realm->freeze(), ClosedRealm, msg);
2✔
3288
        REQUIRE_FALSE(realm->is_frozen());
2!
3289
        REQUIRE_EXCEPTION(realm->get_number_of_versions(), ClosedRealm, msg);
2✔
3290
        REQUIRE_EXCEPTION(realm->read_transaction_version(), ClosedRealm, msg);
2✔
3291
        REQUIRE_EXCEPTION(realm->duplicate(), ClosedRealm, msg);
2✔
3292

3293
        REQUIRE_EXCEPTION(realm->enable_wait_for_change(), ClosedRealm, msg);
2✔
3294
        REQUIRE_EXCEPTION(realm->wait_for_change(), ClosedRealm, msg);
2✔
3295
        REQUIRE_EXCEPTION(realm->wait_for_change_release(), ClosedRealm, msg);
2✔
3296

3297
        REQUIRE_NOTHROW(realm->notify());
2✔
3298
        REQUIRE_EXCEPTION(realm->refresh(), ClosedRealm, msg);
2✔
3299
        REQUIRE_EXCEPTION(realm->invalidate(), ClosedRealm, msg);
2✔
3300
        REQUIRE_EXCEPTION(realm->compact(), ClosedRealm, msg);
2✔
3301
        REQUIRE_EXCEPTION(realm->convert(realm->config()), ClosedRealm, msg);
2✔
3302
        REQUIRE_EXCEPTION(realm->write_copy(), ClosedRealm, msg);
2✔
3303

3304
#if REALM_ENABLE_SYNC
2✔
3305
        REQUIRE_FALSE(realm->sync_session());
2!
3306
        msg = "Flexible sync is not enabled";
2✔
3307
        REQUIRE_EXCEPTION(realm->get_latest_subscription_set(), IllegalOperation, msg);
2✔
3308
        REQUIRE_EXCEPTION(realm->get_active_subscription_set(), IllegalOperation, msg);
2✔
3309
#endif
2✔
3310
    }
2✔
3311

3312
    SECTION("fully closes database file even with live notifiers") {
4✔
3313
        auto& group = realm->read_group();
2✔
3314
        realm->begin_transaction();
2✔
3315
        auto obj = ObjectStore::table_for_object_type(group, "list")->create_object();
2✔
3316
        realm->commit_transaction();
2✔
3317

3318
        Results results(realm, ObjectStore::table_for_object_type(group, "object"));
2✔
3319
        List list(realm, obj.get_linklist("list"));
2✔
3320
        Object object(realm, obj);
2✔
3321

3322
        auto obj_token = object.add_notification_callback([](CollectionChangeSet) {});
2✔
3323
        auto list_token = list.add_notification_callback([](CollectionChangeSet) {});
2✔
3324
        auto results_token = results.add_notification_callback([](CollectionChangeSet) {});
2✔
3325

3326
        // Perform a dummy transaction to ensure the notifiers actually acquire
3327
        // resources that need to be closed
3328
        realm->begin_transaction();
2✔
3329
        realm->commit_transaction();
2✔
3330

3331
        realm->close();
2✔
3332

3333
        // Verify that we're able to acquire an exclusive lock
3334
        REQUIRE(DB::call_with_lock(config.path, [](auto) {}));
2!
3335
    }
2✔
3336
}
4✔
3337

3338
TEST_CASE("Realm::delete_files()") {
12✔
3339
    TestFile config;
12✔
3340
    config.schema_version = 1;
12✔
3341
    config.schema = Schema{{"object", {{"value", PropertyType::Int}}}};
12✔
3342
    auto realm = Realm::get_shared_realm(config);
12✔
3343
    auto path = config.path;
12✔
3344

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

3350
    SECTION("Deleting files of a closed Realm succeeds.") {
12✔
3351
        realm->close();
2✔
3352
        bool did_delete = false;
2✔
3353
        Realm::delete_files(path, &did_delete);
2✔
3354
        REQUIRE(did_delete);
2!
3355
        REQUIRE_FALSE(util::File::exists(path));
2!
3356
        REQUIRE_FALSE(util::File::exists(path + ".management"));
2!
3357
        REQUIRE_FALSE(util::File::exists(path + ".note"));
2!
3358
        REQUIRE_FALSE(util::File::exists(path + ".log"));
2!
3359

3360
        // Deleting the .lock file is not safe. It must still exist.
3361
        REQUIRE(util::File::exists(path + ".lock"));
2!
3362
    }
2✔
3363

3364
    SECTION("Trying to delete files of an open Realm fails.") {
12✔
3365
        REQUIRE_EXCEPTION(Realm::delete_files(path), ErrorCodes::DeleteOnOpenRealm,
2✔
3366
                          util::format("Cannot delete files of an open Realm: '%1' is still in use.", path));
2✔
3367
        REQUIRE(util::File::exists(path + ".lock"));
2!
3368
        REQUIRE(util::File::exists(path));
2!
3369
        REQUIRE(util::File::exists(path + ".management"));
2!
3370
#ifndef _WIN32
2✔
3371
        REQUIRE(util::File::exists(path + ".note"));
2!
3372
#endif
2✔
3373
        REQUIRE(util::File::exists(path + ".log"));
2!
3374
    }
2✔
3375

3376
    SECTION("Deleting the same Realm multiple times.") {
12✔
3377
        realm->close();
2✔
3378
        Realm::delete_files(path);
2✔
3379
        Realm::delete_files(path);
2✔
3380
        Realm::delete_files(path);
2✔
3381
    }
2✔
3382

3383
    SECTION("Calling delete on a folder that does not exist.") {
12✔
3384
        auto fake_path = "/tmp/doesNotExist/realm.424242";
2✔
3385
        bool did_delete = false;
2✔
3386
        Realm::delete_files(fake_path, &did_delete);
2✔
3387
        REQUIRE_FALSE(did_delete);
2!
3388
    }
2✔
3389

3390
    SECTION("passing did_delete is optional") {
12✔
3391
        realm->close();
2✔
3392
        Realm::delete_files(path, nullptr);
2✔
3393
    }
2✔
3394

3395
    SECTION("Deleting a Realm which does not exist does not set did_delete") {
12✔
3396
        TestFile new_config;
2✔
3397
        bool did_delete = false;
2✔
3398
        Realm::delete_files(new_config.path, &did_delete);
2✔
3399
        REQUIRE_FALSE(did_delete);
2!
3400
    }
2✔
3401
}
12✔
3402

3403
TEST_CASE("ShareRealm: in-memory mode from buffer") {
2✔
3404
    TestFile config;
2✔
3405
    config.schema_version = 1;
2✔
3406
    config.schema = Schema{
2✔
3407
        {"object", {{"value", PropertyType::Int}}},
2✔
3408
    };
2✔
3409

3410
    SECTION("Save and open Realm from in-memory buffer") {
2✔
3411
        // Write in-memory copy of Realm to a buffer
3412
        auto realm = Realm::get_shared_realm(config);
2✔
3413
        OwnedBinaryData realm_buffer = realm->write_copy();
2✔
3414

3415
        // Open the buffer as a new (immutable in-memory) Realm
3416
        realm::Realm::Config config2;
2✔
3417
        config2.in_memory = true;
2✔
3418
        config2.schema_mode = SchemaMode::Immutable;
2✔
3419
        config2.realm_data = realm_buffer.get();
2✔
3420

3421
        auto realm2 = Realm::get_shared_realm(config2);
2✔
3422

3423
        // Verify that it can read the schema and that it is the same
3424
        REQUIRE(realm->schema().size() == 1);
2!
3425
        auto it = realm->schema().find("object");
2✔
3426
        auto table = realm->read_group().get_table("class_object");
2✔
3427
        REQUIRE(it != realm->schema().end());
2!
3428
        REQUIRE(it->table_key == table->get_key());
2!
3429
        REQUIRE(it->persisted_properties.size() == 1);
2!
3430
        REQUIRE(it->persisted_properties[0].name == "value");
2!
3431
        REQUIRE(it->persisted_properties[0].column_key == table->get_column_key("value"));
2!
3432

3433
        // Test invalid configs
3434
        realm::Realm::Config config3;
2✔
3435
        config3.realm_data = realm_buffer.get();
2✔
3436
        REQUIRE_EXCEPTION(Realm::get_shared_realm(config3), IllegalCombination,
2✔
3437
                          "In-memory realms initialized from memory buffers can only be opened in read-only mode");
2✔
3438

3439
        config3.in_memory = true;
2✔
3440
        config3.schema_mode = SchemaMode::Immutable;
2✔
3441
        config3.path = "path";
2✔
3442
        REQUIRE_EXCEPTION(Realm::get_shared_realm(config3), IllegalCombination,
2✔
3443
                          "Specifying both memory buffer and path is invalid");
2✔
3444

3445
        config3.path = "";
2✔
3446
        config3.encryption_key = std::vector<char>(64, 'a');
2✔
3447
        REQUIRE_EXCEPTION(Realm::get_shared_realm(config3), IllegalCombination,
2✔
3448
                          "Memory buffers do not support encryption");
2✔
3449
    }
2✔
3450
}
2✔
3451

3452
TEST_CASE("ShareRealm: realm closed in did_change callback") {
6✔
3453
    TestFile config;
6✔
3454
    config.schema_version = 1;
6✔
3455
    config.schema = Schema{
6✔
3456
        {"object", {{"value", PropertyType::Int}}},
6✔
3457
    };
6✔
3458
    config.automatic_change_notifications = false;
6✔
3459
    auto r1 = Realm::get_shared_realm(config);
6✔
3460

3461
    r1->begin_transaction();
6✔
3462
    auto table = r1->read_group().get_table("class_object");
6✔
3463
    table->create_object();
6✔
3464
    r1->commit_transaction();
6✔
3465

3466
    struct Context : public BindingContext {
6✔
3467
        Context(std::shared_ptr<Realm>& realm)
6✔
3468
            : realm(&realm)
6✔
3469
        {
6✔
3470
        }
6✔
3471
        std::shared_ptr<Realm>* realm;
6✔
3472
        void did_change(std::vector<ObserverState> const&, std::vector<void*> const&, bool) override
6✔
3473
        {
6✔
3474
            auto realm = this->realm; // close() will delete `this`
6✔
3475
            (*realm)->close();
6✔
3476
            realm->reset();
6✔
3477
        }
6✔
3478
    };
6✔
3479

3480
    SECTION("did_change") {
6✔
3481
        r1->m_binding_context.reset(new Context(r1));
2✔
3482
        r1->invalidate();
2✔
3483

3484
        auto r2 = Realm::get_shared_realm(config);
2✔
3485
        r2->begin_transaction();
2✔
3486
        r2->read_group().get_table("class_object")->create_object();
2✔
3487
        r2->commit_transaction();
2✔
3488
        r2.reset();
2✔
3489

3490
        r1->notify();
2✔
3491
    }
2✔
3492

3493
    SECTION("did_change with async results") {
6✔
3494
        r1->m_binding_context.reset(new Context(r1));
2✔
3495
        Results results(r1, table->where());
2✔
3496
        auto token = results.add_notification_callback([&](CollectionChangeSet) {
2✔
3497
            // Should not be called.
3498
            REQUIRE(false);
×
3499
        });
×
3500

3501
        auto r2 = Realm::get_shared_realm(config);
2✔
3502
        r2->begin_transaction();
2✔
3503
        r2->read_group().get_table("class_object")->create_object();
2✔
3504
        r2->commit_transaction();
2✔
3505
        r2.reset();
2✔
3506

3507
        auto coordinator = _impl::RealmCoordinator::get_coordinator(config.path);
2✔
3508
        coordinator->on_change();
2✔
3509

3510
        r1->notify();
2✔
3511
    }
2✔
3512

3513
    SECTION("refresh") {
6✔
3514
        r1->m_binding_context.reset(new Context(r1));
2✔
3515

3516
        auto r2 = Realm::get_shared_realm(config);
2✔
3517
        r2->begin_transaction();
2✔
3518
        r2->read_group().get_table("class_object")->create_object();
2✔
3519
        r2->commit_transaction();
2✔
3520
        r2.reset();
2✔
3521

3522
        REQUIRE_FALSE(r1->refresh());
2!
3523
    }
2✔
3524
}
6✔
3525

3526
TEST_CASE("RealmCoordinator: schema cache") {
16✔
3527
    TestFile config;
16✔
3528
    auto coordinator = _impl::RealmCoordinator::get_coordinator(config.path);
16✔
3529

3530
    Schema cache_schema;
16✔
3531
    uint64_t cache_sv = -1, cache_tv = -1;
16✔
3532

3533
    Schema schema{
16✔
3534
        {"object", {{"value", PropertyType::Int}}},
16✔
3535
    };
16✔
3536
    Schema schema2{
16✔
3537
        {"object",
16✔
3538
         {
16✔
3539
             {"value", PropertyType::Int},
16✔
3540
         }},
16✔
3541
        {"object 2",
16✔
3542
         {
16✔
3543
             {"value", PropertyType::Int},
16✔
3544
         }},
16✔
3545
    };
16✔
3546

3547
    SECTION("valid initial schema sets cache") {
16✔
3548
        coordinator->cache_schema(schema, 5, 10);
2✔
3549
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3550
        REQUIRE(cache_schema == schema);
2!
3551
        REQUIRE(cache_sv == 5);
2!
3552
        REQUIRE(cache_tv == 10);
2!
3553
    }
2✔
3554

3555
    SECTION("cache can be updated with newer schema") {
16✔
3556
        coordinator->cache_schema(schema, 5, 10);
2✔
3557
        coordinator->cache_schema(schema2, 6, 11);
2✔
3558
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3559
        REQUIRE(cache_schema == schema2);
2!
3560
        REQUIRE(cache_sv == 6);
2!
3561
        REQUIRE(cache_tv == 11);
2!
3562
    }
2✔
3563

3564
    SECTION("empty schema is ignored") {
16✔
3565
        coordinator->cache_schema(Schema{}, 5, 10);
2✔
3566
        REQUIRE_FALSE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3567

3568
        coordinator->cache_schema(schema, 5, 10);
2✔
3569
        coordinator->cache_schema(Schema{}, 5, 10);
2✔
3570
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3571
        REQUIRE(cache_schema == schema);
2!
3572
        REQUIRE(cache_sv == 5);
2!
3573
        REQUIRE(cache_tv == 10);
2!
3574
    }
2✔
3575

3576
    SECTION("schema for older transaction is ignored") {
16✔
3577
        coordinator->cache_schema(schema, 5, 10);
2✔
3578
        coordinator->cache_schema(schema2, 4, 8);
2✔
3579

3580
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3581
        REQUIRE(cache_schema == schema);
2!
3582
        REQUIRE(cache_sv == 5);
2!
3583
        REQUIRE(cache_tv == 10);
2!
3584

3585
        coordinator->advance_schema_cache(10, 20);
2✔
3586
        coordinator->cache_schema(schema, 6, 15);
2✔
3587
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3588
        REQUIRE(cache_tv == 20); // should not have dropped to 15
2!
3589
    }
2✔
3590

3591
    SECTION("advance_schema() from transaction version bumps transaction version") {
16✔
3592
        coordinator->cache_schema(schema, 5, 10);
2✔
3593
        coordinator->advance_schema_cache(10, 12);
2✔
3594
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3595
        REQUIRE(cache_schema == schema);
2!
3596
        REQUIRE(cache_sv == 5);
2!
3597
        REQUIRE(cache_tv == 12);
2!
3598
    }
2✔
3599

3600
    SECTION("advance_schema() ending before transaction version does nothing") {
16✔
3601
        coordinator->cache_schema(schema, 5, 10);
2✔
3602
        coordinator->advance_schema_cache(8, 9);
2✔
3603
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3604
        REQUIRE(cache_schema == schema);
2!
3605
        REQUIRE(cache_sv == 5);
2!
3606
        REQUIRE(cache_tv == 10);
2!
3607
    }
2✔
3608

3609
    SECTION("advance_schema() extending over transaction version bumps version") {
16✔
3610
        coordinator->cache_schema(schema, 5, 10);
2✔
3611
        coordinator->advance_schema_cache(3, 15);
2✔
3612
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3613
        REQUIRE(cache_schema == schema);
2!
3614
        REQUIRE(cache_sv == 5);
2!
3615
        REQUIRE(cache_tv == 15);
2!
3616
    }
2✔
3617

3618
    SECTION("advance_schema() with no cahced schema does nothing") {
16✔
3619
        coordinator->advance_schema_cache(3, 15);
2✔
3620
        REQUIRE_FALSE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3621
    }
2✔
3622
}
16✔
3623

3624
TEST_CASE("SharedRealm: coordinator schema cache") {
26✔
3625
    TestFile config;
26✔
3626
    auto r = Realm::get_shared_realm(config);
26✔
3627
    auto coordinator = _impl::RealmCoordinator::get_coordinator(config.path);
26✔
3628

3629
    Schema cache_schema;
26✔
3630
    uint64_t cache_sv = -1, cache_tv = -1;
26✔
3631

3632
    Schema schema{
26✔
3633
        {"object", {{"value", PropertyType::Int}}},
26✔
3634
    };
26✔
3635
    Schema schema2{
26✔
3636
        {"object",
26✔
3637
         {
26✔
3638
             {"value", PropertyType::Int},
26✔
3639
         }},
26✔
3640
        {"object 2",
26✔
3641
         {
26✔
3642
             {"value", PropertyType::Int},
26✔
3643
         }},
26✔
3644
    };
26✔
3645

3646
    class ExternalWriter {
26✔
3647
    private:
26✔
3648
        std::shared_ptr<Realm> m_realm;
26✔
3649

3650
    public:
26✔
3651
        WriteTransaction wt;
26✔
3652
        ExternalWriter(Realm::Config const& config)
26✔
3653
            : m_realm([&] {
26✔
3654
                auto c = config;
18✔
3655
                c.scheduler = util::Scheduler::make_frozen(VersionID());
18✔
3656
                return _impl::RealmCoordinator::get_coordinator(c.path)->get_realm(c, util::none);
18✔
3657
            }())
18✔
3658
            , wt(TestHelper::get_db(m_realm))
26✔
3659
        {
26✔
3660
        }
18✔
3661
    };
26✔
3662

3663
    auto external_write = [&](Realm::Config const& config, auto&& fn) {
26✔
3664
        ExternalWriter wt(config);
16✔
3665
        fn(wt.wt);
16✔
3666
        wt.wt.commit();
16✔
3667
    };
16✔
3668

3669
    SECTION("is initially empty for uninitialized file") {
26✔
3670
        REQUIRE_FALSE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3671
    }
2✔
3672
    r->update_schema(schema);
26✔
3673

3674
    SECTION("is populated after calling update_schema()") {
26✔
3675
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3676
        REQUIRE(cache_sv == 0);
2!
3677
        REQUIRE(cache_schema == schema);
2!
3678
        REQUIRE(cache_schema.begin()->persisted_properties[0].column_key != ColKey{});
2!
3679
    }
2✔
3680

3681
    coordinator = nullptr;
26✔
3682
    r = nullptr;
26✔
3683
    r = Realm::get_shared_realm(config);
26✔
3684
    coordinator = _impl::RealmCoordinator::get_coordinator(config.path);
26✔
3685
    REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
26!
3686

3687
    SECTION("is populated after opening an initialized file") {
26✔
3688
        REQUIRE(cache_sv == 0);
2!
3689
        REQUIRE(cache_tv == 2); // with in-realm history the version doesn't reset
2!
3690
        REQUIRE(cache_schema == schema);
2!
3691
        REQUIRE(cache_schema.begin()->persisted_properties[0].column_key != ColKey{});
2!
3692
    }
2✔
3693

3694
    SECTION("transaction version is bumped after a local write") {
26✔
3695
        auto tv = cache_tv;
2✔
3696
        r->begin_transaction();
2✔
3697
        r->commit_transaction();
2✔
3698
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3699
        REQUIRE(cache_tv == tv + 1);
2!
3700
    }
2✔
3701

3702
    SECTION("notify() without a read transaction does not bump transaction version") {
26✔
3703
        auto tv = cache_tv;
4✔
3704

3705
        SECTION("non-schema change") {
4✔
3706
            external_write(config, [](auto& wt) {
2✔
3707
                wt.get_table("class_object")->create_object();
2✔
3708
            });
2✔
3709
        }
2✔
3710
        SECTION("schema change") {
4✔
3711
            external_write(config, [](auto& wt) {
2✔
3712
                wt.add_table("class_object 2");
2✔
3713
            });
2✔
3714
        }
2✔
3715

3716
        r->notify();
4✔
3717
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
4!
3718
        REQUIRE(cache_tv == tv);
4!
3719
        REQUIRE(cache_schema == schema);
4!
3720
    }
4✔
3721

3722
    SECTION("notify() with a read transaction bumps transaction version") {
26✔
3723
        r->read_group();
2✔
3724
        external_write(config, [](auto& wt) {
2✔
3725
            wt.get_table("class_object")->create_object();
2✔
3726
        });
2✔
3727

3728
        r->notify();
2✔
3729
        auto tv = cache_tv;
2✔
3730
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3731
        REQUIRE(cache_tv == tv + 1);
2!
3732
    }
2✔
3733

3734
    SECTION("notify() with a read transaction updates schema folloing external schema change") {
26✔
3735
        r->read_group();
2✔
3736
        external_write(config, [](auto& wt) {
2✔
3737
            wt.add_table("class_object 2");
2✔
3738
        });
2✔
3739

3740
        r->notify();
2✔
3741
        auto tv = cache_tv;
2✔
3742
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3743
        REQUIRE(cache_tv == tv + 1);
2!
3744
        REQUIRE(cache_schema.size() == 2);
2!
3745
        REQUIRE(cache_schema.find("object 2") != cache_schema.end());
2!
3746
    }
2✔
3747

3748
    SECTION("transaction version is bumped after refresh() following external non-schema write") {
26✔
3749
        external_write(config, [](auto& wt) {
2✔
3750
            wt.get_table("class_object")->create_object();
2✔
3751
        });
2✔
3752

3753
        r->refresh();
2✔
3754
        auto tv = cache_tv;
2✔
3755
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3756
        REQUIRE(cache_tv == tv + 1);
2!
3757
    }
2✔
3758

3759
    SECTION("schema is reread following refresh() over external schema change") {
26✔
3760
        external_write(config, [](auto& wt) {
2✔
3761
            wt.add_table("class_object 2");
2✔
3762
        });
2✔
3763

3764
        r->refresh();
2✔
3765
        auto tv = cache_tv;
2✔
3766
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3767
        REQUIRE(cache_tv == tv + 1);
2!
3768
        REQUIRE(cache_schema.size() == 2);
2!
3769
        REQUIRE(cache_schema.find("object 2") != cache_schema.end());
2!
3770
    }
2✔
3771

3772
    SECTION("update_schema() to version already on disk updates cache") {
26✔
3773
        r->read_group();
2✔
3774
        external_write(config, [](auto& wt) {
2✔
3775
            auto table = wt.add_table("class_object 2");
2✔
3776
            table->add_column(type_Int, "value");
2✔
3777
        });
2✔
3778

3779
        auto tv = cache_tv;
2✔
3780
        r->update_schema(schema2);
2✔
3781

3782
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3783
        REQUIRE(cache_tv == tv + 1); // only +1 because update_schema() did not perform a write
2!
3784
        REQUIRE(cache_schema.size() == 2);
2!
3785
        REQUIRE(cache_schema.find("object 2") != cache_schema.end());
2!
3786
    }
2✔
3787

3788
    SECTION("update_schema() to version already on disk updates cache") {
26✔
3789
        r->read_group();
2✔
3790
        external_write(config, [](auto& wt) {
2✔
3791
            auto table = wt.add_table("class_object 2");
2✔
3792
            table->add_column(type_Int, "value");
2✔
3793
        });
2✔
3794

3795
        auto tv = cache_tv;
2✔
3796
        r->update_schema(schema2);
2✔
3797

3798
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3799
        REQUIRE(cache_tv == tv + 1); // only +1 because update_schema() did not perform a write
2!
3800
        REQUIRE(cache_schema.size() == 2);
2!
3801
        REQUIRE(cache_schema.find("object 2") != cache_schema.end());
2!
3802
    }
2✔
3803

3804
    SECTION("update_schema() to version populated on disk while waiting for the write lock updates cache") {
26✔
3805
        r->read_group();
2✔
3806

3807
        // We want to commit the write while we're waiting on the write lock on
3808
        // this thread, which can't really be done in a properly synchronized manner
3809
        std::chrono::microseconds wait_time{5000};
2✔
3810
#if REALM_ANDROID
3811
        // When running on device or in an emulator we need to wait longer due
3812
        // to them being slow
3813
        wait_time *= 10;
3814
#endif
3815

3816
        bool did_run = false;
2✔
3817
        JoiningThread thread([&] {
2✔
3818
            ExternalWriter writer(config);
2✔
3819
            if (writer.wt.get_table("class_object 2"))
2✔
3820
                return;
×
3821
            did_run = true;
2✔
3822

3823
            auto table = writer.wt.add_table("class_object 2");
2✔
3824
            table->add_column(type_Int, "value");
2✔
3825
            std::this_thread::sleep_for(wait_time * 2);
2✔
3826
            writer.wt.commit();
2✔
3827
        });
2✔
3828
        std::this_thread::sleep_for(wait_time);
2✔
3829

3830
        auto tv = cache_tv;
2✔
3831
        r->update_schema(Schema{
2✔
3832
            {"object", {{"value", PropertyType::Int}}},
2✔
3833
            {"object 2", {{"value", PropertyType::Int}}},
2✔
3834
        });
2✔
3835

3836
        // just skip the test if the timing was wrong to avoid spurious failures
3837
        if (!did_run)
2✔
3838
            return;
×
3839

3840
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3841
        REQUIRE(cache_tv == tv + 1); // only +1 because update_schema()'s write was rolled back
2!
3842
        REQUIRE(cache_schema.size() == 2);
2!
3843
        REQUIRE(cache_schema.find("object 2") != cache_schema.end());
2!
3844
    }
2✔
3845
}
26✔
3846

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

3850
    // Prepopulate the Realm with the schema.
3851
    Realm::Config config_with_schema = config;
2✔
3852
    config_with_schema.schema_version = 1;
2✔
3853
    config_with_schema.schema_mode = SchemaMode::Automatic;
2✔
3854
    config_with_schema.schema =
2✔
3855
        Schema{{"object",
2✔
3856
                {
2✔
3857
                    {"value", PropertyType::Int, Property::IsPrimary{true}},
2✔
3858
                    {"value 2", PropertyType::Int, Property::IsPrimary{false}, Property::IsIndexed{true}},
2✔
3859
                }}};
2✔
3860
    auto r1 = Realm::get_shared_realm(config_with_schema);
2✔
3861

3862
    // Retrieve the object schema in dynamic mode.
3863
    auto r2 = Realm::get_shared_realm(config);
2✔
3864
    auto* object_schema = &*r2->schema().find("object");
2✔
3865

3866
    // Perform an empty write to create a new version, resulting in the other Realm needing to re-read the schema.
3867
    r1->begin_transaction();
2✔
3868
    r1->commit_transaction();
2✔
3869

3870
    // Advance to the latest version, and verify the object schema is at the same location in memory.
3871
    r2->read_group();
2✔
3872
    REQUIRE(object_schema == &*r2->schema().find("object"));
2!
3873
}
2✔
3874

3875
TEST_CASE("SharedRealm: declaring an object as embedded results in creating an embedded table") {
2✔
3876
    TestFile config;
2✔
3877

3878
    // Prepopulate the Realm with the schema.
3879
    config.schema = Schema{{"object1",
2✔
3880
                            ObjectSchema::ObjectType::Embedded,
2✔
3881
                            {
2✔
3882
                                {"value", PropertyType::Int},
2✔
3883
                            }},
2✔
3884
                           {"object2",
2✔
3885
                            {
2✔
3886
                                {"value", PropertyType::Object | PropertyType::Nullable, "object1"},
2✔
3887
                            }}};
2✔
3888
    auto r1 = Realm::get_shared_realm(config);
2✔
3889

3890
    Group& g = r1->read_group();
2✔
3891
    auto t = g.get_table("class_object1");
2✔
3892
    REQUIRE(t->is_embedded());
2!
3893
}
2✔
3894

3895
TEST_CASE("SharedRealm: SchemaChangedFunction") {
16✔
3896
    struct Context : BindingContext {
16✔
3897
        size_t* change_count;
16✔
3898
        Schema* schema;
16✔
3899
        Context(size_t* count_out, Schema* schema_out)
16✔
3900
            : change_count(count_out)
19✔
3901
            , schema(schema_out)
19✔
3902
        {
22✔
3903
        }
22✔
3904

3905
        void schema_did_change(Schema const& changed_schema) override
16✔
3906
        {
16✔
3907
            ++*change_count;
10✔
3908
            *schema = changed_schema;
10✔
3909
        }
10✔
3910
    };
16✔
3911

3912
    size_t schema_changed_called = 0;
16✔
3913
    Schema changed_fixed_schema;
16✔
3914
    TestFile config;
16✔
3915
    RealmConfig dynamic_config = config;
16✔
3916

3917
    config.schema = Schema{{"object1",
16✔
3918
                            {
16✔
3919
                                {"value", PropertyType::Int},
16✔
3920
                            }},
16✔
3921
                           {"object2",
16✔
3922
                            {
16✔
3923
                                {"value", PropertyType::Int},
16✔
3924
                            }}};
16✔
3925
    config.schema_version = 1;
16✔
3926
    auto r1 = Realm::get_shared_realm(config);
16✔
3927
    r1->read_group();
16✔
3928
    r1->m_binding_context.reset(new Context(&schema_changed_called, &changed_fixed_schema));
16✔
3929

3930
    SECTION("Fixed schema") {
16✔
3931
        SECTION("update_schema") {
10✔
3932
            auto new_schema = Schema{{"object3",
2✔
3933
                                      {
2✔
3934
                                          {"value", PropertyType::Int},
2✔
3935
                                      }}};
2✔
3936
            r1->update_schema(new_schema, 2);
2✔
3937
            REQUIRE(schema_changed_called == 1);
2!
3938
            REQUIRE(changed_fixed_schema.find("object3")->property_for_name("value")->column_key != ColKey{});
2!
3939
        }
2✔
3940

3941
        SECTION("Open a new Realm instance with same config won't trigger") {
10✔
3942
            auto r2 = Realm::get_shared_realm(config);
2✔
3943
            REQUIRE(schema_changed_called == 0);
2!
3944
        }
2✔
3945

3946
        SECTION("Non schema related transaction doesn't trigger") {
10✔
3947
            auto r2 = Realm::get_shared_realm(config);
2✔
3948
            r2->begin_transaction();
2✔
3949
            r2->commit_transaction();
2✔
3950
            r1->refresh();
2✔
3951
            REQUIRE(schema_changed_called == 0);
2!
3952
        }
2✔
3953

3954
        SECTION("Schema is changed by another Realm") {
10✔
3955
            auto r2 = Realm::get_shared_realm(config);
2✔
3956
            r2->begin_transaction();
2✔
3957
            r2->read_group().get_table("class_object1")->add_column(type_String, "new col");
2✔
3958
            r2->commit_transaction();
2✔
3959
            r1->refresh();
2✔
3960
            REQUIRE(schema_changed_called == 1);
2!
3961
            REQUIRE(changed_fixed_schema.find("object1")->property_for_name("value")->column_key != ColKey{});
2!
3962
        }
2✔
3963

3964
        // This is not a valid use case. m_schema won't be refreshed.
3965
        SECTION("Schema is changed by this Realm won't trigger") {
10✔
3966
            r1->begin_transaction();
2✔
3967
            r1->read_group().get_table("class_object1")->add_column(type_String, "new col");
2✔
3968
            r1->commit_transaction();
2✔
3969
            REQUIRE(schema_changed_called == 0);
2!
3970
        }
2✔
3971
    }
10✔
3972

3973
    SECTION("Dynamic schema") {
16✔
3974
        size_t dynamic_schema_changed_called = 0;
6✔
3975
        Schema changed_dynamic_schema;
6✔
3976
        auto r2 = Realm::get_shared_realm(dynamic_config);
6✔
3977
        r2->m_binding_context.reset(new Context(&dynamic_schema_changed_called, &changed_dynamic_schema));
6✔
3978

3979
        SECTION("set_schema_subset") {
6✔
3980
            auto new_schema = Schema{{"object1",
2✔
3981
                                      {
2✔
3982
                                          {"value", PropertyType::Int},
2✔
3983
                                      }}};
2✔
3984
            r2->set_schema_subset(new_schema);
2✔
3985
            REQUIRE(schema_changed_called == 0);
2!
3986
            REQUIRE(dynamic_schema_changed_called == 1);
2!
3987
            REQUIRE(changed_dynamic_schema.find("object1")->property_for_name("value")->column_key != ColKey{});
2!
3988
        }
2✔
3989

3990
        SECTION("Non schema related transaction will always trigger in dynamic mode") {
6✔
3991
            auto r1 = Realm::get_shared_realm(config);
2✔
3992
            // An empty transaction will trigger the schema changes always in dynamic mode.
3993
            r1->begin_transaction();
2✔
3994
            r1->commit_transaction();
2✔
3995
            r2->refresh();
2✔
3996
            REQUIRE(dynamic_schema_changed_called == 1);
2!
3997
            REQUIRE(changed_dynamic_schema.find("object1")->property_for_name("value")->column_key != ColKey{});
2!
3998
        }
2✔
3999

4000
        SECTION("Schema is changed by another Realm") {
6✔
4001
            r1->begin_transaction();
2✔
4002
            r1->read_group().get_table("class_object1")->add_column(type_String, "new col");
2✔
4003
            r1->commit_transaction();
2✔
4004
            r2->refresh();
2✔
4005
            REQUIRE(dynamic_schema_changed_called == 1);
2!
4006
            REQUIRE(changed_dynamic_schema.find("object1")->property_for_name("value")->column_key != ColKey{});
2!
4007
        }
2✔
4008
    }
6✔
4009
}
16✔
4010

4011
TEST_CASE("SharedRealm: compact on launch") {
4✔
4012
    // Make compactable Realm
4013
    TestFile config;
4✔
4014
    config.automatic_change_notifications = false;
4✔
4015
    int num_opens = 0;
4✔
4016
    config.should_compact_on_launch_function = [&](uint64_t total_bytes, uint64_t used_bytes) {
12✔
4017
        REQUIRE(total_bytes > used_bytes);
12!
4018
        num_opens++;
12✔
4019
        return num_opens != 2;
12✔
4020
    };
12✔
4021
    config.schema = Schema{
4✔
4022
        {"object", {{"value", PropertyType::String}}},
4✔
4023
    };
4✔
4024
    REQUIRE(num_opens == 0);
4!
4025
    auto r = Realm::get_shared_realm(config);
4✔
4026
    REQUIRE(num_opens == 1);
4!
4027
    r->begin_transaction();
4✔
4028
    auto table = r->read_group().get_table("class_object");
4✔
4029
    size_t count = 1000;
4✔
4030
    for (size_t i = 0; i < count; ++i)
4,004✔
4031
        table->create_object().set_all(util::format("Foo_%1", i % 10).c_str());
4,000✔
4032
    r->commit_transaction();
4✔
4033
    REQUIRE(table->size() == count);
4!
4034
    r->close();
4✔
4035

4036
    SECTION("compact reduces the file size") {
4✔
4037
#ifndef _WIN32
2✔
4038
        // Confirm expected sizes before and after opening the Realm
4039
        size_t size_before = size_t(util::File(config.path).get_size());
2✔
4040
        r = Realm::get_shared_realm(config);
2✔
4041
        REQUIRE(num_opens == 2);
2!
4042
        r->close();
2✔
4043
        REQUIRE(size_t(util::File(config.path).get_size()) == size_before); // File size after returning false
2!
4044
        r = Realm::get_shared_realm(config);
2✔
4045
        REQUIRE(num_opens == 3);
2!
4046
        REQUIRE(size_t(util::File(config.path).get_size()) < size_before); // File size after returning true
2!
4047

4048
        // Validate that the file still contains what it should
4049
        REQUIRE(r->read_group().get_table("class_object")->size() == count);
2!
4050

4051
        // Registering for a collection notification shouldn't crash when compact on launch is used.
4052
        Results results(r, r->read_group().get_table("class_object"));
2✔
4053
        results.add_notification_callback([](CollectionChangeSet const&) {});
2✔
4054
        r->close();
2✔
4055
#endif
2✔
4056
    }
2✔
4057

4058
    SECTION("compact function does not get invoked if realm is open on another thread") {
4✔
4059
        config.scheduler = util::Scheduler::make_frozen(VersionID());
2✔
4060
        r = Realm::get_shared_realm(config);
2✔
4061
        REQUIRE(num_opens == 2);
2!
4062
        std::thread([&] {
2✔
4063
            auto r2 = Realm::get_shared_realm(config);
2✔
4064
            REQUIRE(num_opens == 2);
2!
4065
        }).join();
2✔
4066
        r->close();
2✔
4067
        std::thread([&] {
2✔
4068
            auto r3 = Realm::get_shared_realm(config);
2✔
4069
            REQUIRE(num_opens == 3);
2!
4070
        }).join();
2✔
4071
    }
2✔
4072
}
4✔
4073

4074
struct ModeAutomatic {
4075
    static constexpr SchemaMode mode = SchemaMode::Automatic;
4076
    static constexpr bool should_call_init_on_version_bump = false;
4077
};
4078
struct ModeAdditive {
4079
    static constexpr SchemaMode mode = SchemaMode::AdditiveExplicit;
4080
    static constexpr bool should_call_init_on_version_bump = false;
4081
};
4082
struct ModeManual {
4083
    static constexpr SchemaMode mode = SchemaMode::Manual;
4084
    static constexpr bool should_call_init_on_version_bump = false;
4085
};
4086
struct ModeSoftResetFile {
4087
    static constexpr SchemaMode mode = SchemaMode::SoftResetFile;
4088
    static constexpr bool should_call_init_on_version_bump = true;
4089
};
4090
struct ModeHardResetFile {
4091
    static constexpr SchemaMode mode = SchemaMode::HardResetFile;
4092
    static constexpr bool should_call_init_on_version_bump = true;
4093
};
4094

4095
TEMPLATE_TEST_CASE("SharedRealm: update_schema with initialization_function", "[init][update schema]", ModeAutomatic,
4096
                   ModeAdditive, ModeManual, ModeSoftResetFile, ModeHardResetFile)
4097
{
30✔
4098
    TestFile config;
30✔
4099
    config.schema_mode = TestType::mode;
30✔
4100
    bool initialization_function_called = false;
30✔
4101
    uint64_t schema_version_in_callback = -1;
30✔
4102
    Schema schema_in_callback;
30✔
4103
    auto initialization_function = [&initialization_function_called, &schema_version_in_callback,
30✔
4104
                                    &schema_in_callback](auto shared_realm) {
30✔
4105
        REQUIRE(shared_realm->is_in_transaction());
24!
4106
        initialization_function_called = true;
24✔
4107
        schema_version_in_callback = shared_realm->schema_version();
24✔
4108
        schema_in_callback = shared_realm->schema();
24✔
4109
    };
24✔
4110

4111
    Schema schema{
30✔
4112
        {"object", {{"value", PropertyType::String}}},
30✔
4113
    };
30✔
4114

4115
    SECTION("call initialization function directly by update_schema") {
30✔
4116
        // Open in dynamic mode with no schema specified
4117
        auto realm = Realm::get_shared_realm(config);
10✔
4118
        REQUIRE_FALSE(initialization_function_called);
10!
4119

4120
        realm->update_schema(schema, 0, nullptr, initialization_function);
10✔
4121
        REQUIRE(initialization_function_called);
10!
4122
        REQUIRE(schema_version_in_callback == 0);
10!
4123
        REQUIRE(schema_in_callback.compare(schema).size() == 0);
10!
4124
    }
10✔
4125

4126
    config.schema_version = 0;
30✔
4127
    config.schema = schema;
30✔
4128

4129
    SECTION("initialization function should be called for unversioned realm") {
30✔
4130
        config.initialization_function = initialization_function;
10✔
4131
        Realm::get_shared_realm(config);
10✔
4132
        REQUIRE(initialization_function_called);
10!
4133
        REQUIRE(schema_version_in_callback == 0);
10!
4134
        REQUIRE(schema_in_callback.compare(schema).size() == 0);
10!
4135
    }
10✔
4136

4137
    SECTION("initialization function for versioned realm") {
30✔
4138
        // Initialize v0
4139
        Realm::get_shared_realm(config);
10✔
4140

4141
        config.schema_version = 1;
10✔
4142
        config.initialization_function = initialization_function;
10✔
4143
        Realm::get_shared_realm(config);
10✔
4144
        REQUIRE(initialization_function_called == TestType::should_call_init_on_version_bump);
10!
4145
        if (TestType::should_call_init_on_version_bump) {
10✔
4146
            REQUIRE(schema_version_in_callback == 1);
4!
4147
            REQUIRE(schema_in_callback.compare(schema).size() == 0);
4!
4148
        }
4✔
4149
    }
10✔
4150
}
30✔
4151

4152
TEST_CASE("BindingContext is notified about delivery of change notifications") {
16✔
4153
    _impl::RealmCoordinator::assert_no_open_realms();
16✔
4154
    InMemoryTestFile config;
16✔
4155
    config.automatic_change_notifications = false;
16✔
4156

4157
    auto r = Realm::get_shared_realm(config);
16✔
4158
    r->update_schema({
16✔
4159
        {"object", {{"value", PropertyType::Int}}},
16✔
4160
    });
16✔
4161

4162
    auto coordinator = _impl::RealmCoordinator::get_coordinator(config.path);
16✔
4163
    auto table = r->read_group().get_table("class_object");
16✔
4164

4165
    SECTION("BindingContext notified even if no callbacks are registered") {
16✔
4166
        static int binding_context_start_notify_calls = 0;
4✔
4167
        static int binding_context_end_notify_calls = 0;
4✔
4168
        struct Context : BindingContext {
4✔
4169
            void will_send_notifications() override
4✔
4170
            {
4✔
4171
                ++binding_context_start_notify_calls;
4✔
4172
            }
4✔
4173

4174
            void did_send_notifications() override
4✔
4175
            {
4✔
4176
                ++binding_context_end_notify_calls;
4✔
4177
            }
4✔
4178
        };
4✔
4179
        r->m_binding_context.reset(new Context());
4✔
4180

4181
        SECTION("local commit") {
4✔
4182
            binding_context_start_notify_calls = 0;
2✔
4183
            binding_context_end_notify_calls = 0;
2✔
4184
            coordinator->on_change();
2✔
4185
            r->begin_transaction();
2✔
4186
            REQUIRE(binding_context_start_notify_calls == 1);
2!
4187
            REQUIRE(binding_context_end_notify_calls == 1);
2!
4188
            r->cancel_transaction();
2✔
4189
        }
2✔
4190

4191
        SECTION("remote commit") {
4✔
4192
            binding_context_start_notify_calls = 0;
2✔
4193
            binding_context_end_notify_calls = 0;
2✔
4194
            JoiningThread([&] {
2✔
4195
                auto r2 = coordinator->get_realm(util::Scheduler::make_frozen(VersionID()));
2✔
4196
                r2->begin_transaction();
2✔
4197
                auto table2 = r2->read_group().get_table("class_object");
2✔
4198
                table2->create_object();
2✔
4199
                r2->commit_transaction();
2✔
4200
            });
2✔
4201
            advance_and_notify(*r);
2✔
4202
            REQUIRE(binding_context_start_notify_calls == 1);
2!
4203
            REQUIRE(binding_context_end_notify_calls == 1);
2!
4204
        }
2✔
4205
    }
4✔
4206

4207
    SECTION("notify BindingContext before and after sending notifications") {
16✔
4208
        static int binding_context_start_notify_calls = 0;
4✔
4209
        static int binding_context_end_notify_calls = 0;
4✔
4210
        static int notification_calls = 0;
4✔
4211

4212
        auto col = table->get_column_key("value");
4✔
4213
        Results results1(r, table->where().greater_equal(col, 0));
4✔
4214
        Results results2(r, table->where().less(col, 10));
4✔
4215

4216
        auto token1 = results1.add_notification_callback([&](CollectionChangeSet) {
4✔
4217
            ++notification_calls;
4✔
4218
        });
4✔
4219

4220
        auto token2 = results2.add_notification_callback([&](CollectionChangeSet) {
4✔
4221
            ++notification_calls;
4✔
4222
        });
4✔
4223

4224
        struct Context : BindingContext {
4✔
4225
            void will_send_notifications() override
4✔
4226
            {
4✔
4227
                REQUIRE(notification_calls == 0);
4!
4228
                REQUIRE(binding_context_end_notify_calls == 0);
4!
4229
                ++binding_context_start_notify_calls;
4✔
4230
            }
4✔
4231

4232
            void did_send_notifications() override
4✔
4233
            {
4✔
4234
                REQUIRE(notification_calls == 2);
4!
4235
                REQUIRE(binding_context_start_notify_calls == 1);
4!
4236
                ++binding_context_end_notify_calls;
4✔
4237
            }
4✔
4238
        };
4✔
4239
        r->m_binding_context.reset(new Context());
4✔
4240

4241
        SECTION("local commit") {
4✔
4242
            binding_context_start_notify_calls = 0;
2✔
4243
            binding_context_end_notify_calls = 0;
2✔
4244
            notification_calls = 0;
2✔
4245
            coordinator->on_change();
2✔
4246
            r->begin_transaction();
2✔
4247
            table->create_object();
2✔
4248
            r->commit_transaction();
2✔
4249
            REQUIRE(binding_context_start_notify_calls == 1);
2!
4250
            REQUIRE(binding_context_end_notify_calls == 1);
2!
4251
        }
2✔
4252

4253
        SECTION("remote commit") {
4✔
4254
            binding_context_start_notify_calls = 0;
2✔
4255
            binding_context_end_notify_calls = 0;
2✔
4256
            notification_calls = 0;
2✔
4257
            JoiningThread([&] {
2✔
4258
                auto r2 = coordinator->get_realm(util::Scheduler::make_frozen(VersionID()));
2✔
4259
                r2->begin_transaction();
2✔
4260
                auto table2 = r2->read_group().get_table("class_object");
2✔
4261
                table2->create_object();
2✔
4262
                r2->commit_transaction();
2✔
4263
            });
2✔
4264
            advance_and_notify(*r);
2✔
4265
            REQUIRE(binding_context_start_notify_calls == 1);
2!
4266
            REQUIRE(binding_context_end_notify_calls == 1);
2!
4267
        }
2✔
4268
    }
4✔
4269

4270
    SECTION("did_send() is skipped if the Realm is closed first") {
16✔
4271
        Results results(r, table->where());
8✔
4272
        bool do_close = true;
8✔
4273
        auto token = results.add_notification_callback([&](CollectionChangeSet) {
8✔
4274
            if (do_close)
8✔
4275
                r->close();
4✔
4276
        });
8✔
4277

4278
        struct FailOnDidSend : BindingContext {
8✔
4279
            void did_send_notifications() override
8✔
4280
            {
8✔
4281
                FAIL("did_send_notifications() should not have been called");
×
4282
            }
×
4283
        };
8✔
4284
        struct CloseOnWillChange : FailOnDidSend {
8✔
4285
            Realm& realm;
8✔
4286
            CloseOnWillChange(Realm& realm)
8✔
4287
                : realm(realm)
8✔
4288
            {
8✔
4289
            }
4✔
4290

4291
            void will_send_notifications() override
8✔
4292
            {
8✔
4293
                realm.close();
4✔
4294
            }
4✔
4295
        };
8✔
4296

4297
        SECTION("closed in notification callback for notify()") {
8✔
4298
            r->m_binding_context.reset(new FailOnDidSend);
2✔
4299
            coordinator->on_change();
2✔
4300
            r->notify();
2✔
4301
        }
2✔
4302

4303
        SECTION("closed in notification callback for refresh()") {
8✔
4304
            do_close = false;
2✔
4305
            coordinator->on_change();
2✔
4306
            r->notify();
2✔
4307
            do_close = true;
2✔
4308

4309
            JoiningThread([&] {
2✔
4310
                auto r = coordinator->get_realm(util::Scheduler::make_frozen(VersionID()));
2✔
4311
                r->begin_transaction();
2✔
4312
                r->read_group().get_table("class_object")->create_object();
2✔
4313
                r->commit_transaction();
2✔
4314
            });
2✔
4315

4316
            r->m_binding_context.reset(new FailOnDidSend);
2✔
4317
            coordinator->on_change();
2✔
4318
            r->refresh();
2✔
4319
        }
2✔
4320

4321
        SECTION("closed in will_send() for notify()") {
8✔
4322
            r->m_binding_context.reset(new CloseOnWillChange(*r));
2✔
4323
            coordinator->on_change();
2✔
4324
            r->notify();
2✔
4325
        }
2✔
4326

4327
        SECTION("closed in will_send() for refresh()") {
8✔
4328
            do_close = false;
2✔
4329
            coordinator->on_change();
2✔
4330
            r->notify();
2✔
4331
            do_close = true;
2✔
4332

4333
            JoiningThread([&] {
2✔
4334
                auto r = coordinator->get_realm(util::Scheduler::make_frozen(VersionID()));
2✔
4335
                r->begin_transaction();
2✔
4336
                r->read_group().get_table("class_object")->create_object();
2✔
4337
                r->commit_transaction();
2✔
4338
            });
2✔
4339

4340
            r->m_binding_context.reset(new CloseOnWillChange(*r));
2✔
4341
            coordinator->on_change();
2✔
4342
            r->refresh();
2✔
4343
        }
2✔
4344
    }
8✔
4345
#ifdef _WIN32
4346
    _impl::RealmCoordinator::clear_all_caches();
4347
#endif
4348
}
16✔
4349

4350
TEST_CASE("RealmCoordinator: get_unbound_realm()") {
8✔
4351
    TestFile config;
8✔
4352
    config.cache = true;
8✔
4353
    config.schema = Schema{
8✔
4354
        {"object", {{"value", PropertyType::Int}}},
8✔
4355
    };
8✔
4356

4357
    ThreadSafeReference ref;
8✔
4358
    std::thread([&] {
8✔
4359
        ref = _impl::RealmCoordinator::get_coordinator(config)->get_unbound_realm();
8✔
4360
    }).join();
8✔
4361

4362
    SECTION("checks thread after being resolved") {
8✔
4363
        auto realm = Realm::get_shared_realm(std::move(ref));
2✔
4364
        REQUIRE_NOTHROW(realm->verify_thread());
2✔
4365
        std::thread([&] {
2✔
4366
            REQUIRE_EXCEPTION(realm->verify_thread(), WrongThread, "Realm accessed from incorrect thread.");
2✔
4367
        }).join();
2✔
4368
    }
2✔
4369

4370
    SECTION("delivers notifications to the thread it is resolved on") {
8✔
4371
#ifndef _WIN32
2✔
4372
        if (!util::EventLoop::has_implementation())
2✔
4373
            return;
×
4374
        auto realm = Realm::get_shared_realm(std::move(ref));
2✔
4375
        Results results(realm, ObjectStore::table_for_object_type(realm->read_group(), "object")->where());
2✔
4376
        bool called = false;
2✔
4377
        auto token = results.add_notification_callback([&](CollectionChangeSet) {
2✔
4378
            called = true;
2✔
4379
        });
2✔
4380
        util::EventLoop::main().run_until([&] {
16✔
4381
            return called;
16✔
4382
        });
16✔
4383
#endif
2✔
4384
    }
2✔
4385

4386
    SECTION("resolves to existing cached Realm for the thread if caching is enabled") {
8✔
4387
        auto r1 = Realm::get_shared_realm(config);
2✔
4388
        auto r2 = Realm::get_shared_realm(std::move(ref));
2✔
4389
        REQUIRE(r1 == r2);
2!
4390
    }
2✔
4391

4392
    SECTION("resolves to a new Realm if caching is disabled") {
8✔
4393
        config.cache = false;
2✔
4394
        auto r1 = Realm::get_shared_realm(config);
2✔
4395
        auto r2 = Realm::get_shared_realm(std::move(ref));
2✔
4396
        REQUIRE(r1 != r2);
2!
4397

4398
        // New unbound with cache disabled
4399
        std::thread([&] {
2✔
4400
            ref = _impl::RealmCoordinator::get_coordinator(config)->get_unbound_realm();
2✔
4401
        }).join();
2✔
4402
        auto r3 = Realm::get_shared_realm(std::move(ref));
2✔
4403
        REQUIRE(r1 != r3);
2!
4404
        REQUIRE(r2 != r3);
2!
4405

4406
        // New local with cache enabled should grab the resolved unbound
4407
        config.cache = true;
2✔
4408
        auto r4 = Realm::get_shared_realm(config);
2✔
4409
        REQUIRE(r4 == r2);
2!
4410
    }
2✔
4411
}
8✔
4412

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

4418
    {
40✔
4419
        auto realm = Realm::get_shared_realm(config);
40✔
4420
        realm->begin_transaction();
40✔
4421
        realm->read_group().get_table("class_object")->create_object();
40✔
4422
        realm->commit_transaction();
40✔
4423
    }
40✔
4424

4425
    config.schema_mode = SchemaMode::Immutable;
40✔
4426
    auto realm = Realm::get_shared_realm(config);
40✔
4427
    realm->read_group();
40✔
4428

4429
    SECTION("unsupported functions") {
40✔
4430
        SECTION("update_schema()") {
10✔
4431
            REQUIRE_THROWS_AS(realm->compact(), WrongTransactionState);
2✔
4432
        }
2✔
4433
        SECTION("begin_transaction()") {
10✔
4434
            REQUIRE_THROWS_AS(realm->begin_transaction(), WrongTransactionState);
2✔
4435
        }
2✔
4436
        SECTION("async_begin_transaction()") {
10✔
4437
            REQUIRE_THROWS_AS(realm->async_begin_transaction(nullptr), WrongTransactionState);
2✔
4438
        }
2✔
4439
        SECTION("refresh()") {
10✔
4440
            REQUIRE_THROWS_AS(realm->refresh(), WrongTransactionState);
2✔
4441
        }
2✔
4442
        SECTION("compact()") {
10✔
4443
            REQUIRE_THROWS_AS(realm->compact(), WrongTransactionState);
2✔
4444
        }
2✔
4445
    }
10✔
4446

4447
    SECTION("supported functions") {
40✔
4448
        SECTION("is_in_transaction()") {
30✔
4449
            REQUIRE_FALSE(realm->is_in_transaction());
2!
4450
        }
2✔
4451
        SECTION("is_in_async_transaction()") {
30✔
4452
            REQUIRE_FALSE(realm->is_in_transaction());
2!
4453
        }
2✔
4454
        SECTION("freeze()") {
30✔
4455
            std::shared_ptr<Realm> frozen;
2✔
4456
            REQUIRE_NOTHROW(frozen = realm->freeze());
2✔
4457
            REQUIRE(frozen->read_group().get_table("class_object")->size() == 1);
2!
4458
            REQUIRE_NOTHROW(frozen = Realm::get_frozen_realm(config, realm->read_transaction_version()));
2✔
4459
            REQUIRE(frozen->read_group().get_table("class_object")->size() == 1);
2!
4460
        }
2✔
4461
        SECTION("notify()") {
30✔
4462
            REQUIRE_NOTHROW(realm->notify());
2✔
4463
        }
2✔
4464
        SECTION("is_in_read_transaction()") {
30✔
4465
            REQUIRE(realm->is_in_read_transaction());
2!
4466
        }
2✔
4467
        SECTION("last_seen_transaction_version()") {
30✔
4468
            REQUIRE(realm->last_seen_transaction_version() == 1);
2!
4469
        }
2✔
4470
        SECTION("get_number_of_versions()") {
30✔
4471
            REQUIRE(realm->get_number_of_versions() == 1);
2!
4472
        }
2✔
4473
        SECTION("read_transaction_version()") {
30✔
4474
            REQUIRE(realm->read_transaction_version() == VersionID{1, 0});
2!
4475
        }
2✔
4476
        SECTION("current_transaction_version()") {
30✔
4477
            REQUIRE(realm->current_transaction_version() == VersionID{1, 0});
2!
4478
        }
2✔
4479
        SECTION("latest_snapshot_version()") {
30✔
4480
            REQUIRE(realm->latest_snapshot_version() == 1);
2!
4481
        }
2✔
4482
        SECTION("duplicate()") {
30✔
4483
            auto duplicate = realm->duplicate();
2✔
4484
            REQUIRE(duplicate->get_table("class_object")->size() == 1);
2!
4485
        }
2✔
4486
        SECTION("invalidate()") {
30✔
4487
            REQUIRE_NOTHROW(realm->invalidate());
2✔
4488
            REQUIRE_FALSE(realm->is_in_read_transaction());
2!
4489
            REQUIRE(realm->read_group().get_table("class_object")->size() == 1);
2!
4490
        }
2✔
4491
        SECTION("close()") {
30✔
4492
            REQUIRE_NOTHROW(realm->close());
2✔
4493
            REQUIRE(realm->is_closed());
2!
4494
        }
2✔
4495
        SECTION("has_pending_async_work()") {
30✔
4496
            REQUIRE_FALSE(realm->has_pending_async_work());
2!
4497
        }
2✔
4498
        SECTION("wait_for_change()") {
30✔
4499
            REQUIRE_FALSE(realm->wait_for_change());
2!
4500
        }
2✔
4501
    }
30✔
4502
}
40✔
4503

4504
TEST_CASE("KeyPathMapping generation") {
2✔
4505
    TestFile config;
2✔
4506
    realm::query_parser::KeyPathMapping mapping;
2✔
4507

4508
    SECTION("class aliasing") {
2✔
4509
        Schema schema = {
2✔
4510
            {"PersistedName", {{"age", PropertyType::Int}}, {}, "AlternativeName"},
2✔
4511
            {"class_with_policy",
2✔
4512
             {{"value", PropertyType::Int},
2✔
4513
              {"child", PropertyType::Object | PropertyType::Nullable, "class_with_policy"}},
2✔
4514
             {{"parents", PropertyType::LinkingObjects | PropertyType::Array, "class_with_policy", "child"}},
2✔
4515
             "ClassWithPolicy"},
2✔
4516
        };
2✔
4517
        schema.validate();
2✔
4518
        config.schema = schema;
2✔
4519
        auto realm = Realm::get_shared_realm(config);
2✔
4520
        realm::populate_keypath_mapping(mapping, *realm);
2✔
4521
        REQUIRE(mapping.has_table_mapping("AlternativeName"));
2!
4522
        REQUIRE("class_PersistedName" == mapping.get_table_mapping("AlternativeName"));
2!
4523

4524
        auto table = realm->read_group().get_table("class_class_with_policy");
2✔
4525
        std::vector<Mixed> args{0};
2✔
4526
        auto q = table->query("parents.value = $0", args, mapping);
2✔
4527
        REQUIRE(q.count() == 0);
2!
4528
    }
2✔
4529
}
2✔
4530

4531
TEST_CASE("Concurrent operations") {
4✔
4532
    SECTION("Async commits together with online compaction") {
4✔
4533
        // This is a reproduction test for issue https://github.com/realm/realm-dart/issues/1396
4534
        // First create a relatively large realm, then delete the content and do some more
4535
        // commits using async commits. If a compaction is started when doing an async commit
4536
        // then the subsequent committing done in the helper thread will illegally COW the
4537
        // top array. When the next mutation is done, the top array will be reported as being
4538
        // already freed.
4539
        TestFile config;
2✔
4540
        config.schema_version = 1;
2✔
4541
        config.schema = Schema{{"object", {{"value", PropertyType::Int}}}};
2✔
4542

4543
        auto realm_1 = Realm::get_shared_realm(config);
2✔
4544
        Results res(realm_1, realm_1->read_group().get_table("class_object")->where());
2✔
4545
        auto realm_2 = Realm::get_shared_realm(config);
2✔
4546

4547
        {
2✔
4548
            // Create a lot of objects
4549
            realm_2->begin_transaction();
2✔
4550
            auto table = realm_2->read_group().get_table("class_object");
2✔
4551
            for (int i = 0; i < 400000; i++) {
800,002✔
4552
                table->create_object().set("value", i);
800,000✔
4553
            }
800,000✔
4554
            realm_2->commit_transaction();
2✔
4555
        }
2✔
4556

4557
        int commit_1 = 0;
2✔
4558
        int commit_2 = 0;
2✔
4559

4560
        for (int i = 0; i < 4; i++) {
10✔
4561
            realm_1->async_begin_transaction([&]() {
8✔
4562
                // Clearing the DB will reduce the need for space
4563
                // This will trigger an online compaction
4564
                // Before the fix, the probram would crash here next time around.
4565
                res.clear();
8✔
4566
                realm_1->async_commit_transaction([&](std::exception_ptr) {
8✔
4567
                    commit_1++;
8✔
4568
                });
8✔
4569
            });
8✔
4570
            realm_2->async_begin_transaction([&]() {
8✔
4571
                // Make sure we will continue to have something to delete
4572
                auto table = realm_2->read_group().get_table("class_object");
8✔
4573
                for (int i = 0; i < 100; i++) {
808✔
4574
                    table->create_object().set("value", i);
800✔
4575
                }
800✔
4576
                realm_2->async_commit_transaction([&](std::exception_ptr) {
8✔
4577
                    commit_2++;
8✔
4578
                });
8✔
4579
            });
8✔
4580
        }
8✔
4581

4582
        util::EventLoop::main().run_until([&] {
18,048✔
4583
            return commit_1 == 4 && commit_2 == 4;
18,048✔
4584
        });
18,048✔
4585
    }
2✔
4586

4587
    SECTION("No open realms") {
4✔
4588
        // This is just to check that the section above did not leave any realms open
4589
        _impl::RealmCoordinator::assert_no_open_realms();
2✔
4590
    }
2✔
4591
}
4✔
4592

4593
TEST_CASE("Notification logging") {
2✔
4594
    using namespace std::chrono_literals;
2✔
4595
    TestFile config;
2✔
4596
    // util::LogCategory::realm.set_default_level_threshold(util::Logger::Level::all);
4597
    config.schema_version = 1;
2✔
4598
    config.schema = Schema{{"object", {{"value", PropertyType::Int}}}};
2✔
4599

4600
    auto realm = Realm::get_shared_realm(config);
2✔
4601
    auto table = realm->read_group().get_table("class_object");
2✔
4602
    int changed = 0;
2✔
4603
    Results res(realm, table->query("value == 5"));
2✔
4604
    auto token = res.add_notification_callback([&changed](CollectionChangeSet const&) {
24✔
4605
        changed++;
24✔
4606
    });
24✔
4607

4608
    int commit_nr = 0;
2✔
4609
    util::EventLoop::main().run_until([&] {
22✔
4610
        for (int64_t i = 0; i < 10; i++) {
242✔
4611
            realm->begin_transaction();
220✔
4612
            table->create_object().set("value", i);
220✔
4613
            realm->commit_transaction();
220✔
4614
            std::this_thread::sleep_for(2ms);
220✔
4615
        }
220✔
4616
        return ++commit_nr == 10;
22✔
4617
    });
22✔
4618
}
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

© 2026 Coveralls, Inc