#include "catch.hpp" #include "util/index_helpers.hpp" #include "util/test_file.hpp" #include "impl/realm_coordinator.hpp" #include "object_schema.hpp" #include "property.hpp" #include "results.hpp" #include "schema.hpp" #include #include #include #include using namespace realm; TEST_CASE("Results") { InMemoryTestFile config; config.cache = false; config.automatic_change_notifications = false; config.schema = std::make_unique(Schema{ {"object", "", { {"value", PropertyTypeInt}, {"link", PropertyTypeObject, "linked to object", false, false, true} }}, {"other object", "", { {"value", PropertyTypeInt} }}, {"linking object", "", { {"link", PropertyTypeObject, "object", false, false, true} }}, {"linked to object", "", { {"value", PropertyTypeInt} }} }); auto r = Realm::get_shared_realm(config); auto coordinator = _impl::RealmCoordinator::get_existing_coordinator(config.path); auto table = r->read_group()->get_table("class_object"); r->begin_transaction(); table->add_empty_row(10); for (int i = 0; i < 10; ++i) table->set_int(0, i, i * 2); r->commit_transaction(); Results results(r, *config.schema->find("object"), table->where().greater(0, 0).less(0, 10)); SECTION("unsorted notifications") { int notification_calls = 0; CollectionChangeIndices change; auto token = results.add_notification_callback([&](CollectionChangeIndices c, std::exception_ptr err) { REQUIRE_FALSE(err); change = c; ++notification_calls; }); coordinator->on_change(); r->notify(); auto write = [&](auto&& f) { r->begin_transaction(); f(); r->commit_transaction(); coordinator->on_change(); r->notify(); }; SECTION("initial results are delivered") { REQUIRE(notification_calls == 1); } SECTION("notifications are sent asynchronously") { r->begin_transaction(); table->set_int(0, 0, 4); r->commit_transaction(); REQUIRE(notification_calls == 1); coordinator->on_change(); r->notify(); REQUIRE(notification_calls == 2); } SECTION("notifications are not delivered when the token is destroyed before they are calculated") { r->begin_transaction(); table->set_int(0, 0, 4); r->commit_transaction(); REQUIRE(notification_calls == 1); token = {}; coordinator->on_change(); r->notify(); REQUIRE(notification_calls == 1); } SECTION("notifications are not delivered when the token is destroyed before they are delivered") { r->begin_transaction(); table->set_int(0, 0, 4); r->commit_transaction(); REQUIRE(notification_calls == 1); coordinator->on_change(); token = {}; r->notify(); REQUIRE(notification_calls == 1); } SECTION("notifications are delivered when a new callback is added from within a callback") { NotificationToken token2, token3; bool called = false; token2 = results.add_notification_callback([&](CollectionChangeIndices, std::exception_ptr) { token3 = results.add_notification_callback([&](CollectionChangeIndices, std::exception_ptr) { called = true; }); }); coordinator->on_change(); r->notify(); REQUIRE(called); } SECTION("notifications are not delivered when a callback is removed from within a callback") { NotificationToken token2, token3; token2 = results.add_notification_callback([&](CollectionChangeIndices, std::exception_ptr) { token3 = {}; }); token3 = results.add_notification_callback([&](CollectionChangeIndices, std::exception_ptr) { REQUIRE(false); }); coordinator->on_change(); r->notify(); } SECTION("removing the current callback does not stop later ones from being called") { NotificationToken token2, token3; bool called = false; token2 = results.add_notification_callback([&](CollectionChangeIndices, std::exception_ptr) { token2 = {}; }); token3 = results.add_notification_callback([&](CollectionChangeIndices, std::exception_ptr) { called = true; }); coordinator->on_change(); r->notify(); REQUIRE(called); } SECTION("modifications to unrelated tables do not send notifications") { write([&] { r->read_group()->get_table("class_other object")->add_empty_row(); }); REQUIRE(notification_calls == 1); } SECTION("irrelevant modifications to linked tables do not send notifications") { write([&] { r->read_group()->get_table("class_linked to object")->add_empty_row(); }); REQUIRE(notification_calls == 1); } SECTION("irrelevant modifications to linking tables do not send notifications") { write([&] { r->read_group()->get_table("class_linking object")->add_empty_row(); }); REQUIRE(notification_calls == 1); } SECTION("modifications that leave a non-matching row non-matching do not send notifications") { write([&] { table->set_int(0, 6, 13); }); REQUIRE(notification_calls == 1); } SECTION("deleting non-matching rows does not send a notification") { write([&] { table->move_last_over(0); table->move_last_over(6); }); REQUIRE(notification_calls == 1); } SECTION("modifying a matching row and leaving it matching marks that row as modified") { write([&] { table->set_int(0, 1, 3); }); REQUIRE(notification_calls == 2); REQUIRE_INDICES(change.modifications, 0); } SECTION("modifying a matching row to no longer match marks that row as deleted") { write([&] { table->set_int(0, 2, 0); }); REQUIRE(notification_calls == 2); REQUIRE_INDICES(change.deletions, 1); } SECTION("modifying a non-matching row to match marks that row as inserted, but not modified") { write([&] { table->set_int(0, 7, 3); }); REQUIRE(notification_calls == 2); REQUIRE_INDICES(change.insertions, 4); REQUIRE(change.modifications.empty()); } SECTION("deleting a matching row marks that row as deleted") { write([&] { table->move_last_over(3); }); REQUIRE(notification_calls == 2); REQUIRE_INDICES(change.deletions, 2); } SECTION("moving a matching row via deletion marks that row as moved") { write([&] { table->where().greater_equal(0, 10).find_all().clear(RemoveMode::unordered); table->move_last_over(0); }); REQUIRE(notification_calls == 2); REQUIRE_MOVES(change, {3, 0}); } SECTION("modifications from multiple transactions are collapsed") { r->begin_transaction(); table->set_int(0, 0, 6); r->commit_transaction(); r->begin_transaction(); table->set_int(0, 1, 0); r->commit_transaction(); REQUIRE(notification_calls == 1); coordinator->on_change(); r->notify(); REQUIRE(notification_calls == 2); } } // Sort in descending order results = results.sort({{0}, {false}}); SECTION("sorted notifications") { int notification_calls = 0; CollectionChangeIndices change; auto token = results.add_notification_callback([&](CollectionChangeIndices c, std::exception_ptr err) { REQUIRE_FALSE(err); change = c; ++notification_calls; }); coordinator->on_change(); r->notify(); auto write = [&](auto&& f) { r->begin_transaction(); f(); r->commit_transaction(); coordinator->on_change(); r->notify(); }; SECTION("modifications that leave a non-matching row non-matching do not send notifications") { write([&] { table->set_int(0, 6, 13); }); REQUIRE(notification_calls == 1); } SECTION("deleting non-matching rows does not send a notification") { write([&] { table->move_last_over(0); table->move_last_over(6); }); REQUIRE(notification_calls == 1); } SECTION("modifying a matching row and leaving it matching marks that row as modified") { write([&] { table->set_int(0, 1, 3); }); REQUIRE(notification_calls == 2); REQUIRE_INDICES(change.modifications, 3); } SECTION("modifying a matching row to no longer match marks that row as deleted") { write([&] { table->set_int(0, 2, 0); }); REQUIRE(notification_calls == 2); REQUIRE_INDICES(change.deletions, 2); } SECTION("modifying a non-matching row to match marks that row as inserted") { write([&] { table->set_int(0, 7, 3); }); REQUIRE(notification_calls == 2); REQUIRE_INDICES(change.insertions, 3); } SECTION("deleting a matching row marks that row as deleted") { write([&] { table->move_last_over(3); }); REQUIRE(notification_calls == 2); REQUIRE_INDICES(change.deletions, 1); } SECTION("moving a matching row via deletion does not send a notification") { write([&] { table->where().greater_equal(0, 10).find_all().clear(RemoveMode::unordered); table->move_last_over(0); }); REQUIRE(notification_calls == 1); } SECTION("modifying a matching row to change its position sends insert+delete") { write([&] { table->set_int(0, 2, 9); }); REQUIRE(notification_calls == 2); REQUIRE_INDICES(change.deletions, 2); REQUIRE_INDICES(change.insertions, 0); } SECTION("modifications from multiple transactions are collapsed") { r->begin_transaction(); table->set_int(0, 0, 5); r->commit_transaction(); r->begin_transaction(); table->set_int(0, 1, 0); r->commit_transaction(); REQUIRE(notification_calls == 1); coordinator->on_change(); r->notify(); REQUIRE(notification_calls == 2); } } } TEST_CASE("Async Results error handling") { InMemoryTestFile config; config.cache = false; config.automatic_change_notifications = false; config.schema = std::make_unique(Schema{ {"object", "", { {"value", PropertyTypeInt}, }}, }); auto r = Realm::get_shared_realm(config); auto coordinator = _impl::RealmCoordinator::get_existing_coordinator(config.path); Results results(r, *config.schema->find("object"), *r->read_group()->get_table("class_object")); class OpenFileLimiter { public: OpenFileLimiter() { // Set the max open files to zero so that opening new files will fail getrlimit(RLIMIT_NOFILE, &m_old); rlimit rl = m_old; rl.rlim_cur = 0; setrlimit(RLIMIT_NOFILE, &rl); } ~OpenFileLimiter() { setrlimit(RLIMIT_NOFILE, &m_old); } private: rlimit m_old; }; SECTION("error when opening the advancer SG") { OpenFileLimiter limiter; SECTION("error is delivered asynchronously") { bool called = false; auto token = results.add_notification_callback([&](CollectionChangeIndices, std::exception_ptr err) { REQUIRE(err); called = true; }); REQUIRE(!called); coordinator->on_change(); REQUIRE(!called); r->notify(); REQUIRE(called); } SECTION("adding another callback does not send the error again") { bool called = false; auto token = results.add_notification_callback([&](CollectionChangeIndices, std::exception_ptr err) { REQUIRE(err); REQUIRE_FALSE(called); called = true; }); coordinator->on_change(); r->notify(); bool called2 = false; auto token2 = results.add_notification_callback([&](CollectionChangeIndices, std::exception_ptr err) { REQUIRE(err); REQUIRE_FALSE(called2); called2 = true; }); coordinator->on_change(); r->notify(); REQUIRE(called2); } } SECTION("error when opening the executor SG") { SECTION("error is delivered asynchronously") { bool called = false; auto token = results.add_notification_callback([&](CollectionChangeIndices, std::exception_ptr err) { REQUIRE(err); called = true; }); OpenFileLimiter limiter; REQUIRE(!called); coordinator->on_change(); REQUIRE(!called); r->notify(); REQUIRE(called); } SECTION("adding another callback does not send the error again") { bool called = false; auto token = results.add_notification_callback([&](CollectionChangeIndices, std::exception_ptr err) { REQUIRE(err); REQUIRE_FALSE(called); called = true; }); OpenFileLimiter limiter; coordinator->on_change(); r->notify(); bool called2 = false; auto token2 = results.add_notification_callback([&](CollectionChangeIndices, std::exception_ptr err) { REQUIRE(err); REQUIRE_FALSE(called2); called2 = true; }); coordinator->on_change(); r->notify(); REQUIRE(called2); } } }