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

realm / realm-core / jorgen.edelbo_389

12 Aug 2024 02:13PM UTC coverage: 91.085% (-0.02%) from 91.107%
jorgen.edelbo_389

Pull #7826

Evergreen

jedelbo
Bump file format version
Pull Request #7826: Merge Next major

103458 of 182206 branches covered (56.78%)

3138 of 3500 new or added lines in 53 files covered. (89.66%)

175 existing lines in 17 files now uncovered.

219944 of 241471 relevant lines covered (91.09%)

6840929.52 hits per line

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

98.09
/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([&] {
480,979✔
1069
            std::lock_guard<std::mutex> guard(mutex);
480,979✔
1070
            return task2_completed;
480,979✔
1071
        });
480,979✔
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([&] {
78,912✔
1116
            return completed == 4;
78,912✔
1117
        });
78,912✔
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([&] {
183,816✔
1155
            return called.load();
183,816✔
1156
        });
183,816✔
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✔
UNCOV
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
UNCOV
1483
        if (error.status.code() == ErrorCodes::SyncConnectFailed) {
×
UNCOV
1484
            REQUIRE_FALSE(error.is_fatal);
×
UNCOV
1485
            return;
×
UNCOV
1486
        }
×
1487
        // If it's not SyncConnectFailed, then it should be AuthError
UNCOV
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✔
UNCOV
1502
            token_refresh_called = true;
×
UNCOV
1503
            if (failure == FailureMode::token_not_authorized) {
×
UNCOV
1504
                return app::Response{403, 0, {}, "403 not authorized"};
×
UNCOV
1505
            }
×
UNCOV
1506
            if (failure == FailureMode::token_fails) {
×
UNCOV
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✔
UNCOV
1517
        return std::nullopt;
×
1518
    };
2✔
1519

1520
    socket_provider->websocket_connect_func = [&]() -> std::optional<SocketProviderError> {
2✔
1521
        if (not_authorized) {
2✔
UNCOV
1522
            not_authorized = false; // one shot
×
1523
            return SocketProviderError(sync::websocket::WebSocketError::websocket_unauthorized, "403 not authorized");
×
UNCOV
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✔
UNCOV
1544
            REQUIRE(token_refresh_called);
×
UNCOV
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]") {
12✔
1557
    TestSyncManager tsm;
12✔
1558
    ObjectSchema object_schema = {"object",
12✔
1559
                                  {
12✔
1560
                                      {"_id", PropertyType::Int, Property::IsPrimary{true}},
12✔
1561
                                      {"value", PropertyType::Int},
12✔
1562
                                  }};
12✔
1563
    Schema schema{object_schema};
12✔
1564

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

1571
    SECTION("can copy a synced realm to a synced realm") {
12✔
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") {
12✔
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") {
12✔
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") {
12✔
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

1651
    SECTION("synced realm must be fully uploaded") {
12✔
1652
        auto realm = Realm::get_shared_realm(sync_config1);
2✔
1653
        realm->sync_session()->pause();
2✔
1654
        realm->begin_transaction();
2✔
1655
        realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1656
        realm->commit_transaction();
2✔
1657

1658
        SyncTestFile sync_config2(tsm, "default");
2✔
1659
        sync_config2.schema = schema;
2✔
1660
        REQUIRE_EXCEPTION(realm->convert(sync_config2), IllegalOperation,
2✔
1661
                          "All client changes must be integrated in server before writing copy");
2✔
1662

1663
        realm->sync_session()->resume();
2✔
1664
        wait_for_upload(*realm);
2✔
1665
        REQUIRE_NOTHROW(realm->convert(sync_config2));
2✔
1666
    }
2✔
1667

1668
    SECTION("can convert synced realm from within upload complete callback") {
12✔
1669
        auto realm = Realm::get_shared_realm(sync_config1);
2✔
1670
        realm->sync_session()->pause();
2✔
1671
        realm->begin_transaction();
2✔
1672
        realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1673
        realm->commit_transaction();
2✔
1674

1675
        SyncTestFile sync_config2(tsm, "default");
2✔
1676
        sync_config2.schema = schema;
2✔
1677
        auto pf = util::make_promise_future();
2✔
1678
        realm->sync_session()->wait_for_upload_completion([&](Status) {
2✔
1679
            sync_config1.scheduler = util::Scheduler::make_dummy();
2✔
1680
            auto realm = Realm::get_shared_realm(sync_config1);
2✔
1681
            REQUIRE_NOTHROW(realm->convert(sync_config2));
2✔
1682
            pf.promise.emplace_value();
2✔
1683
        });
2✔
1684
        realm->sync_session()->resume();
2✔
1685
        pf.future.get();
2✔
1686
    }
2✔
1687
}
12✔
1688

1689
TEST_CASE("SharedRealm: convert - embedded objects", "[sync][pbs][convert][embedded objects]") {
16✔
1690
    TestSyncManager tsm;
16✔
1691
    ObjectSchema object_schema = {"object",
16✔
1692
                                  {
16✔
1693
                                      {"_id", PropertyType::Int, Property::IsPrimary{true}},
16✔
1694
                                      {"value", PropertyType::Int},
16✔
1695
                                      {"embedded_link", PropertyType::Object | PropertyType::Nullable, "embedded"},
16✔
1696
                                  }};
16✔
1697
    ObjectSchema embedded_schema = {"embedded",
16✔
1698
                                    ObjectSchema::ObjectType::Embedded,
16✔
1699
                                    {
16✔
1700
                                        {"name", PropertyType::String | PropertyType::Nullable},
16✔
1701
                                    }};
16✔
1702
    Schema schema{object_schema, embedded_schema};
16✔
1703

1704
    SyncTestFile sync_config1(tsm, "default");
16✔
1705
    sync_config1.schema = schema;
16✔
1706
    TestFile local_config1;
16✔
1707
    local_config1.schema = schema;
16✔
1708
    local_config1.schema_version = sync_config1.schema_version;
16✔
1709

1710
    SECTION("can copy a synced realm to a synced realm") {
16✔
1711
        auto sync_realm1 = Realm::get_shared_realm(sync_config1);
4✔
1712
        sync_realm1->begin_transaction();
4✔
1713

1714
        SECTION("null embedded object") {
4✔
1715
            sync_realm1->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1716
        }
2✔
1717

1718
        SECTION("embedded object") {
4✔
1719
            auto obj = sync_realm1->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1720
            auto col_key = sync_realm1->read_group().get_table("class_object")->get_column_key("embedded_link");
2✔
1721
            obj.create_and_set_linked_object(col_key);
2✔
1722
        }
2✔
1723

1724
        sync_realm1->commit_transaction();
4✔
1725
        wait_for_upload(*sync_realm1);
4✔
1726
        wait_for_download(*sync_realm1);
4✔
1727

1728
        // Copy to a new sync config
1729
        SyncTestFile sync_config2(tsm, "default");
4✔
1730
        sync_config2.schema = schema;
4✔
1731

1732
        sync_realm1->convert(sync_config2);
4✔
1733

1734
        auto sync_realm2 = Realm::get_shared_realm(sync_config2);
4✔
1735

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

1739
        // Verify that sync works and objects created in the new copy will get
1740
        // synchronized to the old copy
1741
        sync_realm2->begin_transaction();
4✔
1742
        sync_realm2->read_group().get_table("class_object")->create_object_with_primary_key(1);
4✔
1743
        sync_realm2->commit_transaction();
4✔
1744
        wait_for_upload(*sync_realm2);
4✔
1745
        wait_for_download(*sync_realm1);
4✔
1746

1747
        sync_realm1->refresh();
4✔
1748
        REQUIRE(sync_realm1->read_group().get_table("class_object")->size() == 2);
4!
1749
    }
4✔
1750

1751
    SECTION("can convert a synced realm to a local realm") {
16✔
1752
        auto sync_realm = Realm::get_shared_realm(sync_config1);
4✔
1753
        sync_realm->begin_transaction();
4✔
1754

1755
        SECTION("null embedded object") {
4✔
1756
            sync_realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1757
        }
2✔
1758

1759
        SECTION("embedded object") {
4✔
1760
            auto obj = sync_realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1761
            auto col_key = sync_realm->read_group().get_table("class_object")->get_column_key("embedded_link");
2✔
1762
            obj.create_and_set_linked_object(col_key);
2✔
1763
        }
2✔
1764

1765
        sync_realm->commit_transaction();
4✔
1766
        wait_for_upload(*sync_realm);
4✔
1767
        wait_for_download(*sync_realm);
4✔
1768

1769
        sync_realm->convert(local_config1);
4✔
1770

1771
        auto local_realm = Realm::get_shared_realm(local_config1);
4✔
1772

1773
        // Check that the data also exists in the new realm
1774
        REQUIRE(local_realm->read_group().get_table("class_object")->size() == 1);
4!
1775
    }
4✔
1776

1777
    SECTION("can convert a local realm to a synced realm") {
16✔
1778
        auto local_realm = Realm::get_shared_realm(local_config1);
4✔
1779
        local_realm->begin_transaction();
4✔
1780

1781
        SECTION("null embedded object") {
4✔
1782
            local_realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1783
        }
2✔
1784

1785
        SECTION("embedded object") {
4✔
1786
            auto obj = local_realm->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1787
            auto col_key = local_realm->read_group().get_table("class_object")->get_column_key("embedded_link");
2✔
1788
            obj.create_and_set_linked_object(col_key);
2✔
1789
        }
2✔
1790

1791
        local_realm->commit_transaction();
4✔
1792

1793
        // Copy to a new sync config
1794
        local_realm->convert(sync_config1);
4✔
1795

1796
        auto sync_realm = Realm::get_shared_realm(sync_config1);
4✔
1797

1798
        // Check that the data also exists in the new realm
1799
        REQUIRE(sync_realm->read_group().get_table("class_object")->size() == 1);
4!
1800
    }
4✔
1801

1802
    SECTION("can copy a local realm to a local realm") {
16✔
1803
        auto local_realm1 = Realm::get_shared_realm(local_config1);
4✔
1804
        local_realm1->begin_transaction();
4✔
1805

1806
        SECTION("null embedded object") {
4✔
1807
            local_realm1->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1808
        }
2✔
1809

1810
        SECTION("embedded object") {
4✔
1811
            auto obj = local_realm1->read_group().get_table("class_object")->create_object_with_primary_key(0);
2✔
1812
            auto col_key = local_realm1->read_group().get_table("class_object")->get_column_key("embedded_link");
2✔
1813
            obj.create_and_set_linked_object(col_key);
2✔
1814
        }
2✔
1815

1816
        local_realm1->commit_transaction();
4✔
1817

1818
        // Copy to a new local config
1819
        TestFile local_config2;
4✔
1820
        local_config2.schema = schema;
4✔
1821
        local_config2.schema_version = local_config1.schema_version;
4✔
1822
        local_realm1->convert(local_config2);
4✔
1823

1824
        auto local_realm2 = Realm::get_shared_realm(local_config2);
4✔
1825

1826
        // Check that the data also exists in the new realm
1827
        REQUIRE(local_realm2->read_group().get_table("class_object")->size() == 1);
4!
1828
    }
4✔
1829
}
16✔
1830
#endif // REALM_ENABLE_SYNC
1831

1832
TEST_CASE("SharedRealm: async writes") {
104✔
1833
    _impl::RealmCoordinator::assert_no_open_realms();
104✔
1834
    if (!util::EventLoop::has_implementation())
104✔
UNCOV
1835
        return;
×
1836

1837
    TestFile config;
104✔
1838
    config.schema_version = 0;
104✔
1839
    config.schema = Schema{
104✔
1840
        {"object",
104✔
1841
         {
104✔
1842
             {"value", PropertyType::Int},
104✔
1843
             {"ints", PropertyType::Array | PropertyType::Int},
104✔
1844
             {"int set", PropertyType::Set | PropertyType::Int},
104✔
1845
             {"int dictionary", PropertyType::Dictionary | PropertyType::Int},
104✔
1846
         }},
104✔
1847
    };
104✔
1848
    bool done = false;
104✔
1849
    auto realm = Realm::get_shared_realm(config);
104✔
1850
    auto table = realm->read_group().get_table("class_object");
104✔
1851
    auto col = table->get_column_key("value");
104✔
1852
    int write_nr = 0;
104✔
1853
    int commit_nr = 0;
104✔
1854

1855
    auto wait_for_done = [&]() {
104✔
1856
        util::EventLoop::main().run_until([&] {
200,942✔
1857
            return done;
200,942✔
1858
        });
200,942✔
1859
        REQUIRE(done);
96!
1860
    };
96✔
1861

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

1889
    auto verify_persisted_count = [&](size_t expected) {
104✔
1890
        if (realm)
52✔
1891
            realm->close();
50✔
1892
        _impl::RealmCoordinator::assert_no_open_realms();
52✔
1893

1894
        auto new_realm = Realm::get_shared_realm(config);
52✔
1895
        auto table = new_realm->read_group().get_table("class_object");
52✔
1896
        REQUIRE(table->size() == expected);
52!
1897
    };
52✔
1898

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

1916
                    // Wait until the main thread is waiting for the lock.
1917
                    while (!db->other_writers_waiting_for_lock()) {
8✔
1918
                        millisleep(1);
4✔
1919
                    }
4✔
1920
                    write->close();
4✔
1921
                });
4✔
1922

1923
                // Wait for the background thread to have acquired the lock
1924
                sema.get_stone();
4✔
1925

1926
                auto scheduler = realm->scheduler();
4✔
1927
                realm->async_begin_transaction([&] {
4✔
1928
                    // We should never get here as the realm is closed
UNCOV
1929
                    FAIL();
×
UNCOV
1930
                });
×
1931

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

1935
                {
4✔
1936
                    // Verify that we released the write lock
1937
                    auto db = DB::create(make_in_realm_history(), config.path, options);
4✔
1938
                    REQUIRE(db->start_write(/* nonblocking */ true));
4!
1939
                }
4✔
1940

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

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

2062
                wait_for_done();
4✔
2063
                std::invoke(close_functions[i], *realm);
4✔
2064
                REQUIRE(complete_count == 1);
4!
2065
                verify_persisted_count(1);
4✔
2066
            }
4✔
2067
            SECTION("within did_change()") {
40✔
2068
                struct Context : public BindingContext {
4✔
2069
                    int i;
4✔
2070
                    Context(int i)
4✔
2071
                        : i(i)
4✔
2072
                    {
4✔
2073
                    }
4✔
2074
                    void did_change(std::vector<ObserverState> const&, std::vector<void*> const&, bool) override
4✔
2075
                    {
6✔
2076
                        std::invoke(close_functions[i], *realm.lock());
6✔
2077
                    }
6✔
2078
                };
4✔
2079
                realm->m_binding_context.reset(new Context(i));
4✔
2080
                realm->m_binding_context->realm = realm;
4✔
2081

2082
                realm->async_begin_transaction([&] {
4✔
2083
                    table->create_object().set(col, 45);
4✔
2084
                    realm->async_commit_transaction([&](std::exception_ptr) {
4✔
2085
                        done = true;
4✔
2086
                    });
4✔
2087
                });
4✔
2088

2089
                wait_for_done();
4✔
2090
                verify_persisted_count(1);
4✔
2091
            }
4✔
2092
        }
40✔
2093
    }
208✔
2094

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

2149
        // Transaction should have been rolled back
2150
        REQUIRE_FALSE(realm->is_in_transaction());
2!
2151
        REQUIRE(table->size() == 0);
2!
2152
        REQUIRE(called);
2!
2153

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

2178
        // Transaction should have been rolled back
2179
        REQUIRE_FALSE(realm->is_in_transaction());
2!
2180
        REQUIRE(table->size() == 0);
2!
2181

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

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

2236
    if (_impl::SimulatedFailure::is_enabled()) {
104✔
2237
        SECTION("error in the synchronous part of async commit") {
104✔
2238
            realm->begin_transaction();
2✔
2239
            table->create_object();
2✔
2240

2241
            using sf = _impl::SimulatedFailure;
2✔
2242
            sf::OneShotPrimeGuard pg(sf::shared_group__grow_reader_mapping);
2✔
2243
            REQUIRE_THROWS_AS(realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2244
                FAIL("should not call completion");
2✔
2245
            }),
2✔
2246
                              _impl::SimulatedFailure);
2✔
2247
            REQUIRE_FALSE(realm->is_in_transaction());
2!
2248
        }
2✔
2249
        SECTION("error in the async part of async commit") {
104✔
2250
            realm->begin_transaction();
2✔
2251
            table->create_object();
2✔
2252

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

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

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

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

2429
        util::EventLoop::main().run_until([&] {
2,195✔
2430
            return !realm->is_in_async_transaction();
2,195✔
2431
        });
2,195✔
2432

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

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

2511
        Observer observer(obj);
2✔
2512
        observer.realm = realm;
2✔
2513
        realm->m_binding_context.reset(&observer);
2✔
2514

2515
        realm->async_begin_transaction([&]() {
2✔
2516
            list.clear();
2✔
2517
            set.clear();
2✔
2518
            dict.clear();
2✔
2519
            done = true;
2✔
2520
        });
2✔
2521
        wait_for_done();
2✔
2522
        REQUIRE(observer.array_change(0, list_col) == IndexSet{0, 1, 2});
2!
2523
        REQUIRE(observer.array_change(0, set_col) == IndexSet{});
2!
2524
        REQUIRE(observer.array_change(0, dict_col) == IndexSet{});
2!
2525
        realm->m_binding_context.release();
2✔
2526
    }
2✔
2527

2528
    SECTION("begin_transaction() from within did_change()") {
104✔
2529
        struct Context : public BindingContext {
2✔
2530
            void did_change(std::vector<ObserverState> const&, std::vector<void*> const&, bool) override
2✔
2531
            {
4✔
2532
                auto r = realm.lock();
4✔
2533
                r->begin_transaction();
4✔
2534
                auto table = r->read_group().get_table("class_object");
4✔
2535
                table->create_object();
4✔
2536
                if (++change_count == 1) {
4✔
2537
                    r->commit_transaction();
2✔
2538
                }
2✔
2539
                else {
2✔
2540
                    r->cancel_transaction();
2✔
2541
                }
2✔
2542
            }
4✔
2543
            int change_count = 0;
2✔
2544
        };
2✔
2545

2546
        realm->m_binding_context.reset(new Context());
2✔
2547
        realm->m_binding_context->realm = realm;
2✔
2548

2549
        realm->begin_transaction();
2✔
2550
        auto table = realm->read_group().get_table("class_object");
2✔
2551
        table->create_object();
2✔
2552
        bool persisted = false;
2✔
2553
        realm->async_commit_transaction([&persisted](auto) {
2✔
2554
            persisted = true;
2✔
2555
        });
2✔
2556
        REQUIRE(table->size() == 2);
2!
2557
        REQUIRE(persisted);
2!
2558
    }
2✔
2559

2560
    SECTION("async write grouping") {
104✔
2561
        size_t completion_calls = 0;
2✔
2562
        for (size_t i = 0; i < 41; ++i) {
84✔
2563
            realm->async_begin_transaction([&, i, realm] {
82✔
2564
                // The top ref in the Realm file should only be updated once every 20 commits
2565
                CHECK(Group(config.path, config.encryption_key.data()).get_table("class_object")->size() ==
82!
2566
                      (i / 20) * 20);
82✔
2567

2568
                table->create_object();
82✔
2569
                realm->async_commit_transaction(
82✔
2570
                    [&](std::exception_ptr) {
82✔
2571
                        ++completion_calls;
82✔
2572
                    },
82✔
2573
                    true);
82✔
2574
            });
82✔
2575
        }
82✔
2576
        util::EventLoop::main().run_until([&] {
9,406✔
2577
            return completion_calls == 41;
9,406✔
2578
        });
9,406✔
2579
    }
2✔
2580

2581
    SECTION("async write grouping with manual barriers") {
104✔
2582
        size_t completion_calls = 0;
2✔
2583
        for (size_t i = 0; i < 41; ++i) {
84✔
2584
            realm->async_begin_transaction([&, i, realm] {
82✔
2585
                // The top ref in the Realm file should only be updated once every 6 commits
2586
                CHECK(Group(config.path, config.encryption_key.data()).get_table("class_object")->size() ==
82!
2587
                      (i / 6) * 6);
82✔
2588

2589
                table->create_object();
82✔
2590
                realm->async_commit_transaction(
82✔
2591
                    [&](std::exception_ptr) {
82✔
2592
                        ++completion_calls;
82✔
2593
                    },
82✔
2594
                    (i + 1) % 6 != 0);
82✔
2595
            });
82✔
2596
        }
82✔
2597
        util::EventLoop::main().run_until([&] {
17,531✔
2598
            return completion_calls == 41;
17,531✔
2599
        });
17,531✔
2600
    }
2✔
2601

2602
    SECTION("async writes scheduled inside sync write") {
104✔
2603
        realm->begin_transaction();
2✔
2604
        realm->async_begin_transaction([&] {
2✔
2605
            REQUIRE(table->size() == 1);
2!
2606
            table->create_object();
2✔
2607
            realm->async_commit_transaction();
2✔
2608
        });
2✔
2609
        realm->async_begin_transaction([&] {
2✔
2610
            REQUIRE(table->size() == 2);
2!
2611
            table->create_object();
2✔
2612
            realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2613
                done = true;
2✔
2614
            });
2✔
2615
        });
2✔
2616
        REQUIRE(table->size() == 0);
2!
2617
        table->create_object();
2✔
2618
        realm->commit_transaction();
2✔
2619
        wait_for_done();
2✔
2620
        REQUIRE(table->size() == 3);
2!
2621
    }
2✔
2622

2623
    SECTION("async writes scheduled inside multiple sync write") {
104✔
2624
        realm->begin_transaction();
2✔
2625
        realm->async_begin_transaction([&] {
2✔
2626
            REQUIRE(table->size() == 2);
2!
2627
            table->create_object();
2✔
2628
            realm->async_commit_transaction();
2✔
2629
        });
2✔
2630
        realm->async_begin_transaction([&] {
2✔
2631
            REQUIRE(table->size() == 3);
2!
2632
            table->create_object();
2✔
2633
            realm->async_commit_transaction();
2✔
2634
        });
2✔
2635
        REQUIRE(table->size() == 0);
2!
2636
        table->create_object();
2✔
2637
        realm->commit_transaction();
2✔
2638

2639
        realm->begin_transaction();
2✔
2640
        realm->async_begin_transaction([&] {
2✔
2641
            REQUIRE(table->size() == 4);
2!
2642
            table->create_object();
2✔
2643
            realm->async_commit_transaction();
2✔
2644
        });
2✔
2645
        realm->async_begin_transaction([&] {
2✔
2646
            REQUIRE(table->size() == 5);
2!
2647
            table->create_object();
2✔
2648
            realm->async_commit_transaction([&](std::exception_ptr) {
2✔
2649
                done = true;
2✔
2650
            });
2✔
2651
        });
2✔
2652
        REQUIRE(table->size() == 1);
2!
2653
        table->create_object();
2✔
2654
        realm->commit_transaction();
2✔
2655

2656

2657
        wait_for_done();
2✔
2658
        REQUIRE(table->size() == 6);
2!
2659
    }
2✔
2660

2661
    SECTION("async writes which would run inside sync writes are deferred") {
104✔
2662
        realm->async_begin_transaction([&] {
2✔
2663
            done = true;
2✔
2664
        });
2✔
2665

2666
        // Wait for the background thread to hold the write lock (without letting
2667
        // the event loop run so that the scheduled task isn't run)
2668
        DBOptions options;
2✔
2669
        options.encryption_key = config.encryption_key.data();
2✔
2670
        auto db = DB::create(make_in_realm_history(), config.path, options);
2✔
2671
        while (db->start_write(true))
2✔
UNCOV
2672
            millisleep(1);
×
2673

2674
        realm->begin_transaction();
2✔
2675

2676
        // Invoke the pending callback
2677
        util::EventLoop::main().run_pending();
2✔
2678
        // Should not have run the async write block
2679
        REQUIRE(done == false);
2!
2680

2681
        // Should run the async write block once the synchronous transaction is done
2682
        realm->cancel_transaction();
2✔
2683
        REQUIRE(done == false);
2!
2684
        util::EventLoop::main().run_pending();
2✔
2685
        REQUIRE(done == true);
2!
2686
    }
2✔
2687

2688
    util::EventLoop::main().run_until([&] {
162✔
2689
        return !realm || !realm->has_pending_async_work();
162✔
2690
    });
162✔
2691

2692
    _impl::RealmCoordinator::clear_all_caches();
104✔
2693
}
104✔
2694

2695
TEST_CASE("Call run_async_completions after realm has been closed") {
2✔
2696
    // This requires a special scheduler as we have to call Realm::close
2697
    // just after DB::AsyncCommitHelper has made a callback to the function
2698
    // that asks the scheduler to invoke run_async_completions()
2699

2700
    struct ManualScheduler : util::Scheduler {
2✔
2701
        std::mutex mutex;
2✔
2702
        std::condition_variable cv;
2✔
2703
        std::vector<util::UniqueFunction<void()>> callbacks;
2✔
2704

2705
        void invoke(util::UniqueFunction<void()>&& cb) override
2✔
2706
        {
2✔
2707
            {
2✔
2708
                std::lock_guard lock(mutex);
2✔
2709
                callbacks.push_back(std::move(cb));
2✔
2710
            }
2✔
2711
            cv.notify_all();
2✔
2712
        }
2✔
2713

2714
        bool is_on_thread() const noexcept override
2✔
2715
        {
4✔
2716
            return true;
4✔
2717
        }
4✔
2718
        bool is_same_as(const Scheduler*) const noexcept override
2✔
2719
        {
2✔
UNCOV
2720
            return false;
×
UNCOV
2721
        }
×
2722
        bool can_invoke() const noexcept override
2✔
2723
        {
2✔
UNCOV
2724
            return true;
×
UNCOV
2725
        }
×
2726
    };
2✔
2727

2728
    auto scheduler = std::make_shared<ManualScheduler>();
2✔
2729

2730
    TestFile config;
2✔
2731
    config.schema_version = 0;
2✔
2732
    config.schema = Schema{{"object", {{"value", PropertyType::Int}}}};
2✔
2733
    config.scheduler = scheduler;
2✔
2734
    config.automatic_change_notifications = false;
2✔
2735

2736
    auto realm = Realm::get_shared_realm(config);
2✔
2737

2738
    realm->begin_transaction();
2✔
2739
    realm->async_commit_transaction([](std::exception_ptr) {});
2✔
2740

2741
    std::vector<util::UniqueFunction<void()>> callbacks;
2✔
2742
    {
2✔
2743
        std::unique_lock lock(scheduler->mutex);
2✔
2744
        // Wait for scheduler to be invoked
2745
        scheduler->cv.wait(lock, [&] {
4✔
2746
            return !scheduler->callbacks.empty();
4✔
2747
        });
4✔
2748
        callbacks.swap(scheduler->callbacks);
2✔
2749
    }
2✔
2750
    realm->close();
2✔
2751
    // Call whatever functions that was added to scheduler.
2752
    for (auto& cb : callbacks)
2✔
2753
        cb();
2✔
2754
}
2✔
2755

2756
// Our libuv scheduler currently does not support background threads, so we can
2757
// only run this on apple platforms
2758
#if REALM_PLATFORM_APPLE
2759
TEST_CASE("SharedRealm: async writes on multiple threads") {
5✔
2760
    _impl::RealmCoordinator::assert_no_open_realms();
5✔
2761

2762
    TestFile config;
5✔
2763
    config.cache = true;
5✔
2764
    config.schema_version = 0;
5✔
2765
    config.schema = Schema{{"object", {{"value", PropertyType::Int}}}};
5✔
2766
    auto realm = Realm::get_shared_realm(config);
5✔
2767
    auto table_key = realm->read_group().get_table("class_object")->get_key();
5✔
2768
    realm->close();
5✔
2769

2770
    struct QueueState {
5✔
2771
        dispatch_queue_t queue;
5✔
2772
        Realm::Config config;
5✔
2773
    };
5✔
2774
    std::vector<QueueState> queues;
5✔
2775
    for (int i = 0; i < 10; ++i) {
55✔
2776
        auto queue = dispatch_queue_create(util::format("queue %1", i).c_str(), 0);
50✔
2777
        Realm::Config queue_config = config;
50✔
2778
        queue_config.scheduler = util::Scheduler::make_dispatch(static_cast<void*>(queue));
50✔
2779
        queues.push_back({queue, std::move(queue_config)});
50✔
2780
    }
50✔
2781

2782
    std::atomic<size_t> completions = 0;
5✔
2783
    // Capturing by reference when mixing lambda and blocks is weird, so capture
2784
    // a pointer instead
2785
    auto completions_ptr = &completions;
5✔
2786

2787
    auto async_write_and_async_commit = [=](const Realm::Config& config) {
124✔
2788
        Realm::get_shared_realm(config)->async_begin_transaction([=] {
124✔
2789
            auto realm = Realm::get_shared_realm(config);
124✔
2790
            realm->read_group().get_table(table_key)->create_object();
124✔
2791
            realm->async_commit_transaction([=](std::exception_ptr) {
124✔
2792
                ++*completions_ptr;
124✔
2793
            });
124✔
2794
        });
124✔
2795
    };
124✔
2796
    auto async_write_and_sync_commit = [=](const Realm::Config& config) {
123✔
2797
        Realm::get_shared_realm(config)->async_begin_transaction([=] {
124✔
2798
            auto realm = Realm::get_shared_realm(config);
124✔
2799
            realm->read_group().get_table(table_key)->create_object();
124✔
2800
            realm->commit_transaction();
124✔
2801
            ++*completions_ptr;
124✔
2802
        });
124✔
2803
    };
123✔
2804
    auto sync_write_and_async_commit = [=](const Realm::Config& config) {
124✔
2805
        auto realm = Realm::get_shared_realm(config);
124✔
2806
        realm->begin_transaction();
124✔
2807
        realm->read_group().get_table(table_key)->create_object();
124✔
2808
        realm->async_commit_transaction([=](std::exception_ptr) {
124✔
2809
            ++*completions_ptr;
124✔
2810
        });
124✔
2811
    };
124✔
2812
    auto sync_write_and_sync_commit = [=](const Realm::Config& config) {
124✔
2813
        auto realm = Realm::get_shared_realm(config);
124✔
2814
        realm->begin_transaction();
124✔
2815
        realm->read_group().get_table(table_key)->create_object();
124✔
2816
        realm->commit_transaction();
124✔
2817
        ++*completions_ptr;
124✔
2818
    };
124✔
2819

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

2895
        util::EventLoop::main().run_until([&] {
1,570✔
2896
            return completions == expected_completions;
1,570✔
2897
        });
1,570✔
2898
    }
1✔
2899

2900

2901
    realm = Realm::get_shared_realm(config);
5✔
2902
    REQUIRE(realm->read_group().get_table(table_key)->size() == completions);
5!
2903

2904
    for (auto& queue : queues) {
50✔
2905
        dispatch_sync(queue.queue, ^{
50✔
2906
                      });
50✔
2907
    }
50✔
2908
}
5✔
2909
#endif
2910

2911
class LooperDelegate {
2912
public:
2913
    LooperDelegate() {}
2✔
2914
    void run_once()
2915
    {
4,577✔
2916
        for (auto it = m_tasks.begin(); it != m_tasks.end(); ++it) {
6,664✔
2917
            if (it->may_run && *it->may_run) {
2,093✔
2918
                it->the_job();
6✔
2919
                m_tasks.erase(it);
6✔
2920
                return;
6✔
2921
            }
6✔
2922
        }
2,093✔
2923
    }
4,577✔
2924
    std::shared_ptr<bool> add_task(util::UniqueFunction<void()>&& the_job)
2925
    {
6✔
2926
        m_tasks.push_back(Task{std::make_shared<bool>(false), std::move(the_job)});
6✔
2927
        return m_tasks.back().may_run;
6✔
2928
    }
6✔
2929
    bool has_tasks()
UNCOV
2930
    {
×
UNCOV
2931
        return !m_tasks.empty();
×
UNCOV
2932
    }
×
2933

2934
private:
2935
    struct Task {
2936
        std::shared_ptr<bool> may_run;
2937
        util::UniqueFunction<void()> the_job;
2938
    };
2939
    std::vector<Task> m_tasks;
2940
};
2941

2942
#ifndef _WIN32
2943
TEST_CASE("SharedRealm: async_writes_2") {
2✔
2944
    _impl::RealmCoordinator::assert_no_open_realms();
2✔
2945
    if (!util::EventLoop::has_implementation())
2✔
UNCOV
2946
        return;
×
2947

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

2985
    // Make some notify_only transactions
2986
    realm->async_begin_transaction(
2✔
2987
        [&]() {
2✔
2988
            *t1_rdy = true;
2✔
2989
        },
2✔
2990
        true);
2✔
2991
    realm->async_begin_transaction(
2✔
2992
        [&]() {
2✔
2993
            *t2_rdy = true;
2✔
2994
        },
2✔
2995
        true);
2✔
2996
    realm->async_begin_transaction(
2✔
2997
        [&]() {
2✔
2998
            *t3_rdy = true;
2✔
2999
        },
2✔
3000
        true);
2✔
3001

3002
    util::EventLoop::main().run_until([&, realm] {
4,577✔
3003
        ld.run_once();
4,577✔
3004
        return done;
4,577✔
3005
    });
4,577✔
3006
    REQUIRE(done);
2!
3007
}
2✔
3008
#endif
3009

3010
TEST_CASE("SharedRealm: notifications") {
14✔
3011
    if (!util::EventLoop::has_implementation())
14✔
UNCOV
3012
        return;
×
3013

3014
    TestFile config;
14✔
3015
    config.schema_version = 0;
14✔
3016
    config.schema = Schema{
14✔
3017
        {"object", {{"value", PropertyType::Int}}},
14✔
3018
    };
14✔
3019

3020
    struct Context : BindingContext {
14✔
3021
        size_t* change_count;
14✔
3022
        util::UniqueFunction<void()> did_change_fn;
14✔
3023
        util::UniqueFunction<void()> changes_available_fn;
14✔
3024

3025
        Context(size_t* out)
14✔
3026
            : change_count(out)
14✔
3027
        {
14✔
3028
        }
14✔
3029

3030
        void did_change(std::vector<ObserverState> const&, std::vector<void*> const&, bool) override
14✔
3031
        {
24✔
3032
            ++*change_count;
24✔
3033
            if (did_change_fn)
24✔
3034
                did_change_fn();
12✔
3035
        }
24✔
3036

3037
        void changes_available() override
14✔
3038
        {
14✔
3039
            if (changes_available_fn)
10✔
3040
                changes_available_fn();
2✔
3041
        }
10✔
3042
    };
14✔
3043

3044
    size_t change_count = 0;
14✔
3045
    auto realm = Realm::get_shared_realm(config);
14✔
3046
    realm->read_group();
14✔
3047
    auto context = new Context{&change_count};
14✔
3048
    realm->m_binding_context.reset(context);
14✔
3049
    realm->m_binding_context->realm = realm;
14✔
3050

3051
    SECTION("local notifications are sent synchronously") {
14✔
3052
        realm->begin_transaction();
2✔
3053
        REQUIRE(change_count == 0);
2!
3054
        realm->commit_transaction();
2✔
3055
        REQUIRE(change_count == 1);
2!
3056
    }
2✔
3057
#ifndef _WIN32
14✔
3058
    SECTION("remote notifications are sent asynchronously") {
14✔
3059
        auto r2 = Realm::get_shared_realm(config);
2✔
3060
        r2->begin_transaction();
2✔
3061
        r2->commit_transaction();
2✔
3062
        REQUIRE(change_count == 0);
2!
3063
        util::EventLoop::main().run_until([&] {
9✔
3064
            return change_count > 0;
9✔
3065
        });
9✔
3066
        REQUIRE(change_count == 1);
2!
3067
    }
2✔
3068

3069
    SECTION("notifications created in async transaction are sent synchronously") {
14✔
3070
        realm->async_begin_transaction([&] {
2✔
3071
            REQUIRE(change_count == 0);
2!
3072
            realm->async_commit_transaction();
2✔
3073
            REQUIRE(change_count == 1);
2!
3074
        });
2✔
3075
        REQUIRE(change_count == 0);
2!
3076
        util::EventLoop::main().run_until([&] {
16✔
3077
            return change_count > 0;
16✔
3078
        });
16✔
3079
        REQUIRE(change_count == 1);
2!
3080
        util::EventLoop::main().run_until([&] {
2,247✔
3081
            return !realm->has_pending_async_work();
2,247✔
3082
        });
2,247✔
3083
    }
2✔
3084
#endif
14✔
3085
    SECTION("refresh() from within changes_available() refreshes") {
14✔
3086
        context->changes_available_fn = [&] {
2✔
3087
            REQUIRE(realm->refresh());
2!
3088
        };
2✔
3089
        realm->set_auto_refresh(false);
2✔
3090

3091
        auto r2 = Realm::get_shared_realm(config);
2✔
3092
        r2->begin_transaction();
2✔
3093
        r2->commit_transaction();
2✔
3094
        realm->notify();
2✔
3095
        // Should return false as the realm was already advanced
3096
        REQUIRE_FALSE(realm->refresh());
2!
3097
    }
2✔
3098

3099
    SECTION("refresh() from within did_change() is a no-op") {
14✔
3100
        context->did_change_fn = [&] {
4✔
3101
            if (change_count > 1)
4✔
3102
                return;
2✔
3103

3104
            // Create another version so that refresh() advances the version
3105
            auto r2 = Realm::get_shared_realm(realm->config());
2✔
3106
            r2->begin_transaction();
2✔
3107
            r2->commit_transaction();
2✔
3108

3109
            REQUIRE_FALSE(realm->refresh());
2!
3110
        };
2✔
3111

3112
        auto r2 = Realm::get_shared_realm(config);
2✔
3113
        r2->begin_transaction();
2✔
3114
        r2->commit_transaction();
2✔
3115

3116
        REQUIRE(realm->refresh());
2!
3117
        REQUIRE(change_count == 1);
2!
3118

3119
        REQUIRE(realm->refresh());
2!
3120
        REQUIRE(change_count == 2);
2!
3121
        REQUIRE_FALSE(realm->refresh());
2!
3122
    }
2✔
3123

3124
    SECTION("begin_write() from within did_change() produces recursive notifications") {
14✔
3125
        context->did_change_fn = [&] {
8✔
3126
            if (realm->is_in_transaction())
8✔
3127
                realm->cancel_transaction();
6✔
3128
            if (change_count > 3)
8✔
3129
                return;
2✔
3130

3131
            // Create another version so that begin_write() advances the version
3132
            auto r2 = Realm::get_shared_realm(realm->config());
6✔
3133
            r2->begin_transaction();
6✔
3134
            r2->commit_transaction();
6✔
3135

3136
            realm->begin_transaction();
6✔
3137
            REQUIRE(change_count == 4);
6!
3138
        };
6✔
3139

3140
        auto r2 = Realm::get_shared_realm(config);
2✔
3141
        r2->begin_transaction();
2✔
3142
        r2->commit_transaction();
2✔
3143
        REQUIRE(realm->refresh());
2!
3144
        REQUIRE(change_count == 4);
2!
3145
        REQUIRE_FALSE(realm->refresh());
2!
3146
    }
2✔
3147

3148
#if REALM_ENABLE_SYNC
14✔
3149
    SECTION("SubscriptionStore writes produce notifications") {
14✔
3150
        auto subscription_store = sync::SubscriptionStore::create(TestHelper::get_db(realm));
2✔
3151
        REQUIRE(change_count == 0);
2!
3152
        util::EventLoop::main().run_until([&] {
11✔
3153
            return change_count > 0;
11✔
3154
        });
11✔
3155
        REQUIRE(change_count == 1);
2!
3156

3157
        subscription_store->get_active().make_mutable_copy().commit();
2✔
3158
        REQUIRE(change_count == 1);
2!
3159
        util::EventLoop::main().run_until([&] {
11✔
3160
            return change_count > 1;
11✔
3161
        });
11✔
3162
        REQUIRE(change_count == 2);
2!
3163
    }
2✔
3164
#endif
14✔
3165
}
14✔
3166

3167
TEST_CASE("SharedRealm: schema updating from external changes") {
14✔
3168
    TestFile config;
14✔
3169
    config.schema_version = 0;
14✔
3170
    config.schema_mode = SchemaMode::AdditiveExplicit;
14✔
3171
    config.schema = Schema{
14✔
3172
        {"object",
14✔
3173
         {
14✔
3174
             {"value", PropertyType::Int, Property::IsPrimary{true}},
14✔
3175
             {"value 2", PropertyType::Int, Property::IsPrimary{false}, Property::IsIndexed{true}},
14✔
3176
         }},
14✔
3177
    };
14✔
3178

3179
    SECTION("newly added columns update table columns but are not added to properties") {
14✔
3180
        // Does this test add any value when column keys are stable?
3181
        auto r1 = Realm::get_shared_realm(config);
4✔
3182
        auto r2 = Realm::get_shared_realm(config);
4✔
3183
        auto test = [&] {
4✔
3184
            r2->begin_transaction();
4✔
3185
            r2->read_group().get_table("class_object")->add_column(type_String, "new col");
4✔
3186
            r2->commit_transaction();
4✔
3187

3188
            auto& object_schema = *r1->schema().find("object");
4✔
3189
            REQUIRE(object_schema.persisted_properties.size() == 2);
4!
3190
            ColKey col = object_schema.persisted_properties[0].column_key;
4✔
3191
            r1->refresh();
4✔
3192
            REQUIRE(object_schema.persisted_properties[0].column_key == col);
4!
3193
        };
4✔
3194
        SECTION("with an active read transaction") {
4✔
3195
            r1->read_group();
2✔
3196
            test();
2✔
3197
        }
2✔
3198
        SECTION("without an active read transaction") {
4✔
3199
            r1->invalidate();
2✔
3200
            test();
2✔
3201
        }
2✔
3202
    }
4✔
3203

3204
    SECTION("beginning a read transaction checks for incompatible changes") {
14✔
3205
        auto r = Realm::get_shared_realm(config);
10✔
3206
        r->invalidate();
10✔
3207

3208
        auto& db = TestHelper::get_db(r);
10✔
3209
        WriteTransaction wt(db);
10✔
3210
        auto& table = *wt.get_table("class_object");
10✔
3211

3212
        SECTION("removing a property") {
10✔
3213
            table.remove_column(table.get_column_key("value"));
2✔
3214
            wt.commit();
2✔
3215
            REQUIRE_THROWS_CONTAINING(r->refresh(), "Property 'object.value' has been removed.");
2✔
3216
        }
2✔
3217

3218
        SECTION("change property type") {
10✔
3219
            table.remove_column(table.get_column_key("value 2"));
2✔
3220
            table.add_column(type_Float, "value 2");
2✔
3221
            wt.commit();
2✔
3222
            REQUIRE_THROWS_CONTAINING(r->refresh(),
2✔
3223
                                      "Property 'object.value 2' has been changed from 'int' to 'float'");
2✔
3224
        }
2✔
3225

3226
        SECTION("make property optional") {
10✔
3227
            table.remove_column(table.get_column_key("value 2"));
2✔
3228
            table.add_column(type_Int, "value 2", true);
2✔
3229
            wt.commit();
2✔
3230
            REQUIRE_THROWS_CONTAINING(r->refresh(), "Property 'object.value 2' has been made optional");
2✔
3231
        }
2✔
3232

3233
        SECTION("recreate column with no changes") {
10✔
3234
            table.remove_column(table.get_column_key("value 2"));
2✔
3235
            table.add_column(type_Int, "value 2");
2✔
3236
            wt.commit();
2✔
3237
            REQUIRE_NOTHROW(r->refresh());
2✔
3238
        }
2✔
3239

3240
        SECTION("remove index from non-PK") {
10✔
3241
            table.remove_search_index(table.get_column_key("value 2"));
2✔
3242
            wt.commit();
2✔
3243
            REQUIRE_NOTHROW(r->refresh());
2✔
3244
        }
2✔
3245
    }
10✔
3246
}
14✔
3247

3248
TEST_CASE("SharedRealm: close()") {
4✔
3249
    TestFile config;
4✔
3250
    config.schema_version = 1;
4✔
3251
    config.schema = Schema{
4✔
3252
        {"object", {{"value", PropertyType::Int}}},
4✔
3253
        {"list", {{"list", PropertyType::Object | PropertyType::Array, "object"}}},
4✔
3254
    };
4✔
3255

3256
    auto realm = Realm::get_shared_realm(config);
4✔
3257

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

3261
        realm->close();
2✔
3262
        REQUIRE(realm->is_closed());
2!
3263
        REQUIRE_EXCEPTION(realm->verify_open(), ClosedRealm, msg);
2✔
3264

3265
        REQUIRE_EXCEPTION(realm->update_schema(Schema{}), ClosedRealm, msg);
2✔
3266
        REQUIRE_EXCEPTION(realm->rename_property(Schema{}, "", "", ""), ClosedRealm, msg);
2✔
3267
        REQUIRE_EXCEPTION(realm->set_schema_subset(Schema{}), ClosedRealm, msg);
2✔
3268

3269
        REQUIRE_EXCEPTION(realm->begin_transaction(), ClosedRealm, msg);
2✔
3270
        REQUIRE_EXCEPTION(realm->commit_transaction(), ClosedRealm, msg);
2✔
3271
        REQUIRE_EXCEPTION(realm->cancel_transaction(), ClosedRealm, msg);
2✔
3272
        REQUIRE(!realm->is_in_transaction());
2!
3273

3274
        REQUIRE_EXCEPTION(realm->async_begin_transaction(nullptr), ClosedRealm, msg);
2✔
3275
        REQUIRE_EXCEPTION(realm->async_commit_transaction(nullptr), ClosedRealm, msg);
2✔
3276
        REQUIRE_EXCEPTION(realm->async_cancel_transaction(0), ClosedRealm, msg);
2✔
3277
        REQUIRE_FALSE(realm->is_in_async_transaction());
2!
3278

3279
        REQUIRE_EXCEPTION(realm->freeze(), ClosedRealm, msg);
2✔
3280
        REQUIRE_FALSE(realm->is_frozen());
2!
3281
        REQUIRE_EXCEPTION(realm->get_number_of_versions(), ClosedRealm, msg);
2✔
3282
        REQUIRE_EXCEPTION(realm->read_transaction_version(), ClosedRealm, msg);
2✔
3283
        REQUIRE_EXCEPTION(realm->duplicate(), ClosedRealm, msg);
2✔
3284

3285
        REQUIRE_EXCEPTION(realm->enable_wait_for_change(), ClosedRealm, msg);
2✔
3286
        REQUIRE_EXCEPTION(realm->wait_for_change(), ClosedRealm, msg);
2✔
3287
        REQUIRE_EXCEPTION(realm->wait_for_change_release(), ClosedRealm, msg);
2✔
3288

3289
        REQUIRE_NOTHROW(realm->notify());
2✔
3290
        REQUIRE_EXCEPTION(realm->refresh(), ClosedRealm, msg);
2✔
3291
        REQUIRE_EXCEPTION(realm->invalidate(), ClosedRealm, msg);
2✔
3292
        REQUIRE_EXCEPTION(realm->compact(), ClosedRealm, msg);
2✔
3293
        REQUIRE_EXCEPTION(realm->convert(realm->config()), ClosedRealm, msg);
2✔
3294
        REQUIRE_EXCEPTION(realm->write_copy(), ClosedRealm, msg);
2✔
3295

3296
#if REALM_ENABLE_SYNC
2✔
3297
        REQUIRE_FALSE(realm->sync_session());
2!
3298
        msg = "Flexible sync is not enabled";
2✔
3299
        REQUIRE_EXCEPTION(realm->get_latest_subscription_set(), IllegalOperation, msg);
2✔
3300
        REQUIRE_EXCEPTION(realm->get_active_subscription_set(), IllegalOperation, msg);
2✔
3301
#endif
2✔
3302
    }
2✔
3303

3304
    SECTION("fully closes database file even with live notifiers") {
4✔
3305
        auto& group = realm->read_group();
2✔
3306
        realm->begin_transaction();
2✔
3307
        auto obj = ObjectStore::table_for_object_type(group, "list")->create_object();
2✔
3308
        realm->commit_transaction();
2✔
3309

3310
        Results results(realm, ObjectStore::table_for_object_type(group, "object"));
2✔
3311
        List list(realm, obj.get_linklist("list"));
2✔
3312
        Object object(realm, obj);
2✔
3313

3314
        auto obj_token = object.add_notification_callback([](CollectionChangeSet) {});
2✔
3315
        auto list_token = list.add_notification_callback([](CollectionChangeSet) {});
2✔
3316
        auto results_token = results.add_notification_callback([](CollectionChangeSet) {});
2✔
3317

3318
        // Perform a dummy transaction to ensure the notifiers actually acquire
3319
        // resources that need to be closed
3320
        realm->begin_transaction();
2✔
3321
        realm->commit_transaction();
2✔
3322

3323
        realm->close();
2✔
3324

3325
        // Verify that we're able to acquire an exclusive lock
3326
        REQUIRE(DB::call_with_lock(config.path, [](auto) {}));
2!
3327
    }
2✔
3328
}
4✔
3329

3330
TEST_CASE("Realm::delete_files()") {
12✔
3331
    TestFile config;
12✔
3332
    config.schema_version = 1;
12✔
3333
    config.schema = Schema{{"object", {{"value", PropertyType::Int}}}};
12✔
3334
    auto realm = Realm::get_shared_realm(config);
12✔
3335
    auto path = config.path;
12✔
3336

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

3342
    SECTION("Deleting files of a closed Realm succeeds.") {
12✔
3343
        realm->close();
2✔
3344
        bool did_delete = false;
2✔
3345
        Realm::delete_files(path, &did_delete);
2✔
3346
        REQUIRE(did_delete);
2!
3347
        REQUIRE_FALSE(util::File::exists(path));
2!
3348
        REQUIRE_FALSE(util::File::exists(path + ".management"));
2!
3349
        REQUIRE_FALSE(util::File::exists(path + ".note"));
2!
3350
        REQUIRE_FALSE(util::File::exists(path + ".log"));
2!
3351

3352
        // Deleting the .lock file is not safe. It must still exist.
3353
        REQUIRE(util::File::exists(path + ".lock"));
2!
3354
    }
2✔
3355

3356
    SECTION("Trying to delete files of an open Realm fails.") {
12✔
3357
        REQUIRE_EXCEPTION(Realm::delete_files(path), ErrorCodes::DeleteOnOpenRealm,
2✔
3358
                          util::format("Cannot delete files of an open Realm: '%1' is still in use.", path));
2✔
3359
        REQUIRE(util::File::exists(path + ".lock"));
2!
3360
        REQUIRE(util::File::exists(path));
2!
3361
        REQUIRE(util::File::exists(path + ".management"));
2!
3362
#ifndef _WIN32
2✔
3363
        REQUIRE(util::File::exists(path + ".note"));
2!
3364
#endif
2✔
3365
        REQUIRE(util::File::exists(path + ".log"));
2!
3366
    }
2✔
3367

3368
    SECTION("Deleting the same Realm multiple times.") {
12✔
3369
        realm->close();
2✔
3370
        Realm::delete_files(path);
2✔
3371
        Realm::delete_files(path);
2✔
3372
        Realm::delete_files(path);
2✔
3373
    }
2✔
3374

3375
    SECTION("Calling delete on a folder that does not exist.") {
12✔
3376
        auto fake_path = "/tmp/doesNotExist/realm.424242";
2✔
3377
        bool did_delete = false;
2✔
3378
        Realm::delete_files(fake_path, &did_delete);
2✔
3379
        REQUIRE_FALSE(did_delete);
2!
3380
    }
2✔
3381

3382
    SECTION("passing did_delete is optional") {
12✔
3383
        realm->close();
2✔
3384
        Realm::delete_files(path, nullptr);
2✔
3385
    }
2✔
3386

3387
    SECTION("Deleting a Realm which does not exist does not set did_delete") {
12✔
3388
        TestFile new_config;
2✔
3389
        bool did_delete = false;
2✔
3390
        Realm::delete_files(new_config.path, &did_delete);
2✔
3391
        REQUIRE_FALSE(did_delete);
2!
3392
    }
2✔
3393
}
12✔
3394

3395
TEST_CASE("ShareRealm: in-memory mode from buffer") {
2✔
3396
    TestFile config;
2✔
3397
    config.schema_version = 1;
2✔
3398
    config.schema = Schema{
2✔
3399
        {"object", {{"value", PropertyType::Int}}},
2✔
3400
    };
2✔
3401

3402
    SECTION("Save and open Realm from in-memory buffer") {
2✔
3403
        // Write in-memory copy of Realm to a buffer
3404
        auto realm = Realm::get_shared_realm(config);
2✔
3405
        OwnedBinaryData realm_buffer = realm->write_copy();
2✔
3406

3407
        // Open the buffer as a new (immutable in-memory) Realm
3408
        realm::Realm::Config config2;
2✔
3409
        config2.in_memory = true;
2✔
3410
        config2.schema_mode = SchemaMode::Immutable;
2✔
3411
        config2.realm_data = realm_buffer.get();
2✔
3412

3413
        auto realm2 = Realm::get_shared_realm(config2);
2✔
3414

3415
        // Verify that it can read the schema and that it is the same
3416
        REQUIRE(realm->schema().size() == 1);
2!
3417
        auto it = realm->schema().find("object");
2✔
3418
        auto table = realm->read_group().get_table("class_object");
2✔
3419
        REQUIRE(it != realm->schema().end());
2!
3420
        REQUIRE(it->table_key == table->get_key());
2!
3421
        REQUIRE(it->persisted_properties.size() == 1);
2!
3422
        REQUIRE(it->persisted_properties[0].name == "value");
2!
3423
        REQUIRE(it->persisted_properties[0].column_key == table->get_column_key("value"));
2!
3424

3425
        // Test invalid configs
3426
        realm::Realm::Config config3;
2✔
3427
        config3.realm_data = realm_buffer.get();
2✔
3428
        REQUIRE_EXCEPTION(Realm::get_shared_realm(config3), IllegalCombination,
2✔
3429
                          "In-memory realms initialized from memory buffers can only be opened in read-only mode");
2✔
3430

3431
        config3.in_memory = true;
2✔
3432
        config3.schema_mode = SchemaMode::Immutable;
2✔
3433
        config3.path = "path";
2✔
3434
        REQUIRE_EXCEPTION(Realm::get_shared_realm(config3), IllegalCombination,
2✔
3435
                          "Specifying both memory buffer and path is invalid");
2✔
3436

3437
        config3.path = "";
2✔
3438
        config3.encryption_key = std::vector<char>(64, 'a');
2✔
3439
        REQUIRE_EXCEPTION(Realm::get_shared_realm(config3), IllegalCombination,
2✔
3440
                          "Memory buffers do not support encryption");
2✔
3441
    }
2✔
3442
}
2✔
3443

3444
TEST_CASE("ShareRealm: realm closed in did_change callback") {
6✔
3445
    TestFile config;
6✔
3446
    config.schema_version = 1;
6✔
3447
    config.schema = Schema{
6✔
3448
        {"object", {{"value", PropertyType::Int}}},
6✔
3449
    };
6✔
3450
    config.automatic_change_notifications = false;
6✔
3451
    auto r1 = Realm::get_shared_realm(config);
6✔
3452

3453
    r1->begin_transaction();
6✔
3454
    auto table = r1->read_group().get_table("class_object");
6✔
3455
    table->create_object();
6✔
3456
    r1->commit_transaction();
6✔
3457

3458
    struct Context : public BindingContext {
6✔
3459
        Context(std::shared_ptr<Realm>& realm)
6✔
3460
            : realm(&realm)
6✔
3461
        {
6✔
3462
        }
6✔
3463
        std::shared_ptr<Realm>* realm;
6✔
3464
        void did_change(std::vector<ObserverState> const&, std::vector<void*> const&, bool) override
6✔
3465
        {
6✔
3466
            auto realm = this->realm; // close() will delete `this`
6✔
3467
            (*realm)->close();
6✔
3468
            realm->reset();
6✔
3469
        }
6✔
3470
    };
6✔
3471

3472
    SECTION("did_change") {
6✔
3473
        r1->m_binding_context.reset(new Context(r1));
2✔
3474
        r1->invalidate();
2✔
3475

3476
        auto r2 = Realm::get_shared_realm(config);
2✔
3477
        r2->begin_transaction();
2✔
3478
        r2->read_group().get_table("class_object")->create_object();
2✔
3479
        r2->commit_transaction();
2✔
3480
        r2.reset();
2✔
3481

3482
        r1->notify();
2✔
3483
    }
2✔
3484

3485
    SECTION("did_change with async results") {
6✔
3486
        r1->m_binding_context.reset(new Context(r1));
2✔
3487
        Results results(r1, table->where());
2✔
3488
        auto token = results.add_notification_callback([&](CollectionChangeSet) {
2✔
3489
            // Should not be called.
UNCOV
3490
            REQUIRE(false);
×
UNCOV
3491
        });
×
3492

3493
        auto r2 = Realm::get_shared_realm(config);
2✔
3494
        r2->begin_transaction();
2✔
3495
        r2->read_group().get_table("class_object")->create_object();
2✔
3496
        r2->commit_transaction();
2✔
3497
        r2.reset();
2✔
3498

3499
        auto coordinator = _impl::RealmCoordinator::get_coordinator(config.path);
2✔
3500
        coordinator->on_change();
2✔
3501

3502
        r1->notify();
2✔
3503
    }
2✔
3504

3505
    SECTION("refresh") {
6✔
3506
        r1->m_binding_context.reset(new Context(r1));
2✔
3507

3508
        auto r2 = Realm::get_shared_realm(config);
2✔
3509
        r2->begin_transaction();
2✔
3510
        r2->read_group().get_table("class_object")->create_object();
2✔
3511
        r2->commit_transaction();
2✔
3512
        r2.reset();
2✔
3513

3514
        REQUIRE_FALSE(r1->refresh());
2!
3515
    }
2✔
3516
}
6✔
3517

3518
TEST_CASE("RealmCoordinator: schema cache") {
16✔
3519
    TestFile config;
16✔
3520
    auto coordinator = _impl::RealmCoordinator::get_coordinator(config.path);
16✔
3521

3522
    Schema cache_schema;
16✔
3523
    uint64_t cache_sv = -1, cache_tv = -1;
16✔
3524

3525
    Schema schema{
16✔
3526
        {"object", {{"value", PropertyType::Int}}},
16✔
3527
    };
16✔
3528
    Schema schema2{
16✔
3529
        {"object",
16✔
3530
         {
16✔
3531
             {"value", PropertyType::Int},
16✔
3532
         }},
16✔
3533
        {"object 2",
16✔
3534
         {
16✔
3535
             {"value", PropertyType::Int},
16✔
3536
         }},
16✔
3537
    };
16✔
3538

3539
    SECTION("valid initial schema sets cache") {
16✔
3540
        coordinator->cache_schema(schema, 5, 10);
2✔
3541
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3542
        REQUIRE(cache_schema == schema);
2!
3543
        REQUIRE(cache_sv == 5);
2!
3544
        REQUIRE(cache_tv == 10);
2!
3545
    }
2✔
3546

3547
    SECTION("cache can be updated with newer schema") {
16✔
3548
        coordinator->cache_schema(schema, 5, 10);
2✔
3549
        coordinator->cache_schema(schema2, 6, 11);
2✔
3550
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3551
        REQUIRE(cache_schema == schema2);
2!
3552
        REQUIRE(cache_sv == 6);
2!
3553
        REQUIRE(cache_tv == 11);
2!
3554
    }
2✔
3555

3556
    SECTION("empty schema is ignored") {
16✔
3557
        coordinator->cache_schema(Schema{}, 5, 10);
2✔
3558
        REQUIRE_FALSE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3559

3560
        coordinator->cache_schema(schema, 5, 10);
2✔
3561
        coordinator->cache_schema(Schema{}, 5, 10);
2✔
3562
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3563
        REQUIRE(cache_schema == schema);
2!
3564
        REQUIRE(cache_sv == 5);
2!
3565
        REQUIRE(cache_tv == 10);
2!
3566
    }
2✔
3567

3568
    SECTION("schema for older transaction is ignored") {
16✔
3569
        coordinator->cache_schema(schema, 5, 10);
2✔
3570
        coordinator->cache_schema(schema2, 4, 8);
2✔
3571

3572
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3573
        REQUIRE(cache_schema == schema);
2!
3574
        REQUIRE(cache_sv == 5);
2!
3575
        REQUIRE(cache_tv == 10);
2!
3576

3577
        coordinator->advance_schema_cache(10, 20);
2✔
3578
        coordinator->cache_schema(schema, 6, 15);
2✔
3579
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3580
        REQUIRE(cache_tv == 20); // should not have dropped to 15
2!
3581
    }
2✔
3582

3583
    SECTION("advance_schema() from transaction version bumps transaction version") {
16✔
3584
        coordinator->cache_schema(schema, 5, 10);
2✔
3585
        coordinator->advance_schema_cache(10, 12);
2✔
3586
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3587
        REQUIRE(cache_schema == schema);
2!
3588
        REQUIRE(cache_sv == 5);
2!
3589
        REQUIRE(cache_tv == 12);
2!
3590
    }
2✔
3591

3592
    SECTION("advance_schema() ending before transaction version does nothing") {
16✔
3593
        coordinator->cache_schema(schema, 5, 10);
2✔
3594
        coordinator->advance_schema_cache(8, 9);
2✔
3595
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3596
        REQUIRE(cache_schema == schema);
2!
3597
        REQUIRE(cache_sv == 5);
2!
3598
        REQUIRE(cache_tv == 10);
2!
3599
    }
2✔
3600

3601
    SECTION("advance_schema() extending over transaction version bumps version") {
16✔
3602
        coordinator->cache_schema(schema, 5, 10);
2✔
3603
        coordinator->advance_schema_cache(3, 15);
2✔
3604
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3605
        REQUIRE(cache_schema == schema);
2!
3606
        REQUIRE(cache_sv == 5);
2!
3607
        REQUIRE(cache_tv == 15);
2!
3608
    }
2✔
3609

3610
    SECTION("advance_schema() with no cahced schema does nothing") {
16✔
3611
        coordinator->advance_schema_cache(3, 15);
2✔
3612
        REQUIRE_FALSE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3613
    }
2✔
3614
}
16✔
3615

3616
TEST_CASE("SharedRealm: coordinator schema cache") {
26✔
3617
    TestFile config;
26✔
3618
    auto r = Realm::get_shared_realm(config);
26✔
3619
    auto coordinator = _impl::RealmCoordinator::get_coordinator(config.path);
26✔
3620

3621
    Schema cache_schema;
26✔
3622
    uint64_t cache_sv = -1, cache_tv = -1;
26✔
3623

3624
    Schema schema{
26✔
3625
        {"object", {{"value", PropertyType::Int}}},
26✔
3626
    };
26✔
3627
    Schema schema2{
26✔
3628
        {"object",
26✔
3629
         {
26✔
3630
             {"value", PropertyType::Int},
26✔
3631
         }},
26✔
3632
        {"object 2",
26✔
3633
         {
26✔
3634
             {"value", PropertyType::Int},
26✔
3635
         }},
26✔
3636
    };
26✔
3637

3638
    class ExternalWriter {
26✔
3639
    private:
26✔
3640
        std::shared_ptr<Realm> m_realm;
26✔
3641

3642
    public:
26✔
3643
        WriteTransaction wt;
26✔
3644
        ExternalWriter(Realm::Config const& config)
26✔
3645
            : m_realm([&] {
26✔
3646
                auto c = config;
18✔
3647
                c.scheduler = util::Scheduler::make_frozen(VersionID());
18✔
3648
                return _impl::RealmCoordinator::get_coordinator(c.path)->get_realm(c, util::none);
18✔
3649
            }())
18✔
3650
            , wt(TestHelper::get_db(m_realm))
26✔
3651
        {
26✔
3652
        }
18✔
3653
    };
26✔
3654

3655
    auto external_write = [&](Realm::Config const& config, auto&& fn) {
26✔
3656
        ExternalWriter wt(config);
16✔
3657
        fn(wt.wt);
16✔
3658
        wt.wt.commit();
16✔
3659
    };
16✔
3660

3661
    SECTION("is initially empty for uninitialized file") {
26✔
3662
        REQUIRE_FALSE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3663
    }
2✔
3664
    r->update_schema(schema);
26✔
3665

3666
    SECTION("is populated after calling update_schema()") {
26✔
3667
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3668
        REQUIRE(cache_sv == 0);
2!
3669
        REQUIRE(cache_schema == schema);
2!
3670
        REQUIRE(cache_schema.begin()->persisted_properties[0].column_key != ColKey{});
2!
3671
    }
2✔
3672

3673
    coordinator = nullptr;
26✔
3674
    r = nullptr;
26✔
3675
    r = Realm::get_shared_realm(config);
26✔
3676
    coordinator = _impl::RealmCoordinator::get_coordinator(config.path);
26✔
3677
    REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
26!
3678

3679
    SECTION("is populated after opening an initialized file") {
26✔
3680
        REQUIRE(cache_sv == 0);
2!
3681
        REQUIRE(cache_tv == 2); // with in-realm history the version doesn't reset
2!
3682
        REQUIRE(cache_schema == schema);
2!
3683
        REQUIRE(cache_schema.begin()->persisted_properties[0].column_key != ColKey{});
2!
3684
    }
2✔
3685

3686
    SECTION("transaction version is bumped after a local write") {
26✔
3687
        auto tv = cache_tv;
2✔
3688
        r->begin_transaction();
2✔
3689
        r->commit_transaction();
2✔
3690
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3691
        REQUIRE(cache_tv == tv + 1);
2!
3692
    }
2✔
3693

3694
    SECTION("notify() without a read transaction does not bump transaction version") {
26✔
3695
        auto tv = cache_tv;
4✔
3696

3697
        SECTION("non-schema change") {
4✔
3698
            external_write(config, [](auto& wt) {
2✔
3699
                wt.get_table("class_object")->create_object();
2✔
3700
            });
2✔
3701
        }
2✔
3702
        SECTION("schema change") {
4✔
3703
            external_write(config, [](auto& wt) {
2✔
3704
                wt.add_table("class_object 2");
2✔
3705
            });
2✔
3706
        }
2✔
3707

3708
        r->notify();
4✔
3709
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
4!
3710
        REQUIRE(cache_tv == tv);
4!
3711
        REQUIRE(cache_schema == schema);
4!
3712
    }
4✔
3713

3714
    SECTION("notify() with a read transaction bumps transaction version") {
26✔
3715
        r->read_group();
2✔
3716
        external_write(config, [](auto& wt) {
2✔
3717
            wt.get_table("class_object")->create_object();
2✔
3718
        });
2✔
3719

3720
        r->notify();
2✔
3721
        auto tv = cache_tv;
2✔
3722
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3723
        REQUIRE(cache_tv == tv + 1);
2!
3724
    }
2✔
3725

3726
    SECTION("notify() with a read transaction updates schema folloing external schema change") {
26✔
3727
        r->read_group();
2✔
3728
        external_write(config, [](auto& wt) {
2✔
3729
            wt.add_table("class_object 2");
2✔
3730
        });
2✔
3731

3732
        r->notify();
2✔
3733
        auto tv = cache_tv;
2✔
3734
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3735
        REQUIRE(cache_tv == tv + 1);
2!
3736
        REQUIRE(cache_schema.size() == 2);
2!
3737
        REQUIRE(cache_schema.find("object 2") != cache_schema.end());
2!
3738
    }
2✔
3739

3740
    SECTION("transaction version is bumped after refresh() following external non-schema write") {
26✔
3741
        external_write(config, [](auto& wt) {
2✔
3742
            wt.get_table("class_object")->create_object();
2✔
3743
        });
2✔
3744

3745
        r->refresh();
2✔
3746
        auto tv = cache_tv;
2✔
3747
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3748
        REQUIRE(cache_tv == tv + 1);
2!
3749
    }
2✔
3750

3751
    SECTION("schema is reread following refresh() over external schema change") {
26✔
3752
        external_write(config, [](auto& wt) {
2✔
3753
            wt.add_table("class_object 2");
2✔
3754
        });
2✔
3755

3756
        r->refresh();
2✔
3757
        auto tv = cache_tv;
2✔
3758
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3759
        REQUIRE(cache_tv == tv + 1);
2!
3760
        REQUIRE(cache_schema.size() == 2);
2!
3761
        REQUIRE(cache_schema.find("object 2") != cache_schema.end());
2!
3762
    }
2✔
3763

3764
    SECTION("update_schema() to version already on disk updates cache") {
26✔
3765
        r->read_group();
2✔
3766
        external_write(config, [](auto& wt) {
2✔
3767
            auto table = wt.add_table("class_object 2");
2✔
3768
            table->add_column(type_Int, "value");
2✔
3769
        });
2✔
3770

3771
        auto tv = cache_tv;
2✔
3772
        r->update_schema(schema2);
2✔
3773

3774
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3775
        REQUIRE(cache_tv == tv + 1); // only +1 because update_schema() did not perform a write
2!
3776
        REQUIRE(cache_schema.size() == 2);
2!
3777
        REQUIRE(cache_schema.find("object 2") != cache_schema.end());
2!
3778
    }
2✔
3779

3780
    SECTION("update_schema() to version already on disk updates cache") {
26✔
3781
        r->read_group();
2✔
3782
        external_write(config, [](auto& wt) {
2✔
3783
            auto table = wt.add_table("class_object 2");
2✔
3784
            table->add_column(type_Int, "value");
2✔
3785
        });
2✔
3786

3787
        auto tv = cache_tv;
2✔
3788
        r->update_schema(schema2);
2✔
3789

3790
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3791
        REQUIRE(cache_tv == tv + 1); // only +1 because update_schema() did not perform a write
2!
3792
        REQUIRE(cache_schema.size() == 2);
2!
3793
        REQUIRE(cache_schema.find("object 2") != cache_schema.end());
2!
3794
    }
2✔
3795

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

3799
        // We want to commit the write while we're waiting on the write lock on
3800
        // this thread, which can't really be done in a properly synchronized manner
3801
        std::chrono::microseconds wait_time{5000};
2✔
3802
#if REALM_ANDROID
3803
        // When running on device or in an emulator we need to wait longer due
3804
        // to them being slow
3805
        wait_time *= 10;
3806
#endif
3807

3808
        bool did_run = false;
2✔
3809
        JoiningThread thread([&] {
2✔
3810
            ExternalWriter writer(config);
2✔
3811
            if (writer.wt.get_table("class_object 2"))
2✔
UNCOV
3812
                return;
×
3813
            did_run = true;
2✔
3814

3815
            auto table = writer.wt.add_table("class_object 2");
2✔
3816
            table->add_column(type_Int, "value");
2✔
3817
            std::this_thread::sleep_for(wait_time * 2);
2✔
3818
            writer.wt.commit();
2✔
3819
        });
2✔
3820
        std::this_thread::sleep_for(wait_time);
2✔
3821

3822
        auto tv = cache_tv;
2✔
3823
        r->update_schema(Schema{
2✔
3824
            {"object", {{"value", PropertyType::Int}}},
2✔
3825
            {"object 2", {{"value", PropertyType::Int}}},
2✔
3826
        });
2✔
3827

3828
        // just skip the test if the timing was wrong to avoid spurious failures
3829
        if (!did_run)
2✔
UNCOV
3830
            return;
×
3831

3832
        REQUIRE(coordinator->get_cached_schema(cache_schema, cache_sv, cache_tv));
2!
3833
        REQUIRE(cache_tv == tv + 1); // only +1 because update_schema()'s write was rolled back
2!
3834
        REQUIRE(cache_schema.size() == 2);
2!
3835
        REQUIRE(cache_schema.find("object 2") != cache_schema.end());
2!
3836
    }
2✔
3837
}
26✔
3838

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

3842
    // Prepopulate the Realm with the schema.
3843
    Realm::Config config_with_schema = config;
2✔
3844
    config_with_schema.schema_version = 1;
2✔
3845
    config_with_schema.schema_mode = SchemaMode::Automatic;
2✔
3846
    config_with_schema.schema =
2✔
3847
        Schema{{"object",
2✔
3848
                {
2✔
3849
                    {"value", PropertyType::Int, Property::IsPrimary{true}},
2✔
3850
                    {"value 2", PropertyType::Int, Property::IsPrimary{false}, Property::IsIndexed{true}},
2✔
3851
                }}};
2✔
3852
    auto r1 = Realm::get_shared_realm(config_with_schema);
2✔
3853

3854
    // Retrieve the object schema in dynamic mode.
3855
    auto r2 = Realm::get_shared_realm(config);
2✔
3856
    auto* object_schema = &*r2->schema().find("object");
2✔
3857

3858
    // Perform an empty write to create a new version, resulting in the other Realm needing to re-read the schema.
3859
    r1->begin_transaction();
2✔
3860
    r1->commit_transaction();
2✔
3861

3862
    // Advance to the latest version, and verify the object schema is at the same location in memory.
3863
    r2->read_group();
2✔
3864
    REQUIRE(object_schema == &*r2->schema().find("object"));
2!
3865
}
2✔
3866

3867
TEST_CASE("SharedRealm: declaring an object as embedded results in creating an embedded table") {
2✔
3868
    TestFile config;
2✔
3869

3870
    // Prepopulate the Realm with the schema.
3871
    config.schema = Schema{{"object1",
2✔
3872
                            ObjectSchema::ObjectType::Embedded,
2✔
3873
                            {
2✔
3874
                                {"value", PropertyType::Int},
2✔
3875
                            }},
2✔
3876
                           {"object2",
2✔
3877
                            {
2✔
3878
                                {"value", PropertyType::Object | PropertyType::Nullable, "object1"},
2✔
3879
                            }}};
2✔
3880
    auto r1 = Realm::get_shared_realm(config);
2✔
3881

3882
    Group& g = r1->read_group();
2✔
3883
    auto t = g.get_table("class_object1");
2✔
3884
    REQUIRE(t->is_embedded());
2!
3885
}
2✔
3886

3887
TEST_CASE("SharedRealm: SchemaChangedFunction") {
16✔
3888
    struct Context : BindingContext {
16✔
3889
        size_t* change_count;
16✔
3890
        Schema* schema;
16✔
3891
        Context(size_t* count_out, Schema* schema_out)
16✔
3892
            : change_count(count_out)
19✔
3893
            , schema(schema_out)
19✔
3894
        {
22✔
3895
        }
22✔
3896

3897
        void schema_did_change(Schema const& changed_schema) override
16✔
3898
        {
16✔
3899
            ++*change_count;
10✔
3900
            *schema = changed_schema;
10✔
3901
        }
10✔
3902
    };
16✔
3903

3904
    size_t schema_changed_called = 0;
16✔
3905
    Schema changed_fixed_schema;
16✔
3906
    TestFile config;
16✔
3907
    RealmConfig dynamic_config = config;
16✔
3908

3909
    config.schema = Schema{{"object1",
16✔
3910
                            {
16✔
3911
                                {"value", PropertyType::Int},
16✔
3912
                            }},
16✔
3913
                           {"object2",
16✔
3914
                            {
16✔
3915
                                {"value", PropertyType::Int},
16✔
3916
                            }}};
16✔
3917
    config.schema_version = 1;
16✔
3918
    auto r1 = Realm::get_shared_realm(config);
16✔
3919
    r1->read_group();
16✔
3920
    r1->m_binding_context.reset(new Context(&schema_changed_called, &changed_fixed_schema));
16✔
3921

3922
    SECTION("Fixed schema") {
16✔
3923
        SECTION("update_schema") {
10✔
3924
            auto new_schema = Schema{{"object3",
2✔
3925
                                      {
2✔
3926
                                          {"value", PropertyType::Int},
2✔
3927
                                      }}};
2✔
3928
            r1->update_schema(new_schema, 2);
2✔
3929
            REQUIRE(schema_changed_called == 1);
2!
3930
            REQUIRE(changed_fixed_schema.find("object3")->property_for_name("value")->column_key != ColKey{});
2!
3931
        }
2✔
3932

3933
        SECTION("Open a new Realm instance with same config won't trigger") {
10✔
3934
            auto r2 = Realm::get_shared_realm(config);
2✔
3935
            REQUIRE(schema_changed_called == 0);
2!
3936
        }
2✔
3937

3938
        SECTION("Non schema related transaction doesn't trigger") {
10✔
3939
            auto r2 = Realm::get_shared_realm(config);
2✔
3940
            r2->begin_transaction();
2✔
3941
            r2->commit_transaction();
2✔
3942
            r1->refresh();
2✔
3943
            REQUIRE(schema_changed_called == 0);
2!
3944
        }
2✔
3945

3946
        SECTION("Schema is changed by another Realm") {
10✔
3947
            auto r2 = Realm::get_shared_realm(config);
2✔
3948
            r2->begin_transaction();
2✔
3949
            r2->read_group().get_table("class_object1")->add_column(type_String, "new col");
2✔
3950
            r2->commit_transaction();
2✔
3951
            r1->refresh();
2✔
3952
            REQUIRE(schema_changed_called == 1);
2!
3953
            REQUIRE(changed_fixed_schema.find("object1")->property_for_name("value")->column_key != ColKey{});
2!
3954
        }
2✔
3955

3956
        // This is not a valid use case. m_schema won't be refreshed.
3957
        SECTION("Schema is changed by this Realm won't trigger") {
10✔
3958
            r1->begin_transaction();
2✔
3959
            r1->read_group().get_table("class_object1")->add_column(type_String, "new col");
2✔
3960
            r1->commit_transaction();
2✔
3961
            REQUIRE(schema_changed_called == 0);
2!
3962
        }
2✔
3963
    }
10✔
3964

3965
    SECTION("Dynamic schema") {
16✔
3966
        size_t dynamic_schema_changed_called = 0;
6✔
3967
        Schema changed_dynamic_schema;
6✔
3968
        auto r2 = Realm::get_shared_realm(dynamic_config);
6✔
3969
        r2->m_binding_context.reset(new Context(&dynamic_schema_changed_called, &changed_dynamic_schema));
6✔
3970

3971
        SECTION("set_schema_subset") {
6✔
3972
            auto new_schema = Schema{{"object1",
2✔
3973
                                      {
2✔
3974
                                          {"value", PropertyType::Int},
2✔
3975
                                      }}};
2✔
3976
            r2->set_schema_subset(new_schema);
2✔
3977
            REQUIRE(schema_changed_called == 0);
2!
3978
            REQUIRE(dynamic_schema_changed_called == 1);
2!
3979
            REQUIRE(changed_dynamic_schema.find("object1")->property_for_name("value")->column_key != ColKey{});
2!
3980
        }
2✔
3981

3982
        SECTION("Non schema related transaction will always trigger in dynamic mode") {
6✔
3983
            auto r1 = Realm::get_shared_realm(config);
2✔
3984
            // An empty transaction will trigger the schema changes always in dynamic mode.
3985
            r1->begin_transaction();
2✔
3986
            r1->commit_transaction();
2✔
3987
            r2->refresh();
2✔
3988
            REQUIRE(dynamic_schema_changed_called == 1);
2!
3989
            REQUIRE(changed_dynamic_schema.find("object1")->property_for_name("value")->column_key != ColKey{});
2!
3990
        }
2✔
3991

3992
        SECTION("Schema is changed by another Realm") {
6✔
3993
            r1->begin_transaction();
2✔
3994
            r1->read_group().get_table("class_object1")->add_column(type_String, "new col");
2✔
3995
            r1->commit_transaction();
2✔
3996
            r2->refresh();
2✔
3997
            REQUIRE(dynamic_schema_changed_called == 1);
2!
3998
            REQUIRE(changed_dynamic_schema.find("object1")->property_for_name("value")->column_key != ColKey{});
2!
3999
        }
2✔
4000
    }
6✔
4001
}
16✔
4002

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

4028
    SECTION("compact reduces the file size") {
4✔
4029
#ifndef _WIN32
2✔
4030
        // Confirm expected sizes before and after opening the Realm
4031
        size_t size_before = size_t(util::File(config.path).get_size());
2✔
4032
        r = Realm::get_shared_realm(config);
2✔
4033
        REQUIRE(num_opens == 2);
2!
4034
        r->close();
2✔
4035
        REQUIRE(size_t(util::File(config.path).get_size()) == size_before); // File size after returning false
2!
4036
        r = Realm::get_shared_realm(config);
2✔
4037
        REQUIRE(num_opens == 3);
2!
4038
        REQUIRE(size_t(util::File(config.path).get_size()) < size_before); // File size after returning true
2!
4039

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

4043
        // Registering for a collection notification shouldn't crash when compact on launch is used.
4044
        Results results(r, r->read_group().get_table("class_object"));
2✔
4045
        results.add_notification_callback([](CollectionChangeSet const&) {});
2✔
4046
        r->close();
2✔
4047
#endif
2✔
4048
    }
2✔
4049

4050
    SECTION("compact function does not get invoked if realm is open on another thread") {
4✔
4051
        config.scheduler = util::Scheduler::make_frozen(VersionID());
2✔
4052
        r = Realm::get_shared_realm(config);
2✔
4053
        REQUIRE(num_opens == 2);
2!
4054
        JoiningThread([&] {
2✔
4055
            auto r2 = Realm::get_shared_realm(config);
2✔
4056
            REQUIRE(num_opens == 2);
2!
4057
        });
2✔
4058
        r->close();
2✔
4059
        JoiningThread([&] {
2✔
4060
            auto r3 = Realm::get_shared_realm(config);
2✔
4061
            REQUIRE(num_opens == 3);
2!
4062
        });
2✔
4063
    }
2✔
4064
}
4✔
4065

4066
struct ModeAutomatic {
4067
    static constexpr SchemaMode mode = SchemaMode::Automatic;
4068
    static constexpr bool should_call_init_on_version_bump = false;
4069
};
4070
struct ModeAdditive {
4071
    static constexpr SchemaMode mode = SchemaMode::AdditiveExplicit;
4072
    static constexpr bool should_call_init_on_version_bump = false;
4073
};
4074
struct ModeManual {
4075
    static constexpr SchemaMode mode = SchemaMode::Manual;
4076
    static constexpr bool should_call_init_on_version_bump = false;
4077
};
4078
struct ModeSoftResetFile {
4079
    static constexpr SchemaMode mode = SchemaMode::SoftResetFile;
4080
    static constexpr bool should_call_init_on_version_bump = true;
4081
};
4082
struct ModeHardResetFile {
4083
    static constexpr SchemaMode mode = SchemaMode::HardResetFile;
4084
    static constexpr bool should_call_init_on_version_bump = true;
4085
};
4086

4087
TEMPLATE_TEST_CASE("SharedRealm: update_schema with initialization_function", "[init][update schema]", ModeAutomatic,
4088
                   ModeAdditive, ModeManual, ModeSoftResetFile, ModeHardResetFile)
4089
{
30✔
4090
    TestFile config;
30✔
4091
    config.schema_mode = TestType::mode;
30✔
4092
    bool initialization_function_called = false;
30✔
4093
    uint64_t schema_version_in_callback = -1;
30✔
4094
    Schema schema_in_callback;
30✔
4095
    auto initialization_function = [&initialization_function_called, &schema_version_in_callback,
30✔
4096
                                    &schema_in_callback](auto shared_realm) {
30✔
4097
        REQUIRE(shared_realm->is_in_transaction());
24!
4098
        initialization_function_called = true;
24✔
4099
        schema_version_in_callback = shared_realm->schema_version();
24✔
4100
        schema_in_callback = shared_realm->schema();
24✔
4101
    };
24✔
4102

4103
    Schema schema{
30✔
4104
        {"object", {{"value", PropertyType::String}}},
30✔
4105
    };
30✔
4106

4107
    SECTION("call initialization function directly by update_schema") {
30✔
4108
        // Open in dynamic mode with no schema specified
4109
        auto realm = Realm::get_shared_realm(config);
10✔
4110
        REQUIRE_FALSE(initialization_function_called);
10!
4111

4112
        realm->update_schema(schema, 0, nullptr, initialization_function);
10✔
4113
        REQUIRE(initialization_function_called);
10!
4114
        REQUIRE(schema_version_in_callback == 0);
10!
4115
        REQUIRE(schema_in_callback.compare(schema).size() == 0);
10!
4116
    }
10✔
4117

4118
    config.schema_version = 0;
30✔
4119
    config.schema = schema;
30✔
4120

4121
    SECTION("initialization function should be called for unversioned realm") {
30✔
4122
        config.initialization_function = initialization_function;
10✔
4123
        Realm::get_shared_realm(config);
10✔
4124
        REQUIRE(initialization_function_called);
10!
4125
        REQUIRE(schema_version_in_callback == 0);
10!
4126
        REQUIRE(schema_in_callback.compare(schema).size() == 0);
10!
4127
    }
10✔
4128

4129
    SECTION("initialization function for versioned realm") {
30✔
4130
        // Initialize v0
4131
        Realm::get_shared_realm(config);
10✔
4132

4133
        config.schema_version = 1;
10✔
4134
        config.initialization_function = initialization_function;
10✔
4135
        Realm::get_shared_realm(config);
10✔
4136
        REQUIRE(initialization_function_called == TestType::should_call_init_on_version_bump);
10!
4137
        if (TestType::should_call_init_on_version_bump) {
10✔
4138
            REQUIRE(schema_version_in_callback == 1);
4!
4139
            REQUIRE(schema_in_callback.compare(schema).size() == 0);
4!
4140
        }
4✔
4141
    }
10✔
4142
}
30✔
4143

4144
TEST_CASE("BindingContext is notified about delivery of change notifications") {
16✔
4145
    _impl::RealmCoordinator::assert_no_open_realms();
16✔
4146
    InMemoryTestFile config;
16✔
4147
    config.automatic_change_notifications = false;
16✔
4148

4149
    auto r = Realm::get_shared_realm(config);
16✔
4150
    r->update_schema({
16✔
4151
        {"object", {{"value", PropertyType::Int}}},
16✔
4152
    });
16✔
4153

4154
    auto coordinator = _impl::RealmCoordinator::get_coordinator(config.path);
16✔
4155
    auto table = r->read_group().get_table("class_object");
16✔
4156

4157
    SECTION("BindingContext notified even if no callbacks are registered") {
16✔
4158
        static int binding_context_start_notify_calls = 0;
4✔
4159
        static int binding_context_end_notify_calls = 0;
4✔
4160
        struct Context : BindingContext {
4✔
4161
            void will_send_notifications() override
4✔
4162
            {
4✔
4163
                ++binding_context_start_notify_calls;
4✔
4164
            }
4✔
4165

4166
            void did_send_notifications() override
4✔
4167
            {
4✔
4168
                ++binding_context_end_notify_calls;
4✔
4169
            }
4✔
4170
        };
4✔
4171
        r->m_binding_context.reset(new Context());
4✔
4172

4173
        SECTION("local commit") {
4✔
4174
            binding_context_start_notify_calls = 0;
2✔
4175
            binding_context_end_notify_calls = 0;
2✔
4176
            coordinator->on_change();
2✔
4177
            r->begin_transaction();
2✔
4178
            REQUIRE(binding_context_start_notify_calls == 1);
2!
4179
            REQUIRE(binding_context_end_notify_calls == 1);
2!
4180
            r->cancel_transaction();
2✔
4181
        }
2✔
4182

4183
        SECTION("remote commit") {
4✔
4184
            binding_context_start_notify_calls = 0;
2✔
4185
            binding_context_end_notify_calls = 0;
2✔
4186
            JoiningThread([&] {
2✔
4187
                auto r2 = coordinator->get_realm(util::Scheduler::make_frozen(VersionID()));
2✔
4188
                r2->begin_transaction();
2✔
4189
                auto table2 = r2->read_group().get_table("class_object");
2✔
4190
                table2->create_object();
2✔
4191
                r2->commit_transaction();
2✔
4192
            });
2✔
4193
            advance_and_notify(*r);
2✔
4194
            REQUIRE(binding_context_start_notify_calls == 1);
2!
4195
            REQUIRE(binding_context_end_notify_calls == 1);
2!
4196
        }
2✔
4197
    }
4✔
4198

4199
    SECTION("notify BindingContext before and after sending notifications") {
16✔
4200
        static int binding_context_start_notify_calls = 0;
4✔
4201
        static int binding_context_end_notify_calls = 0;
4✔
4202
        static int notification_calls = 0;
4✔
4203

4204
        auto col = table->get_column_key("value");
4✔
4205
        Results results1(r, table->where().greater_equal(col, 0));
4✔
4206
        Results results2(r, table->where().less(col, 10));
4✔
4207

4208
        auto token1 = results1.add_notification_callback([&](CollectionChangeSet) {
4✔
4209
            ++notification_calls;
4✔
4210
        });
4✔
4211

4212
        auto token2 = results2.add_notification_callback([&](CollectionChangeSet) {
4✔
4213
            ++notification_calls;
4✔
4214
        });
4✔
4215

4216
        struct Context : BindingContext {
4✔
4217
            void will_send_notifications() override
4✔
4218
            {
4✔
4219
                REQUIRE(notification_calls == 0);
4!
4220
                REQUIRE(binding_context_end_notify_calls == 0);
4!
4221
                ++binding_context_start_notify_calls;
4✔
4222
            }
4✔
4223

4224
            void did_send_notifications() override
4✔
4225
            {
4✔
4226
                REQUIRE(notification_calls == 2);
4!
4227
                REQUIRE(binding_context_start_notify_calls == 1);
4!
4228
                ++binding_context_end_notify_calls;
4✔
4229
            }
4✔
4230
        };
4✔
4231
        r->m_binding_context.reset(new Context());
4✔
4232

4233
        SECTION("local commit") {
4✔
4234
            binding_context_start_notify_calls = 0;
2✔
4235
            binding_context_end_notify_calls = 0;
2✔
4236
            notification_calls = 0;
2✔
4237
            coordinator->on_change();
2✔
4238
            r->begin_transaction();
2✔
4239
            table->create_object();
2✔
4240
            r->commit_transaction();
2✔
4241
            REQUIRE(binding_context_start_notify_calls == 1);
2!
4242
            REQUIRE(binding_context_end_notify_calls == 1);
2!
4243
        }
2✔
4244

4245
        SECTION("remote commit") {
4✔
4246
            binding_context_start_notify_calls = 0;
2✔
4247
            binding_context_end_notify_calls = 0;
2✔
4248
            notification_calls = 0;
2✔
4249
            JoiningThread([&] {
2✔
4250
                auto r2 = coordinator->get_realm(util::Scheduler::make_frozen(VersionID()));
2✔
4251
                r2->begin_transaction();
2✔
4252
                auto table2 = r2->read_group().get_table("class_object");
2✔
4253
                table2->create_object();
2✔
4254
                r2->commit_transaction();
2✔
4255
            });
2✔
4256
            advance_and_notify(*r);
2✔
4257
            REQUIRE(binding_context_start_notify_calls == 1);
2!
4258
            REQUIRE(binding_context_end_notify_calls == 1);
2!
4259
        }
2✔
4260
    }
4✔
4261

4262
    SECTION("did_send() is skipped if the Realm is closed first") {
16✔
4263
        Results results(r, table->where());
8✔
4264
        bool do_close = true;
8✔
4265
        auto token = results.add_notification_callback([&](CollectionChangeSet) {
8✔
4266
            if (do_close)
8✔
4267
                r->close();
4✔
4268
        });
8✔
4269

4270
        struct FailOnDidSend : BindingContext {
8✔
4271
            void did_send_notifications() override
8✔
4272
            {
8✔
UNCOV
4273
                FAIL("did_send_notifications() should not have been called");
×
UNCOV
4274
            }
×
4275
        };
8✔
4276
        struct CloseOnWillChange : FailOnDidSend {
8✔
4277
            Realm& realm;
8✔
4278
            CloseOnWillChange(Realm& realm)
8✔
4279
                : realm(realm)
8✔
4280
            {
8✔
4281
            }
4✔
4282

4283
            void will_send_notifications() override
8✔
4284
            {
8✔
4285
                realm.close();
4✔
4286
            }
4✔
4287
        };
8✔
4288

4289
        SECTION("closed in notification callback for notify()") {
8✔
4290
            r->m_binding_context.reset(new FailOnDidSend);
2✔
4291
            coordinator->on_change();
2✔
4292
            r->notify();
2✔
4293
        }
2✔
4294

4295
        SECTION("closed in notification callback for refresh()") {
8✔
4296
            do_close = false;
2✔
4297
            coordinator->on_change();
2✔
4298
            r->notify();
2✔
4299
            do_close = true;
2✔
4300

4301
            JoiningThread([&] {
2✔
4302
                auto r = coordinator->get_realm(util::Scheduler::make_frozen(VersionID()));
2✔
4303
                r->begin_transaction();
2✔
4304
                r->read_group().get_table("class_object")->create_object();
2✔
4305
                r->commit_transaction();
2✔
4306
            });
2✔
4307

4308
            r->m_binding_context.reset(new FailOnDidSend);
2✔
4309
            coordinator->on_change();
2✔
4310
            r->refresh();
2✔
4311
        }
2✔
4312

4313
        SECTION("closed in will_send() for notify()") {
8✔
4314
            r->m_binding_context.reset(new CloseOnWillChange(*r));
2✔
4315
            coordinator->on_change();
2✔
4316
            r->notify();
2✔
4317
        }
2✔
4318

4319
        SECTION("closed in will_send() for refresh()") {
8✔
4320
            do_close = false;
2✔
4321
            coordinator->on_change();
2✔
4322
            r->notify();
2✔
4323
            do_close = true;
2✔
4324

4325
            JoiningThread([&] {
2✔
4326
                auto r = coordinator->get_realm(util::Scheduler::make_frozen(VersionID()));
2✔
4327
                r->begin_transaction();
2✔
4328
                r->read_group().get_table("class_object")->create_object();
2✔
4329
                r->commit_transaction();
2✔
4330
            });
2✔
4331

4332
            r->m_binding_context.reset(new CloseOnWillChange(*r));
2✔
4333
            coordinator->on_change();
2✔
4334
            r->refresh();
2✔
4335
        }
2✔
4336
    }
8✔
4337
#ifdef _WIN32
4338
    _impl::RealmCoordinator::clear_all_caches();
4339
#endif
4340
}
16✔
4341

4342
TEST_CASE("RealmCoordinator: get_unbound_realm()") {
8✔
4343
    TestFile config;
8✔
4344
    config.cache = true;
8✔
4345
    config.schema = Schema{
8✔
4346
        {"object", {{"value", PropertyType::Int}}},
8✔
4347
    };
8✔
4348

4349
    ThreadSafeReference ref;
8✔
4350
    JoiningThread([&] {
8✔
4351
        ref = _impl::RealmCoordinator::get_coordinator(config)->get_unbound_realm();
8✔
4352
    });
8✔
4353

4354
    SECTION("checks thread after being resolved") {
8✔
4355
        auto realm = Realm::get_shared_realm(std::move(ref));
2✔
4356
        REQUIRE_NOTHROW(realm->verify_thread());
2✔
4357
        JoiningThread([&] {
2✔
4358
            REQUIRE_EXCEPTION(realm->verify_thread(), WrongThread, "Realm accessed from incorrect thread.");
2✔
4359
        });
2✔
4360
    }
2✔
4361

4362
    SECTION("delivers notifications to the thread it is resolved on") {
8✔
4363
#ifndef _WIN32
2✔
4364
        if (!util::EventLoop::has_implementation())
2✔
UNCOV
4365
            return;
×
4366
        auto realm = Realm::get_shared_realm(std::move(ref));
2✔
4367
        Results results(realm, ObjectStore::table_for_object_type(realm->read_group(), "object")->where());
2✔
4368
        bool called = false;
2✔
4369
        auto token = results.add_notification_callback([&](CollectionChangeSet) {
2✔
4370
            called = true;
2✔
4371
        });
2✔
4372
        util::EventLoop::main().run_until([&] {
37✔
4373
            return called;
37✔
4374
        });
37✔
4375
#endif
2✔
4376
    }
2✔
4377

4378
    SECTION("resolves to existing cached Realm for the thread if caching is enabled") {
8✔
4379
        auto r1 = Realm::get_shared_realm(config);
2✔
4380
        auto r2 = Realm::get_shared_realm(std::move(ref));
2✔
4381
        REQUIRE(r1 == r2);
2!
4382
    }
2✔
4383

4384
    SECTION("resolves to a new Realm if caching is disabled") {
8✔
4385
        config.cache = false;
2✔
4386
        auto r1 = Realm::get_shared_realm(config);
2✔
4387
        auto r2 = Realm::get_shared_realm(std::move(ref));
2✔
4388
        REQUIRE(r1 != r2);
2!
4389

4390
        // New unbound with cache disabled
4391
        JoiningThread([&] {
2✔
4392
            ref = _impl::RealmCoordinator::get_coordinator(config)->get_unbound_realm();
2✔
4393
        });
2✔
4394
        auto r3 = Realm::get_shared_realm(std::move(ref));
2✔
4395
        REQUIRE(r1 != r3);
2!
4396
        REQUIRE(r2 != r3);
2!
4397

4398
        // New local with cache enabled should grab the resolved unbound
4399
        config.cache = true;
2✔
4400
        auto r4 = Realm::get_shared_realm(config);
2✔
4401
        REQUIRE(r4 == r2);
2!
4402
    }
2✔
4403
}
8✔
4404

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

4410
    {
40✔
4411
        auto realm = Realm::get_shared_realm(config);
40✔
4412
        realm->begin_transaction();
40✔
4413
        realm->read_group().get_table("class_object")->create_object();
40✔
4414
        realm->commit_transaction();
40✔
4415
    }
40✔
4416

4417
    config.schema_mode = SchemaMode::Immutable;
40✔
4418
    auto realm = Realm::get_shared_realm(config);
40✔
4419
    realm->read_group();
40✔
4420

4421
    SECTION("unsupported functions") {
40✔
4422
        SECTION("update_schema()") {
10✔
4423
            REQUIRE_THROWS_AS(realm->compact(), WrongTransactionState);
2✔
4424
        }
2✔
4425
        SECTION("begin_transaction()") {
10✔
4426
            REQUIRE_THROWS_AS(realm->begin_transaction(), WrongTransactionState);
2✔
4427
        }
2✔
4428
        SECTION("async_begin_transaction()") {
10✔
4429
            REQUIRE_THROWS_AS(realm->async_begin_transaction(nullptr), WrongTransactionState);
2✔
4430
        }
2✔
4431
        SECTION("refresh()") {
10✔
4432
            REQUIRE_THROWS_AS(realm->refresh(), WrongTransactionState);
2✔
4433
        }
2✔
4434
        SECTION("compact()") {
10✔
4435
            REQUIRE_THROWS_AS(realm->compact(), WrongTransactionState);
2✔
4436
        }
2✔
4437
    }
10✔
4438

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

4496
TEST_CASE("KeyPathMapping generation") {
2✔
4497
    TestFile config;
2✔
4498
    realm::query_parser::KeyPathMapping mapping;
2✔
4499

4500
    SECTION("class aliasing") {
2✔
4501
        Schema schema = {
2✔
4502
            {"PersistedName", {{"age", PropertyType::Int}}, {}, "AlternativeName"},
2✔
4503
            {"class_with_policy",
2✔
4504
             {{"value", PropertyType::Int},
2✔
4505
              {"child", PropertyType::Object | PropertyType::Nullable, "class_with_policy"}},
2✔
4506
             {{"parents", PropertyType::LinkingObjects | PropertyType::Array, "class_with_policy", "child"}},
2✔
4507
             "ClassWithPolicy"},
2✔
4508
        };
2✔
4509
        schema.validate();
2✔
4510
        config.schema = schema;
2✔
4511
        auto realm = Realm::get_shared_realm(config);
2✔
4512
        realm::populate_keypath_mapping(mapping, *realm);
2✔
4513
        REQUIRE(mapping.has_table_mapping("AlternativeName"));
2!
4514
        REQUIRE("class_PersistedName" == mapping.get_table_mapping("AlternativeName"));
2!
4515

4516
        auto table = realm->read_group().get_table("class_class_with_policy");
2✔
4517
        std::vector<Mixed> args{0};
2✔
4518
        auto q = table->query("parents.value = $0", args, mapping);
2✔
4519
        REQUIRE(q.count() == 0);
2!
4520
    }
2✔
4521
}
2✔
4522

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

4535
        auto realm_1 = Realm::get_shared_realm(config);
2✔
4536
        Results res(realm_1, realm_1->read_group().get_table("class_object")->where());
2✔
4537
        auto realm_2 = Realm::get_shared_realm(config);
2✔
4538

4539
        {
2✔
4540
            // Create a lot of objects
4541
            realm_2->begin_transaction();
2✔
4542
            auto table = realm_2->read_group().get_table("class_object");
2✔
4543
            for (int i = 0; i < 400000; i++) {
800,002✔
4544
                table->create_object().set("value", i);
800,000✔
4545
            }
800,000✔
4546
            realm_2->commit_transaction();
2✔
4547
        }
2✔
4548

4549
        int commit_1 = 0;
2✔
4550
        int commit_2 = 0;
2✔
4551

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

4574
        util::EventLoop::main().run_until([&] {
15,562✔
4575
            return commit_1 == 4 && commit_2 == 4;
15,562✔
4576
        });
15,562✔
4577
    }
2✔
4578

4579
    SECTION("No open realms") {
4✔
4580
        // This is just to check that the section above did not leave any realms open
4581
        _impl::RealmCoordinator::assert_no_open_realms();
2✔
4582
    }
2✔
4583
}
4✔
4584

4585
TEST_CASE("Notification logging") {
2✔
4586
    using namespace std::chrono_literals;
2✔
4587
    TestFile config;
2✔
4588
    // util::LogCategory::realm.set_default_level_threshold(util::Logger::Level::all);
4589
    config.schema_version = 1;
2✔
4590
    config.schema = Schema{{"object", {{"value", PropertyType::Int}}}};
2✔
4591

4592
    auto realm = Realm::get_shared_realm(config);
2✔
4593
    auto table = realm->read_group().get_table("class_object");
2✔
4594
    int changed = 0;
2✔
4595
    Results res(realm, table->query("value == 5"));
2✔
4596
    auto token = res.add_notification_callback([&changed](CollectionChangeSet const&) {
24✔
4597
        changed++;
24✔
4598
    });
24✔
4599

4600
    int commit_nr = 0;
2✔
4601
    util::EventLoop::main().run_until([&] {
22✔
4602
        for (int64_t i = 0; i < 10; i++) {
242✔
4603
            realm->begin_transaction();
220✔
4604
            table->create_object().set("value", i);
220✔
4605
            realm->commit_transaction();
220✔
4606
            std::this_thread::sleep_for(2ms);
220✔
4607
        }
220✔
4608
        return ++commit_nr == 10;
22✔
4609
    });
22✔
4610
}
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