realm-js/tests/results.cpp
Thomas Goyne 20d9da973b Make List and Results notifications more consistent
Deliver the initial results for both of them and include the changeset
in the initial delivery for both, rather than having them behave weirdly
differently.
2016-05-11 16:08:30 -07:00

504 lines
16 KiB
C++

#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 <realm/commit_log.hpp>
#include <realm/group_shared.hpp>
#include <realm/link_view.hpp>
#include <unistd.h>
using namespace realm;
TEST_CASE("Results") {
InMemoryTestFile config;
config.cache = false;
config.automatic_change_notifications = false;
config.schema = std::make_unique<Schema>(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;
});
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([&](CollectionChangeIndices, std::exception_ptr) {
token3 = results.add_notification_callback([&](CollectionChangeIndices, 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([&](CollectionChangeIndices, std::exception_ptr) {
token3 = {};
});
token3 = results.add_notification_callback([&](CollectionChangeIndices, 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([&](CollectionChangeIndices, std::exception_ptr) {
token2 = {};
});
token3 = results.add_notification_callback([&](CollectionChangeIndices, 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("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([&](CollectionChangeIndices 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;
CollectionChangeIndices change;
auto token = results.add_notification_callback([&](CollectionChangeIndices 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);
}
}
}
TEST_CASE("Async Results error handling") {
InMemoryTestFile config;
config.cache = false;
config.automatic_change_notifications = false;
config.schema = std::make_unique<Schema>(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;
});
advance_and_notify(*r);
bool called2 = false;
auto token2 = results.add_notification_callback([&](CollectionChangeIndices, 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([&](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;
advance_and_notify(*r);
bool called2 = false;
auto token2 = results.add_notification_callback([&](CollectionChangeIndices, std::exception_ptr err) {
REQUIRE(err);
REQUIRE_FALSE(called2);
called2 = true;
});
advance_and_notify(*r);
REQUIRE(called2);
}
}
}