#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] notifications") { InMemoryTestFile config; config.cache = false; config.automatic_change_notifications = false; config.schema = std::make_unique(Schema{ {"object", "", { {"value", PropertyType::Int}, {"link", PropertyType::Object, "linked to object", "", false, false, true} }}, {"other object", "", { {"value", PropertyType::Int} }}, {"linking object", "", { {"link", PropertyType::Object, "object", "", false, false, true} }}, {"linked to object", "", { {"value", PropertyType::Int} }} }); 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; CollectionChangeSet change; auto token = results.add_notification_callback([&](CollectionChangeSet c, std::exception_ptr err) { REQUIRE_FALSE(err); change = c; ++notification_calls; }); advance_and_notify(*r); auto write = [&](auto&& f) { r->begin_transaction(); f(); r->commit_transaction(); advance_and_notify(*r); }; 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); advance_and_notify(*r); 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 = {}; advance_and_notify(*r); 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([&](CollectionChangeSet, std::exception_ptr) { token3 = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr) { called = true; }); }); advance_and_notify(*r); REQUIRE(called); } SECTION("notifications are not delivered when a callback is removed from within a callback") { NotificationToken token2, token3; token2 = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr) { token3 = {}; }); token3 = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr) { REQUIRE(false); }); advance_and_notify(*r); } SECTION("removing the current callback does not stop later ones from being called") { NotificationToken token2, token3; bool called = false; token2 = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr) { token2 = {}; }); token3 = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr) { called = true; }); advance_and_notify(*r); 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(); coordinator->on_change(); 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); } SECTION("inserting a row then modifying it in a second transaction does not report it as modified") { r->begin_transaction(); size_t ndx = table->add_empty_row(); table->set_int(0, ndx, 6); r->commit_transaction(); coordinator->on_change(); r->begin_transaction(); table->set_int(0, ndx, 7); r->commit_transaction(); advance_and_notify(*r); REQUIRE(notification_calls == 2); REQUIRE_INDICES(change.insertions, 4); REQUIRE(change.modifications.empty()); } SECTION("modification indices are pre-insert/delete") { r->begin_transaction(); table->set_int(0, 2, 0); table->set_int(0, 3, 6); r->commit_transaction(); advance_and_notify(*r); REQUIRE(notification_calls == 2); REQUIRE_INDICES(change.deletions, 1); REQUIRE_INDICES(change.modifications, 2); } SECTION("notifications are not delivered when collapsing transactions results in no net change") { r->begin_transaction(); size_t ndx = table->add_empty_row(); table->set_int(0, ndx, 5); r->commit_transaction(); coordinator->on_change(); r->begin_transaction(); table->move_last_over(ndx); r->commit_transaction(); REQUIRE(notification_calls == 1); coordinator->on_change(); r->notify(); REQUIRE(notification_calls == 1); } SECTION("the first call of a notification can include changes if it previously ran for a different callback") { auto token2 = results.add_notification_callback([&](CollectionChangeSet c, std::exception_ptr) { REQUIRE(!c.empty()); }); write([&] { table->set_int(0, table->add_empty_row(), 5); }); } } // Sort in descending order results = results.sort({{0}, {false}}); SECTION("sorted notifications") { int notification_calls = 0; CollectionChangeSet change; auto token = results.add_notification_callback([&](CollectionChangeSet c, std::exception_ptr err) { REQUIRE_FALSE(err); change = c; ++notification_calls; }); advance_and_notify(*r); auto write = [&](auto&& f) { r->begin_transaction(); f(); r->commit_transaction(); advance_and_notify(*r); }; 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); advance_and_notify(*r); REQUIRE(notification_calls == 2); } SECTION("moving a matching row by deleting all other rows") { r->begin_transaction(); table->clear(); table->add_empty_row(2); table->set_int(0, 0, 15); table->set_int(0, 1, 5); r->commit_transaction(); advance_and_notify(*r); write([&] { table->move_last_over(0); table->add_empty_row(); table->set_int(0, 1, 3); }); REQUIRE(notification_calls == 3); REQUIRE(change.deletions.empty()); REQUIRE_INDICES(change.insertions, 1); } } } TEST_CASE("[results] async error handling") { InMemoryTestFile config; config.cache = false; config.automatic_change_notifications = false; config.schema = std::make_unique(Schema{ {"object", "", { {"value", PropertyType::Int}, }}, }); 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([&](CollectionChangeSet, 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([&](CollectionChangeSet, std::exception_ptr err) { REQUIRE(err); REQUIRE_FALSE(called); called = true; }); advance_and_notify(*r); bool called2 = false; auto token2 = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr err) { REQUIRE(err); REQUIRE_FALSE(called2); called2 = true; }); advance_and_notify(*r); REQUIRE(called2); } } SECTION("error when opening the executor SG") { SECTION("error is delivered asynchronously") { bool called = false; auto token = results.add_notification_callback([&](CollectionChangeSet, 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([&](CollectionChangeSet, std::exception_ptr err) { REQUIRE(err); REQUIRE_FALSE(called); called = true; }); OpenFileLimiter limiter; advance_and_notify(*r); bool called2 = false; auto token2 = results.add_notification_callback([&](CollectionChangeSet, std::exception_ptr err) { REQUIRE(err); REQUIRE_FALSE(called2); called2 = true; }); advance_and_notify(*r); REQUIRE(called2); } } } TEST_CASE("[results] notifications after move") { InMemoryTestFile config; config.cache = false; config.automatic_change_notifications = false; config.schema = std::make_unique(Schema{ {"object", "", { {"value", PropertyType::Int}, }}, }); auto r = Realm::get_shared_realm(config); auto table = r->read_group()->get_table("class_object"); auto results = std::make_unique(r, *config.schema->find("object"), *table); int notification_calls = 0; auto token = results->add_notification_callback([&](CollectionChangeSet c, std::exception_ptr err) { REQUIRE_FALSE(err); ++notification_calls; }); advance_and_notify(*r); auto write = [&](auto&& f) { r->begin_transaction(); f(); r->commit_transaction(); advance_and_notify(*r); }; SECTION("notifications continue to work after Results is moved (move-constructor)") { Results r(std::move(*results)); results.reset(); write([&] { table->set_int(0, table->add_empty_row(), 1); }); REQUIRE(notification_calls == 2); } SECTION("notifications continue to work after Results is moved (move-assignment)") { Results r; r = std::move(*results); results.reset(); write([&] { table->set_int(0, table->add_empty_row(), 1); }); REQUIRE(notification_calls == 2); } } TEST_CASE("[results] error messages") { InMemoryTestFile config; config.schema = std::make_unique(Schema{ {"object", "", { {"value", PropertyType::String}, }}, }); auto r = Realm::get_shared_realm(config); auto table = r->read_group()->get_table("class_object"); Results results(r, *config.schema->find("object"), *table); r->begin_transaction(); table->add_empty_row(); r->commit_transaction(); SECTION("out of bounds access") { REQUIRE_THROWS_WITH(results.get(5), "Requested index 5 greater than max 1"); } SECTION("unsupported aggregate operation") { REQUIRE_THROWS_WITH(results.sum(0), "Cannot sum property 'value': operation not supported for 'string' properties"); } }