536 lines
17 KiB
C++
536 lines
17 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", 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("Async Results error handling") {
|
|
InMemoryTestFile config;
|
|
config.cache = false;
|
|
config.automatic_change_notifications = false;
|
|
config.schema = std::make_unique<Schema>(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);
|
|
}
|
|
}
|
|
}
|