From b54fe1b5f634d5b2045eaff84e3cf9107bba2c03 Mon Sep 17 00:00:00 2001 From: Thomas Goyne Date: Tue, 26 Sep 2023 08:44:51 -0700 Subject: [PATCH] Allow collections of non-embedded objects in asymmetric objects The schema validation checks forbidding this were removed in #6981, but Table also had its own validation. --- CHANGELOG.md | 2 +- src/realm/table.cpp | 12 - test/object-store/audit.cpp | 63 ++--- test/object-store/sync/flx_sync.cpp | 240 +++++++++++++++--- .../util/sync/flx_sync_harness.hpp | 12 +- .../util/sync/sync_test_utils.cpp | 17 ++ .../util/sync/sync_test_utils.hpp | 2 + test/object-store/util/test_file.cpp | 44 ++++ test/object-store/util/test_file.hpp | 3 + 9 files changed, 286 insertions(+), 109 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92df8570809..36bc7c9715a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ### Enhancements * (PR [#????](https://github.com/realm/realm-core/pull/????)) -* None. +* Allow collections of non-embedded links in asymmetric objects. ([PR #7003](https://github.com/realm/realm-core/pull/7003)) ### Fixed * ([#????](https://github.com/realm/realm-core/issues/????), since v?.?.?) diff --git a/src/realm/table.cpp b/src/realm/table.cpp index 3fdbb1b4a10..a701c7db728 100644 --- a/src/realm/table.cpp +++ b/src/realm/table.cpp @@ -459,10 +459,6 @@ ColKey Table::add_column_list(Table& target, StringData name) Group* target_group = target.get_parent_group(); REALM_ASSERT_RELEASE(origin_group && target_group); REALM_ASSERT_RELEASE(origin_group == target_group); - // Only links to embedded objects are allowed. - if (is_asymmetric() && !target.is_embedded()) { - throw IllegalOperation("List of objects not supported in asymmetric table"); - } // Incoming links from an asymmetric table are not allowed. if (target.is_asymmetric()) { throw IllegalOperation("List of ephemeral objects not supported"); @@ -486,10 +482,6 @@ ColKey Table::add_column_set(Table& target, StringData name) REALM_ASSERT_RELEASE(origin_group == target_group); if (target.is_embedded()) throw IllegalOperation("Set of embedded objects not supported"); - // Outgoing links from an asymmetric table are not allowed. - if (is_asymmetric()) { - throw IllegalOperation("Set of objects not supported in asymmetric table"); - } // Incoming links from an asymmetric table are not allowed. if (target.is_asymmetric()) { throw IllegalOperation("Set of ephemeral objects not supported"); @@ -533,10 +525,6 @@ ColKey Table::add_column_dictionary(Table& target, StringData name, DataType key Group* target_group = target.get_parent_group(); REALM_ASSERT_RELEASE(origin_group && target_group); REALM_ASSERT_RELEASE(origin_group == target_group); - // Only links to embedded objects are allowed. - if (is_asymmetric() && !target.is_embedded()) { - throw IllegalOperation("Dictionary of objects not supported in asymmetric table"); - } // Incoming links from an asymmetric table are not allowed. if (target.is_asymmetric()) { throw IllegalOperation("Dictionary of ephemeral objects not supported"); diff --git a/test/object-store/audit.cpp b/test/object-store/audit.cpp index b9bdcd2454c..ec2173989d9 100644 --- a/test/object-store/audit.cpp +++ b/test/object-store/audit.cpp @@ -167,51 +167,28 @@ void sort_events(std::vector& events) static std::vector get_audit_events_from_baas(TestAppSession& session, SyncUser& user, size_t expected_count) { - auto& app_session = session.app_session(); - app::MongoClient remote_client = user.mongo_client("BackingDB"); - app::MongoDatabase db = remote_client.db(app_session.config.mongo_dbname); - app::MongoCollection collection = db["AuditEvent"]; - std::vector events; static const std::set nonmetadata_fields = {"activity", "event", "data", "realm_id"}; - timed_wait_for( - [&] { - uint64_t count = 0; - collection.count({}, [&](uint64_t c, util::Optional error) { - REQUIRE(!error); - count = c; - }); - if (count < expected_count) { - millisleep(500); // slow down the number of retries - return false; - } - return true; - }, - std::chrono::minutes(5)); - - collection.find({}, {}, - [&](util::Optional>&& result, util::Optional error) { - REQUIRE(!error); - REQUIRE(result->size() >= expected_count); - events.reserve(result->size()); - for (auto bson : *result) { - auto doc = static_cast(bson).entries(); - AuditEvent event; - event.activity = static_cast(doc["activity"]); - event.timestamp = static_cast(doc["timestamp"]); - if (auto it = doc.find("event"); it != doc.end() && it->second != bson::Bson()) { - event.event = static_cast(it->second); - } - if (auto it = doc.find("data"); it != doc.end() && it->second != bson::Bson()) { - event.data = json::parse(static_cast(it->second)); - } - for (auto& [key, value] : doc) { - if (value.type() == bson::Bson::Type::String && !nonmetadata_fields.count(key)) - event.metadata.insert({key, static_cast(value)}); - } - events.push_back(event); - } - }); + auto documents = session.get_documents(user, "AuditEvent", expected_count); + std::vector events; + events.reserve(documents.size()); + for (auto document : documents) { + auto doc = document.entries(); + AuditEvent event; + event.activity = static_cast(doc["activity"]); + event.timestamp = static_cast(doc["timestamp"]); + if (auto it = doc.find("event"); it != doc.end() && it->second != bson::Bson()) { + event.event = static_cast(it->second); + } + if (auto it = doc.find("data"); it != doc.end() && it->second != bson::Bson()) { + event.data = json::parse(static_cast(it->second)); + } + for (auto& [key, value] : doc) { + if (value.type() == bson::Bson::Type::String && !nonmetadata_fields.count(key)) + event.metadata.insert({key, static_cast(value)}); + } + events.push_back(event); + } sort_events(events); return events; } diff --git a/test/object-store/sync/flx_sync.cpp b/test/object-store/sync/flx_sync.cpp index 73966726327..1b1fc7ee25e 100644 --- a/test/object-store/sync/flx_sync.cpp +++ b/test/object-store/sync/flx_sync.cpp @@ -1021,7 +1021,7 @@ TEST_CASE("flx: client reset", "[sync][flx][client reset][baas]") { // Open the realm again. This should not crash. auto&& [err_future, err_handler] = make_error_handler(); - config_local.sync_config->error_handler = err_handler; + config_local.sync_config->error_handler = std::move(err_handler); auto realm_post_reset = Realm::get_shared_realm(config_local); sync_error = wait_for_future(std::move(err_future)).get(); @@ -2433,19 +2433,12 @@ TEST_CASE("flx: connect to PBS as FLX returns an error", "[sync][flx][protocol][ std::mutex sync_error_mutex; util::Optional sync_error; config.sync_config->error_handler = [&](std::shared_ptr, SyncError error) mutable { - std::lock_guard lk(sync_error_mutex); + std::lock_guard lk(sync_error_mutex); sync_error = std::move(error); }; auto realm = Realm::get_shared_realm(config); - auto latest_subs = realm->get_latest_subscription_set().make_mutable_copy(); - auto table = realm->read_group().get_table("class_TopLevel"); - Query new_query_a(table); - new_query_a.equal(table->get_column_key("_id"), ObjectId::gen()); - latest_subs.insert_or_assign(std::move(new_query_a)); - latest_subs.commit(); - timed_wait_for([&] { - std::lock_guard lk(sync_error_mutex); + std::lock_guard lk(sync_error_mutex); return static_cast(sync_error); }); @@ -2810,7 +2803,37 @@ TEST_CASE("flx: bootstrap batching prevents orphan documents", "[sync][flx][boot } } +// Check that a document with the given id is present and has the expected fields +static void check_document(const std::vector& documents, ObjectId id, + std::initializer_list> fields) +{ + auto it = std::find_if(documents.begin(), documents.end(), [&](auto&& doc) { + auto it = doc.entries().find("_id"); + REQUIRE(it != doc.entries().end()); + return it->second == id; + }); + REQUIRE(it != documents.end()); + auto& doc = it->entries(); + for (auto& [name, expected_value] : fields) { + auto it = doc.find(name); + REQUIRE(it != doc.end()); + + // bson documents are ordered but Realm dictionaries aren't, so the + // document might validly be in a different order than we expected and + // we need to do a comparison that doesn't check order. + if (expected_value.type() == bson::Bson::Type::Document) { + REQUIRE(static_cast(it->second).entries() == + static_cast(expected_value).entries()); + } + else { + REQUIRE(it->second == expected_value); + } + } +} + TEST_CASE("flx: data ingest", "[sync][flx][data ingest][baas]") { + using namespace ::realm::bson; + static auto server_schema = [] { FLXSyncTestHarness::ServerSchema server_schema; server_schema.queryable_fields = {"queryable_str_field"}; @@ -2820,18 +2843,38 @@ TEST_CASE("flx: data ingest", "[sync][flx][data ingest][baas]") { { {"_id", PropertyType::ObjectId, Property::IsPrimary{true}}, {"location", PropertyType::String | PropertyType::Nullable}, - {"embedded_obj", PropertyType::Object | PropertyType::Nullable, "Embedded"}, + {"embedded obj", PropertyType::Object | PropertyType::Nullable, "Embedded"}, + {"embedded list", PropertyType::Object | PropertyType::Array, "Embedded"}, + {"embedded dictionary", PropertyType::Object | PropertyType::Nullable | PropertyType::Dictionary, + "Embedded"}, + {"link obj", PropertyType::Object | PropertyType::Nullable, "TopLevel"}, + {"link list", PropertyType::Object | PropertyType::Array, "TopLevel"}, + {"link dictionary", PropertyType::Object | PropertyType::Nullable | PropertyType::Dictionary, + "TopLevel"}, }}, - {"Embedded", - ObjectSchema::ObjectType::Embedded, + {"Embedded", ObjectSchema::ObjectType::Embedded, {{"value", PropertyType::String}}}, + {"TopLevel", { - {"value", PropertyType::String | PropertyType::Nullable}, + {"_id", PropertyType::ObjectId, Property::IsPrimary{true}}, + {"value", PropertyType::Int}, }}, }; return server_schema; }(); static auto harness = std::make_unique("asymmetric_sync", server_schema); + // We reuse a single app for each section, so tests will see the documents + // created by previous tests and we need to add those documents to the count + // we're waiting for + static std::unordered_map previous_count; + auto get_documents = [&](const char* name, size_t expected_count) { + auto& count = previous_count[name]; + auto documents = + harness->session().get_documents(*harness->app()->current_user(), name, count + expected_count); + count = documents.size(); + return documents; + }; + SECTION("basic object construction") { auto foo_obj_id = ObjectId::gen(); auto bar_obj_id = ObjectId::gen(); @@ -2841,7 +2884,10 @@ TEST_CASE("flx: data ingest", "[sync][flx][data ingest][baas]") { Object::create(c, realm, "Asymmetric", std::any(AnyDict{{"_id", foo_obj_id}, {"location", "foo"s}})); Object::create(c, realm, "Asymmetric", std::any(AnyDict{{"_id", bar_obj_id}, {"location", "bar"s}})); realm->commit_transaction(); - wait_for_upload(*realm); + + auto documents = get_documents("Asymmetric", 2); + check_document(documents, foo_obj_id, {{"location", "foo"}}); + check_document(documents, bar_obj_id, {{"location", "bar"}}); }); harness->do_with_new_realm([&](SharedRealm realm) { @@ -2849,9 +2895,8 @@ TEST_CASE("flx: data ingest", "[sync][flx][data ingest][baas]") { auto table = realm->read_group().get_table("class_Asymmetric"); REQUIRE(table->size() == 0); - auto new_query = realm->get_latest_subscription_set().make_mutable_copy(); // Cannot query asymmetric tables. - CHECK_THROWS_AS(new_query.insert_or_assign(Query(table)), LogicError); + CHECK_THROWS_AS(Query(table), LogicError); }); } @@ -2866,25 +2911,24 @@ TEST_CASE("flx: data ingest", "[sync][flx][data ingest][baas]") { "Attempting to create an object of type 'Asymmetric' with an existing primary key value 'not " "implemented'"); realm->commit_transaction(); - wait_for_upload(*realm); - }); - harness->do_with_new_realm([&](SharedRealm realm) { - wait_for_download(*realm); - - auto table = realm->read_group().get_table("class_Asymmetric"); - REQUIRE(table->size() == 0); + auto documents = get_documents("Asymmetric", 1); + check_document(documents, foo_obj_id, {{"location", "foo"}}); }); } SECTION("create multiple objects - separate commits") { harness->do_with_new_realm([&](SharedRealm realm) { CppContext c(realm); + std::vector obj_ids; for (int i = 0; i < 100; ++i) { realm->begin_transaction(); - auto obj_id = ObjectId::gen(); + obj_ids.push_back(ObjectId::gen()); Object::create(c, realm, "Asymmetric", - std::any(AnyDict{{"_id", obj_id}, {"location", util::format("foo_%1", i)}})); + std::any(AnyDict{ + {"_id", obj_ids.back()}, + {"location", util::format("foo_%1", i)}, + })); realm->commit_transaction(); } @@ -2893,6 +2937,11 @@ TEST_CASE("flx: data ingest", "[sync][flx][data ingest][baas]") { auto table = realm->read_group().get_table("class_Asymmetric"); REQUIRE(table->size() == 0); + + auto documents = get_documents("Asymmetric", 100); + for (int i = 0; i < 100; ++i) { + check_document(documents, obj_ids[i], {{"location", util::format("foo_%1", i)}}); + } }); } @@ -2900,10 +2949,14 @@ TEST_CASE("flx: data ingest", "[sync][flx][data ingest][baas]") { harness->do_with_new_realm([&](SharedRealm realm) { CppContext c(realm); realm->begin_transaction(); + std::vector obj_ids; for (int i = 0; i < 100; ++i) { - auto obj_id = ObjectId::gen(); + obj_ids.push_back(ObjectId::gen()); Object::create(c, realm, "Asymmetric", - std::any(AnyDict{{"_id", obj_id}, {"location", util::format("foo_%1", i)}})); + std::any(AnyDict{ + {"_id", obj_ids.back()}, + {"location", util::format("bar_%1", i)}, + })); } realm->commit_transaction(); @@ -2912,6 +2965,11 @@ TEST_CASE("flx: data ingest", "[sync][flx][data ingest][baas]") { auto table = realm->read_group().get_table("class_Asymmetric"); REQUIRE(table->size() == 0); + + auto documents = get_documents("Asymmetric", 100); + for (int i = 0; i < 100; ++i) { + check_document(documents, obj_ids[i], {{"location", util::format("bar_%1", i)}}); + } }); } @@ -2948,12 +3006,19 @@ TEST_CASE("flx: data ingest", "[sync][flx][data ingest][baas]") { SECTION("basic embedded object construction") { harness->do_with_new_realm([&](SharedRealm realm) { + auto obj_id = ObjectId::gen(); realm->begin_transaction(); CppContext c(realm); Object::create(c, realm, "Asymmetric", - std::any(AnyDict{{"_id", ObjectId::gen()}, {"embedded_obj", AnyDict{{"value", "foo"s}}}})); + std::any(AnyDict{ + {"_id", obj_id}, + {"embedded obj", AnyDict{{"value", "foo"s}}}, + })); realm->commit_transaction(); wait_for_upload(*realm); + + auto documents = get_documents("Asymmetric", 1); + check_document(documents, obj_id, {{"embedded obj", BsonDocument{{"value", "foo"}}}}); }); harness->do_with_new_realm([&](SharedRealm realm) { @@ -2968,19 +3033,29 @@ TEST_CASE("flx: data ingest", "[sync][flx][data ingest][baas]") { harness->do_with_new_realm([&](SharedRealm realm) { CppContext c(realm); auto foo_obj_id = ObjectId::gen(); + realm->begin_transaction(); Object::create(c, realm, "Asymmetric", - std::any(AnyDict{{"_id", foo_obj_id}, {"embedded_obj", AnyDict{{"value", "foo"s}}}})); + std::any(AnyDict{ + {"_id", foo_obj_id}, + {"embedded obj", AnyDict{{"value", "foo"s}}}, + })); realm->commit_transaction(); - // Update embedded field to `null`. + + // Update embedded field to `null`. The server discards this write + // as asymmetric sync can only create new objects. realm->begin_transaction(); Object::create(c, realm, "Asymmetric", - std::any(AnyDict{{"_id", foo_obj_id}, {"embedded_obj", std::any()}})); + std::any(AnyDict{ + {"_id", foo_obj_id}, + {"embedded obj", std::any()}, + })); realm->commit_transaction(); - // Update embedded field again to a new value. + + // create a second object so that we can know when the translator + // has processed everything realm->begin_transaction(); - Object::create(c, realm, "Asymmetric", - std::any(AnyDict{{"_id", foo_obj_id}, {"embedded_obj", AnyDict{{"value", "bar"s}}}})); + Object::create(c, realm, "Asymmetric", std::any(AnyDict{{"_id", ObjectId::gen()}, {}})); realm->commit_transaction(); wait_for_upload(*realm); @@ -2988,6 +3063,41 @@ TEST_CASE("flx: data ingest", "[sync][flx][data ingest][baas]") { auto table = realm->read_group().get_table("class_Asymmetric"); REQUIRE(table->size() == 0); + + auto documents = get_documents("Asymmetric", 2); + check_document(documents, foo_obj_id, {{"embedded obj", BsonDocument{{"value", "foo"}}}}); + }); + } + + SECTION("embedded collections") { + harness->do_with_new_realm([&](SharedRealm realm) { + CppContext c(realm); + auto obj_id = ObjectId::gen(); + + realm->begin_transaction(); + Object::create(c, realm, "Asymmetric", + std::any(AnyDict{ + {"_id", obj_id}, + {"embedded list", AnyVector{AnyDict{{"value", "foo"s}}, AnyDict{{"value", "bar"s}}}}, + {"embedded dictionary", + AnyDict{ + {"key1", AnyDict{{"value", "foo"s}}}, + {"key2", AnyDict{{"value", "bar"s}}}, + }}, + })); + realm->commit_transaction(); + + auto documents = get_documents("Asymmetric", 1); + check_document( + documents, obj_id, + { + {"embedded list", BsonArray{BsonDocument{{"value", "foo"}}, BsonDocument{{"value", "bar"}}}}, + {"embedded dictionary", + BsonDocument{ + {"key1", BsonDocument{{"value", "foo"}}}, + {"key2", BsonDocument{{"value", "bar"}}}, + }}, + }); }); } @@ -3002,12 +3112,61 @@ TEST_CASE("flx: data ingest", "[sync][flx][data ingest][baas]") { }}, }; - SyncTestFile config(harness->app(), bson::Bson{}, schema); + SyncTestFile config(harness->app(), Bson{}, schema); REQUIRE_EXCEPTION( Realm::get_shared_realm(config), SchemaValidationFailed, Catch::Matchers::ContainsSubstring("Asymmetric table 'Asymmetric2' not allowed in partition based sync")); } + SECTION("links to top-level objects") { + harness->do_with_new_realm([&](SharedRealm realm) { + subscribe_to_all_and_bootstrap(*realm); + + ObjectId obj_id = ObjectId::gen(); + std::array target_obj_ids; + for (auto& id : target_obj_ids) { + id = ObjectId::gen(); + } + + realm->begin_transaction(); + CppContext c(realm); + Object::create(c, realm, "Asymmetric", + std::any(AnyDict{ + {"_id", obj_id}, + {"link obj", AnyDict{{"_id", target_obj_ids[0]}, {"value", INT64_C(10)}}}, + {"link list", + AnyVector{ + AnyDict{{"_id", target_obj_ids[1]}, {"value", INT64_C(11)}}, + AnyDict{{"_id", target_obj_ids[2]}, {"value", INT64_C(12)}}, + }}, + {"link dictionary", + AnyDict{ + {"key1", AnyDict{{"_id", target_obj_ids[3]}, {"value", INT64_C(13)}}}, + {"key2", AnyDict{{"_id", target_obj_ids[4]}, {"value", INT64_C(14)}}}, + }}, + })); + realm->commit_transaction(); + wait_for_upload(*realm); + + auto docs1 = get_documents("Asymmetric", 1); + check_document(docs1, obj_id, + {{"link obj", target_obj_ids[0]}, + {"link list", BsonArray{{target_obj_ids[1], target_obj_ids[2]}}}, + { + "link dictionary", + BsonDocument{ + {"key1", target_obj_ids[3]}, + {"key2", target_obj_ids[4]}, + }, + }}); + + auto docs2 = get_documents("TopLevel", 5); + for (int64_t i = 0; i < 5; ++i) { + check_document(docs2, target_obj_ids[i], {{"value", 10 + i}}); + } + }); + } + // Add any new test sections above this point SECTION("teardown") { @@ -3039,18 +3198,15 @@ TEST_CASE("flx: data ingest - dev mode", "[sync][flx][data ingest][baas]") { harness.do_with_new_realm( [&](SharedRealm realm) { - auto table = realm->read_group().get_table("class_TopLevel"); - auto new_query = realm->get_latest_subscription_set().make_mutable_copy(); - new_query.insert_or_assign(Query(table)); - std::move(new_query).commit(); - CppContext c(realm); realm->begin_transaction(); Object::create(c, realm, "Asymmetric", std::any(AnyDict{{"_id", foo_obj_id}, {"location", "foo"s}})); Object::create(c, realm, "Asymmetric", std::any(AnyDict{{"_id", bar_obj_id}, {"location", "bar"s}})); realm->commit_transaction(); - wait_for_upload(*realm); + auto docs = harness.session().get_documents(*realm->config().sync_config->user, "Asymmetric", 2); + check_document(docs, foo_obj_id, {{"location", "foo"}}); + check_document(docs, bar_obj_id, {{"location", "bar"}}); }, schema); } diff --git a/test/object-store/util/sync/flx_sync_harness.hpp b/test/object-store/util/sync/flx_sync_harness.hpp index ff409e26f55..c8f2736bbd1 100644 --- a/test/object-store/util/sync/flx_sync_harness.hpp +++ b/test/object-store/util/sync/flx_sync_harness.hpp @@ -115,17 +115,7 @@ class FLXSyncTestHarness { { SyncTestFile config(m_test_session.app()->current_user(), schema(), SyncConfig::FLXSyncEnabled{}); auto realm = Realm::get_shared_realm(config); - auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy(); - for (const auto& table : realm->schema()) { - if (table.table_type != ObjectSchema::ObjectType::TopLevel) { - continue; - } - Query query_for_table(realm->read_group().get_table(table.table_key)); - mut_subs.insert_or_assign(query_for_table); - } - auto subs = std::move(mut_subs).commit(); - subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get(); - wait_for_download(*realm); + subscribe_to_all_and_bootstrap(*realm); realm->begin_transaction(); func(realm); diff --git a/test/object-store/util/sync/sync_test_utils.cpp b/test/object-store/util/sync/sync_test_utils.cpp index c594ff6bb5b..58b739dc204 100644 --- a/test/object-store/util/sync/sync_test_utils.cpp +++ b/test/object-store/util/sync/sync_test_utils.cpp @@ -173,6 +173,23 @@ ExpectedRealmPaths::ExpectedRealmPaths(const std::string& base_path, const std:: #if REALM_ENABLE_SYNC +void subscribe_to_all_and_bootstrap(Realm& realm) +{ + auto mut_subs = realm.get_latest_subscription_set().make_mutable_copy(); + auto& group = realm.read_group(); + for (auto key : group.get_table_keys()) { + if (group.table_is_public(key)) { + auto table = group.get_table(key); + if (table->get_table_type() == Table::Type::TopLevel) { + mut_subs.insert_or_assign(table->where()); + } + } + } + auto subs = std::move(mut_subs).commit(); + subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get(); + wait_for_download(realm); +} + #if REALM_ENABLE_AUTH_TESTS #ifdef REALM_MONGODB_ENDPOINT diff --git a/test/object-store/util/sync/sync_test_utils.hpp b/test/object-store/util/sync/sync_test_utils.hpp index 4a81eca8677..b7f0b285cdf 100644 --- a/test/object-store/util/sync/sync_test_utils.hpp +++ b/test/object-store/util/sync/sync_test_utils.hpp @@ -141,6 +141,8 @@ TestSyncManager::Config get_config(Transport&& transport) return config; } +void subscribe_to_all_and_bootstrap(Realm& realm); + #if REALM_ENABLE_AUTH_TESTS #ifdef REALM_MONGODB_ENDPOINT diff --git a/test/object-store/util/test_file.cpp b/test/object-store/util/test_file.cpp index 4040a02d82e..7c2a2f00db3 100644 --- a/test/object-store/util/test_file.cpp +++ b/test/object-store/util/test_file.cpp @@ -32,6 +32,9 @@ #include #if REALM_ENABLE_SYNC +#include +#include +#include #include #include #include @@ -374,6 +377,47 @@ TestAppSession::~TestAppSession() } } +std::vector TestAppSession::get_documents(SyncUser& user, const std::string& object_type, + size_t expected_count) const +{ + app::MongoClient remote_client = user.mongo_client("BackingDB"); + app::MongoDatabase db = remote_client.db(m_app_session->config.mongo_dbname); + app::MongoCollection collection = db[object_type]; + int sleep_time = 10; + timed_wait_for( + [&] { + uint64_t count = 0; + collection.count({}, [&](uint64_t c, util::Optional error) { + REQUIRE(!error); + count = c; + }); + if (count < expected_count) { + // querying the server too frequently makes it take longer to process the sync changesets we're + // waiting for + millisleep(sleep_time); + if (sleep_time < 500) { + sleep_time *= 2; + } + return false; + } + return true; + }, + std::chrono::minutes(5)); + + std::vector documents; + collection.find({}, {}, + [&](util::Optional>&& result, util::Optional error) { + REQUIRE(result); + REQUIRE(!error); + REQUIRE(result->size() == expected_count); + documents.reserve(result->size()); + for (auto&& bson : *result) { + REQUIRE(bson.type() == bson::Bson::Type::Document); + documents.push_back(std::move(static_cast(bson))); + } + }); + return documents; +} #endif // REALM_ENABLE_AUTH_TESTS // MARK: - TestSyncManager diff --git a/test/object-store/util/test_file.hpp b/test/object-store/util/test_file.hpp index 1bb2eb154b7..905cb95299a 100644 --- a/test/object-store/util/test_file.hpp +++ b/test/object-store/util/test_file.hpp @@ -204,6 +204,9 @@ class TestAppSession { return m_transport.get(); } + std::vector get_documents(realm::SyncUser& user, const std::string& object_type, + size_t expected_count) const; + private: std::shared_ptr m_app; std::unique_ptr m_app_session;