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

realm / realm-core / jorgen.edelbo_402

21 Aug 2024 11:10AM UTC coverage: 91.054% (-0.03%) from 91.085%
jorgen.edelbo_402

Pull #7803

Evergreen

jedelbo
Small fix to Table::typed_write

When writing the realm to a new file from a write transaction,
the Table may be COW so that the top ref is changed. So don't
use the ref that is present in the group when the operation starts.
Pull Request #7803: Feature/string compression

103494 of 181580 branches covered (57.0%)

1929 of 1999 new or added lines in 46 files covered. (96.5%)

695 existing lines in 51 files now uncovered.

220142 of 241772 relevant lines covered (91.05%)

7344461.76 hits per line

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

98.07
/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,360✔
77
        return Realm::Internal::get_db(*shared_realm);
7,360✔
78
    }
7,360✔
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()") {
75✔
127
    TestFile config;
75✔
128
    config.schema_version = 1;
75✔
129
    config.schema = Schema{
75✔
130
        {"object", {{"value", PropertyType::Int}}},
75✔
131
    };
75✔
132

133
    SECTION("should return the same instance when caching is enabled") {
75✔
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") {
75✔
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") {
75✔
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") {
75✔
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
// Windows doesn't use fifos
252
#ifndef _WIN32
75✔
253
    SECTION("should be able to set a FIFO fallback path") {
75✔
254
        std::string fallback_dir = util::make_temp_dir() + "/fallback/";
2✔
255
        realm::util::try_make_dir(fallback_dir);
2✔
256
        TestFile config;
2✔
257
        config.fifo_files_fallback_path = fallback_dir;
2✔
258
        config.schema_version = 1;
2✔
259
        config.schema = Schema{
2✔
260
            {"object", {{"value", PropertyType::Int}}},
2✔
261
        };
2✔
262

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

453
        REQUIRE(realm1->schema().size() == 1);
2!
454
        REQUIRE(realm1->schema().find("object") != realm1->schema().end());
2!
455
        REQUIRE(realm2->schema().size() == 1);
2!
456
        REQUIRE(realm2->schema().find("object 2") != realm2->schema().end());
2!
457
        REQUIRE(realm3->schema().size() == 2);
2!
458
        REQUIRE(realm3->schema().find("object") != realm3->schema().end());
2!
459
        REQUIRE(realm3->schema().find("object 2") != realm3->schema().end());
2!
460
        REQUIRE(realm4->schema().size() == 1);
2!
461
        REQUIRE(realm4->schema().find("object") != realm4->schema().end());
2!
462
    }
2✔
463

464
#ifndef _WIN32
75✔
465
    SECTION("should throw when creating the notification pipe fails") {
75✔
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
75✔
482

483
#if !REALM_USE_UV && !TEST_SCHEDULER_UV // uv scheduler does not support background threads
38✔
484
    SECTION("should get different instances on different threads") {
38✔
485
        config.cache = true;
1✔
486
        auto realm1 = Realm::get_shared_realm(config);
1✔
487
        JoiningThread([&] {
1✔
488
            auto realm2 = Realm::get_shared_realm(config);
1✔
489
            REQUIRE(realm1 != realm2);
1!
490
        });
1✔
491
    }
1✔
492
#endif
38✔
493

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

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

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

526
    protected:
75✔
527
        size_t m_id;
75✔
528
    };
75✔
529

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

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

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

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

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

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

578
        REQUIRE(frozen != realm);
2!
579
        REQUIRE(realm->read_transaction_version() == frozen->read_transaction_version());
2!
580

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

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

592
        REQUIRE(frozen1 != realm);
2!
593
        REQUIRE(realm->read_transaction_version() == frozen1->read_transaction_version());
2!
594

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

600
        REQUIRE(realm->read_transaction_version() > frozen1->read_transaction_version());
2!
601

602
        auto frozen2 = realm->freeze();
2✔
603
        frozen2->read_group();
2✔
604

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

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

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

621
        config.schema = full_schema;
2✔
622

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

626
        config.schema = subset_schema;
2✔
627

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

944
    std::mutex mutex;
48✔
945

946
    SECTION("can open synced Realms that don't already exist") {
48✔
947
        auto realm = successfully_async_open_realm(config);
2✔
948
        REQUIRE(realm->read_group().get_table("class_object"));
2!
949
    }
2✔
950

951
    SECTION("can write a realm file without client file id") {
48✔
952
        ThreadSafeReference realm_ref;
2✔
953
        SyncTestFile config3(tsm, "default");
2✔
954
        config3.schema = config.schema;
2✔
955
        uint64_t client_file_id;
2✔
956

957
        // Create some content
958
        auto origin = Realm::get_shared_realm(config);
2✔
959
        origin->begin_transaction();
2✔
960
        Class cls = origin->get_class("object");
2✔
961
        cls.create_object(0);
2✔
962
        origin->commit_transaction();
2✔
963
        wait_for_upload(*origin);
2✔
964

965
        // Create realm file without client file id
966
        {
2✔
967
            auto realm = successfully_async_open_realm(config);
2✔
968
            // Write some data
969
            realm->begin_transaction();
2✔
970
            realm->get_class("object").create_object(2);
2✔
971
            realm->commit_transaction();
2✔
972
            wait_for_upload(*realm);
2✔
973
            wait_for_download(*realm);
2✔
974
            client_file_id = realm->read_group().get_sync_file_id();
2✔
975

976
            realm->convert(config3);
2✔
977
        }
2✔
978

979
        // Create some more content on the server
980
        origin->begin_transaction();
2✔
981
        cls.create_object(7);
2✔
982
        origin->commit_transaction();
2✔
983
        wait_for_upload(*origin);
2✔
984

985
        // Now open a realm based on the realm file created above
986
        auto realm = Realm::get_shared_realm(config3);
2✔
987
        Class cls2 = realm->get_class("object");
2✔
988
        wait_for_download(*realm);
2✔
989
        wait_for_upload(*realm);
2✔
990

991
        // Make sure we have got a new client file id
992
        REQUIRE(realm->read_group().get_sync_file_id() != client_file_id);
2!
993
        REQUIRE(cls.num_objects() == 3);
2!
994

995
        // Check that we can continue committing to this realm
996
        realm->begin_transaction();
2✔
997
        cls2.create_object(5);
2✔
998
        realm->commit_transaction();
2✔
999
        wait_for_upload(*realm);
2✔
1000

1001
        // Check that this change is now in the original realm
1002
        wait_for_download(*origin);
2✔
1003
        origin->refresh();
2✔
1004
        REQUIRE(cls.num_objects() == 4);
2!
1005
    }
2✔
1006

1007
    SECTION("downloads Realms which exist on the server") {
48✔
1008
        {
2✔
1009
            auto realm = Realm::get_shared_realm(config2);
2✔
1010
            realm->begin_transaction();
2✔
1011
            realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1012
            realm->commit_transaction();
2✔
1013
            wait_for_upload(*realm);
2✔
1014
        }
2✔
1015

1016
        auto realm = successfully_async_open_realm(config);
2✔
1017
        REQUIRE(realm->read_group().get_table("class_object"));
2!
1018
    }
2✔
1019

1020
    SECTION("progress notifiers of a task are cancelled if the task is cancelled") {
48✔
1021
        bool progress_notifier1_called = false;
2✔
1022
        bool task1_completed = false;
2✔
1023
        bool progress_notifier2_called = false;
2✔
1024
        bool task2_completed = false;
2✔
1025
        {
2✔
1026
            auto realm = Realm::get_shared_realm(config2);
2✔
1027
            realm->begin_transaction();
2✔
1028
            realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1029
            realm->commit_transaction();
2✔
1030
            wait_for_upload(*realm);
2✔
1031
        }
2✔
1032

1033
        DBOptions options;
2✔
1034
        options.encryption_key = config.encryption_key.data();
2✔
1035
        auto db = DB::create(sync::make_client_replication(), config.path, options);
2✔
1036
        auto write = db->start_write(); // block sync from writing until we cancel
2✔
1037

1038
        std::shared_ptr<AsyncOpenTask> task = Realm::get_synchronized_realm(config);
2✔
1039
        std::shared_ptr<AsyncOpenTask> task2 = Realm::get_synchronized_realm(config);
2✔
1040
        REQUIRE(task);
2!
1041
        REQUIRE(task2);
2!
1042
        task->register_download_progress_notifier([&](uint64_t, uint64_t, double) {
2✔
1043
            std::lock_guard<std::mutex> guard(mutex);
×
1044
            REQUIRE(!task1_completed);
×
1045
            progress_notifier1_called = true;
×
1046
        });
×
1047
        task2->register_download_progress_notifier([&](uint64_t, uint64_t, double) {
2✔
1048
            std::lock_guard<std::mutex> guard(mutex);
2✔
1049
            REQUIRE(!task2_completed);
2!
1050
            progress_notifier2_called = true;
2✔
1051
        });
2✔
1052
        task->start([&](ThreadSafeReference realm_ref, std::exception_ptr err) {
2✔
1053
            std::lock_guard<std::mutex> guard(mutex);
×
1054
            REQUIRE(!err);
×
1055
            REQUIRE(realm_ref);
×
1056
            task1_completed = true;
×
1057
        });
×
1058
        task->cancel();
2✔
1059
        ThreadSafeReference rref;
2✔
1060
        task2->start([&](ThreadSafeReference realm_ref, std::exception_ptr err) {
2✔
1061
            std::lock_guard<std::mutex> guard(mutex);
2✔
1062
            REQUIRE(!err);
2!
1063
            REQUIRE(realm_ref);
2!
1064
            rref = std::move(realm_ref);
2✔
1065
            task2_completed = true;
2✔
1066
        });
2✔
1067
        write = nullptr; // unblock sync
2✔
1068
        util::EventLoop::main().run_until([&] {
498,351✔
1069
            std::lock_guard<std::mutex> guard(mutex);
498,351✔
1070
            return task2_completed;
498,351✔
1071
        });
498,351✔
1072
        std::lock_guard<std::mutex> guard(mutex);
2✔
1073
        REQUIRE(!progress_notifier1_called);
2!
1074
        REQUIRE(!task1_completed);
2!
1075
        REQUIRE(progress_notifier2_called);
2!
1076
        REQUIRE(task2_completed);
2!
1077
        SharedRealm realm = Realm::get_shared_realm(std::move(rref));
2✔
1078
        REQUIRE(realm);
2!
1079
    }
2✔
1080

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

1084
        {
2✔
1085
            auto realm = Realm::get_shared_realm(config2);
2✔
1086
            realm->begin_transaction();
2✔
1087
            realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1088
            realm->commit_transaction();
2✔
1089
            wait_for_upload(*realm);
2✔
1090
        }
2✔
1091

1092
        auto realm = successfully_async_open_realm(config);
2✔
1093
        REQUIRE(realm->read_group().get_table("class_object")->size() == 1);
2!
1094
    }
2✔
1095

1096
    SECTION("can download multiple Realms at a time") {
48✔
1097
        SyncTestFile config1(tsm, "realm1");
2✔
1098
        SyncTestFile config2(tsm, "realm2");
2✔
1099
        SyncTestFile config3(tsm, "realm3");
2✔
1100
        SyncTestFile config4(tsm, "realm4");
2✔
1101

1102
        std::vector<std::shared_ptr<AsyncOpenTask>> tasks = {
2✔
1103
            Realm::get_synchronized_realm(config1),
2✔
1104
            Realm::get_synchronized_realm(config2),
2✔
1105
            Realm::get_synchronized_realm(config3),
2✔
1106
            Realm::get_synchronized_realm(config4),
2✔
1107
        };
2✔
1108

1109
        std::atomic<int> completed{0};
2✔
1110
        for (auto& task : tasks) {
8✔
1111
            task->start([&](auto, auto) {
8✔
1112
                ++completed;
8✔
1113
            });
8✔
1114
        }
8✔
1115
        util::EventLoop::main().run_until([&] {
107,963✔
1116
            return completed == 4;
107,963✔
1117
        });
107,963✔
1118
    }
2✔
1119

1120
    auto expired_token = encode_fake_jwt("", 123, 456);
48✔
1121

1122
    SECTION("can async open while waiting for a token refresh") {
48✔
1123
        struct User : TestUser {
2✔
1124
            using TestUser::TestUser;
2✔
1125
            CompletionHandler stored_completion;
2✔
1126
            void request_access_token(CompletionHandler&& completion) override
2✔
1127
            {
2✔
1128
                stored_completion = std::move(completion);
2✔
1129
            }
2✔
1130
            bool access_token_refresh_required() const override
2✔
1131
            {
2✔
1132
                return !stored_completion;
2✔
1133
            }
2✔
1134
        };
2✔
1135
        auto user = std::make_shared<User>("realm", tsm.sync_manager());
2✔
1136
        SyncTestFile config(user, "realm");
2✔
1137
        auto valid_token = user->access_token();
2✔
1138
        user->m_access_token = expired_token;
2✔
1139

1140
        REQUIRE_FALSE(user->stored_completion);
2!
1141
        std::atomic<bool> called{false};
2✔
1142
        auto task = Realm::get_synchronized_realm(config);
2✔
1143
        task->start([&](auto ref, auto error) {
2✔
1144
            std::lock_guard<std::mutex> lock(mutex);
2✔
1145
            REQUIRE(ref);
2!
1146
            REQUIRE(!error);
2!
1147
            called = true;
2✔
1148
        });
2✔
1149
        REQUIRE(user->stored_completion);
2!
1150
        user->m_access_token = valid_token;
2✔
1151
        user->stored_completion({});
2✔
1152
        user->stored_completion = {};
2✔
1153

1154
        util::EventLoop::main().run_until([&] {
186,707✔
1155
            return called.load();
186,707✔
1156
        });
186,707✔
1157
        std::lock_guard<std::mutex> lock(mutex);
2✔
1158
        REQUIRE(called);
2!
1159
    }
2✔
1160

1161
    SECTION("cancels download and reports an error on auth error") {
48✔
1162
        struct User : TestUser {
2✔
1163
            using TestUser::TestUser;
2✔
1164
            void request_access_token(CompletionHandler&& completion) override
2✔
1165
            {
2✔
1166
                completion(app::AppError(ErrorCodes::HTTPError, "403 error", "", 403));
2✔
1167
            }
2✔
1168
            bool access_token_refresh_required() const override
2✔
1169
            {
2✔
1170
                return true;
2✔
1171
            }
2✔
1172
        };
2✔
1173
        auto user = std::make_shared<User>("realm", tsm.sync_manager());
2✔
1174
        user->m_access_token = expired_token;
2✔
1175
        user->m_refresh_token = expired_token;
2✔
1176
        SyncTestFile config(user, "realm");
2✔
1177

1178
        bool got_error = false;
2✔
1179
        config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError) {
2✔
1180
            got_error = true;
2✔
1181
        };
2✔
1182
        std::atomic<bool> called{false};
2✔
1183
        auto task = Realm::get_synchronized_realm(config);
2✔
1184
        task->start([&](auto ref, auto error) {
2✔
1185
            std::lock_guard<std::mutex> lock(mutex);
2✔
1186
            REQUIRE(error);
2!
1187
            REQUIRE_EXCEPTION(std::rethrow_exception(error), HTTPError,
2✔
1188
                              "Unable to refresh the user access token: 403 error. Client Error: 403");
2✔
1189
            REQUIRE(!ref);
2!
1190
            called = true;
2✔
1191
        });
2✔
1192
        util::EventLoop::main().run_until([&] {
3✔
1193
            return called.load();
3✔
1194
        });
3✔
1195
        std::lock_guard<std::mutex> lock(mutex);
2✔
1196
        REQUIRE(called);
2!
1197
        REQUIRE(got_error);
2!
1198
    }
2✔
1199

1200
#if REALM_APP_SERVICES
48✔
1201

1202
    SECTION("waiters are cancelled if cancel_waits_on_nonfatal_error") {
48✔
1203
        auto logger = util::Logger::get_default_logger();
18✔
1204
        auto transport = std::make_shared<HookedTransport<UnitTestTransport>>();
18✔
1205
        auto socket_provider = std::make_shared<HookedSocketProvider>(logger, "some user agent");
18✔
1206
        enum TestMode { expired_at_start, expired_by_websocket, websocket_fails };
18✔
1207
        enum FailureMode { location_fails, token_fails, token_not_authorized };
18✔
1208
        auto txt_test_mode = [](TestMode mode) {
18✔
1209
            switch (mode) {
18✔
1210
                case TestMode::expired_at_start:
6✔
1211
                    return "access token expired when realm is opened";
6✔
1212
                case TestMode::expired_by_websocket:
6✔
1213
                    return "access token expired by websocket";
6✔
1214
                case TestMode::websocket_fails:
6✔
1215
                    return "websocket returns connection failed";
6✔
1216
                default:
✔
1217
                    return "Unknown TestMode";
×
1218
            }
18✔
1219
        };
18✔
1220
        auto txt_failure_mode = [](FailureMode mode) {
18✔
1221
            switch (mode) {
18✔
1222
                case FailureMode::location_fails:
6✔
1223
                    return "location update fails";
6✔
1224
                case FailureMode::token_fails:
6✔
1225
                    return "access token refresh fails";
6✔
1226
                case FailureMode::token_not_authorized:
6✔
1227
                    return "websocket connect not authorized";
6✔
1228
                default:
✔
1229
                    return "Unknown FailureMode";
×
1230
            }
18✔
1231
        };
18✔
1232

1233
        app::AppConfig app_config;
18✔
1234
        set_app_config_defaults(app_config, transport);
18✔
1235
        app_config.sync_client_config.socket_provider = socket_provider;
18✔
1236
        app_config.base_file_path = util::make_temp_dir();
18✔
1237
        app_config.metadata_mode = app::AppConfig::MetadataMode::NoEncryption;
18✔
1238

1239
        auto app = app::App::get_app(app::App::CacheMode::Disabled, app_config);
18✔
1240
        create_user_and_log_in(app);
18✔
1241
        auto user = app->current_user();
18✔
1242
        // User should be logged in at this point
1243
        REQUIRE(user->is_logged_in());
18!
1244

1245
        bool not_authorized = false;
18✔
1246
        bool token_refresh_called = false;
18✔
1247
        bool location_refresh_called = false;
18✔
1248

1249
        TestMode test_mode = GENERATE(expired_at_start, expired_by_websocket, websocket_fails);
18✔
1250
        FailureMode failure = GENERATE(location_fails, token_fails, token_not_authorized);
18✔
1251

1252
        logger->info("TEST: %1 - %2", txt_test_mode(test_mode), txt_failure_mode(failure));
18✔
1253
        if (test_mode == TestMode::expired_at_start) {
18✔
1254
            // invalidate the user's cached access token
1255
            auto app_user = app->current_user();
6✔
1256
            app_user->update_data_for_testing([&](app::UserData& data) {
6✔
1257
                data.access_token = RealmJWT(expired_token);
6✔
1258
            });
6✔
1259
        }
6✔
1260
        else if (test_mode == TestMode::expired_by_websocket) {
12✔
1261
            // tell websocket to return not authorized to refresh access token
1262
            not_authorized = true;
6✔
1263
        }
6✔
1264

1265
        app.reset();
18✔
1266

1267
        auto err_handler = [](std::shared_ptr<SyncSession> session, SyncError error) {
18✔
1268
            auto logger = util::Logger::get_default_logger();
6✔
1269
            logger->debug("The sync error handler caught an error: '%1' for '%2'", error.status, session->path());
6✔
1270
            // Ignore connection failed non-fatal errors and check for access token refresh unauthorized fatal errors
1271
            if (error.status.code() == ErrorCodes::SyncConnectFailed) {
6✔
1272
                REQUIRE_FALSE(error.is_fatal);
×
1273
                return;
×
1274
            }
×
1275
            // If it's not SyncConnectFailed, then it should be AuthError
1276
            REQUIRE(error.status.code() == ErrorCodes::AuthError);
6!
1277
            REQUIRE(error.is_fatal);
6!
1278
        };
6✔
1279

1280
        transport->request_hook = [&](const app::Request& req) -> std::optional<app::Response> {
30✔
1281
            static constexpr int CURLE_OPERATION_TIMEDOUT = 28;
30✔
1282
            std::lock_guard<std::mutex> lock(mutex);
30✔
1283
            if (req.url.find("/auth/session") != std::string::npos) {
30✔
1284
                token_refresh_called = true;
12✔
1285
                if (failure == FailureMode::token_not_authorized) {
12✔
1286
                    return app::Response{403, 0, {}, "403 not authorized"};
6✔
1287
                }
6✔
1288
                if (failure == FailureMode::token_fails) {
6✔
1289
                    return app::Response{0, CURLE_OPERATION_TIMEDOUT, {}, "Operation timed out"};
6✔
1290
                }
6✔
1291
            }
6✔
1292
            else if (req.url.find("/location") != std::string::npos) {
18✔
1293
                location_refresh_called = true;
18✔
1294
                if (failure == FailureMode::location_fails) {
18✔
1295
                    // Fake "offline/request timed out" custom error response
1296
                    return app::Response{0, CURLE_OPERATION_TIMEDOUT, {}, "Operation timed out"};
6✔
1297
                }
6✔
1298
            }
18✔
1299
            return std::nullopt;
12✔
1300
        };
30✔
1301

1302
        socket_provider->websocket_connect_func = [&]() -> std::optional<SocketProviderError> {
18✔
1303
            if (not_authorized) {
18✔
1304
                not_authorized = false; // one shot
6✔
1305
                return SocketProviderError(sync::websocket::WebSocketError::websocket_unauthorized,
6✔
1306
                                           "403 not authorized");
6✔
1307
            }
6✔
1308
            return SocketProviderError(sync::websocket::WebSocketError::websocket_connection_failed,
12✔
1309
                                       "Operation timed out");
12✔
1310
        };
18✔
1311

1312
        app = app::App::get_app(app::App::CacheMode::Disabled, app_config);
18✔
1313
        SyncTestFile config(app->current_user(), "realm");
18✔
1314
        config.sync_config->cancel_waits_on_nonfatal_error = true;
18✔
1315
        config.sync_config->error_handler = err_handler;
18✔
1316

1317
        // User should be logged in at this point
1318
        REQUIRE(config.sync_config->user->is_logged_in());
18!
1319

1320
        auto status = async_open_realm(config);
18✔
1321
        REQUIRE_FALSE(status.is_ok());
18!
1322

1323
        {
18✔
1324
            std::lock_guard lock(mutex);
18✔
1325
            REQUIRE(location_refresh_called);
18!
1326
            if (failure != FailureMode::location_fails) {
18✔
1327
                REQUIRE(token_refresh_called);
12!
1328
            }
12✔
1329
        }
18✔
1330

1331
        app->sync_manager()->tear_down_for_testing();
18✔
1332
    }
18✔
1333

1334
#endif // REALM_APP_SERVICES
48✔
1335

1336
    SECTION("read-only mode sets the schema version") {
48✔
1337
        {
2✔
1338
            SharedRealm realm = Realm::get_shared_realm(config);
2✔
1339
            wait_for_upload(*realm);
2✔
1340
            realm->close();
2✔
1341
        }
2✔
1342

1343
        config2.schema_mode = SchemaMode::ReadOnly;
2✔
1344
        auto realm = successfully_async_open_realm(config2);
2✔
1345
        REQUIRE(realm->schema_version() == 1);
2!
1346
    }
2✔
1347

1348
    Schema with_added_object = Schema{object_schema,
48✔
1349
                                      {"added",
48✔
1350
                                       {
48✔
1351
                                           {"_id", PropertyType::Int, Property::IsPrimary{true}},
48✔
1352
                                       }}};
48✔
1353

1354
    SECTION("read-only mode applies remote schema changes") {
48✔
1355
        // Create the local file without "added"
1356
        Realm::get_shared_realm(config2);
2✔
1357

1358
        // Add the table server-side
1359
        config.schema = with_added_object;
2✔
1360
        config2.schema = with_added_object;
2✔
1361
        {
2✔
1362
            SharedRealm realm = Realm::get_shared_realm(config);
2✔
1363
            wait_for_upload(*realm);
2✔
1364
            realm->close();
2✔
1365
        }
2✔
1366

1367
        // Verify that the table gets added when reopening
1368
        config2.schema_mode = SchemaMode::ReadOnly;
2✔
1369
        auto realm = successfully_async_open_realm(config2);
2✔
1370
        REQUIRE(realm->schema().find("added") != realm->schema().end());
2!
1371
        REQUIRE(realm->read_group().get_table("class_added"));
2!
1372
    }
2✔
1373

1374
    SECTION("read-only mode does not create tables not present on the server") {
48✔
1375
        // Create the local file without "added"
1376
        Realm::get_shared_realm(config2);
2✔
1377

1378
        config2.schema = with_added_object;
2✔
1379
        config2.schema_mode = SchemaMode::ReadOnly;
2✔
1380
        auto realm = successfully_async_open_realm(config2);
2✔
1381
        REQUIRE(realm->schema().find("added") != realm->schema().end());
2!
1382
        REQUIRE_FALSE(realm->read_group().get_table("class_added"));
2!
1383
    }
2✔
1384

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

1389
        config.schema_mode = SchemaMode::ReadOnly;
2✔
1390
        config.schema = Schema{{"object",
2✔
1391
                                {
2✔
1392
                                    {"_id", PropertyType::Int, Property::IsPrimary{true}},
2✔
1393
                                    {"value", PropertyType::Int},
2✔
1394
                                    {"value2", PropertyType::Int},
2✔
1395
                                }}};
2✔
1396

1397
        auto status = async_open_realm(config);
2✔
1398
        REQUIRE_FALSE(status.is_ok());
2!
1399
        REQUIRE_THAT(status.get_status().reason(),
2✔
1400
                     Catch::Matchers::ContainsSubstring("Property 'object.value2' has been added."));
2✔
1401
    }
2✔
1402

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

1406
        config.schema_mode = SchemaMode::ReadOnly;
2✔
1407
        config.schema = Schema{{"object",
2✔
1408
                                {
2✔
1409
                                    {"_id", PropertyType::Int, Property::IsPrimary{true}},
2✔
1410
                                    {"value", PropertyType::Int},
2✔
1411
                                    {"value2", PropertyType::Int},
2✔
1412
                                }}};
2✔
1413
        REQUIRE_THROWS_CONTAINING(Realm::get_shared_realm(config), "Property 'object.value2' has been added.");
2✔
1414

1415
        auto status = async_open_realm(config);
2✔
1416
        REQUIRE_FALSE(status.is_ok());
2!
1417
        REQUIRE_THAT(status.get_status().reason(),
2✔
1418
                     Catch::Matchers::ContainsSubstring("Property 'object.value2' has been added."));
2✔
1419
    }
2✔
1420

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

1425
        // Remove the "value" property from the schema
1426
        config.schema_mode = SchemaMode::ReadOnly;
2✔
1427
        config.schema = Schema{{"object",
2✔
1428
                                {
2✔
1429
                                    {"_id", PropertyType::Int, Property::IsPrimary{true}},
2✔
1430
                                }}};
2✔
1431

1432
        auto realm = successfully_async_open_realm(config);
2✔
1433
        REQUIRE(realm->read_group().get_table("class_object")->get_column_key("value") != ColKey{});
2!
1434
    }
2✔
1435

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

1439
        // Remove the "value" property from the schema
1440
        config.schema_mode = SchemaMode::ReadOnly;
2✔
1441
        config.schema = Schema{{"object",
2✔
1442
                                {
2✔
1443
                                    {"_id", PropertyType::Int, Property::IsPrimary{true}},
2✔
1444
                                }}};
2✔
1445

1446
        auto realm = successfully_async_open_realm(config);
2✔
1447
        REQUIRE(realm->read_group().get_table("class_object")->get_column_key("value") != ColKey{});
2!
1448
    }
2✔
1449

1450
    _impl::RealmCoordinator::assert_no_open_realms();
48✔
1451
}
48✔
1452

1453
#if REALM_ENABLE_AUTH_TESTS
1454

1455
TEST_CASE("Synchronized realm: AutoOpen", "[sync][baas][pbs][async open]") {
2✔
1456
    const auto partition = random_string(100);
2✔
1457
    auto schema = get_default_schema();
2✔
1458
    enum TestMode { expired_at_start, expired_by_websocket, websocket_fails };
2✔
1459
    enum FailureMode { location_fails, token_fails, token_not_authorized };
2✔
1460

1461
    auto logger = util::Logger::get_default_logger();
2✔
1462
    auto transport = std::make_shared<HookedTransport<>>();
2✔
1463
    auto socket_provider = std::make_shared<HookedSocketProvider>(logger, "some user agent");
2✔
1464
    std::mutex mutex;
2✔
1465

1466
    // Create the app session and get the logged in user identity
1467
    auto server_app_config = minimal_app_config("autoopen-realm", schema);
2✔
1468
    TestAppSession session(create_app(server_app_config), transport, DeleteApp{true}, realm::ReconnectMode::normal,
2✔
1469
                           socket_provider);
2✔
1470
    auto user = session.app()->current_user();
2✔
1471
    std::string identity = user->user_id();
2✔
1472
    REQUIRE(user->is_logged_in());
2!
1473
    REQUIRE(!identity.empty());
2!
1474
    // Reopen the App instance and retrieve the cached user
1475
    session.reopen(false);
2✔
1476
    user = session.app()->get_existing_logged_in_user(identity);
2✔
1477

1478
    SyncTestFile config(user, partition, schema);
2✔
1479
    config.sync_config->cancel_waits_on_nonfatal_error = true;
2✔
1480
    config.sync_config->error_handler = [&logger](std::shared_ptr<SyncSession> session, SyncError error) {
2✔
1481
        logger->debug("The sync error handler caught an error: '%1' for '%2'", error.status, session->path());
×
1482
        // Ignore connection failed non-fatal errors and check for access token refresh unauthorized fatal errors
1483
        if (error.status.code() == ErrorCodes::SyncConnectFailed) {
×
1484
            REQUIRE_FALSE(error.is_fatal);
×
1485
            return;
×
1486
        }
×
1487
        // If it's not SyncConnectFailed, then it should be AuthError
1488
        REQUIRE(error.status.code() == ErrorCodes::AuthError);
×
1489
        REQUIRE(error.is_fatal);
×
1490
    };
×
1491

1492
    bool not_authorized = false;
2✔
1493
    bool token_refresh_called = false;
2✔
1494
    bool location_refresh_called = false;
2✔
1495

1496
    FailureMode failure = FailureMode::location_fails;
2✔
1497

1498
    transport->request_hook = [&](const app::Request& req) -> std::optional<app::Response> {
2✔
1499
        static constexpr int CURLE_OPERATION_TIMEDOUT = 28;
2✔
1500
        std::lock_guard<std::mutex> lock(mutex);
2✔
1501
        if (req.url.find("/auth/session") != std::string::npos) {
2✔
1502
            token_refresh_called = true;
×
1503
            if (failure == FailureMode::token_not_authorized) {
×
1504
                return app::Response{403, 0, {}, "403 not authorized"};
×
1505
            }
×
1506
            if (failure == FailureMode::token_fails) {
×
1507
                return app::Response{0, CURLE_OPERATION_TIMEDOUT, {}, "Operation timed out"};
×
1508
            }
×
1509
        }
×
1510
        else if (req.url.find("/location") != std::string::npos) {
2✔
1511
            location_refresh_called = true;
2✔
1512
            if (failure == FailureMode::location_fails) {
2✔
1513
                // Fake "offline/request timed out" custom error response
1514
                return app::Response{0, CURLE_OPERATION_TIMEDOUT, {}, "Operation timed out"};
2✔
1515
            }
2✔
1516
        }
2✔
1517
        return std::nullopt;
×
1518
    };
2✔
1519

1520
    socket_provider->websocket_connect_func = [&]() -> std::optional<SocketProviderError> {
2✔
1521
        if (not_authorized) {
2✔
1522
            not_authorized = false; // one shot
×
1523
            return SocketProviderError(sync::websocket::WebSocketError::websocket_unauthorized, "403 not authorized");
×
1524
        }
×
1525
        return SocketProviderError(sync::websocket::WebSocketError::websocket_connection_failed,
2✔
1526
                                   "Operation timed out");
2✔
1527
    };
2✔
1528

1529
    auto task = Realm::get_synchronized_realm(config);
2✔
1530
    auto pf = util::make_promise_future<std::exception_ptr>();
2✔
1531
    task->start([&pf](auto ref, auto error) mutable {
2✔
1532
        REQUIRE(!ref);
2!
1533
        REQUIRE(error);
2!
1534
        pf.promise.emplace_value(error);
2✔
1535
    });
2✔
1536

1537
    auto result = pf.future.get_no_throw();
2✔
1538
    REQUIRE(result.is_ok());
2!
1539
    REQUIRE(result.get_value());
2!
1540
    {
2✔
1541
        std::lock_guard<std::mutex> lock(mutex);
2✔
1542
        REQUIRE(location_refresh_called);
2!
1543
        if (failure != FailureMode::location_fails) {
2✔
1544
            REQUIRE(token_refresh_called);
×
1545
        }
×
1546
    }
2✔
1547

1548
    transport->request_hook = nullptr;
2✔
1549
    socket_provider->websocket_connect_func = nullptr;
2✔
1550
    auto r = Realm::get_shared_realm(config);
2✔
1551
    wait_for_download(*r);
2✔
1552
}
2✔
1553

1554
#endif // REALM_ENABLE_AUTH_TESTS
1555

1556
TEST_CASE("SharedRealm: convert", "[sync][pbs][convert]") {
8✔
1557
    TestSyncManager tsm;
8✔
1558
    ObjectSchema object_schema = {"object",
8✔
1559
                                  {
8✔
1560
                                      {"_id", PropertyType::Int, Property::IsPrimary{true}},
8✔
1561
                                      {"value", PropertyType::Int},
8✔
1562
                                  }};
8✔
1563
    Schema schema{object_schema};
8✔
1564

1565
    SyncTestFile sync_config1(tsm, "default");
8✔
1566
    sync_config1.schema = schema;
8✔
1567
    TestFile local_config1;
8✔
1568
    local_config1.schema = schema;
8✔
1569
    local_config1.schema_version = sync_config1.schema_version;
8✔
1570

1571
    SECTION("can copy a synced realm to a synced realm") {
8✔
1572
        auto sync_realm1 = Realm::get_shared_realm(sync_config1);
2✔
1573
        sync_realm1->begin_transaction();
2✔
1574
        sync_realm1->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1575
        sync_realm1->commit_transaction();
2✔
1576
        wait_for_upload(*sync_realm1);
2✔
1577
        wait_for_download(*sync_realm1);
2✔
1578

1579
        // Copy to a new sync config
1580
        SyncTestFile sync_config2(tsm, "default");
2✔
1581
        sync_config2.schema = schema;
2✔
1582

1583
        sync_realm1->convert(sync_config2);
2✔
1584

1585
        auto sync_realm2 = Realm::get_shared_realm(sync_config2);
2✔
1586

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

1590
        // Verify that sync works and objects created in the new copy will get
1591
        // synchronized to the old copy
1592
        sync_realm2->begin_transaction();
2✔
1593
        sync_realm2->read_group().get_table("class_object")->create_object_with_primary_key(1);
2✔
1594
        sync_realm2->commit_transaction();
2✔
1595
        wait_for_upload(*sync_realm2);
2✔
1596
        wait_for_download(*sync_realm1);
2✔
1597

1598
        sync_realm1->refresh();
2✔
1599
        REQUIRE(sync_realm1->read_group().get_table("class_object")->size() == 2);
2!
1600
    }
2✔
1601

1602
    SECTION("can convert a synced realm to a local realm") {
8✔
1603
        auto sync_realm = Realm::get_shared_realm(sync_config1);
2✔
1604
        sync_realm->begin_transaction();
2✔
1605
        sync_realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1606
        sync_realm->commit_transaction();
2✔
1607
        wait_for_upload(*sync_realm);
2✔
1608
        wait_for_download(*sync_realm);
2✔
1609

1610
        sync_realm->convert(local_config1);
2✔
1611

1612
        auto local_realm = Realm::get_shared_realm(local_config1);
2✔
1613

1614
        // Check that the data also exists in the new realm
1615
        REQUIRE(local_realm->read_group().get_table("class_object")->size() == 1);
2!
1616
    }
2✔
1617

1618
    SECTION("can convert a local realm to a synced realm") {
8✔
1619
        auto local_realm = Realm::get_shared_realm(local_config1);
2✔
1620
        local_realm->begin_transaction();
2✔
1621
        local_realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1622
        local_realm->commit_transaction();
2✔
1623

1624
        // Copy to a new sync config
1625
        local_realm->convert(sync_config1);
2✔
1626

1627
        auto sync_realm = Realm::get_shared_realm(sync_config1);
2✔
1628

1629
        // Check that the data also exists in the new realm
1630
        REQUIRE(sync_realm->read_group().get_table("class_object")->size() == 1);
2!
1631
    }
2✔
1632

1633
    SECTION("can copy a local realm to a local realm") {
8✔
1634
        auto local_realm1 = Realm::get_shared_realm(local_config1);
2✔
1635
        local_realm1->begin_transaction();
2✔
1636
        local_realm1->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1637
        local_realm1->commit_transaction();
2✔
1638

1639
        // Copy to a new local config
1640
        TestFile local_config2;
2✔
1641
        local_config2.schema = schema;
2✔
1642
        local_config2.schema_version = local_config1.schema_version;
2✔
1643
        local_realm1->convert(local_config2);
2✔
1644

1645
        auto local_realm2 = Realm::get_shared_realm(local_config2);
2✔
1646

1647
        // Check that the data also exists in the new realm
1648
        REQUIRE(local_realm2->read_group().get_table("class_object")->size() == 1);
2!
1649
    }
2✔
1650
}
8✔
1651

1652
TEST_CASE("SharedRealm: convert - embedded objects", "[sync][pbs][convert][embedded objects]") {
16✔
1653
    TestSyncManager tsm;
16✔
1654
    ObjectSchema object_schema = {"object",
16✔
1655
                                  {
16✔
1656
                                      {"_id", PropertyType::Int, Property::IsPrimary{true}},
16✔
1657
                                      {"value", PropertyType::Int},
16✔
1658
                                      {"embedded_link", PropertyType::Object | PropertyType::Nullable, "embedded"},
16✔
1659
                                  }};
16✔
1660
    ObjectSchema embedded_schema = {"embedded",
16✔
1661
                                    ObjectSchema::ObjectType::Embedded,
16✔
1662
                                    {
16✔
1663
                                        {"name", PropertyType::String | PropertyType::Nullable},
16✔
1664
                                    }};
16✔
1665
    Schema schema{object_schema, embedded_schema};
16✔
1666

1667
    SyncTestFile sync_config1(tsm, "default");
16✔
1668
    sync_config1.schema = schema;
16✔
1669
    TestFile local_config1;
16✔
1670
    local_config1.schema = schema;
16✔
1671
    local_config1.schema_version = sync_config1.schema_version;
16✔
1672

1673
    SECTION("can copy a synced realm to a synced realm") {
16✔
1674
        auto sync_realm1 = Realm::get_shared_realm(sync_config1);
4✔
1675
        sync_realm1->begin_transaction();
4✔
1676

1677
        SECTION("null embedded object") {
4✔
1678
            sync_realm1->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1679
        }
2✔
1680

1681
        SECTION("embedded object") {
4✔
1682
            auto obj = sync_realm1->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1683
            auto col_key = sync_realm1->read_group().get_table("class_object")->get_column_key("embedded_link");
2✔
1684
            obj.create_and_set_linked_object(col_key);
2✔
1685
        }
2✔
1686

1687
        sync_realm1->commit_transaction();
4✔
1688
        wait_for_upload(*sync_realm1);
4✔
1689
        wait_for_download(*sync_realm1);
4✔
1690

1691
        // Copy to a new sync config
1692
        SyncTestFile sync_config2(tsm, "default");
4✔
1693
        sync_config2.schema = schema;
4✔
1694

1695
        sync_realm1->convert(sync_config2);
4✔
1696

1697
        auto sync_realm2 = Realm::get_shared_realm(sync_config2);
4✔
1698

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

1702
        // Verify that sync works and objects created in the new copy will get
1703
        // synchronized to the old copy
1704
        sync_realm2->begin_transaction();
4✔
1705
        sync_realm2->read_group().get_table("class_object")->create_object_with_primary_key(1);
4✔
1706
        sync_realm2->commit_transaction();
4✔
1707
        wait_for_upload(*sync_realm2);
4✔
1708
        wait_for_download(*sync_realm1);
4✔
1709

1710
        sync_realm1->refresh();
4✔
1711
        REQUIRE(sync_realm1->read_group().get_table("class_object")->size() == 2);
4!
1712
    }
4✔
1713

1714
    SECTION("can convert a synced realm to a local realm") {
16✔
1715
        auto sync_realm = Realm::get_shared_realm(sync_config1);
4✔
1716
        sync_realm->begin_transaction();
4✔
1717

1718
        SECTION("null embedded object") {
4✔
1719
            sync_realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1720
        }
2✔
1721

1722
        SECTION("embedded object") {
4✔
1723
            auto obj = sync_realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1724
            auto col_key = sync_realm->read_group().get_table("class_object")->get_column_key("embedded_link");
2✔
1725
            obj.create_and_set_linked_object(col_key);
2✔
1726
        }
2✔
1727

1728
        sync_realm->commit_transaction();
4✔
1729
        wait_for_upload(*sync_realm);
4✔
1730
        wait_for_download(*sync_realm);
4✔
1731

1732
        sync_realm->convert(local_config1);
4✔
1733

1734
        auto local_realm = Realm::get_shared_realm(local_config1);
4✔
1735

1736
        // Check that the data also exists in the new realm
1737
        REQUIRE(local_realm->read_group().get_table("class_object")->size() == 1);
4!
1738
    }
4✔
1739

1740
    SECTION("can convert a local realm to a synced realm") {
16✔
1741
        auto local_realm = Realm::get_shared_realm(local_config1);
4✔
1742
        local_realm->begin_transaction();
4✔
1743

1744
        SECTION("null embedded object") {
4✔
1745
            local_realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1746
        }
2✔
1747

1748
        SECTION("embedded object") {
4✔
1749
            auto obj = local_realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1750
            auto col_key = local_realm->read_group().get_table("class_object")->get_column_key("embedded_link");
2✔
1751
            obj.create_and_set_linked_object(col_key);
2✔
1752
        }
2✔
1753

1754
        local_realm->commit_transaction();
4✔
1755

1756
        // Copy to a new sync config
1757
        local_realm->convert(sync_config1);
4✔
1758

1759
        auto sync_realm = Realm::get_shared_realm(sync_config1);
4✔
1760

1761
        // Check that the data also exists in the new realm
1762
        REQUIRE(sync_realm->read_group().get_table("class_object")->size() == 1);
4!
1763
    }
4✔
1764

1765
    SECTION("can copy a local realm to a local realm") {
16✔
1766
        auto local_realm1 = Realm::get_shared_realm(local_config1);
4✔
1767
        local_realm1->begin_transaction();
4✔
1768

1769
        SECTION("null embedded object") {
4✔
1770
            local_realm1->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1771
        }
2✔
1772

1773
        SECTION("embedded object") {
4✔
1774
            auto obj = local_realm1->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1775
            auto col_key = local_realm1->read_group().get_table("class_object")->get_column_key("embedded_link");
2✔
1776
            obj.create_and_set_linked_object(col_key);
2✔
1777
        }
2✔
1778

1779
        local_realm1->commit_transaction();
4✔
1780

1781
        // Copy to a new local config
1782
        TestFile local_config2;
4✔
1783
        local_config2.schema = schema;
4✔
1784
        local_config2.schema_version = local_config1.schema_version;
4✔
1785
        local_realm1->convert(local_config2);
4✔
1786

1787
        auto local_realm2 = Realm::get_shared_realm(local_config2);
4✔
1788

1789
        // Check that the data also exists in the new realm
1790
        REQUIRE(local_realm2->read_group().get_table("class_object")->size() == 1);
4!
1791
    }
4✔
1792
}
16✔
1793
#endif // REALM_ENABLE_SYNC
1794

1795
TEST_CASE("SharedRealm: async writes") {
104✔
1796
    _impl::RealmCoordinator::assert_no_open_realms();
104✔
1797
    if (!util::EventLoop::has_implementation())
104✔
UNCOV
1798
        return;
×
1799

1800
    TestFile config;
104✔
1801
    config.schema_version = 0;
104✔
1802
    config.schema = Schema{
104✔
1803
        {"object",
104✔
1804
         {
104✔
1805
             {"value", PropertyType::Int},
104✔
1806
             {"ints", PropertyType::Array | PropertyType::Int},
104✔
1807
             {"int set", PropertyType::Set | PropertyType::Int},
104✔
1808
             {"int dictionary", PropertyType::Dictionary | PropertyType::Int},
104✔
1809
         }},
104✔
1810
    };
104✔
1811
    bool done = false;
104✔
1812
    auto realm = Realm::get_shared_realm(config);
104✔
1813
    auto table = realm->read_group().get_table("class_object");
104✔
1814
    auto col = table->get_column_key("value");
104✔
1815
    int write_nr = 0;
104✔
1816
    int commit_nr = 0;
104✔
1817

1818
    auto wait_for_done = [&]() {
104✔
1819
        util::EventLoop::main().run_until([&] {
295,443✔
1820
            return done;
295,443✔
1821
        });
295,443✔
1822
        REQUIRE(done);
96!
1823
    };
96✔
1824

1825
    SECTION("async commit transaction") {
104✔
1826
        realm->async_begin_transaction([&]() {
2✔
1827
            REQUIRE(write_nr == 0);
2!
1828
            ++write_nr;
2✔
1829
            table->create_object().set(col, 45);
2✔
1830
            realm->async_commit_transaction([&](std::exception_ptr) {
2✔
1831
                REQUIRE(commit_nr == 0);
2!
1832
                ++commit_nr;
2✔
1833
            });
2✔
1834
        });
2✔
1835
        for (int expected = 1; expected < 1000; ++expected) {
2,000✔
1836
            realm->async_begin_transaction([&, expected]() {
1,998✔
1837
                REQUIRE(write_nr == expected);
1,998!
1838
                ++write_nr;
1,998✔
1839
                auto o = table->get_object(0);
1,998✔
1840
                o.set(col, o.get<int64_t>(col) + 37);
1,998✔
1841
                realm->async_commit_transaction(
1,998✔
1842
                    [&](auto) {
1,998✔
1843
                        ++commit_nr;
1,998✔
1844
                        done = commit_nr == 1000;
1,998✔
1845
                    },
1,998✔
1846
                    true);
1,998✔
1847
            });
1,998✔
1848
        }
1,998✔
1849
        wait_for_done();
2✔
1850
    }
2✔
1851

1852
    auto verify_persisted_count = [&](size_t expected) {
104✔
1853
        if (realm)
52✔
1854
            realm->close();
50✔
1855
        _impl::RealmCoordinator::assert_no_open_realms();
52✔
1856

1857
        auto new_realm = Realm::get_shared_realm(config);
52✔
1858
        auto table = new_realm->read_group().get_table("class_object");
52✔
1859
        REQUIRE(table->size() == expected);
52!
1860
    };
52✔
1861

1862
    using RealmCloseFunction = void (Realm::*)();
104✔
1863
    static RealmCloseFunction close_functions[] = {&Realm::close, &Realm::invalidate};
104✔
1864
    static const char* close_function_names[] = {"close()", "invalidate()"};
104✔
1865
    for (int i = 0; i < 2; ++i) {
312✔
1866
        SECTION(close_function_names[i]) {
208✔
1867
            bool persisted = false;
40✔
1868
            SECTION("before write lock is acquired") {
40✔
1869
                DBOptions options;
4✔
1870
                options.encryption_key = config.encryption_key.data();
4✔
1871
                // Acquire the write lock with a different DB instance so that we'll
1872
                // be stuck in the Requesting stage
1873
                realm::test_util::BowlOfStonesSemaphore sema;
4✔
1874
                JoiningThread thread([&] {
4✔
1875
                    auto db = DB::create(make_in_realm_history(), config.path, options);
4✔
1876
                    auto write = db->start_write();
4✔
1877
                    sema.add_stone();
4✔
1878

1879
                    // Wait until the main thread is waiting for the lock.
1880
                    while (!db->other_writers_waiting_for_lock()) {
8✔
1881
                        millisleep(1);
4✔
1882
                    }
4✔
1883
                    write->close();
4✔
1884
                });
4✔
1885

1886
                // Wait for the background thread to have acquired the lock
1887
                sema.get_stone();
4✔
1888

1889
                auto scheduler = realm->scheduler();
4✔
1890
                realm->async_begin_transaction([&] {
4✔
1891
                    // We should never get here as the realm is closed
UNCOV
1892
                    FAIL();
×
UNCOV
1893
                });
×
1894

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

1898
                {
4✔
1899
                    // Verify that we released the write lock
1900
                    auto db = DB::create(make_in_realm_history(), config.path, options);
4✔
1901
                    REQUIRE(db->start_write(/* nonblocking */ true));
4!
1902
                }
4✔
1903

1904
                // Verify that the transaction callback never got enqueued
1905
                scheduler->invoke([&] {
4✔
1906
                    done = true;
4✔
1907
                });
4✔
1908
                wait_for_done();
4✔
1909
            }
4✔
1910
            SECTION("before async_begin_transaction() callback") {
40✔
1911
                auto scheduler = realm->scheduler();
4✔
1912
                realm->async_begin_transaction([&] {
4✔
1913
                    // We should never get here as the realm is closed
UNCOV
1914
                    FAIL();
×
UNCOV
1915
                });
×
1916
                std::invoke(close_functions[i], *realm);
4✔
1917
                scheduler->invoke([&] {
4✔
1918
                    done = true;
4✔
1919
                });
4✔
1920
                wait_for_done();
4✔
1921
                verify_persisted_count(0);
4✔
1922
            }
4✔
1923
            SECTION("inside async_begin_transaction() callback before commit") {
40✔
1924
                realm->async_begin_transaction([&] {
4✔
1925
                    table->create_object().set(col, 45);
4✔
1926
                    std::invoke(close_functions[i], *realm);
4✔
1927
                    done = true;
4✔
1928
                });
4✔
1929
                wait_for_done();
4✔
1930
                verify_persisted_count(0);
4✔
1931
            }
4✔
1932
            SECTION("inside async_begin_transaction() callback after sync commit") {
40✔
1933
                realm->async_begin_transaction([&] {
4✔
1934
                    table->create_object().set(col, 45);
4✔
1935
                    realm->commit_transaction();
4✔
1936
                    std::invoke(close_functions[i], *realm);
4✔
1937
                    done = true;
4✔
1938
                });
4✔
1939
                wait_for_done();
4✔
1940
                verify_persisted_count(1);
4✔
1941
            }
4✔
1942
            SECTION("inside async_begin_transaction() callback after async commit") {
40✔
1943
                realm->async_begin_transaction([&] {
4✔
1944
                    table->create_object().set(col, 45);
4✔
1945
                    realm->async_commit_transaction([&](std::exception_ptr) {
4✔
1946
                        persisted = true;
4✔
1947
                    });
4✔
1948
                    std::invoke(close_functions[i], *realm);
4✔
1949
                    REQUIRE(persisted);
4!
1950
                    done = true;
4✔
1951
                });
4✔
1952
                wait_for_done();
4✔
1953
                verify_persisted_count(1);
4✔
1954
            }
4✔
1955
            SECTION("inside async commit completion") {
40✔
1956
                realm->async_begin_transaction([&] {
4✔
1957
                    table->create_object().set(col, 45);
4✔
1958
                    realm->async_commit_transaction([&](std::exception_ptr) {
4✔
1959
                        done = true;
4✔
1960
                        std::invoke(close_functions[i], *realm);
4✔
1961
                    });
4✔
1962
                });
4✔
1963
                wait_for_done();
4✔
1964
                verify_persisted_count(1);
4✔
1965
            }
4✔
1966
            SECTION("between commit and sync") {
40✔
1967
                realm->async_begin_transaction([&] {
4✔
1968
                    table->create_object().set(col, 45);
4✔
1969
                    realm->async_commit_transaction([&](std::exception_ptr) {
4✔
1970
                        persisted = true;
4✔
1971
                    });
4✔
1972
                    done = true;
4✔
1973
                });
4✔
1974
                wait_for_done();
4✔
1975
                std::invoke(close_functions[i], *realm);
4✔
1976
                REQUIRE(persisted);
4!
1977
                verify_persisted_count(1);
4✔
1978
            }
4✔
1979
            SECTION("with multiple pending commits") {
40✔
1980
                int complete_count = 0;
4✔
1981
                realm->async_begin_transaction([&] {
4✔
1982
                    table->create_object().set(col, 45);
4✔
1983
                    realm->async_commit_transaction([&](std::exception_ptr) {
4✔
1984
                        ++complete_count;
4✔
1985
                    });
4✔
1986
                });
4✔
1987
                realm->async_begin_transaction([&] {
4✔
1988
                    table->create_object().set(col, 45);
4✔
1989
                    realm->async_commit_transaction(
4✔
1990
                        [&](auto) {
4✔
1991
                            ++complete_count;
4✔
1992
                        },
4✔
1993
                        true);
4✔
1994
                });
4✔
1995
                realm->async_begin_transaction([&] {
4✔
1996
                    table->create_object().set(col, 45);
4✔
1997
                    realm->async_commit_transaction(
4✔
1998
                        [&](auto) {
4✔
1999
                            ++complete_count;
4✔
2000
                        },
4✔
2001
                        true);
4✔
2002
                    done = true;
4✔
2003
                });
4✔
2004

2005
                wait_for_done();
4✔
2006
                std::invoke(close_functions[i], *realm);
4✔
2007
                REQUIRE(complete_count == 3);
4!
2008
                verify_persisted_count(3);
4✔
2009
            }
4✔
2010
            SECTION("inside async_begin_transaction() with pending commits") {
40✔
2011
                int complete_count = 0;
4✔
2012
                realm->async_begin_transaction([&] {
4✔
2013
                    table->create_object().set(col, 45);
4✔
2014
                    realm->async_commit_transaction([&](std::exception_ptr) {
4✔
2015
                        ++complete_count;
4✔
2016
                    });
4✔
2017
                });
4✔
2018
                realm->async_begin_transaction([&] {
4✔
2019
                    // This create should be discarded
2020
                    table->create_object().set(col, 45);
4✔
2021
                    std::invoke(close_functions[i], *realm);
4✔
2022
                    done = true;
4✔
2023
                });
4✔
2024

2025
                wait_for_done();
4✔
2026
                std::invoke(close_functions[i], *realm);
4✔
2027
                REQUIRE(complete_count == 1);
4!
2028
                verify_persisted_count(1);
4✔
2029
            }
4✔
2030
            SECTION("within did_change()") {
40✔
2031
                struct Context : public BindingContext {
4✔
2032
                    int i;
4✔
2033
                    Context(int i)
4✔
2034
                        : i(i)
4✔
2035
                    {
4✔
2036
                    }
4✔
2037
                    void did_change(std::vector<ObserverState> const&, std::vector<void*> const&, bool) override
4✔
2038
                    {
6✔
2039
                        std::invoke(close_functions[i], *realm.lock());
6✔
2040
                    }
6✔
2041
                };
4✔
2042
                realm->m_binding_context.reset(new Context(i));
4✔
2043
                realm->m_binding_context->realm = realm;
4✔
2044

2045
                realm->async_begin_transaction([&] {
4✔
2046
                    table->create_object().set(col, 45);
4✔
2047
                    realm->async_commit_transaction([&](std::exception_ptr) {
4✔
2048
                        done = true;
4✔
2049
                    });
4✔
2050
                });
4✔
2051

2052
                wait_for_done();
4✔
2053
                verify_persisted_count(1);
4✔
2054
            }
4✔
2055
        }
40✔
2056
    }
208✔
2057

2058
    SECTION("notify only with no further actions") {
104✔
2059
        realm->async_begin_transaction(
2✔
2060
            [&] {
2✔
2061
                done = true;
2✔
2062
            },
2✔
2063
            true);
2✔
2064
        wait_for_done();
2✔
2065
        realm->cancel_transaction();
2✔
2066
    }
2✔
2067
    SECTION("notify only with synchronous commit") {
104✔
2068
        realm->async_begin_transaction(
2✔
2069
            [&] {
2✔
2070
                done = true;
2✔
2071
            },
2✔
2072
            true);
2✔
2073
        wait_for_done();
2✔
2074
        table->create_object();
2✔
2075
        realm->commit_transaction();
2✔
2076
    }
2✔
2077
    SECTION("schedule async commits after notify only") {
104✔
2078
        realm->async_begin_transaction(
2✔
2079
            [&] {
2✔
2080
                done = true;
2✔
2081
            },
2✔
2082
            true);
2✔
2083
        wait_for_done();
2✔
2084
        done = false;
2✔
2085
        realm->async_begin_transaction([&] {
2✔
2086
            table->create_object();
2✔
2087
            done = true;
2✔
2088
            realm->commit_transaction();
2✔
2089
        });
2✔
2090
        table->create_object();
2✔
2091
        realm->commit_transaction();
2✔
2092
        REQUIRE(table->size() == 1);
2!
2093
        wait_for_done();
2✔
2094
        REQUIRE(table->size() == 2);
2!
2095
    }
2✔
2096
    SECTION("exception thrown during transaction with error handler") {
104✔
2097
        Realm::AsyncHandle h = 7;
2✔
2098
        bool called = false;
2✔
2099
        realm->set_async_error_handler([&](Realm::AsyncHandle handle, std::exception_ptr error) {
2✔
2100
            REQUIRE(error);
2!
2101
            REQUIRE_THROWS_CONTAINING(std::rethrow_exception(error), "an error");
2✔
2102
            CHECK(handle == h);
2!
2103
            called = true;
2✔
2104
        });
2✔
2105
        h = realm->async_begin_transaction([&] {
2✔
2106
            table->create_object();
2✔
2107
            done = true;
2✔
2108
            throw std::runtime_error("an error");
2✔
2109
        });
2✔
2110
        wait_for_done();
2✔
2111

2112
        // Transaction should have been rolled back
2113
        REQUIRE_FALSE(realm->is_in_transaction());
2!
2114
        REQUIRE(table->size() == 0);
2!
2115
        REQUIRE(called);
2!
2116

2117
        // Should be able to perform another write afterwards
2118
        done = false;
2✔
2119
        called = false;
2✔
2120
        h = realm->async_begin_transaction([&] {
2✔
2121
            table->create_object();
2✔
2122
            realm->commit_transaction();
2✔
2123
            done = true;
2✔
2124
        });
2✔
2125
        wait_for_done();
2✔
2126
        REQUIRE(table->size() == 1);
2!
2127
        REQUIRE_FALSE(called);
2!
2128
    }
2✔
2129
#ifndef _WIN32
104✔
2130
    SECTION("exception thrown during transaction without error handler") {
104✔
2131
        realm->set_async_error_handler(nullptr);
2✔
2132
        realm->async_begin_transaction([&] {
2✔
2133
            table->create_object();
2✔
2134
            throw std::runtime_error("an error");
2✔
2135
        });
2✔
2136
        REQUIRE_THROWS_CONTAINING(util::EventLoop::main().run_until([&] {
2✔
2137
            return false;
2✔
2138
        }),
2✔
2139
                                  "an error");
2✔
2140

2141
        // Transaction should have been rolled back
2142
        REQUIRE_FALSE(realm->is_in_transaction());
2!
2143
        REQUIRE(table->size() == 0);
2!
2144

2145
        // Should be able to perform another write afterwards
2146
        realm->async_begin_transaction([&, realm] {
2✔
2147
            table->create_object();
2✔
2148
            realm->commit_transaction();
2✔
2149
            done = true;
2✔
2150
        });
2✔
2151
        wait_for_done();
2✔
2152
        REQUIRE(table->size() == 1);
2!
2153
    }
2✔
2154
    SECTION("exception thrown during transaction without error handler after closing Realm") {
104✔
2155
        realm->set_async_error_handler(nullptr);
2✔
2156
        realm->async_begin_transaction([&] {
2✔
2157
            realm->close();
2✔
2158
            throw std::runtime_error("an error");
2✔
2159
        });
2✔
2160
        REQUIRE_THROWS_CONTAINING(util::EventLoop::main().run_until([&] {
2✔
2161
            return false;
2✔
2162
        }),
2✔
2163
                                  "an error");
2✔
2164
        REQUIRE(realm->is_closed());
2!
2165
    }
2✔
2166
#endif
104✔
2167
    SECTION("exception thrown from async commit completion callback with error handler") {
104✔
2168
        Realm::AsyncHandle h;
2✔
2169
        realm->set_async_error_handler([&](Realm::AsyncHandle handle, std::exception_ptr error) {
2✔
2170
            REQUIRE(error);
2!
2171
            REQUIRE_THROWS_CONTAINING(std::rethrow_exception(error), "an error");
2✔
2172
            CHECK(handle == h);
2!
2173
            done = true;
2✔
2174
        });
2✔
2175

2176
        realm->begin_transaction();
2✔
2177
        table->create_object();
2✔
2178
        h = realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2179
            throw std::runtime_error("an error");
2✔
2180
        });
2✔
2181
        wait_for_done();
2✔
2182
        verify_persisted_count(1);
2✔
2183
    }
2✔
2184
#ifndef _WIN32
104✔
2185
    SECTION("exception thrown from async commit completion callback without error handler") {
104✔
2186
        realm->begin_transaction();
2✔
2187
        table->create_object();
2✔
2188
        realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2189
            throw std::runtime_error("an error");
2✔
2190
        });
2✔
2191
        REQUIRE_THROWS_CONTAINING(util::EventLoop::main().run_until([&] {
2✔
2192
            return false;
2✔
2193
        }),
2✔
2194
                                  "an error");
2✔
2195
        REQUIRE(table->size() == 1);
2!
2196
    }
2✔
2197
#endif
104✔
2198

2199
    if (_impl::SimulatedFailure::is_enabled()) {
104✔
2200
        SECTION("error in the synchronous part of async commit") {
104✔
2201
            realm->begin_transaction();
2✔
2202
            table->create_object();
2✔
2203

2204
            using sf = _impl::SimulatedFailure;
2✔
2205
            sf::OneShotPrimeGuard pg(sf::shared_group__grow_reader_mapping);
2✔
2206
            REQUIRE_THROWS_AS(realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2207
                FAIL("should not call completion");
2✔
2208
            }),
2✔
2209
                              _impl::SimulatedFailure);
2✔
2210
            REQUIRE_FALSE(realm->is_in_transaction());
2!
2211
        }
2✔
2212
        SECTION("error in the async part of async commit") {
104✔
2213
            realm->begin_transaction();
2✔
2214
            table->create_object();
2✔
2215

2216
            using sf = _impl::SimulatedFailure;
2✔
2217
            sf::set_thread_local(false);
2✔
2218
            sf::OneShotPrimeGuard pg(sf::group_writer__commit);
2✔
2219
            realm->async_commit_transaction([&](std::exception_ptr e) {
2✔
2220
                REQUIRE(e);
2!
2221
                REQUIRE_THROWS_AS(std::rethrow_exception(e), _impl::SimulatedFailure);
2✔
2222
                done = true;
2✔
2223
            });
2✔
2224
            wait_for_done();
2✔
2225
            sf::set_thread_local(true);
2✔
2226
        }
2✔
2227
    }
104✔
2228
    SECTION("throw exception from did_change()") {
104✔
2229
        struct Context : public BindingContext {
2✔
2230
            void did_change(std::vector<ObserverState> const&, std::vector<void*> const&, bool) override
2✔
2231
            {
2✔
2232
                throw std::runtime_error("expected error");
2✔
2233
            }
2✔
2234
        };
2✔
2235
        realm->m_binding_context.reset(new Context);
2✔
2236

2237
        realm->begin_transaction();
2✔
2238
        auto table = realm->read_group().get_table("class_object");
2✔
2239
        table->create_object();
2✔
2240
        REQUIRE_THROWS_WITH(realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2241
            done = true;
2✔
2242
        }),
2✔
2243
                            "expected error");
2✔
2244
        wait_for_done();
2✔
2245
    }
2✔
2246
    SECTION("cancel scheduled async transaction") {
104✔
2247
        auto handle = realm->async_begin_transaction([&, realm]() {
2✔
UNCOV
2248
            table->create_object().set(col, 45);
×
UNCOV
2249
            realm->async_commit_transaction(
×
UNCOV
2250
                [&](auto) {
×
UNCOV
2251
                    done = true;
×
UNCOV
2252
                },
×
UNCOV
2253
                true);
×
UNCOV
2254
        });
×
2255
        realm->async_begin_transaction([&, realm]() {
2✔
2256
            table->create_object().set(col, 90);
2✔
2257
            realm->async_commit_transaction(
2✔
2258
                [&](auto) {
2✔
2259
                    done = true;
2✔
2260
                },
2✔
2261
                true);
2✔
2262
        });
2✔
2263
        realm->async_cancel_transaction(handle);
2✔
2264
        wait_for_done();
2✔
2265
        auto table = realm->read_group().get_table("class_object");
2✔
2266
        REQUIRE(table->size() == 1);
2!
2267
        REQUIRE(table->begin()->get<Int>("value") == 90);
2!
2268
    }
2✔
2269
    SECTION("synchronous cancel inside async transaction") {
104✔
2270
        realm->async_begin_transaction([&, realm]() {
2✔
2271
            REQUIRE(table->size() == 0);
2!
2272
            table->create_object().set(col, 45);
2✔
2273
            REQUIRE(table->size() == 1);
2!
2274
            realm->cancel_transaction();
2✔
2275
            REQUIRE(table->size() == 0);
2!
2276
            done = true;
2✔
2277
        });
2✔
2278
        wait_for_done();
2✔
2279
    }
2✔
2280
    SECTION("synchronous commit of async transaction after async commit which allows grouping") {
104✔
2281
        realm->async_begin_transaction([&, realm]() {
2✔
2282
            table->create_object().set(col, 45);
2✔
2283
            realm->async_commit_transaction(
2✔
2284
                [&](auto) {
2✔
2285
                    done = true;
2✔
2286
                },
2✔
2287
                true);
2✔
2288
        });
2✔
2289
        realm->async_begin_transaction([&, realm]() {
2✔
2290
            table->create_object().set(col, 45);
2✔
2291
            realm->commit_transaction();
2✔
2292
        });
2✔
2293
        wait_for_done();
2✔
2294
        auto table = realm->read_group().get_table("class_object");
2✔
2295
        REQUIRE(table->size() == 2);
2!
2296
    }
2✔
2297
    SECTION("synchronous transaction after async transaction with no commit") {
104✔
2298
        realm->async_begin_transaction([&]() {
2✔
2299
            table->create_object().set(col, 80);
2✔
2300
            done = true;
2✔
2301
        });
2✔
2302
        wait_for_done();
2✔
2303
        realm->begin_transaction();
2✔
2304
        table->create_object().set(col, 90);
2✔
2305
        realm->commit_transaction();
2✔
2306
        verify_persisted_count(1);
2✔
2307
    }
2✔
2308
    SECTION("synchronous transaction with scheduled async transaction with no commit") {
104✔
2309
        realm->async_begin_transaction([&]() {
2✔
2310
            table->create_object().set(col, 80);
2✔
2311
            done = true;
2✔
2312
        });
2✔
2313
        realm->begin_transaction();
2✔
2314
        table->create_object().set(col, 90);
2✔
2315
        realm->commit_transaction();
2✔
2316
        wait_for_done();
2✔
2317
        verify_persisted_count(1);
2✔
2318
    }
2✔
2319
    SECTION("synchronous transaction with scheduled async transaction") {
104✔
2320
        realm->async_begin_transaction([&, realm]() {
2✔
2321
            table->create_object().set(col, 80);
2✔
2322
            realm->commit_transaction();
2✔
2323
            done = true;
2✔
2324
        });
2✔
2325
        realm->begin_transaction();
2✔
2326
        table->create_object().set(col, 90);
2✔
2327
        realm->commit_transaction();
2✔
2328
        wait_for_done();
2✔
2329
        REQUIRE(table->size() == 2);
2!
2330
        REQUIRE(table->get_object(0).get<Int>(col) == 90);
2!
2331
        REQUIRE(table->get_object(1).get<Int>(col) == 80);
2!
2332
    }
2✔
2333
    SECTION("synchronous transaction with async write") {
104✔
2334
        realm->begin_transaction();
2✔
2335
        table->create_object().set(col, 45);
2✔
2336
        realm->async_commit_transaction();
2✔
2337

2338
        realm->begin_transaction();
2✔
2339
        table->create_object().set(col, 90);
2✔
2340
        realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2341
            done = true;
2✔
2342
        });
2✔
2343
        wait_for_done();
2✔
2344
        verify_persisted_count(2);
2✔
2345
    }
2✔
2346
    SECTION("synchronous transaction mixed with async transactions") {
104✔
2347
        realm->async_begin_transaction([&, realm]() {
2✔
2348
            table->create_object().set(col, 45);
2✔
2349
            done = true;
2✔
2350
            realm->async_commit_transaction();
2✔
2351
        });
2✔
2352
        realm->async_begin_transaction([&, realm]() {
2✔
2353
            table->create_object().set(col, 45);
2✔
2354
            realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2355
                done = true;
2✔
2356
            });
2✔
2357
        });
2✔
2358
        wait_for_done();
2✔
2359
        realm->begin_transaction(); // Here syncing of first async tr has not completed
2✔
2360
        REQUIRE(table->size() == 1);
2!
2361
        table->create_object().set(col, 90);
2✔
2362
        realm->commit_transaction(); // Will re-initiate async writes
2✔
2363

2364
        done = false;
2✔
2365
        wait_for_done();
2✔
2366
        verify_persisted_count(3);
2✔
2367
    }
2✔
2368
    SECTION("asynchronous transaction mixed with sync transaction that is cancelled") {
104✔
2369
        bool persisted = false;
2✔
2370
        realm->async_begin_transaction([&, realm]() {
2✔
2371
            table->create_object().set(col, 45);
2✔
2372
            done = true;
2✔
2373
            realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2374
                persisted = true;
2✔
2375
            });
2✔
2376
        });
2✔
2377
        realm->async_begin_transaction([&, realm]() {
2✔
2378
            table->create_object().set(col, 45);
2✔
2379
            auto handle = realm->async_commit_transaction([&](std::exception_ptr) {
2✔
UNCOV
2380
                FAIL();
×
UNCOV
2381
            });
×
2382
            realm->async_cancel_transaction(handle);
2✔
2383
        });
2✔
2384
        wait_for_done();
2✔
2385
        realm->begin_transaction();
2✔
2386
        CHECK(persisted);
2!
2387
        persisted = false;
2✔
2388
        REQUIRE(table->size() == 1);
2!
2389
        table->create_object().set(col, 90);
2✔
2390
        realm->cancel_transaction();
2✔
2391

2392
        util::EventLoop::main().run_until([&] {
2,443✔
2393
            return !realm->is_in_async_transaction();
2,443✔
2394
        });
2,443✔
2395

2396
        REQUIRE(table->size() == 2);
2!
2397
        REQUIRE(!table->find_first_int(col, 90));
2!
2398
    }
2✔
2399
    SECTION("cancelled sync transaction with pending async transaction") {
104✔
2400
        realm->async_begin_transaction([&, realm]() {
2✔
2401
            table->create_object().set(col, 45);
2✔
2402
            realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2403
                done = true;
2✔
2404
            });
2✔
2405
        });
2✔
2406
        realm->begin_transaction();
2✔
2407
        REQUIRE(table->size() == 0);
2!
2408
        table->create_object();
2✔
2409
        realm->cancel_transaction();
2✔
2410
        REQUIRE(table->size() == 0);
2!
2411
        wait_for_done();
2✔
2412
        verify_persisted_count(1);
2✔
2413
    }
2✔
2414
    SECTION("cancelled sync transaction with pending async commit") {
104✔
2415
        bool persisted = false;
2✔
2416
        realm->async_begin_transaction([&, realm]() {
2✔
2417
            table->create_object().set(col, 45);
2✔
2418
            done = true;
2✔
2419
            realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2420
                persisted = true;
2✔
2421
            });
2✔
2422
        });
2✔
2423
        wait_for_done();
2✔
2424
        realm->begin_transaction();
2✔
2425
        REQUIRE(table->size() == 1);
2!
2426
        table->create_object();
2✔
2427
        realm->cancel_transaction();
2✔
2428

2429
        util::EventLoop::main().run_until([&] {
3✔
2430
            return persisted;
3✔
2431
        });
3✔
2432
        verify_persisted_count(1);
2✔
2433
    }
2✔
2434
    SECTION("sync commit of async transaction with subsequent pending async transaction") {
104✔
2435
        realm->async_begin_transaction([&, realm]() {
2✔
2436
            table->create_object();
2✔
2437
            realm->commit_transaction();
2✔
2438
        });
2✔
2439
        realm->async_begin_transaction([&, realm]() {
2✔
2440
            table->create_object();
2✔
2441
            realm->commit_transaction();
2✔
2442
            done = true;
2✔
2443
        });
2✔
2444
        wait_for_done();
2✔
2445
        REQUIRE(table->size() == 2);
2!
2446
    }
2✔
2447
    SECTION("release reference to Realm after async begin") {
104✔
2448
        std::weak_ptr<Realm> weak_realm = realm;
2✔
2449
        realm->async_begin_transaction([&]() {
2✔
2450
            table->create_object().set(col, 45);
2✔
2451
            weak_realm.lock()->async_commit_transaction([&](std::exception_ptr) {
2✔
2452
                done = true;
2✔
2453
            });
2✔
2454
        });
2✔
2455
        realm = nullptr;
2✔
2456
        wait_for_done();
2✔
2457
        verify_persisted_count(1);
2✔
2458
    }
2✔
2459
    SECTION("object change information") {
104✔
2460
        realm->begin_transaction();
2✔
2461
        auto list_col = table->get_column_key("ints");
2✔
2462
        auto set_col = table->get_column_key("int set");
2✔
2463
        auto dict_col = table->get_column_key("int dictionary");
2✔
2464
        auto obj = table->create_object();
2✔
2465
        auto list = obj.get_list<Int>(list_col);
2✔
2466
        for (int i = 0; i < 3; ++i)
8✔
2467
            list.add(i);
6✔
2468
        auto set = obj.get_set<Int>(set_col);
2✔
2469
        set.insert(0);
2✔
2470
        auto dict = obj.get_dictionary(dict_col);
2✔
2471
        dict.insert("a", 0);
2✔
2472
        realm->commit_transaction();
2✔
2473

2474
        Observer observer(obj);
2✔
2475
        observer.realm = realm;
2✔
2476
        realm->m_binding_context.reset(&observer);
2✔
2477

2478
        realm->async_begin_transaction([&]() {
2✔
2479
            list.clear();
2✔
2480
            set.clear();
2✔
2481
            dict.clear();
2✔
2482
            done = true;
2✔
2483
        });
2✔
2484
        wait_for_done();
2✔
2485
        REQUIRE(observer.array_change(0, list_col) == IndexSet{0, 1, 2});
2!
2486
        REQUIRE(observer.array_change(0, set_col) == IndexSet{});
2!
2487
        REQUIRE(observer.array_change(0, dict_col) == IndexSet{});
2!
2488
        realm->m_binding_context.release();
2✔
2489
    }
2✔
2490

2491
    SECTION("begin_transaction() from within did_change()") {
104✔
2492
        struct Context : public BindingContext {
2✔
2493
            void did_change(std::vector<ObserverState> const&, std::vector<void*> const&, bool) override
2✔
2494
            {
4✔
2495
                auto r = realm.lock();
4✔
2496
                r->begin_transaction();
4✔
2497
                auto table = r->read_group().get_table("class_object");
4✔
2498
                table->create_object();
4✔
2499
                if (++change_count == 1) {
4✔
2500
                    r->commit_transaction();
2✔
2501
                }
2✔
2502
                else {
2✔
2503
                    r->cancel_transaction();
2✔
2504
                }
2✔
2505
            }
4✔
2506
            int change_count = 0;
2✔
2507
        };
2✔
2508

2509
        realm->m_binding_context.reset(new Context());
2✔
2510
        realm->m_binding_context->realm = realm;
2✔
2511

2512
        realm->begin_transaction();
2✔
2513
        auto table = realm->read_group().get_table("class_object");
2✔
2514
        table->create_object();
2✔
2515
        bool persisted = false;
2✔
2516
        realm->async_commit_transaction([&persisted](auto) {
2✔
2517
            persisted = true;
2✔
2518
        });
2✔
2519
        REQUIRE(table->size() == 2);
2!
2520
        REQUIRE(persisted);
2!
2521
    }
2✔
2522

2523
    SECTION("async write grouping") {
104✔
2524
        size_t completion_calls = 0;
2✔
2525
        for (size_t i = 0; i < 41; ++i) {
84✔
2526
            realm->async_begin_transaction([&, i, realm] {
82✔
2527
                // The top ref in the Realm file should only be updated once every 20 commits
2528
                CHECK(Group(config.path, config.encryption_key.data()).get_table("class_object")->size() ==
82!
2529
                      (i / 20) * 20);
82✔
2530

2531
                table->create_object();
82✔
2532
                realm->async_commit_transaction(
82✔
2533
                    [&](std::exception_ptr) {
82✔
2534
                        ++completion_calls;
82✔
2535
                    },
82✔
2536
                    true);
82✔
2537
            });
82✔
2538
        }
82✔
2539
        util::EventLoop::main().run_until([&] {
10,078✔
2540
            return completion_calls == 41;
10,078✔
2541
        });
10,078✔
2542
    }
2✔
2543

2544
    SECTION("async write grouping with manual barriers") {
104✔
2545
        size_t completion_calls = 0;
2✔
2546
        for (size_t i = 0; i < 41; ++i) {
84✔
2547
            realm->async_begin_transaction([&, i, realm] {
82✔
2548
                // The top ref in the Realm file should only be updated once every 6 commits
2549
                CHECK(Group(config.path, config.encryption_key.data()).get_table("class_object")->size() ==
82!
2550
                      (i / 6) * 6);
82✔
2551

2552
                table->create_object();
82✔
2553
                realm->async_commit_transaction(
82✔
2554
                    [&](std::exception_ptr) {
82✔
2555
                        ++completion_calls;
82✔
2556
                    },
82✔
2557
                    (i + 1) % 6 != 0);
82✔
2558
            });
82✔
2559
        }
82✔
2560
        util::EventLoop::main().run_until([&] {
30,527✔
2561
            return completion_calls == 41;
30,527✔
2562
        });
30,527✔
2563
    }
2✔
2564

2565
    SECTION("async writes scheduled inside sync write") {
104✔
2566
        realm->begin_transaction();
2✔
2567
        realm->async_begin_transaction([&] {
2✔
2568
            REQUIRE(table->size() == 1);
2!
2569
            table->create_object();
2✔
2570
            realm->async_commit_transaction();
2✔
2571
        });
2✔
2572
        realm->async_begin_transaction([&] {
2✔
2573
            REQUIRE(table->size() == 2);
2!
2574
            table->create_object();
2✔
2575
            realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2576
                done = true;
2✔
2577
            });
2✔
2578
        });
2✔
2579
        REQUIRE(table->size() == 0);
2!
2580
        table->create_object();
2✔
2581
        realm->commit_transaction();
2✔
2582
        wait_for_done();
2✔
2583
        REQUIRE(table->size() == 3);
2!
2584
    }
2✔
2585

2586
    SECTION("async writes scheduled inside multiple sync write") {
104✔
2587
        realm->begin_transaction();
2✔
2588
        realm->async_begin_transaction([&] {
2✔
2589
            REQUIRE(table->size() == 2);
2!
2590
            table->create_object();
2✔
2591
            realm->async_commit_transaction();
2✔
2592
        });
2✔
2593
        realm->async_begin_transaction([&] {
2✔
2594
            REQUIRE(table->size() == 3);
2!
2595
            table->create_object();
2✔
2596
            realm->async_commit_transaction();
2✔
2597
        });
2✔
2598
        REQUIRE(table->size() == 0);
2!
2599
        table->create_object();
2✔
2600
        realm->commit_transaction();
2✔
2601

2602
        realm->begin_transaction();
2✔
2603
        realm->async_begin_transaction([&] {
2✔
2604
            REQUIRE(table->size() == 4);
2!
2605
            table->create_object();
2✔
2606
            realm->async_commit_transaction();
2✔
2607
        });
2✔
2608
        realm->async_begin_transaction([&] {
2✔
2609
            REQUIRE(table->size() == 5);
2!
2610
            table->create_object();
2✔
2611
            realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2612
                done = true;
2✔
2613
            });
2✔
2614
        });
2✔
2615
        REQUIRE(table->size() == 1);
2!
2616
        table->create_object();
2✔
2617
        realm->commit_transaction();
2✔
2618

2619

2620
        wait_for_done();
2✔
2621
        REQUIRE(table->size() == 6);
2!
2622
    }
2✔
2623

2624
    SECTION("async writes which would run inside sync writes are deferred") {
104✔
2625
        realm->async_begin_transaction([&] {
2✔
2626
            done = true;
2✔
2627
        });
2✔
2628

2629
        // Wait for the background thread to hold the write lock (without letting
2630
        // the event loop run so that the scheduled task isn't run)
2631
        DBOptions options;
2✔
2632
        options.encryption_key = config.encryption_key.data();
2✔
2633
        auto db = DB::create(make_in_realm_history(), config.path, options);
2✔
2634
        while (db->start_write(true))
2✔
UNCOV
2635
            millisleep(1);
×
2636

2637
        realm->begin_transaction();
2✔
2638

2639
        // Invoke the pending callback
2640
        util::EventLoop::main().run_pending();
2✔
2641
        // Should not have run the async write block
2642
        REQUIRE(done == false);
2!
2643

2644
        // Should run the async write block once the synchronous transaction is done
2645
        realm->cancel_transaction();
2✔
2646
        REQUIRE(done == false);
2!
2647
        util::EventLoop::main().run_pending();
2✔
2648
        REQUIRE(done == true);
2!
2649
    }
2✔
2650

2651
    util::EventLoop::main().run_until([&] {
162✔
2652
        return !realm || !realm->has_pending_async_work();
162✔
2653
    });
162✔
2654

2655
    _impl::RealmCoordinator::clear_all_caches();
104✔
2656
}
104✔
2657

2658
TEST_CASE("Call run_async_completions after realm has been closed") {
2✔
2659
    // This requires a special scheduler as we have to call Realm::close
2660
    // just after DB::AsyncCommitHelper has made a callback to the function
2661
    // that asks the scheduler to invoke run_async_completions()
2662

2663
    struct ManualScheduler : util::Scheduler {
2✔
2664
        std::mutex mutex;
2✔
2665
        std::condition_variable cv;
2✔
2666
        std::vector<util::UniqueFunction<void()>> callbacks;
2✔
2667

2668
        void invoke(util::UniqueFunction<void()>&& cb) override
2✔
2669
        {
2✔
2670
            {
2✔
2671
                std::lock_guard lock(mutex);
2✔
2672
                callbacks.push_back(std::move(cb));
2✔
2673
            }
2✔
2674
            cv.notify_all();
2✔
2675
        }
2✔
2676

2677
        bool is_on_thread() const noexcept override
2✔
2678
        {
4✔
2679
            return true;
4✔
2680
        }
4✔
2681
        bool is_same_as(const Scheduler*) const noexcept override
2✔
2682
        {
2✔
UNCOV
2683
            return false;
×
UNCOV
2684
        }
×
2685
        bool can_invoke() const noexcept override
2✔
2686
        {
2✔
UNCOV
2687
            return true;
×
UNCOV
2688
        }
×
2689
    };
2✔
2690

2691
    auto scheduler = std::make_shared<ManualScheduler>();
2✔
2692

2693
    TestFile config;
2✔
2694
    config.schema_version = 0;
2✔
2695
    config.schema = Schema{{"object", {{"value", PropertyType::Int}}}};
2✔
2696
    config.scheduler = scheduler;
2✔
2697
    config.automatic_change_notifications = false;
2✔
2698

2699
    auto realm = Realm::get_shared_realm(config);
2✔
2700

2701
    realm->begin_transaction();
2✔
2702
    realm->async_commit_transaction([](std::exception_ptr) {});
2✔
2703

2704
    std::vector<util::UniqueFunction<void()>> callbacks;
2✔
2705
    {
2✔
2706
        std::unique_lock lock(scheduler->mutex);
2✔
2707
        // Wait for scheduler to be invoked
2708
        scheduler->cv.wait(lock, [&] {
4✔
2709
            return !scheduler->callbacks.empty();
4✔
2710
        });
4✔
2711
        callbacks.swap(scheduler->callbacks);
2✔
2712
    }
2✔
2713
    realm->close();
2✔
2714
    // Call whatever functions that was added to scheduler.
2715
    for (auto& cb : callbacks)
2✔
2716
        cb();
2✔
2717
}
2✔
2718

2719
// Our libuv scheduler currently does not support background threads, so we can
2720
// only run this on apple platforms
2721
#if REALM_PLATFORM_APPLE
2722
TEST_CASE("SharedRealm: async writes on multiple threads") {
5✔
2723
    _impl::RealmCoordinator::assert_no_open_realms();
5✔
2724

2725
    TestFile config;
5✔
2726
    config.cache = true;
5✔
2727
    config.schema_version = 0;
5✔
2728
    config.schema = Schema{{"object", {{"value", PropertyType::Int}}}};
5✔
2729
    auto realm = Realm::get_shared_realm(config);
5✔
2730
    auto table_key = realm->read_group().get_table("class_object")->get_key();
5✔
2731
    realm->close();
5✔
2732

2733
    struct QueueState {
5✔
2734
        dispatch_queue_t queue;
5✔
2735
        Realm::Config config;
5✔
2736
    };
5✔
2737
    std::vector<QueueState> queues;
5✔
2738
    for (int i = 0; i < 10; ++i) {
55✔
2739
        auto queue = dispatch_queue_create(util::format("queue %1", i).c_str(), 0);
50✔
2740
        Realm::Config queue_config = config;
50✔
2741
        queue_config.scheduler = util::Scheduler::make_dispatch(static_cast<void*>(queue));
50✔
2742
        queues.push_back({queue, std::move(queue_config)});
50✔
2743
    }
50✔
2744

2745
    std::atomic<size_t> completions = 0;
5✔
2746
    // Capturing by reference when mixing lambda and blocks is weird, so capture
2747
    // a pointer instead
2748
    auto completions_ptr = &completions;
5✔
2749

2750
    auto async_write_and_async_commit = [=](const Realm::Config& config) {
124✔
2751
        Realm::get_shared_realm(config)->async_begin_transaction([=] {
124✔
2752
            auto realm = Realm::get_shared_realm(config);
124✔
2753
            realm->read_group().get_table(table_key)->create_object();
124✔
2754
            realm->async_commit_transaction([=](std::exception_ptr) {
124✔
2755
                ++*completions_ptr;
124✔
2756
            });
124✔
2757
        });
124✔
2758
    };
124✔
2759
    auto async_write_and_sync_commit = [=](const Realm::Config& config) {
124✔
2760
        Realm::get_shared_realm(config)->async_begin_transaction([=] {
124✔
2761
            auto realm = Realm::get_shared_realm(config);
124✔
2762
            realm->read_group().get_table(table_key)->create_object();
124✔
2763
            realm->commit_transaction();
124✔
2764
            ++*completions_ptr;
124✔
2765
        });
124✔
2766
    };
124✔
2767
    auto sync_write_and_async_commit = [=](const Realm::Config& config) {
124✔
2768
        auto realm = Realm::get_shared_realm(config);
124✔
2769
        realm->begin_transaction();
124✔
2770
        realm->read_group().get_table(table_key)->create_object();
124✔
2771
        realm->async_commit_transaction([=](std::exception_ptr) {
124✔
2772
            ++*completions_ptr;
124✔
2773
        });
124✔
2774
    };
124✔
2775
    auto sync_write_and_sync_commit = [=](const Realm::Config& config) {
124✔
2776
        auto realm = Realm::get_shared_realm(config);
124✔
2777
        realm->begin_transaction();
124✔
2778
        realm->read_group().get_table(table_key)->create_object();
124✔
2779
        realm->commit_transaction();
124✔
2780
        ++*completions_ptr;
124✔
2781
    };
124✔
2782

2783
    SECTION("async begin and async commit") {
5✔
2784
        for (auto& queue : queues) {
10✔
2785
            dispatch_async(queue.queue, ^{
10✔
2786
                for (int i = 0; i < 10; ++i) {
110✔
2787
                    async_write_and_async_commit(queue.config);
100✔
2788
                }
100✔
2789
            });
10✔
2790
        }
10✔
2791
        util::EventLoop::main().run_until([&] {
1,690✔
2792
            return completions == 100;
1,690✔
2793
        });
1,690✔
2794
    }
1✔
2795
    SECTION("async begin and sync commit") {
5✔
2796
        for (auto& queue : queues) {
10✔
2797
            dispatch_async(queue.queue, ^{
10✔
2798
                for (int i = 0; i < 10; ++i) {
110✔
2799
                    async_write_and_sync_commit(queue.config);
100✔
2800
                }
100✔
2801
            });
10✔
2802
        }
10✔
2803
        util::EventLoop::main().run_until([&] {
1,690✔
2804
            return completions == 100;
1,690✔
2805
        });
1,690✔
2806
    }
1✔
2807
    SECTION("sync begin and async commit") {
5✔
2808
        for (auto& queue : queues) {
10✔
2809
            dispatch_async(queue.queue, ^{
10✔
2810
                for (int i = 0; i < 10; ++i) {
110✔
2811
                    sync_write_and_async_commit(queue.config);
100✔
2812
                }
100✔
2813
            });
10✔
2814
        }
10✔
2815
        util::EventLoop::main().run_until([&] {
1,622✔
2816
            return completions == 100;
1,622✔
2817
        });
1,622✔
2818
    }
1✔
2819
    SECTION("sync begin and sync commit") {
5✔
2820
        for (auto& queue : queues) {
10✔
2821
            dispatch_async(queue.queue, ^{
10✔
2822
                for (int i = 0; i < 10; ++i) {
110✔
2823
                    sync_write_and_sync_commit(queue.config);
100✔
2824
                }
100✔
2825
            });
10✔
2826
        }
10✔
2827
        util::EventLoop::main().run_until([&] {
1,606✔
2828
            return completions == 100;
1,606✔
2829
        });
1,606✔
2830
    }
1✔
2831
    SECTION("mixed sync and async") {
5✔
2832
        // Test every permutation of each of the variants
2833
        struct IndexedOp {
1✔
2834
            int index;
1✔
2835
            std::function<void(const Realm::Config& config)> fn;
1✔
2836
        };
1✔
2837
        std::array<IndexedOp, 4> functions{{
1✔
2838
            {0, async_write_and_async_commit},
1✔
2839
            {1, sync_write_and_async_commit},
1✔
2840
            {2, async_write_and_sync_commit},
1✔
2841
            {3, sync_write_and_sync_commit},
1✔
2842
        }};
1✔
2843
        size_t i = 0;
1✔
2844
        size_t expected_completions = 0;
1✔
2845
        do {
24✔
2846
            auto& queue = queues[i++ % 10];
24✔
2847
            auto functions_copy = functions;
24✔
2848
            dispatch_async(queue.queue, ^{
24✔
2849
                for (auto& fn : functions_copy) {
96✔
2850
                    fn.fn(queue.config);
96✔
2851
                }
96✔
2852
            });
24✔
2853
            expected_completions += 4;
24✔
2854
        } while (std::next_permutation(functions.begin(), functions.end(), [](auto& a, auto& b) {
70✔
2855
            return a.index < b.index;
70✔
2856
        }));
70✔
2857

2858
        util::EventLoop::main().run_until([&] {
1,614✔
2859
            return completions == expected_completions;
1,614✔
2860
        });
1,614✔
2861
    }
1✔
2862

2863

2864
    realm = Realm::get_shared_realm(config);
5✔
2865
    REQUIRE(realm->read_group().get_table(table_key)->size() == completions);
5!
2866

2867
    for (auto& queue : queues) {
50✔
2868
        dispatch_sync(queue.queue, ^{
50✔
2869
                      });
50✔
2870
    }
50✔
2871
}
5✔
2872
#endif
2873

2874
class LooperDelegate {
2875
public:
2876
    LooperDelegate() {}
2✔
2877
    void run_once()
2878
    {
6,168✔
2879
        for (auto it = m_tasks.begin(); it != m_tasks.end(); ++it) {
9,095✔
2880
            if (it->may_run && *it->may_run) {
2,933✔
2881
                it->the_job();
6✔
2882
                m_tasks.erase(it);
6✔
2883
                return;
6✔
2884
            }
6✔
2885
        }
2,933✔
2886
    }
6,168✔
2887
    std::shared_ptr<bool> add_task(util::UniqueFunction<void()>&& the_job)
2888
    {
6✔
2889
        m_tasks.push_back(Task{std::make_shared<bool>(false), std::move(the_job)});
6✔
2890
        return m_tasks.back().may_run;
6✔
2891
    }
6✔
2892
    bool has_tasks()
UNCOV
2893
    {
×
UNCOV
2894
        return !m_tasks.empty();
×
UNCOV
2895
    }
×
2896

2897
private:
2898
    struct Task {
2899
        std::shared_ptr<bool> may_run;
2900
        util::UniqueFunction<void()> the_job;
2901
    };
2902
    std::vector<Task> m_tasks;
2903
};
2904

2905
#ifndef _WIN32
2906
TEST_CASE("SharedRealm: async_writes_2") {
2✔
2907
    _impl::RealmCoordinator::assert_no_open_realms();
2✔
2908
    if (!util::EventLoop::has_implementation())
2✔
UNCOV
2909
        return;
×
2910

2911
    TestFile config;
2✔
2912
    config.schema_version = 0;
2✔
2913
    config.schema = Schema{
2✔
2914
        {"object", {{"value", PropertyType::Int}}},
2✔
2915
    };
2✔
2916
    bool done = false;
2✔
2917
    auto realm = Realm::get_shared_realm(config);
2✔
2918
    int write_nr = 0;
2✔
2919
    int commit_nr = 0;
2✔
2920
    auto table = realm->read_group().get_table("class_object");
2✔
2921
    auto col = table->get_column_key("value");
2✔
2922
    LooperDelegate ld;
2✔
2923
    std::shared_ptr<bool> t1_rdy = ld.add_task([&, realm]() {
2✔
2924
        REQUIRE(write_nr == 0);
2!
2925
        ++write_nr;
2✔
2926
        table->create_object().set(col, 45);
2✔
2927
        realm->cancel_transaction();
2✔
2928
    });
2✔
2929
    std::shared_ptr<bool> t2_rdy = ld.add_task([&, realm]() {
2✔
2930
        REQUIRE(write_nr == 1);
2!
2931
        ++write_nr;
2✔
2932
        table->create_object().set(col, 45);
2✔
2933
        realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2934
            REQUIRE(commit_nr == 0);
2!
2935
            ++commit_nr;
2✔
2936
        });
2✔
2937
    });
2✔
2938
    std::shared_ptr<bool> t3_rdy = ld.add_task([&, realm]() {
2✔
2939
        ++write_nr;
2✔
2940
        auto o = table->get_object(0);
2✔
2941
        o.set(col, o.get<int64_t>(col) + 37);
2✔
2942
        realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2943
            ++commit_nr;
2✔
2944
            done = true;
2✔
2945
        });
2✔
2946
    });
2✔
2947

2948
    // Make some notify_only transactions
2949
    realm->async_begin_transaction(
2✔
2950
        [&]() {
2✔
2951
            *t1_rdy = true;
2✔
2952
        },
2✔
2953
        true);
2✔
2954
    realm->async_begin_transaction(
2✔
2955
        [&]() {
2✔
2956
            *t2_rdy = true;
2✔
2957
        },
2✔
2958
        true);
2✔
2959
    realm->async_begin_transaction(
2✔
2960
        [&]() {
2✔
2961
            *t3_rdy = true;
2✔
2962
        },
2✔
2963
        true);
2✔
2964

2965
    util::EventLoop::main().run_until([&, realm] {
6,168✔
2966
        ld.run_once();
6,168✔
2967
        return done;
6,168✔
2968
    });
6,168✔
2969
    REQUIRE(done);
2!
2970
}
2✔
2971
#endif
2972

2973
TEST_CASE("SharedRealm: notifications") {
14✔
2974
    if (!util::EventLoop::has_implementation())
14✔
UNCOV
2975
        return;
×
2976

2977
    TestFile config;
14✔
2978
    config.schema_version = 0;
14✔
2979
    config.schema = Schema{
14✔
2980
        {"object", {{"value", PropertyType::Int}}},
14✔
2981
    };
14✔
2982

2983
    struct Context : BindingContext {
14✔
2984
        size_t* change_count;
14✔
2985
        util::UniqueFunction<void()> did_change_fn;
14✔
2986
        util::UniqueFunction<void()> changes_available_fn;
14✔
2987

2988
        Context(size_t* out)
14✔
2989
            : change_count(out)
14✔
2990
        {
14✔
2991
        }
14✔
2992

2993
        void did_change(std::vector<ObserverState> const&, std::vector<void*> const&, bool) override
14✔
2994
        {
24✔
2995
            ++*change_count;
24✔
2996
            if (did_change_fn)
24✔
2997
                did_change_fn();
12✔
2998
        }
24✔
2999

3000
        void changes_available() override
14✔
3001
        {
14✔
3002
            if (changes_available_fn)
10✔
3003
                changes_available_fn();
2✔
3004
        }
10✔
3005
    };
14✔
3006

3007
    size_t change_count = 0;
14✔
3008
    auto realm = Realm::get_shared_realm(config);
14✔
3009
    realm->read_group();
14✔
3010
    auto context = new Context{&change_count};
14✔
3011
    realm->m_binding_context.reset(context);
14✔
3012
    realm->m_binding_context->realm = realm;
14✔
3013

3014
    SECTION("local notifications are sent synchronously") {
14✔
3015
        realm->begin_transaction();
2✔
3016
        REQUIRE(change_count == 0);
2!
3017
        realm->commit_transaction();
2✔
3018
        REQUIRE(change_count == 1);
2!
3019
    }
2✔
3020
#ifndef _WIN32
14✔
3021
    SECTION("remote notifications are sent asynchronously") {
14✔
3022
        auto r2 = Realm::get_shared_realm(config);
2✔
3023
        r2->begin_transaction();
2✔
3024
        r2->commit_transaction();
2✔
3025
        REQUIRE(change_count == 0);
2!
3026
        util::EventLoop::main().run_until([&] {
11✔
3027
            return change_count > 0;
11✔
3028
        });
11✔
3029
        REQUIRE(change_count == 1);
2!
3030
    }
2✔
3031

3032
    SECTION("notifications created in async transaction are sent synchronously") {
14✔
3033
        realm->async_begin_transaction([&] {
2✔
3034
            REQUIRE(change_count == 0);
2!
3035
            realm->async_commit_transaction();
2✔
3036
            REQUIRE(change_count == 1);
2!
3037
        });
2✔
3038
        REQUIRE(change_count == 0);
2!
3039
        util::EventLoop::main().run_until([&] {
23✔
3040
            return change_count > 0;
23✔
3041
        });
23✔
3042
        REQUIRE(change_count == 1);
2!
3043
        util::EventLoop::main().run_until([&] {
2,513✔
3044
            return !realm->has_pending_async_work();
2,513✔
3045
        });
2,513✔
3046
    }
2✔
3047
#endif
14✔
3048
    SECTION("refresh() from within changes_available() refreshes") {
14✔
3049
        context->changes_available_fn = [&] {
2✔
3050
            REQUIRE(realm->refresh());
2!
3051
        };
2✔
3052
        realm->set_auto_refresh(false);
2✔
3053

3054
        auto r2 = Realm::get_shared_realm(config);
2✔
3055
        r2->begin_transaction();
2✔
3056
        r2->commit_transaction();
2✔
3057
        realm->notify();
2✔
3058
        // Should return false as the realm was already advanced
3059
        REQUIRE_FALSE(realm->refresh());
2!
3060
    }
2✔
3061

3062
    SECTION("refresh() from within did_change() is a no-op") {
14✔
3063
        context->did_change_fn = [&] {
4✔
3064
            if (change_count > 1)
4✔
3065
                return;
2✔
3066

3067
            // Create another version so that refresh() advances the version
3068
            auto r2 = Realm::get_shared_realm(realm->config());
2✔
3069
            r2->begin_transaction();
2✔
3070
            r2->commit_transaction();
2✔
3071

3072
            REQUIRE_FALSE(realm->refresh());
2!
3073
        };
2✔
3074

3075
        auto r2 = Realm::get_shared_realm(config);
2✔
3076
        r2->begin_transaction();
2✔
3077
        r2->commit_transaction();
2✔
3078

3079
        REQUIRE(realm->refresh());
2!
3080
        REQUIRE(change_count == 1);
2!
3081

3082
        REQUIRE(realm->refresh());
2!
3083
        REQUIRE(change_count == 2);
2!
3084
        REQUIRE_FALSE(realm->refresh());
2!
3085
    }
2✔
3086

3087
    SECTION("begin_write() from within did_change() produces recursive notifications") {
14✔
3088
        context->did_change_fn = [&] {
8✔
3089
            if (realm->is_in_transaction())
8✔
3090
                realm->cancel_transaction();
6✔
3091
            if (change_count > 3)
8✔
3092
                return;
2✔
3093

3094
            // Create another version so that begin_write() advances the version
3095
            auto r2 = Realm::get_shared_realm(realm->config());
6✔
3096
            r2->begin_transaction();
6✔
3097
            r2->commit_transaction();
6✔
3098

3099
            realm->begin_transaction();
6✔
3100
            REQUIRE(change_count == 4);
6!
3101
        };
6✔
3102

3103
        auto r2 = Realm::get_shared_realm(config);
2✔
3104
        r2->begin_transaction();
2✔
3105
        r2->commit_transaction();
2✔
3106
        REQUIRE(realm->refresh());
2!
3107
        REQUIRE(change_count == 4);
2!
3108
        REQUIRE_FALSE(realm->refresh());
2!
3109
    }
2✔
3110

3111
#if REALM_ENABLE_SYNC
14✔
3112
    SECTION("SubscriptionStore writes produce notifications") {
14✔
3113
        auto subscription_store = sync::SubscriptionStore::create(TestHelper::get_db(realm));
2✔
3114
        REQUIRE(change_count == 0);
2!
3115
        util::EventLoop::main().run_until([&] {
11✔
3116
            return change_count > 0;
11✔
3117
        });
11✔
3118
        REQUIRE(change_count == 1);
2!
3119

3120
        subscription_store->get_active().make_mutable_copy().commit();
2✔
3121
        REQUIRE(change_count == 1);
2!
3122
        util::EventLoop::main().run_until([&] {
11✔
3123
            return change_count > 1;
11✔
3124
        });
11✔
3125
        REQUIRE(change_count == 2);
2!
3126
    }
2✔
3127
#endif
14✔
3128
}
14✔
3129

3130
TEST_CASE("SharedRealm: schema updating from external changes") {
14✔
3131
    TestFile config;
14✔
3132
    config.schema_version = 0;
14✔
3133
    config.schema_mode = SchemaMode::AdditiveExplicit;
14✔
3134
    config.schema = Schema{
14✔
3135
        {"object",
14✔
3136
         {
14✔
3137
             {"value", PropertyType::Int, Property::IsPrimary{true}},
14✔
3138
             {"value 2", PropertyType::Int, Property::IsPrimary{false}, Property::IsIndexed{true}},
14✔
3139
         }},
14✔
3140
    };
14✔
3141

3142
    SECTION("newly added columns update table columns but are not added to properties") {
14✔
3143
        // Does this test add any value when column keys are stable?
3144
        auto r1 = Realm::get_shared_realm(config);
4✔
3145
        auto r2 = Realm::get_shared_realm(config);
4✔
3146
        auto test = [&] {
4✔
3147
            r2->begin_transaction();
4✔
3148
            r2->read_group().get_table("class_object")->add_column(type_String, "new col");
4✔
3149
            r2->commit_transaction();
4✔
3150

3151
            auto& object_schema = *r1->schema().find("object");
4✔
3152
            REQUIRE(object_schema.persisted_properties.size() == 2);
4!
3153
            ColKey col = object_schema.persisted_properties[0].column_key;
4✔
3154
            r1->refresh();
4✔
3155
            REQUIRE(object_schema.persisted_properties[0].column_key == col);
4!
3156
        };
4✔
3157
        SECTION("with an active read transaction") {
4✔
3158
            r1->read_group();
2✔
3159
            test();
2✔
3160
        }
2✔
3161
        SECTION("without an active read transaction") {
4✔
3162
            r1->invalidate();
2✔
3163
            test();
2✔
3164
        }
2✔
3165
    }
4✔
3166

3167
    SECTION("beginning a read transaction checks for incompatible changes") {
14✔
3168
        auto r = Realm::get_shared_realm(config);
10✔
3169
        r->invalidate();
10✔
3170

3171
        auto& db = TestHelper::get_db(r);
10✔
3172
        WriteTransaction wt(db);
10✔
3173
        auto& table = *wt.get_table("class_object");
10✔
3174

3175
        SECTION("removing a property") {
10✔
3176
            table.remove_column(table.get_column_key("value"));
2✔
3177
            wt.commit();
2✔
3178
            REQUIRE_THROWS_CONTAINING(r->refresh(), "Property 'object.value' has been removed.");
2✔
3179
        }
2✔
3180

3181
        SECTION("change property type") {
10✔
3182
            table.remove_column(table.get_column_key("value 2"));
2✔
3183
            table.add_column(type_Float, "value 2");
2✔
3184
            wt.commit();
2✔
3185
            REQUIRE_THROWS_CONTAINING(r->refresh(),
2✔
3186
                                      "Property 'object.value 2' has been changed from 'int' to 'float'");
2✔
3187
        }
2✔
3188

3189
        SECTION("make property optional") {
10✔
3190
            table.remove_column(table.get_column_key("value 2"));
2✔
3191
            table.add_column(type_Int, "value 2", true);
2✔
3192
            wt.commit();
2✔
3193
            REQUIRE_THROWS_CONTAINING(r->refresh(), "Property 'object.value 2' has been made optional");
2✔
3194
        }
2✔
3195

3196
        SECTION("recreate column with no changes") {
10✔
3197
            table.remove_column(table.get_column_key("value 2"));
2✔
3198
            table.add_column(type_Int, "value 2");
2✔
3199
            wt.commit();
2✔
3200
            REQUIRE_NOTHROW(r->refresh());
2✔
3201
        }
2✔
3202

3203
        SECTION("remove index from non-PK") {
10✔
3204
            table.remove_search_index(table.get_column_key("value 2"));
2✔
3205
            wt.commit();
2✔
3206
            REQUIRE_NOTHROW(r->refresh());
2✔
3207
        }
2✔
3208
    }
10✔
3209
}
14✔
3210

3211
TEST_CASE("SharedRealm: close()") {
4✔
3212
    TestFile config;
4✔
3213
    config.schema_version = 1;
4✔
3214
    config.schema = Schema{
4✔
3215
        {"object", {{"value", PropertyType::Int}}},
4✔
3216
        {"list", {{"list", PropertyType::Object | PropertyType::Array, "object"}}},
4✔
3217
    };
4✔
3218

3219
    auto realm = Realm::get_shared_realm(config);
4✔
3220

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

3224
        realm->close();
2✔
3225
        REQUIRE(realm->is_closed());
2!
3226
        REQUIRE_EXCEPTION(realm->verify_open(), ClosedRealm, msg);
2✔
3227

3228
        REQUIRE_EXCEPTION(realm->update_schema(Schema{}), ClosedRealm, msg);
2✔
3229
        REQUIRE_EXCEPTION(realm->rename_property(Schema{}, "", "", ""), ClosedRealm, msg);
2✔
3230
        REQUIRE_EXCEPTION(realm->set_schema_subset(Schema{}), ClosedRealm, msg);
2✔
3231

3232
        REQUIRE_EXCEPTION(realm->begin_transaction(), ClosedRealm, msg);
2✔
3233
        REQUIRE_EXCEPTION(realm->commit_transaction(), ClosedRealm, msg);
2✔
3234
        REQUIRE_EXCEPTION(realm->cancel_transaction(), ClosedRealm, msg);
2✔
3235
        REQUIRE(!realm->is_in_transaction());
2!
3236

3237
        REQUIRE_EXCEPTION(realm->async_begin_transaction(nullptr), ClosedRealm, msg);
2✔
3238
        REQUIRE_EXCEPTION(realm->async_commit_transaction(nullptr), ClosedRealm, msg);
2✔
3239
        REQUIRE_EXCEPTION(realm->async_cancel_transaction(0), ClosedRealm, msg);
2✔
3240
        REQUIRE_FALSE(realm->is_in_async_transaction());
2!
3241

3242
        REQUIRE_EXCEPTION(realm->freeze(), ClosedRealm, msg);
2✔
3243
        REQUIRE_FALSE(realm->is_frozen());
2!
3244
        REQUIRE_EXCEPTION(realm->get_number_of_versions(), ClosedRealm, msg);
2✔
3245
        REQUIRE_EXCEPTION(realm->read_transaction_version(), ClosedRealm, msg);
2✔
3246
        REQUIRE_EXCEPTION(realm->duplicate(), ClosedRealm, msg);
2✔
3247

3248
        REQUIRE_EXCEPTION(realm->enable_wait_for_change(), ClosedRealm, msg);
2✔
3249
        REQUIRE_EXCEPTION(realm->wait_for_change(), ClosedRealm, msg);
2✔
3250
        REQUIRE_EXCEPTION(realm->wait_for_change_release(), ClosedRealm, msg);
2✔
3251

3252
        REQUIRE_NOTHROW(realm->notify());
2✔
3253
        REQUIRE_EXCEPTION(realm->refresh(), ClosedRealm, msg);
2✔
3254
        REQUIRE_EXCEPTION(realm->invalidate(), ClosedRealm, msg);
2✔
3255
        REQUIRE_EXCEPTION(realm->compact(), ClosedRealm, msg);
2✔
3256
        REQUIRE_EXCEPTION(realm->convert(realm->config()), ClosedRealm, msg);
2✔
3257
        REQUIRE_EXCEPTION(realm->write_copy(), ClosedRealm, msg);
2✔
3258

3259
#if REALM_ENABLE_SYNC
2✔
3260
        REQUIRE_FALSE(realm->sync_session());
2!
3261
        msg = "Flexible sync is not enabled";
2✔
3262
        REQUIRE_EXCEPTION(realm->get_latest_subscription_set(), IllegalOperation, msg);
2✔
3263
        REQUIRE_EXCEPTION(realm->get_active_subscription_set(), IllegalOperation, msg);
2✔
3264
#endif
2✔
3265
    }
2✔
3266

3267
    SECTION("fully closes database file even with live notifiers") {
4✔
3268
        auto& group = realm->read_group();
2✔
3269
        realm->begin_transaction();
2✔
3270
        auto obj = ObjectStore::table_for_object_type(group, "list")->create_object();
2✔
3271
        realm->commit_transaction();
2✔
3272

3273
        Results results(realm, ObjectStore::table_for_object_type(group, "object"));
2✔
3274
        List list(realm, obj.get_linklist("list"));
2✔
3275
        Object object(realm, obj);
2✔
3276

3277
        auto obj_token = object.add_notification_callback([](CollectionChangeSet) {});
2✔
3278
        auto list_token = list.add_notification_callback([](CollectionChangeSet) {});
2✔
3279
        auto results_token = results.add_notification_callback([](CollectionChangeSet) {});
2✔
3280

3281
        // Perform a dummy transaction to ensure the notifiers actually acquire
3282
        // resources that need to be closed
3283
        realm->begin_transaction();
2✔
3284
        realm->commit_transaction();
2✔
3285

3286
        realm->close();
2✔
3287

3288
        // Verify that we're able to acquire an exclusive lock
3289
        REQUIRE(DB::call_with_lock(config.path, [](auto) {}));
2!
3290
    }
2✔
3291
}
4✔
3292

3293
TEST_CASE("Realm::delete_files()") {
12✔
3294
    TestFile config;
12✔
3295
    config.schema_version = 1;
12✔
3296
    config.schema = Schema{{"object", {{"value", PropertyType::Int}}}};
12✔
3297
    auto realm = Realm::get_shared_realm(config);
12✔
3298
    auto path = config.path;
12✔
3299

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

3305
    SECTION("Deleting files of a closed Realm succeeds.") {
12✔
3306
        realm->close();
2✔
3307
        bool did_delete = false;
2✔
3308
        Realm::delete_files(path, &did_delete);
2✔
3309
        REQUIRE(did_delete);
2!
3310
        REQUIRE_FALSE(util::File::exists(path));
2!
3311
        REQUIRE_FALSE(util::File::exists(path + ".management"));
2!
3312
        REQUIRE_FALSE(util::File::exists(path + ".note"));
2!
3313
        REQUIRE_FALSE(util::File::exists(path + ".log"));
2!
3314

3315
        // Deleting the .lock file is not safe. It must still exist.
3316
        REQUIRE(util::File::exists(path + ".lock"));
2!
3317
    }
2✔
3318

3319
    SECTION("Trying to delete files of an open Realm fails.") {
12✔
3320
        REQUIRE_EXCEPTION(Realm::delete_files(path), ErrorCodes::DeleteOnOpenRealm,
2✔
3321
                          util::format("Cannot delete files of an open Realm: '%1' is still in use.", path));
2✔
3322
        REQUIRE(util::File::exists(path + ".lock"));
2!
3323
        REQUIRE(util::File::exists(path));
2!
3324
        REQUIRE(util::File::exists(path + ".management"));
2!
3325
#ifndef _WIN32
2✔
3326
        REQUIRE(util::File::exists(path + ".note"));
2!
3327
#endif
2✔
3328
        REQUIRE(util::File::exists(path + ".log"));
2!
3329
    }
2✔
3330

3331
    SECTION("Deleting the same Realm multiple times.") {
12✔
3332
        realm->close();
2✔
3333
        Realm::delete_files(path);
2✔
3334
        Realm::delete_files(path);
2✔
3335
        Realm::delete_files(path);
2✔
3336
    }
2✔
3337

3338
    SECTION("Calling delete on a folder that does not exist.") {
12✔
3339
        auto fake_path = "/tmp/doesNotExist/realm.424242";
2✔
3340
        bool did_delete = false;
2✔
3341
        Realm::delete_files(fake_path, &did_delete);
2✔
3342
        REQUIRE_FALSE(did_delete);
2!
3343
    }
2✔
3344

3345
    SECTION("passing did_delete is optional") {
12✔
3346
        realm->close();
2✔
3347
        Realm::delete_files(path, nullptr);
2✔
3348
    }
2✔
3349

3350
    SECTION("Deleting a Realm which does not exist does not set did_delete") {
12✔
3351
        TestFile new_config;
2✔
3352
        bool did_delete = false;
2✔
3353
        Realm::delete_files(new_config.path, &did_delete);
2✔
3354
        REQUIRE_FALSE(did_delete);
2!
3355
    }
2✔
3356
}
12✔
3357

3358
TEST_CASE("ShareRealm: in-memory mode from buffer") {
2✔
3359
    TestFile config;
2✔
3360
    config.schema_version = 1;
2✔
3361
    config.schema = Schema{
2✔
3362
        {"object", {{"value", PropertyType::Int}}},
2✔
3363
    };
2✔
3364

3365
    SECTION("Save and open Realm from in-memory buffer") {
2✔
3366
        // Write in-memory copy of Realm to a buffer
3367
        auto realm = Realm::get_shared_realm(config);
2✔
3368
        OwnedBinaryData realm_buffer = realm->write_copy();
2✔
3369

3370
        // Open the buffer as a new (immutable in-memory) Realm
3371
        realm::Realm::Config config2;
2✔
3372
        config2.in_memory = true;
2✔
3373
        config2.schema_mode = SchemaMode::Immutable;
2✔
3374
        config2.realm_data = realm_buffer.get();
2✔
3375

3376
        auto realm2 = Realm::get_shared_realm(config2);
2✔
3377

3378
        // Verify that it can read the schema and that it is the same
3379
        REQUIRE(realm->schema().size() == 1);
2!
3380
        auto it = realm->schema().find("object");
2✔
3381
        auto table = realm->read_group().get_table("class_object");
2✔
3382
        REQUIRE(it != realm->schema().end());
2!
3383
        REQUIRE(it->table_key == table->get_key());
2!
3384
        REQUIRE(it->persisted_properties.size() == 1);
2!
3385
        REQUIRE(it->persisted_properties[0].name == "value");
2!
3386
        REQUIRE(it->persisted_properties[0].column_key == table->get_column_key("value"));
2!
3387

3388
        // Test invalid configs
3389
        realm::Realm::Config config3;
2✔
3390
        config3.realm_data = realm_buffer.get();
2✔
3391
        REQUIRE_EXCEPTION(Realm::get_shared_realm(config3), IllegalCombination,
2✔
3392
                          "In-memory realms initialized from memory buffers can only be opened in read-only mode");
2✔
3393

3394
        config3.in_memory = true;
2✔
3395
        config3.schema_mode = SchemaMode::Immutable;
2✔
3396
        config3.path = "path";
2✔
3397
        REQUIRE_EXCEPTION(Realm::get_shared_realm(config3), IllegalCombination,
2✔
3398
                          "Specifying both memory buffer and path is invalid");
2✔
3399

3400
        config3.path = "";
2✔
3401
        config3.encryption_key = std::vector<char>(64, 'a');
2✔
3402
        REQUIRE_EXCEPTION(Realm::get_shared_realm(config3), IllegalCombination,
2✔
3403
                          "Memory buffers do not support encryption");
2✔
3404
    }
2✔
3405
}
2✔
3406

3407
TEST_CASE("ShareRealm: realm closed in did_change callback") {
6✔
3408
    TestFile config;
6✔
3409
    config.schema_version = 1;
6✔
3410
    config.schema = Schema{
6✔
3411
        {"object", {{"value", PropertyType::Int}}},
6✔
3412
    };
6✔
3413
    config.automatic_change_notifications = false;
6✔
3414
    auto r1 = Realm::get_shared_realm(config);
6✔
3415

3416
    r1->begin_transaction();
6✔
3417
    auto table = r1->read_group().get_table("class_object");
6✔
3418
    table->create_object();
6✔
3419
    r1->commit_transaction();
6✔
3420

3421
    struct Context : public BindingContext {
6✔
3422
        Context(std::shared_ptr<Realm>& realm)
6✔
3423
            : realm(&realm)
6✔
3424
        {
6✔
3425
        }
6✔
3426
        std::shared_ptr<Realm>* realm;
6✔
3427
        void did_change(std::vector<ObserverState> const&, std::vector<void*> const&, bool) override
6✔
3428
        {
6✔
3429
            auto realm = this->realm; // close() will delete `this`
6✔
3430
            (*realm)->close();
6✔
3431
            realm->reset();
6✔
3432
        }
6✔
3433
    };
6✔
3434

3435
    SECTION("did_change") {
6✔
3436
        r1->m_binding_context.reset(new Context(r1));
2✔
3437
        r1->invalidate();
2✔
3438

3439
        auto r2 = Realm::get_shared_realm(config);
2✔
3440
        r2->begin_transaction();
2✔
3441
        r2->read_group().get_table("class_object")->create_object();
2✔
3442
        r2->commit_transaction();
2✔
3443
        r2.reset();
2✔
3444

3445
        r1->notify();
2✔
3446
    }
2✔
3447

3448
    SECTION("did_change with async results") {
6✔
3449
        r1->m_binding_context.reset(new Context(r1));
2✔
3450
        Results results(r1, table->where());
2✔
3451
        auto token = results.add_notification_callback([&](CollectionChangeSet) {
2✔
3452
            // Should not be called.
UNCOV
3453
            REQUIRE(false);
×
UNCOV
3454
        });
×
3455

3456
        auto r2 = Realm::get_shared_realm(config);
2✔
3457
        r2->begin_transaction();
2✔
3458
        r2->read_group().get_table("class_object")->create_object();
2✔
3459
        r2->commit_transaction();
2✔
3460
        r2.reset();
2✔
3461

3462
        auto coordinator = _impl::RealmCoordinator::get_coordinator(config.path);
2✔
3463
        coordinator->on_change();
2✔
3464

3465
        r1->notify();
2✔
3466
    }
2✔
3467

3468
    SECTION("refresh") {
6✔
3469
        r1->m_binding_context.reset(new Context(r1));
2✔
3470

3471
        auto r2 = Realm::get_shared_realm(config);
2✔
3472
        r2->begin_transaction();
2✔
3473
        r2->read_group().get_table("class_object")->create_object();
2✔
3474
        r2->commit_transaction();
2✔
3475
        r2.reset();
2✔
3476

3477
        REQUIRE_FALSE(r1->refresh());
2!
3478
    }
2✔
3479
}
6✔
3480

3481
TEST_CASE("RealmCoordinator: schema cache") {
16✔
3482
    TestFile config;
16✔
3483
    auto coordinator = _impl::RealmCoordinator::get_coordinator(config.path);
16✔
3484

3485
    Schema cache_schema;
16✔
3486
    uint64_t cache_sv = -1, cache_tv = -1;
16✔
3487

3488
    Schema schema{
16✔
3489
        {"object", {{"value", PropertyType::Int}}},
16✔
3490
    };
16✔
3491
    Schema schema2{
16✔
3492
        {"object",
16✔
3493
         {
16✔
3494
             {"value", PropertyType::Int},
16✔
3495
         }},
16✔
3496
        {"object 2",
16✔
3497
         {
16✔
3498
             {"value", PropertyType::Int},
16✔
3499
         }},
16✔
3500
    };
16✔
3501

3502
    SECTION("valid initial schema sets cache") {
16✔
3503
        coordinator->cache_schema(schema, 5, 10);
2✔
3504
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3505
        REQUIRE(cache_schema == schema);
2!
3506
        REQUIRE(cache_sv == 5);
2!
3507
        REQUIRE(cache_tv == 10);
2!
3508
    }
2✔
3509

3510
    SECTION("cache can be updated with newer schema") {
16✔
3511
        coordinator->cache_schema(schema, 5, 10);
2✔
3512
        coordinator->cache_schema(schema2, 6, 11);
2✔
3513
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3514
        REQUIRE(cache_schema == schema2);
2!
3515
        REQUIRE(cache_sv == 6);
2!
3516
        REQUIRE(cache_tv == 11);
2!
3517
    }
2✔
3518

3519
    SECTION("empty schema is ignored") {
16✔
3520
        coordinator->cache_schema(Schema{}, 5, 10);
2✔
3521
        REQUIRE_FALSE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3522

3523
        coordinator->cache_schema(schema, 5, 10);
2✔
3524
        coordinator->cache_schema(Schema{}, 5, 10);
2✔
3525
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3526
        REQUIRE(cache_schema == schema);
2!
3527
        REQUIRE(cache_sv == 5);
2!
3528
        REQUIRE(cache_tv == 10);
2!
3529
    }
2✔
3530

3531
    SECTION("schema for older transaction is ignored") {
16✔
3532
        coordinator->cache_schema(schema, 5, 10);
2✔
3533
        coordinator->cache_schema(schema2, 4, 8);
2✔
3534

3535
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3536
        REQUIRE(cache_schema == schema);
2!
3537
        REQUIRE(cache_sv == 5);
2!
3538
        REQUIRE(cache_tv == 10);
2!
3539

3540
        coordinator->advance_schema_cache(10, 20);
2✔
3541
        coordinator->cache_schema(schema, 6, 15);
2✔
3542
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3543
        REQUIRE(cache_tv == 20); // should not have dropped to 15
2!
3544
    }
2✔
3545

3546
    SECTION("advance_schema() from transaction version bumps transaction version") {
16✔
3547
        coordinator->cache_schema(schema, 5, 10);
2✔
3548
        coordinator->advance_schema_cache(10, 12);
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 == 12);
2!
3553
    }
2✔
3554

3555
    SECTION("advance_schema() ending before transaction version does nothing") {
16✔
3556
        coordinator->cache_schema(schema, 5, 10);
2✔
3557
        coordinator->advance_schema_cache(8, 9);
2✔
3558
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3559
        REQUIRE(cache_schema == schema);
2!
3560
        REQUIRE(cache_sv == 5);
2!
3561
        REQUIRE(cache_tv == 10);
2!
3562
    }
2✔
3563

3564
    SECTION("advance_schema() extending over transaction version bumps version") {
16✔
3565
        coordinator->cache_schema(schema, 5, 10);
2✔
3566
        coordinator->advance_schema_cache(3, 15);
2✔
3567
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3568
        REQUIRE(cache_schema == schema);
2!
3569
        REQUIRE(cache_sv == 5);
2!
3570
        REQUIRE(cache_tv == 15);
2!
3571
    }
2✔
3572

3573
    SECTION("advance_schema() with no cahced schema does nothing") {
16✔
3574
        coordinator->advance_schema_cache(3, 15);
2✔
3575
        REQUIRE_FALSE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3576
    }
2✔
3577
}
16✔
3578

3579
TEST_CASE("SharedRealm: coordinator schema cache") {
26✔
3580
    TestFile config;
26✔
3581
    auto r = Realm::get_shared_realm(config);
26✔
3582
    auto coordinator = _impl::RealmCoordinator::get_coordinator(config.path);
26✔
3583

3584
    Schema cache_schema;
26✔
3585
    uint64_t cache_sv = -1, cache_tv = -1;
26✔
3586

3587
    Schema schema{
26✔
3588
        {"object", {{"value", PropertyType::Int}}},
26✔
3589
    };
26✔
3590
    Schema schema2{
26✔
3591
        {"object",
26✔
3592
         {
26✔
3593
             {"value", PropertyType::Int},
26✔
3594
         }},
26✔
3595
        {"object 2",
26✔
3596
         {
26✔
3597
             {"value", PropertyType::Int},
26✔
3598
         }},
26✔
3599
    };
26✔
3600

3601
    class ExternalWriter {
26✔
3602
    private:
26✔
3603
        std::shared_ptr<Realm> m_realm;
26✔
3604

3605
    public:
26✔
3606
        WriteTransaction wt;
26✔
3607
        ExternalWriter(Realm::Config const& config)
26✔
3608
            : m_realm([&] {
26✔
3609
                auto c = config;
18✔
3610
                c.scheduler = util::Scheduler::make_frozen(VersionID());
18✔
3611
                return _impl::RealmCoordinator::get_coordinator(c.path)->get_realm(c, util::none);
18✔
3612
            }())
18✔
3613
            , wt(TestHelper::get_db(m_realm))
26✔
3614
        {
26✔
3615
        }
18✔
3616
    };
26✔
3617

3618
    auto external_write = [&](Realm::Config const& config, auto&& fn) {
26✔
3619
        ExternalWriter wt(config);
16✔
3620
        fn(wt.wt);
16✔
3621
        wt.wt.commit();
16✔
3622
    };
16✔
3623

3624
    SECTION("is initially empty for uninitialized file") {
26✔
3625
        REQUIRE_FALSE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3626
    }
2✔
3627
    r->update_schema(schema);
26✔
3628

3629
    SECTION("is populated after calling update_schema()") {
26✔
3630
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3631
        REQUIRE(cache_sv == 0);
2!
3632
        REQUIRE(cache_schema == schema);
2!
3633
        REQUIRE(cache_schema.begin()->persisted_properties[0].column_key != ColKey{});
2!
3634
    }
2✔
3635

3636
    coordinator = nullptr;
26✔
3637
    r = nullptr;
26✔
3638
    r = Realm::get_shared_realm(config);
26✔
3639
    coordinator = _impl::RealmCoordinator::get_coordinator(config.path);
26✔
3640
    REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
26!
3641

3642
    SECTION("is populated after opening an initialized file") {
26✔
3643
        REQUIRE(cache_sv == 0);
2!
3644
        REQUIRE(cache_tv == 2); // with in-realm history the version doesn't reset
2!
3645
        REQUIRE(cache_schema == schema);
2!
3646
        REQUIRE(cache_schema.begin()->persisted_properties[0].column_key != ColKey{});
2!
3647
    }
2✔
3648

3649
    SECTION("transaction version is bumped after a local write") {
26✔
3650
        auto tv = cache_tv;
2✔
3651
        r->begin_transaction();
2✔
3652
        r->commit_transaction();
2✔
3653
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3654
        REQUIRE(cache_tv == tv + 1);
2!
3655
    }
2✔
3656

3657
    SECTION("notify() without a read transaction does not bump transaction version") {
26✔
3658
        auto tv = cache_tv;
4✔
3659

3660
        SECTION("non-schema change") {
4✔
3661
            external_write(config, [](auto& wt) {
2✔
3662
                wt.get_table("class_object")->create_object();
2✔
3663
            });
2✔
3664
        }
2✔
3665
        SECTION("schema change") {
4✔
3666
            external_write(config, [](auto& wt) {
2✔
3667
                wt.add_table("class_object 2");
2✔
3668
            });
2✔
3669
        }
2✔
3670

3671
        r->notify();
4✔
3672
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
4!
3673
        REQUIRE(cache_tv == tv);
4!
3674
        REQUIRE(cache_schema == schema);
4!
3675
    }
4✔
3676

3677
    SECTION("notify() with a read transaction bumps transaction version") {
26✔
3678
        r->read_group();
2✔
3679
        external_write(config, [](auto& wt) {
2✔
3680
            wt.get_table("class_object")->create_object();
2✔
3681
        });
2✔
3682

3683
        r->notify();
2✔
3684
        auto tv = cache_tv;
2✔
3685
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3686
        REQUIRE(cache_tv == tv + 1);
2!
3687
    }
2✔
3688

3689
    SECTION("notify() with a read transaction updates schema folloing external schema change") {
26✔
3690
        r->read_group();
2✔
3691
        external_write(config, [](auto& wt) {
2✔
3692
            wt.add_table("class_object 2");
2✔
3693
        });
2✔
3694

3695
        r->notify();
2✔
3696
        auto tv = cache_tv;
2✔
3697
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3698
        REQUIRE(cache_tv == tv + 1);
2!
3699
        REQUIRE(cache_schema.size() == 2);
2!
3700
        REQUIRE(cache_schema.find("object 2") != cache_schema.end());
2!
3701
    }
2✔
3702

3703
    SECTION("transaction version is bumped after refresh() following external non-schema write") {
26✔
3704
        external_write(config, [](auto& wt) {
2✔
3705
            wt.get_table("class_object")->create_object();
2✔
3706
        });
2✔
3707

3708
        r->refresh();
2✔
3709
        auto tv = cache_tv;
2✔
3710
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3711
        REQUIRE(cache_tv == tv + 1);
2!
3712
    }
2✔
3713

3714
    SECTION("schema is reread following refresh() over external schema change") {
26✔
3715
        external_write(config, [](auto& wt) {
2✔
3716
            wt.add_table("class_object 2");
2✔
3717
        });
2✔
3718

3719
        r->refresh();
2✔
3720
        auto tv = cache_tv;
2✔
3721
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3722
        REQUIRE(cache_tv == tv + 1);
2!
3723
        REQUIRE(cache_schema.size() == 2);
2!
3724
        REQUIRE(cache_schema.find("object 2") != cache_schema.end());
2!
3725
    }
2✔
3726

3727
    SECTION("update_schema() to version already on disk updates cache") {
26✔
3728
        r->read_group();
2✔
3729
        external_write(config, [](auto& wt) {
2✔
3730
            auto table = wt.add_table("class_object 2");
2✔
3731
            table->add_column(type_Int, "value");
2✔
3732
        });
2✔
3733

3734
        auto tv = cache_tv;
2✔
3735
        r->update_schema(schema2);
2✔
3736

3737
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3738
        REQUIRE(cache_tv == tv + 1); // only +1 because update_schema() did not perform a write
2!
3739
        REQUIRE(cache_schema.size() == 2);
2!
3740
        REQUIRE(cache_schema.find("object 2") != cache_schema.end());
2!
3741
    }
2✔
3742

3743
    SECTION("update_schema() to version already on disk updates cache") {
26✔
3744
        r->read_group();
2✔
3745
        external_write(config, [](auto& wt) {
2✔
3746
            auto table = wt.add_table("class_object 2");
2✔
3747
            table->add_column(type_Int, "value");
2✔
3748
        });
2✔
3749

3750
        auto tv = cache_tv;
2✔
3751
        r->update_schema(schema2);
2✔
3752

3753
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3754
        REQUIRE(cache_tv == tv + 1); // only +1 because update_schema() did not perform a write
2!
3755
        REQUIRE(cache_schema.size() == 2);
2!
3756
        REQUIRE(cache_schema.find("object 2") != cache_schema.end());
2!
3757
    }
2✔
3758

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

3762
        // We want to commit the write while we're waiting on the write lock on
3763
        // this thread, which can't really be done in a properly synchronized manner
3764
        std::chrono::microseconds wait_time{5000};
2✔
3765
#if REALM_ANDROID
3766
        // When running on device or in an emulator we need to wait longer due
3767
        // to them being slow
3768
        wait_time *= 10;
3769
#endif
3770

3771
        bool did_run = false;
2✔
3772
        JoiningThread thread([&] {
2✔
3773
            ExternalWriter writer(config);
2✔
3774
            if (writer.wt.get_table("class_object 2"))
2✔
UNCOV
3775
                return;
×
3776
            did_run = true;
2✔
3777

3778
            auto table = writer.wt.add_table("class_object 2");
2✔
3779
            table->add_column(type_Int, "value");
2✔
3780
            std::this_thread::sleep_for(wait_time * 2);
2✔
3781
            writer.wt.commit();
2✔
3782
        });
2✔
3783
        std::this_thread::sleep_for(wait_time);
2✔
3784

3785
        auto tv = cache_tv;
2✔
3786
        r->update_schema(Schema{
2✔
3787
            {"object", {{"value", PropertyType::Int}}},
2✔
3788
            {"object 2", {{"value", PropertyType::Int}}},
2✔
3789
        });
2✔
3790

3791
        // just skip the test if the timing was wrong to avoid spurious failures
3792
        if (!did_run)
2✔
UNCOV
3793
            return;
×
3794

3795
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3796
        REQUIRE(cache_tv == tv + 1); // only +1 because update_schema()'s write was rolled back
2!
3797
        REQUIRE(cache_schema.size() == 2);
2!
3798
        REQUIRE(cache_schema.find("object 2") != cache_schema.end());
2!
3799
    }
2✔
3800
}
26✔
3801

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

3805
    // Prepopulate the Realm with the schema.
3806
    Realm::Config config_with_schema = config;
2✔
3807
    config_with_schema.schema_version = 1;
2✔
3808
    config_with_schema.schema_mode = SchemaMode::Automatic;
2✔
3809
    config_with_schema.schema =
2✔
3810
        Schema{{"object",
2✔
3811
                {
2✔
3812
                    {"value", PropertyType::Int, Property::IsPrimary{true}},
2✔
3813
                    {"value 2", PropertyType::Int, Property::IsPrimary{false}, Property::IsIndexed{true}},
2✔
3814
                }}};
2✔
3815
    auto r1 = Realm::get_shared_realm(config_with_schema);
2✔
3816

3817
    // Retrieve the object schema in dynamic mode.
3818
    auto r2 = Realm::get_shared_realm(config);
2✔
3819
    auto* object_schema = &*r2->schema().find("object");
2✔
3820

3821
    // Perform an empty write to create a new version, resulting in the other Realm needing to re-read the schema.
3822
    r1->begin_transaction();
2✔
3823
    r1->commit_transaction();
2✔
3824

3825
    // Advance to the latest version, and verify the object schema is at the same location in memory.
3826
    r2->read_group();
2✔
3827
    REQUIRE(object_schema == &*r2->schema().find("object"));
2!
3828
}
2✔
3829

3830
TEST_CASE("SharedRealm: declaring an object as embedded results in creating an embedded table") {
2✔
3831
    TestFile config;
2✔
3832

3833
    // Prepopulate the Realm with the schema.
3834
    config.schema = Schema{{"object1",
2✔
3835
                            ObjectSchema::ObjectType::Embedded,
2✔
3836
                            {
2✔
3837
                                {"value", PropertyType::Int},
2✔
3838
                            }},
2✔
3839
                           {"object2",
2✔
3840
                            {
2✔
3841
                                {"value", PropertyType::Object | PropertyType::Nullable, "object1"},
2✔
3842
                            }}};
2✔
3843
    auto r1 = Realm::get_shared_realm(config);
2✔
3844

3845
    Group& g = r1->read_group();
2✔
3846
    auto t = g.get_table("class_object1");
2✔
3847
    REQUIRE(t->is_embedded());
2!
3848
}
2✔
3849

3850
TEST_CASE("SharedRealm: SchemaChangedFunction") {
16✔
3851
    struct Context : BindingContext {
16✔
3852
        size_t* change_count;
16✔
3853
        Schema* schema;
16✔
3854
        Context(size_t* count_out, Schema* schema_out)
16✔
3855
            : change_count(count_out)
19✔
3856
            , schema(schema_out)
19✔
3857
        {
22✔
3858
        }
22✔
3859

3860
        void schema_did_change(Schema const& changed_schema) override
16✔
3861
        {
16✔
3862
            ++*change_count;
10✔
3863
            *schema = changed_schema;
10✔
3864
        }
10✔
3865
    };
16✔
3866

3867
    size_t schema_changed_called = 0;
16✔
3868
    Schema changed_fixed_schema;
16✔
3869
    TestFile config;
16✔
3870
    RealmConfig dynamic_config = config;
16✔
3871

3872
    config.schema = Schema{{"object1",
16✔
3873
                            {
16✔
3874
                                {"value", PropertyType::Int},
16✔
3875
                            }},
16✔
3876
                           {"object2",
16✔
3877
                            {
16✔
3878
                                {"value", PropertyType::Int},
16✔
3879
                            }}};
16✔
3880
    config.schema_version = 1;
16✔
3881
    auto r1 = Realm::get_shared_realm(config);
16✔
3882
    r1->read_group();
16✔
3883
    r1->m_binding_context.reset(new Context(&schema_changed_called, &changed_fixed_schema));
16✔
3884

3885
    SECTION("Fixed schema") {
16✔
3886
        SECTION("update_schema") {
10✔
3887
            auto new_schema = Schema{{"object3",
2✔
3888
                                      {
2✔
3889
                                          {"value", PropertyType::Int},
2✔
3890
                                      }}};
2✔
3891
            r1->update_schema(new_schema, 2);
2✔
3892
            REQUIRE(schema_changed_called == 1);
2!
3893
            REQUIRE(changed_fixed_schema.find("object3")->property_for_name("value")->column_key != ColKey{});
2!
3894
        }
2✔
3895

3896
        SECTION("Open a new Realm instance with same config won't trigger") {
10✔
3897
            auto r2 = Realm::get_shared_realm(config);
2✔
3898
            REQUIRE(schema_changed_called == 0);
2!
3899
        }
2✔
3900

3901
        SECTION("Non schema related transaction doesn't trigger") {
10✔
3902
            auto r2 = Realm::get_shared_realm(config);
2✔
3903
            r2->begin_transaction();
2✔
3904
            r2->commit_transaction();
2✔
3905
            r1->refresh();
2✔
3906
            REQUIRE(schema_changed_called == 0);
2!
3907
        }
2✔
3908

3909
        SECTION("Schema is changed by another Realm") {
10✔
3910
            auto r2 = Realm::get_shared_realm(config);
2✔
3911
            r2->begin_transaction();
2✔
3912
            r2->read_group().get_table("class_object1")->add_column(type_String, "new col");
2✔
3913
            r2->commit_transaction();
2✔
3914
            r1->refresh();
2✔
3915
            REQUIRE(schema_changed_called == 1);
2!
3916
            REQUIRE(changed_fixed_schema.find("object1")->property_for_name("value")->column_key != ColKey{});
2!
3917
        }
2✔
3918

3919
        // This is not a valid use case. m_schema won't be refreshed.
3920
        SECTION("Schema is changed by this Realm won't trigger") {
10✔
3921
            r1->begin_transaction();
2✔
3922
            r1->read_group().get_table("class_object1")->add_column(type_String, "new col");
2✔
3923
            r1->commit_transaction();
2✔
3924
            REQUIRE(schema_changed_called == 0);
2!
3925
        }
2✔
3926
    }
10✔
3927

3928
    SECTION("Dynamic schema") {
16✔
3929
        size_t dynamic_schema_changed_called = 0;
6✔
3930
        Schema changed_dynamic_schema;
6✔
3931
        auto r2 = Realm::get_shared_realm(dynamic_config);
6✔
3932
        r2->m_binding_context.reset(new Context(&dynamic_schema_changed_called, &changed_dynamic_schema));
6✔
3933

3934
        SECTION("set_schema_subset") {
6✔
3935
            auto new_schema = Schema{{"object1",
2✔
3936
                                      {
2✔
3937
                                          {"value", PropertyType::Int},
2✔
3938
                                      }}};
2✔
3939
            r2->set_schema_subset(new_schema);
2✔
3940
            REQUIRE(schema_changed_called == 0);
2!
3941
            REQUIRE(dynamic_schema_changed_called == 1);
2!
3942
            REQUIRE(changed_dynamic_schema.find("object1")->property_for_name("value")->column_key != ColKey{});
2!
3943
        }
2✔
3944

3945
        SECTION("Non schema related transaction will always trigger in dynamic mode") {
6✔
3946
            auto r1 = Realm::get_shared_realm(config);
2✔
3947
            // An empty transaction will trigger the schema changes always in dynamic mode.
3948
            r1->begin_transaction();
2✔
3949
            r1->commit_transaction();
2✔
3950
            r2->refresh();
2✔
3951
            REQUIRE(dynamic_schema_changed_called == 1);
2!
3952
            REQUIRE(changed_dynamic_schema.find("object1")->property_for_name("value")->column_key != ColKey{});
2!
3953
        }
2✔
3954

3955
        SECTION("Schema is changed by another Realm") {
6✔
3956
            r1->begin_transaction();
2✔
3957
            r1->read_group().get_table("class_object1")->add_column(type_String, "new col");
2✔
3958
            r1->commit_transaction();
2✔
3959
            r2->refresh();
2✔
3960
            REQUIRE(dynamic_schema_changed_called == 1);
2!
3961
            REQUIRE(changed_dynamic_schema.find("object1")->property_for_name("value")->column_key != ColKey{});
2!
3962
        }
2✔
3963
    }
6✔
3964
}
16✔
3965

3966
TEST_CASE("SharedRealm: compact on launch") {
4✔
3967
    // Make compactable Realm
3968
    TestFile config;
4✔
3969
    config.automatic_change_notifications = false;
4✔
3970
    int num_opens = 0;
4✔
3971
    config.should_compact_on_launch_function = [&](uint64_t total_bytes, uint64_t used_bytes) {
12✔
3972
        REQUIRE(total_bytes > used_bytes);
12!
3973
        num_opens++;
12✔
3974
        return num_opens != 2;
12✔
3975
    };
12✔
3976
    config.schema = Schema{
4✔
3977
        {"object", {{"value", PropertyType::String}}},
4✔
3978
    };
4✔
3979
    REQUIRE(num_opens == 0);
4!
3980
    auto r = Realm::get_shared_realm(config);
4✔
3981
    REQUIRE(num_opens == 1);
4!
3982
    r->begin_transaction();
4✔
3983
    auto table = r->read_group().get_table("class_object");
4✔
3984
    size_t count = 1000;
4✔
3985
    for (size_t i = 0; i < count; ++i)
4,004✔
3986
        table->create_object().set_all(util::format("Foo_%1", i % 10).c_str());
4,000✔
3987
    r->commit_transaction();
4✔
3988
    REQUIRE(table->size() == count);
4!
3989
    r->close();
4✔
3990

3991
    SECTION("compact reduces the file size") {
4✔
3992
#ifndef _WIN32
2✔
3993
        // Confirm expected sizes before and after opening the Realm
3994
        size_t size_before = size_t(util::File(config.path).get_size());
2✔
3995
        r = Realm::get_shared_realm(config);
2✔
3996
        REQUIRE(num_opens == 2);
2!
3997
        r->close();
2✔
3998
        REQUIRE(size_t(util::File(config.path).get_size()) == size_before); // File size after returning false
2!
3999
        r = Realm::get_shared_realm(config);
2✔
4000
        REQUIRE(num_opens == 3);
2!
4001
        REQUIRE(size_t(util::File(config.path).get_size()) < size_before); // File size after returning true
2!
4002

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

4006
        // Registering for a collection notification shouldn't crash when compact on launch is used.
4007
        Results results(r, r->read_group().get_table("class_object"));
2✔
4008
        results.add_notification_callback([](CollectionChangeSet const&) {});
2✔
4009
        r->close();
2✔
4010
#endif
2✔
4011
    }
2✔
4012

4013
    SECTION("compact function does not get invoked if realm is open on another thread") {
4✔
4014
        config.scheduler = util::Scheduler::make_frozen(VersionID());
2✔
4015
        r = Realm::get_shared_realm(config);
2✔
4016
        REQUIRE(num_opens == 2);
2!
4017
        JoiningThread([&] {
2✔
4018
            auto r2 = Realm::get_shared_realm(config);
2✔
4019
            REQUIRE(num_opens == 2);
2!
4020
        });
2✔
4021
        r->close();
2✔
4022
        JoiningThread([&] {
2✔
4023
            auto r3 = Realm::get_shared_realm(config);
2✔
4024
            REQUIRE(num_opens == 3);
2!
4025
        });
2✔
4026
    }
2✔
4027
}
4✔
4028

4029
struct ModeAutomatic {
4030
    static constexpr SchemaMode mode = SchemaMode::Automatic;
4031
    static constexpr bool should_call_init_on_version_bump = false;
4032
};
4033
struct ModeAdditive {
4034
    static constexpr SchemaMode mode = SchemaMode::AdditiveExplicit;
4035
    static constexpr bool should_call_init_on_version_bump = false;
4036
};
4037
struct ModeManual {
4038
    static constexpr SchemaMode mode = SchemaMode::Manual;
4039
    static constexpr bool should_call_init_on_version_bump = false;
4040
};
4041
struct ModeSoftResetFile {
4042
    static constexpr SchemaMode mode = SchemaMode::SoftResetFile;
4043
    static constexpr bool should_call_init_on_version_bump = true;
4044
};
4045
struct ModeHardResetFile {
4046
    static constexpr SchemaMode mode = SchemaMode::HardResetFile;
4047
    static constexpr bool should_call_init_on_version_bump = true;
4048
};
4049

4050
TEMPLATE_TEST_CASE("SharedRealm: update_schema with initialization_function", "[init][update schema]", ModeAutomatic,
4051
                   ModeAdditive, ModeManual, ModeSoftResetFile, ModeHardResetFile)
4052
{
30✔
4053
    TestFile config;
30✔
4054
    config.schema_mode = TestType::mode;
30✔
4055
    bool initialization_function_called = false;
30✔
4056
    uint64_t schema_version_in_callback = -1;
30✔
4057
    Schema schema_in_callback;
30✔
4058
    auto initialization_function = [&initialization_function_called, &schema_version_in_callback,
30✔
4059
                                    &schema_in_callback](auto shared_realm) {
30✔
4060
        REQUIRE(shared_realm->is_in_transaction());
24!
4061
        initialization_function_called = true;
24✔
4062
        schema_version_in_callback = shared_realm->schema_version();
24✔
4063
        schema_in_callback = shared_realm->schema();
24✔
4064
    };
24✔
4065

4066
    Schema schema{
30✔
4067
        {"object", {{"value", PropertyType::String}}},
30✔
4068
    };
30✔
4069

4070
    SECTION("call initialization function directly by update_schema") {
30✔
4071
        // Open in dynamic mode with no schema specified
4072
        auto realm = Realm::get_shared_realm(config);
10✔
4073
        REQUIRE_FALSE(initialization_function_called);
10!
4074

4075
        realm->update_schema(schema, 0, nullptr, initialization_function);
10✔
4076
        REQUIRE(initialization_function_called);
10!
4077
        REQUIRE(schema_version_in_callback == 0);
10!
4078
        REQUIRE(schema_in_callback.compare(schema).size() == 0);
10!
4079
    }
10✔
4080

4081
    config.schema_version = 0;
30✔
4082
    config.schema = schema;
30✔
4083

4084
    SECTION("initialization function should be called for unversioned realm") {
30✔
4085
        config.initialization_function = initialization_function;
10✔
4086
        Realm::get_shared_realm(config);
10✔
4087
        REQUIRE(initialization_function_called);
10!
4088
        REQUIRE(schema_version_in_callback == 0);
10!
4089
        REQUIRE(schema_in_callback.compare(schema).size() == 0);
10!
4090
    }
10✔
4091

4092
    SECTION("initialization function for versioned realm") {
30✔
4093
        // Initialize v0
4094
        Realm::get_shared_realm(config);
10✔
4095

4096
        config.schema_version = 1;
10✔
4097
        config.initialization_function = initialization_function;
10✔
4098
        Realm::get_shared_realm(config);
10✔
4099
        REQUIRE(initialization_function_called == TestType::should_call_init_on_version_bump);
10!
4100
        if (TestType::should_call_init_on_version_bump) {
10✔
4101
            REQUIRE(schema_version_in_callback == 1);
4!
4102
            REQUIRE(schema_in_callback.compare(schema).size() == 0);
4!
4103
        }
4✔
4104
    }
10✔
4105
}
30✔
4106

4107
TEST_CASE("BindingContext is notified about delivery of change notifications") {
16✔
4108
    _impl::RealmCoordinator::assert_no_open_realms();
16✔
4109
    InMemoryTestFile config;
16✔
4110
    config.automatic_change_notifications = false;
16✔
4111

4112
    auto r = Realm::get_shared_realm(config);
16✔
4113
    r->update_schema({
16✔
4114
        {"object", {{"value", PropertyType::Int}}},
16✔
4115
    });
16✔
4116

4117
    auto coordinator = _impl::RealmCoordinator::get_coordinator(config.path);
16✔
4118
    auto table = r->read_group().get_table("class_object");
16✔
4119

4120
    SECTION("BindingContext notified even if no callbacks are registered") {
16✔
4121
        static int binding_context_start_notify_calls = 0;
4✔
4122
        static int binding_context_end_notify_calls = 0;
4✔
4123
        struct Context : BindingContext {
4✔
4124
            void will_send_notifications() override
4✔
4125
            {
4✔
4126
                ++binding_context_start_notify_calls;
4✔
4127
            }
4✔
4128

4129
            void did_send_notifications() override
4✔
4130
            {
4✔
4131
                ++binding_context_end_notify_calls;
4✔
4132
            }
4✔
4133
        };
4✔
4134
        r->m_binding_context.reset(new Context());
4✔
4135

4136
        SECTION("local commit") {
4✔
4137
            binding_context_start_notify_calls = 0;
2✔
4138
            binding_context_end_notify_calls = 0;
2✔
4139
            coordinator->on_change();
2✔
4140
            r->begin_transaction();
2✔
4141
            REQUIRE(binding_context_start_notify_calls == 1);
2!
4142
            REQUIRE(binding_context_end_notify_calls == 1);
2!
4143
            r->cancel_transaction();
2✔
4144
        }
2✔
4145

4146
        SECTION("remote commit") {
4✔
4147
            binding_context_start_notify_calls = 0;
2✔
4148
            binding_context_end_notify_calls = 0;
2✔
4149
            JoiningThread([&] {
2✔
4150
                auto r2 = coordinator->get_realm(util::Scheduler::make_frozen(VersionID()));
2✔
4151
                r2->begin_transaction();
2✔
4152
                auto table2 = r2->read_group().get_table("class_object");
2✔
4153
                table2->create_object();
2✔
4154
                r2->commit_transaction();
2✔
4155
            });
2✔
4156
            advance_and_notify(*r);
2✔
4157
            REQUIRE(binding_context_start_notify_calls == 1);
2!
4158
            REQUIRE(binding_context_end_notify_calls == 1);
2!
4159
        }
2✔
4160
    }
4✔
4161

4162
    SECTION("notify BindingContext before and after sending notifications") {
16✔
4163
        static int binding_context_start_notify_calls = 0;
4✔
4164
        static int binding_context_end_notify_calls = 0;
4✔
4165
        static int notification_calls = 0;
4✔
4166

4167
        auto col = table->get_column_key("value");
4✔
4168
        Results results1(r, table->where().greater_equal(col, 0));
4✔
4169
        Results results2(r, table->where().less(col, 10));
4✔
4170

4171
        auto token1 = results1.add_notification_callback([&](CollectionChangeSet) {
4✔
4172
            ++notification_calls;
4✔
4173
        });
4✔
4174

4175
        auto token2 = results2.add_notification_callback([&](CollectionChangeSet) {
4✔
4176
            ++notification_calls;
4✔
4177
        });
4✔
4178

4179
        struct Context : BindingContext {
4✔
4180
            void will_send_notifications() override
4✔
4181
            {
4✔
4182
                REQUIRE(notification_calls == 0);
4!
4183
                REQUIRE(binding_context_end_notify_calls == 0);
4!
4184
                ++binding_context_start_notify_calls;
4✔
4185
            }
4✔
4186

4187
            void did_send_notifications() override
4✔
4188
            {
4✔
4189
                REQUIRE(notification_calls == 2);
4!
4190
                REQUIRE(binding_context_start_notify_calls == 1);
4!
4191
                ++binding_context_end_notify_calls;
4✔
4192
            }
4✔
4193
        };
4✔
4194
        r->m_binding_context.reset(new Context());
4✔
4195

4196
        SECTION("local commit") {
4✔
4197
            binding_context_start_notify_calls = 0;
2✔
4198
            binding_context_end_notify_calls = 0;
2✔
4199
            notification_calls = 0;
2✔
4200
            coordinator->on_change();
2✔
4201
            r->begin_transaction();
2✔
4202
            table->create_object();
2✔
4203
            r->commit_transaction();
2✔
4204
            REQUIRE(binding_context_start_notify_calls == 1);
2!
4205
            REQUIRE(binding_context_end_notify_calls == 1);
2!
4206
        }
2✔
4207

4208
        SECTION("remote commit") {
4✔
4209
            binding_context_start_notify_calls = 0;
2✔
4210
            binding_context_end_notify_calls = 0;
2✔
4211
            notification_calls = 0;
2✔
4212
            JoiningThread([&] {
2✔
4213
                auto r2 = coordinator->get_realm(util::Scheduler::make_frozen(VersionID()));
2✔
4214
                r2->begin_transaction();
2✔
4215
                auto table2 = r2->read_group().get_table("class_object");
2✔
4216
                table2->create_object();
2✔
4217
                r2->commit_transaction();
2✔
4218
            });
2✔
4219
            advance_and_notify(*r);
2✔
4220
            REQUIRE(binding_context_start_notify_calls == 1);
2!
4221
            REQUIRE(binding_context_end_notify_calls == 1);
2!
4222
        }
2✔
4223
    }
4✔
4224

4225
    SECTION("did_send() is skipped if the Realm is closed first") {
16✔
4226
        Results results(r, table->where());
8✔
4227
        bool do_close = true;
8✔
4228
        auto token = results.add_notification_callback([&](CollectionChangeSet) {
8✔
4229
            if (do_close)
8✔
4230
                r->close();
4✔
4231
        });
8✔
4232

4233
        struct FailOnDidSend : BindingContext {
8✔
4234
            void did_send_notifications() override
8✔
4235
            {
8✔
UNCOV
4236
                FAIL("did_send_notifications() should not have been called");
×
UNCOV
4237
            }
×
4238
        };
8✔
4239
        struct CloseOnWillChange : FailOnDidSend {
8✔
4240
            Realm& realm;
8✔
4241
            CloseOnWillChange(Realm& realm)
8✔
4242
                : realm(realm)
8✔
4243
            {
8✔
4244
            }
4✔
4245

4246
            void will_send_notifications() override
8✔
4247
            {
8✔
4248
                realm.close();
4✔
4249
            }
4✔
4250
        };
8✔
4251

4252
        SECTION("closed in notification callback for notify()") {
8✔
4253
            r->m_binding_context.reset(new FailOnDidSend);
2✔
4254
            coordinator->on_change();
2✔
4255
            r->notify();
2✔
4256
        }
2✔
4257

4258
        SECTION("closed in notification callback for refresh()") {
8✔
4259
            do_close = false;
2✔
4260
            coordinator->on_change();
2✔
4261
            r->notify();
2✔
4262
            do_close = true;
2✔
4263

4264
            JoiningThread([&] {
2✔
4265
                auto r = coordinator->get_realm(util::Scheduler::make_frozen(VersionID()));
2✔
4266
                r->begin_transaction();
2✔
4267
                r->read_group().get_table("class_object")->create_object();
2✔
4268
                r->commit_transaction();
2✔
4269
            });
2✔
4270

4271
            r->m_binding_context.reset(new FailOnDidSend);
2✔
4272
            coordinator->on_change();
2✔
4273
            r->refresh();
2✔
4274
        }
2✔
4275

4276
        SECTION("closed in will_send() for notify()") {
8✔
4277
            r->m_binding_context.reset(new CloseOnWillChange(*r));
2✔
4278
            coordinator->on_change();
2✔
4279
            r->notify();
2✔
4280
        }
2✔
4281

4282
        SECTION("closed in will_send() for refresh()") {
8✔
4283
            do_close = false;
2✔
4284
            coordinator->on_change();
2✔
4285
            r->notify();
2✔
4286
            do_close = true;
2✔
4287

4288
            JoiningThread([&] {
2✔
4289
                auto r = coordinator->get_realm(util::Scheduler::make_frozen(VersionID()));
2✔
4290
                r->begin_transaction();
2✔
4291
                r->read_group().get_table("class_object")->create_object();
2✔
4292
                r->commit_transaction();
2✔
4293
            });
2✔
4294

4295
            r->m_binding_context.reset(new CloseOnWillChange(*r));
2✔
4296
            coordinator->on_change();
2✔
4297
            r->refresh();
2✔
4298
        }
2✔
4299
    }
8✔
4300
#ifdef _WIN32
4301
    _impl::RealmCoordinator::clear_all_caches();
4302
#endif
4303
}
16✔
4304

4305
TEST_CASE("RealmCoordinator: get_unbound_realm()") {
8✔
4306
    TestFile config;
8✔
4307
    config.cache = true;
8✔
4308
    config.schema = Schema{
8✔
4309
        {"object", {{"value", PropertyType::Int}}},
8✔
4310
    };
8✔
4311

4312
    ThreadSafeReference ref;
8✔
4313
    JoiningThread([&] {
8✔
4314
        ref = _impl::RealmCoordinator::get_coordinator(config)->get_unbound_realm();
8✔
4315
    });
8✔
4316

4317
    SECTION("checks thread after being resolved") {
8✔
4318
        auto realm = Realm::get_shared_realm(std::move(ref));
2✔
4319
        REQUIRE_NOTHROW(realm->verify_thread());
2✔
4320
        JoiningThread([&] {
2✔
4321
            REQUIRE_EXCEPTION(realm->verify_thread(), WrongThread, "Realm accessed from incorrect thread.");
2✔
4322
        });
2✔
4323
    }
2✔
4324

4325
    SECTION("delivers notifications to the thread it is resolved on") {
8✔
4326
#ifndef _WIN32
2✔
4327
        if (!util::EventLoop::has_implementation())
2✔
UNCOV
4328
            return;
×
4329
        auto realm = Realm::get_shared_realm(std::move(ref));
2✔
4330
        Results results(realm, ObjectStore::table_for_object_type(realm->read_group(), "object")->where());
2✔
4331
        bool called = false;
2✔
4332
        auto token = results.add_notification_callback([&](CollectionChangeSet) {
2✔
4333
            called = true;
2✔
4334
        });
2✔
4335
        util::EventLoop::main().run_until([&] {
63✔
4336
            return called;
63✔
4337
        });
63✔
4338
#endif
2✔
4339
    }
2✔
4340

4341
    SECTION("resolves to existing cached Realm for the thread if caching is enabled") {
8✔
4342
        auto r1 = Realm::get_shared_realm(config);
2✔
4343
        auto r2 = Realm::get_shared_realm(std::move(ref));
2✔
4344
        REQUIRE(r1 == r2);
2!
4345
    }
2✔
4346

4347
    SECTION("resolves to a new Realm if caching is disabled") {
8✔
4348
        config.cache = false;
2✔
4349
        auto r1 = Realm::get_shared_realm(config);
2✔
4350
        auto r2 = Realm::get_shared_realm(std::move(ref));
2✔
4351
        REQUIRE(r1 != r2);
2!
4352

4353
        // New unbound with cache disabled
4354
        JoiningThread([&] {
2✔
4355
            ref = _impl::RealmCoordinator::get_coordinator(config)->get_unbound_realm();
2✔
4356
        });
2✔
4357
        auto r3 = Realm::get_shared_realm(std::move(ref));
2✔
4358
        REQUIRE(r1 != r3);
2!
4359
        REQUIRE(r2 != r3);
2!
4360

4361
        // New local with cache enabled should grab the resolved unbound
4362
        config.cache = true;
2✔
4363
        auto r4 = Realm::get_shared_realm(config);
2✔
4364
        REQUIRE(r4 == r2);
2!
4365
    }
2✔
4366
}
8✔
4367

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

4373
    {
40✔
4374
        auto realm = Realm::get_shared_realm(config);
40✔
4375
        realm->begin_transaction();
40✔
4376
        realm->read_group().get_table("class_object")->create_object();
40✔
4377
        realm->commit_transaction();
40✔
4378
    }
40✔
4379

4380
    config.schema_mode = SchemaMode::Immutable;
40✔
4381
    auto realm = Realm::get_shared_realm(config);
40✔
4382
    realm->read_group();
40✔
4383

4384
    SECTION("unsupported functions") {
40✔
4385
        SECTION("update_schema()") {
10✔
4386
            REQUIRE_THROWS_AS(realm->compact(), WrongTransactionState);
2✔
4387
        }
2✔
4388
        SECTION("begin_transaction()") {
10✔
4389
            REQUIRE_THROWS_AS(realm->begin_transaction(), WrongTransactionState);
2✔
4390
        }
2✔
4391
        SECTION("async_begin_transaction()") {
10✔
4392
            REQUIRE_THROWS_AS(realm->async_begin_transaction(nullptr), WrongTransactionState);
2✔
4393
        }
2✔
4394
        SECTION("refresh()") {
10✔
4395
            REQUIRE_THROWS_AS(realm->refresh(), WrongTransactionState);
2✔
4396
        }
2✔
4397
        SECTION("compact()") {
10✔
4398
            REQUIRE_THROWS_AS(realm->compact(), WrongTransactionState);
2✔
4399
        }
2✔
4400
    }
10✔
4401

4402
    SECTION("supported functions") {
40✔
4403
        SECTION("is_in_transaction()") {
30✔
4404
            REQUIRE_FALSE(realm->is_in_transaction());
2!
4405
        }
2✔
4406
        SECTION("is_in_async_transaction()") {
30✔
4407
            REQUIRE_FALSE(realm->is_in_transaction());
2!
4408
        }
2✔
4409
        SECTION("freeze()") {
30✔
4410
            std::shared_ptr<Realm> frozen;
2✔
4411
            REQUIRE_NOTHROW(frozen = realm->freeze());
2✔
4412
            REQUIRE(frozen->read_group().get_table("class_object")->size() == 1);
2!
4413
            REQUIRE_NOTHROW(frozen = Realm::get_frozen_realm(config, realm->read_transaction_version()));
2✔
4414
            REQUIRE(frozen->read_group().get_table("class_object")->size() == 1);
2!
4415
        }
2✔
4416
        SECTION("notify()") {
30✔
4417
            REQUIRE_NOTHROW(realm->notify());
2✔
4418
        }
2✔
4419
        SECTION("is_in_read_transaction()") {
30✔
4420
            REQUIRE(realm->is_in_read_transaction());
2!
4421
        }
2✔
4422
        SECTION("last_seen_transaction_version()") {
30✔
4423
            REQUIRE(realm->last_seen_transaction_version() == 1);
2!
4424
        }
2✔
4425
        SECTION("get_number_of_versions()") {
30✔
4426
            REQUIRE(realm->get_number_of_versions() == 1);
2!
4427
        }
2✔
4428
        SECTION("read_transaction_version()") {
30✔
4429
            REQUIRE(realm->read_transaction_version() == VersionID{1, 0});
2!
4430
        }
2✔
4431
        SECTION("current_transaction_version()") {
30✔
4432
            REQUIRE(realm->current_transaction_version() == VersionID{1, 0});
2!
4433
        }
2✔
4434
        SECTION("latest_snapshot_version()") {
30✔
4435
            REQUIRE(realm->latest_snapshot_version() == 1);
2!
4436
        }
2✔
4437
        SECTION("duplicate()") {
30✔
4438
            auto duplicate = realm->duplicate();
2✔
4439
            REQUIRE(duplicate->get_table("class_object")->size() == 1);
2!
4440
        }
2✔
4441
        SECTION("invalidate()") {
30✔
4442
            REQUIRE_NOTHROW(realm->invalidate());
2✔
4443
            REQUIRE_FALSE(realm->is_in_read_transaction());
2!
4444
            REQUIRE(realm->read_group().get_table("class_object")->size() == 1);
2!
4445
        }
2✔
4446
        SECTION("close()") {
30✔
4447
            REQUIRE_NOTHROW(realm->close());
2✔
4448
            REQUIRE(realm->is_closed());
2!
4449
        }
2✔
4450
        SECTION("has_pending_async_work()") {
30✔
4451
            REQUIRE_FALSE(realm->has_pending_async_work());
2!
4452
        }
2✔
4453
        SECTION("wait_for_change()") {
30✔
4454
            REQUIRE_FALSE(realm->wait_for_change());
2!
4455
        }
2✔
4456
    }
30✔
4457
}
40✔
4458

4459
TEST_CASE("KeyPathMapping generation") {
2✔
4460
    TestFile config;
2✔
4461
    realm::query_parser::KeyPathMapping mapping;
2✔
4462

4463
    SECTION("class aliasing") {
2✔
4464
        Schema schema = {
2✔
4465
            {"PersistedName", {{"age", PropertyType::Int}}, {}, "AlternativeName"},
2✔
4466
            {"class_with_policy",
2✔
4467
             {{"value", PropertyType::Int},
2✔
4468
              {"child", PropertyType::Object | PropertyType::Nullable, "class_with_policy"}},
2✔
4469
             {{"parents", PropertyType::LinkingObjects | PropertyType::Array, "class_with_policy", "child"}},
2✔
4470
             "ClassWithPolicy"},
2✔
4471
        };
2✔
4472
        schema.validate();
2✔
4473
        config.schema = schema;
2✔
4474
        auto realm = Realm::get_shared_realm(config);
2✔
4475
        realm::populate_keypath_mapping(mapping, *realm);
2✔
4476
        REQUIRE(mapping.has_table_mapping("AlternativeName"));
2!
4477
        REQUIRE("class_PersistedName" == mapping.get_table_mapping("AlternativeName"));
2!
4478

4479
        auto table = realm->read_group().get_table("class_class_with_policy");
2✔
4480
        std::vector<Mixed> args{0};
2✔
4481
        auto q = table->query("parents.value = $0", args, mapping);
2✔
4482
        REQUIRE(q.count() == 0);
2!
4483
    }
2✔
4484
}
2✔
4485

4486
TEST_CASE("Concurrent operations") {
4✔
4487
    SECTION("Async commits together with online compaction") {
4✔
4488
        // This is a reproduction test for issue https://github.com/realm/realm-dart/issues/1396
4489
        // First create a relatively large realm, then delete the content and do some more
4490
        // commits using async commits. If a compaction is started when doing an async commit
4491
        // then the subsequent committing done in the helper thread will illegally COW the
4492
        // top array. When the next mutation is done, the top array will be reported as being
4493
        // already freed.
4494
        TestFile config;
2✔
4495
        config.schema_version = 1;
2✔
4496
        config.schema = Schema{{"object", {{"value", PropertyType::Int}}}};
2✔
4497

4498
        auto realm_1 = Realm::get_shared_realm(config);
2✔
4499
        Results res(realm_1, realm_1->read_group().get_table("class_object")->where());
2✔
4500
        auto realm_2 = Realm::get_shared_realm(config);
2✔
4501

4502
        {
2✔
4503
            // Create a lot of objects
4504
            realm_2->begin_transaction();
2✔
4505
            auto table = realm_2->read_group().get_table("class_object");
2✔
4506
            for (int i = 0; i < 400000; i++) {
800,002✔
4507
                table->create_object().set("value", i);
800,000✔
4508
            }
800,000✔
4509
            realm_2->commit_transaction();
2✔
4510
        }
2✔
4511

4512
        int commit_1 = 0;
2✔
4513
        int commit_2 = 0;
2✔
4514

4515
        for (int i = 0; i < 4; i++) {
10✔
4516
            realm_1->async_begin_transaction([&]() {
8✔
4517
                // Clearing the DB will reduce the need for space
4518
                // This will trigger an online compaction
4519
                // Before the fix, the probram would crash here next time around.
4520
                res.clear();
8✔
4521
                realm_1->async_commit_transaction([&](std::exception_ptr) {
8✔
4522
                    commit_1++;
8✔
4523
                });
8✔
4524
            });
8✔
4525
            realm_2->async_begin_transaction([&]() {
8✔
4526
                // Make sure we will continue to have something to delete
4527
                auto table = realm_2->read_group().get_table("class_object");
8✔
4528
                for (int i = 0; i < 100; i++) {
808✔
4529
                    table->create_object().set("value", i);
800✔
4530
                }
800✔
4531
                realm_2->async_commit_transaction([&](std::exception_ptr) {
8✔
4532
                    commit_2++;
8✔
4533
                });
8✔
4534
            });
8✔
4535
        }
8✔
4536

4537
        util::EventLoop::main().run_until([&] {
15,863✔
4538
            return commit_1 == 4 && commit_2 == 4;
15,863✔
4539
        });
15,863✔
4540
    }
2✔
4541

4542
    SECTION("No open realms") {
4✔
4543
        // This is just to check that the section above did not leave any realms open
4544
        _impl::RealmCoordinator::assert_no_open_realms();
2✔
4545
    }
2✔
4546
}
4✔
4547

4548
TEST_CASE("Notification logging") {
2✔
4549
    using namespace std::chrono_literals;
2✔
4550
    TestFile config;
2✔
4551
    // util::LogCategory::realm.set_default_level_threshold(util::Logger::Level::all);
4552
    config.schema_version = 1;
2✔
4553
    config.schema = Schema{{"object", {{"value", PropertyType::Int}}}};
2✔
4554

4555
    auto realm = Realm::get_shared_realm(config);
2✔
4556
    auto table = realm->read_group().get_table("class_object");
2✔
4557
    int changed = 0;
2✔
4558
    Results res(realm, table->query("value == 5"));
2✔
4559
    auto token = res.add_notification_callback([&changed](CollectionChangeSet const&) {
24✔
4560
        changed++;
24✔
4561
    });
24✔
4562

4563
    int commit_nr = 0;
2✔
4564
    util::EventLoop::main().run_until([&] {
22✔
4565
        for (int64_t i = 0; i < 10; i++) {
242✔
4566
            realm->begin_transaction();
220✔
4567
            table->create_object().set("value", i);
220✔
4568
            realm->commit_transaction();
220✔
4569
            std::this_thread::sleep_for(2ms);
220✔
4570
        }
220✔
4571
        return ++commit_nr == 10;
22✔
4572
    });
22✔
4573
}
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc