You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
980 lines
36 KiB
980 lines
36 KiB
////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Copyright 2015 Realm Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//
|
|
////////////////////////////////////////////////////////////////////////////
|
|
|
|
#import "RLMTestCase.h"
|
|
|
|
#import "RLMRealmConfiguration_Private.hpp"
|
|
#import "RLMRealm_Private.hpp"
|
|
|
|
#import "impl/realm_coordinator.hpp"
|
|
|
|
#import <realm/string_data.hpp>
|
|
|
|
#import <sys/resource.h>
|
|
|
|
// A whole bunch of blocks don't use their RLMResults parameter
|
|
#pragma clang diagnostic ignored "-Wunused-parameter"
|
|
|
|
@interface ManualRefreshRealm : RLMRealm
|
|
@end
|
|
@implementation ManualRefreshRealm
|
|
- (void)verifyNotificationsAreSupported:(__unused bool)isCollection {
|
|
// The normal implementation of this will reject realms with automatic change notifications disabled
|
|
}
|
|
@end
|
|
|
|
@interface AsyncTests : RLMTestCase
|
|
@end
|
|
|
|
@implementation AsyncTests
|
|
- (void)createObject:(int)value {
|
|
@autoreleasepool {
|
|
RLMRealm *realm = [RLMRealm defaultRealm];
|
|
[realm transactionWithBlock:^{
|
|
[IntObject createInDefaultRealmWithValue:@[@(value)]];
|
|
}];
|
|
}
|
|
}
|
|
|
|
- (void)testInitialResultsAreDelivered {
|
|
[self createObject:1];
|
|
|
|
XCTestExpectation *expectation = [self expectationWithDescription:@""];
|
|
auto token = [[IntObject objectsWhere:@"intCol > 0"] addNotificationBlock:^(RLMResults *results, RLMCollectionChange *change, NSError *e) {
|
|
XCTAssertNil(e);
|
|
XCTAssertEqualObjects(results.objectClassName, @"IntObject");
|
|
XCTAssertEqual(results.count, 1U);
|
|
[expectation fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
[token invalidate];
|
|
}
|
|
|
|
- (void)testNewResultsAreDeliveredAfterLocalCommit {
|
|
__block XCTestExpectation *expectation = [self expectationWithDescription:@""];
|
|
__block NSUInteger expected = 0;
|
|
auto token = [[IntObject objectsWhere:@"intCol > 0"] addNotificationBlock:^(RLMResults *results, RLMCollectionChange *change, NSError *e) {
|
|
XCTAssertEqual(results.count, expected++);
|
|
[expectation fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
|
|
expectation = [self expectationWithDescription:@""];
|
|
[self createObject:1];
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
|
|
expectation = [self expectationWithDescription:@""];
|
|
[self createObject:2];
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
[token invalidate];
|
|
}
|
|
|
|
- (void)testNewResultsAreDeliveredAfterBackgroundCommit {
|
|
__block XCTestExpectation *expectation = [self expectationWithDescription:@""];
|
|
__block NSUInteger expected = 0;
|
|
auto token = [[IntObject objectsWhere:@"intCol > 0"] addNotificationBlock:^(RLMResults *results, RLMCollectionChange *change, NSError *e) {
|
|
XCTAssertEqual(results.count, expected++);
|
|
[expectation fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
|
|
expectation = [self expectationWithDescription:@""];
|
|
[self dispatchAsyncAndWait:^{ [self createObject:1]; }];
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
|
|
expectation = [self expectationWithDescription:@""];
|
|
[self dispatchAsyncAndWait:^{ [self createObject:2]; }];
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
[token invalidate];
|
|
}
|
|
|
|
- (void)testResultsPerserveQuery {
|
|
__block XCTestExpectation *expectation = [self expectationWithDescription:@""];
|
|
__block NSUInteger expected = 0;
|
|
auto token = [[IntObject objectsWhere:@"intCol > 0"] addNotificationBlock:^(RLMResults *results, RLMCollectionChange *change, NSError *e) {
|
|
XCTAssertEqual(results.count, expected);
|
|
[expectation fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
|
|
expectation = [self expectationWithDescription:@""];
|
|
++expected;
|
|
[self dispatchAsyncAndWait:^{
|
|
[self createObject:1];
|
|
[self createObject:-11];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
[token invalidate];
|
|
}
|
|
|
|
- (void)testResultsPerserveSort {
|
|
__block XCTestExpectation *expectation = [self expectationWithDescription:@""];
|
|
__block int expected = 0;
|
|
auto token = [[IntObject.allObjects sortedResultsUsingKeyPath:@"intCol" ascending:NO] addNotificationBlock:^(RLMResults *results, RLMCollectionChange *change, NSError *e) {
|
|
XCTAssertEqual([results.firstObject intCol], expected);
|
|
[expectation fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
|
|
expectation = [self expectationWithDescription:@""];
|
|
expected = 1;
|
|
[self dispatchAsyncAndWait:^{ [self createObject:1]; }];
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
|
|
expectation = [self expectationWithDescription:@""];
|
|
[self dispatchAsyncAndWait:^{ [self createObject:-1]; }];
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
|
|
expectation = [self expectationWithDescription:@""];
|
|
expected = 2;
|
|
[self dispatchAsyncAndWait:^{ [self createObject:2]; }];
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
[token invalidate];
|
|
}
|
|
|
|
- (void)testQueryingDeliveredQueryResults {
|
|
__block XCTestExpectation *expectation = [self expectationWithDescription:@""];
|
|
__block NSUInteger expected = 0;
|
|
auto token = [[IntObject objectsWhere:@"intCol > 0"] addNotificationBlock:^(RLMResults *results, RLMCollectionChange *change, NSError *e) {
|
|
XCTAssertEqual([results objectsWhere:@"intCol < 10"].count, expected++);
|
|
[expectation fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
|
|
expectation = [self expectationWithDescription:@""];
|
|
[self dispatchAsyncAndWait:^{ [self createObject:1]; }];
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
|
|
expectation = [self expectationWithDescription:@""];
|
|
[self dispatchAsyncAndWait:^{ [self createObject:2]; }];
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
[token invalidate];
|
|
}
|
|
|
|
- (void)testQueryingDeliveredTableResults {
|
|
__block XCTestExpectation *expectation = [self expectationWithDescription:@""];
|
|
__block NSUInteger expected = 0;
|
|
auto token = [[IntObject allObjects] addNotificationBlock:^(RLMResults *results, RLMCollectionChange *change, NSError *e) {
|
|
XCTAssertEqual([results objectsWhere:@"intCol < 10"].count, expected++);
|
|
[expectation fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
|
|
expectation = [self expectationWithDescription:@""];
|
|
[self dispatchAsyncAndWait:^{ [self createObject:1]; }];
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
|
|
expectation = [self expectationWithDescription:@""];
|
|
[self dispatchAsyncAndWait:^{ [self createObject:2]; }];
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
[token invalidate];
|
|
}
|
|
|
|
- (void)testQueryingDeliveredSortedResults {
|
|
__block XCTestExpectation *expectation = [self expectationWithDescription:@""];
|
|
__block int expected = 0;
|
|
auto token = [[IntObject.allObjects sortedResultsUsingKeyPath:@"intCol" ascending:NO] addNotificationBlock:^(RLMResults *results, RLMCollectionChange *change, NSError *e) {
|
|
XCTAssertEqual([[results objectsWhere:@"intCol < 10"].firstObject intCol], expected++);
|
|
[expectation fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
|
|
expectation = [self expectationWithDescription:@""];
|
|
[self dispatchAsyncAndWait:^{ [self createObject:1]; }];
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
|
|
expectation = [self expectationWithDescription:@""];
|
|
[self dispatchAsyncAndWait:^{ [self createObject:2]; }];
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
[token invalidate];
|
|
}
|
|
|
|
- (void)testSortingDeliveredResults {
|
|
__block XCTestExpectation *expectation = [self expectationWithDescription:@""];
|
|
__block int expected = 0;
|
|
auto token = [[IntObject allObjects] addNotificationBlock:^(RLMResults *results, RLMCollectionChange *change, NSError *e) {
|
|
XCTAssertEqual([[results sortedResultsUsingKeyPath:@"intCol" ascending:NO].firstObject intCol], expected++);
|
|
[expectation fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
|
|
expectation = [self expectationWithDescription:@""];
|
|
[self dispatchAsyncAndWait:^{ [self createObject:1]; }];
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
|
|
expectation = [self expectationWithDescription:@""];
|
|
[self dispatchAsyncAndWait:^{ [self createObject:2]; }];
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
[token invalidate];
|
|
}
|
|
|
|
- (void)testQueryingLinkList {
|
|
RLMRealm *realm = [RLMRealm defaultRealm];
|
|
[realm beginWriteTransaction];
|
|
ArrayPropertyObject *array = [ArrayPropertyObject createInRealm:realm withValue:@[@"", @[], @[]]];
|
|
[realm commitWriteTransaction];
|
|
|
|
__block XCTestExpectation *expectation = [self expectationWithDescription:@""];
|
|
__block int expected = 0;
|
|
auto token = [[array.intArray objectsWhere:@"intCol > 0"] addNotificationBlock:^(RLMResults *results, RLMCollectionChange *change, NSError *e) {
|
|
XCTAssertNil(e);
|
|
XCTAssertNotNil(results);
|
|
XCTAssertEqual((int)results.count, expected);
|
|
for (int i = 0; i < expected; ++i) {
|
|
XCTAssertEqual([results[i] intCol], i + 1);
|
|
}
|
|
++expected;
|
|
[expectation fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
|
|
for (int i = 0; i < 3; ++i) {
|
|
expectation = [self expectationWithDescription:@""];
|
|
[self dispatchAsyncAndWait:^{
|
|
RLMRealm *realm = [RLMRealm defaultRealm];
|
|
ArrayPropertyObject *array = [[ArrayPropertyObject allObjectsInRealm:realm] firstObject];
|
|
|
|
// Create two objects, one in the list and one not, to verify that the
|
|
// LinkList is actually be used
|
|
[realm beginWriteTransaction];
|
|
[IntObject createInRealm:realm withValue:@[@(i + 1)]];
|
|
[array.intArray addObject:[IntObject createInRealm:realm withValue:@[@(i + 1)]]];
|
|
[realm commitWriteTransaction];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
}
|
|
|
|
[token invalidate];
|
|
}
|
|
|
|
- (RLMNotificationToken *)subscribeAndWaitForInitial:(id<RLMCollection>)query block:(void (^)(id))block {
|
|
__block XCTestExpectation *exp = [self expectationWithDescription:@"wait for initial results"];
|
|
auto token = [query addNotificationBlock:^(id results, RLMCollectionChange *change, NSError *e) {
|
|
XCTAssertNotNil(results);
|
|
XCTAssertNil(e);
|
|
if (exp) {
|
|
[exp fulfill];
|
|
exp = nil;
|
|
}
|
|
else {
|
|
block(results);
|
|
}
|
|
}];
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
return token;
|
|
}
|
|
|
|
- (void)testManualRefreshUsesAsyncResultsWhenPossible {
|
|
__block bool called = false;
|
|
auto token = [self subscribeAndWaitForInitial:IntObject.allObjects block:^(RLMResults *results) {
|
|
called = true;
|
|
}];
|
|
|
|
RLMRealm *realm = [RLMRealm defaultRealm];
|
|
realm.autorefresh = NO;
|
|
|
|
[self waitForNotification:RLMRealmRefreshRequiredNotification realm:realm block:^{
|
|
[self dispatchAsync:^{
|
|
[RLMRealm.defaultRealm transactionWithBlock:^{
|
|
[IntObject createInDefaultRealmWithValue:@[@0]];
|
|
}];
|
|
}];
|
|
}];
|
|
|
|
XCTAssertFalse(called);
|
|
[realm refresh];
|
|
XCTAssertTrue(called);
|
|
|
|
[token invalidate];
|
|
}
|
|
|
|
- (void)testModifyingUnrelatedTableDoesNotTriggerResend {
|
|
__block XCTestExpectation *expectation = [self expectationWithDescription:@""];
|
|
auto token = [[IntObject allObjects] addNotificationBlock:^(RLMResults *results, RLMCollectionChange *change, NSError *e) {
|
|
// will throw if called a second time
|
|
[expectation fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
|
|
[self waitForNotification:RLMRealmDidChangeNotification realm:RLMRealm.defaultRealm block:^{
|
|
RLMRealm *realm = [RLMRealm defaultRealm];
|
|
[realm transactionWithBlock:^{
|
|
[StringObject createInDefaultRealmWithValue:@[@""]];
|
|
}];
|
|
}];
|
|
[token invalidate];
|
|
}
|
|
|
|
- (void)testStaleResultsAreDiscardedWhenThreadIsBlocked {
|
|
XCTestExpectation *expectation = [self expectationWithDescription:@""];
|
|
auto token = [IntObject.allObjects addNotificationBlock:^(RLMResults *results, RLMCollectionChange *change, NSError *e) {
|
|
// Will fail if this is called with the initial results
|
|
XCTAssertEqual(1U, results.count);
|
|
// Will fail if it's called twice
|
|
[expectation fulfill];
|
|
}];
|
|
|
|
// Advance the version on a different thread, and then wait for async work
|
|
// to complete for that new version
|
|
[self dispatchAsyncAndWait:^{
|
|
[RLMRealm.defaultRealm transactionWithBlock:^{
|
|
[IntObject createInDefaultRealmWithValue:@[@0]];
|
|
} error:nil];
|
|
|
|
__block RLMNotificationToken *token;
|
|
CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, ^{
|
|
token = [IntObject.allObjects addNotificationBlock:^(RLMResults *, RLMCollectionChange *, NSError *) {
|
|
[token invalidate];
|
|
token = nil;
|
|
CFRunLoopStop(CFRunLoopGetCurrent());
|
|
}];
|
|
});
|
|
CFRunLoopRun();
|
|
}];
|
|
|
|
// Only now let the main thread pick up the notifications
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
[token invalidate];
|
|
}
|
|
|
|
- (void)testCommitInOneNotificationDoesNotCancelOtherNotifications {
|
|
__block XCTestExpectation *exp1 = nil;
|
|
__block XCTestExpectation *exp2 = nil;
|
|
__block int firstBlockCalls = 0;
|
|
__block int secondBlockCalls = 0;
|
|
|
|
auto token = [self subscribeAndWaitForInitial:IntObject.allObjects block:^(RLMResults *results) {
|
|
++firstBlockCalls;
|
|
if (firstBlockCalls == 2) {
|
|
[exp1 fulfill];
|
|
}
|
|
else {
|
|
[results.realm beginWriteTransaction];
|
|
[IntObject createInDefaultRealmWithValue:@[@1]];
|
|
[results.realm commitWriteTransaction];
|
|
}
|
|
}];
|
|
auto token2 = [self subscribeAndWaitForInitial:IntObject.allObjects block:^(RLMResults *results) {
|
|
++secondBlockCalls;
|
|
if (secondBlockCalls == 2) {
|
|
[exp2 fulfill];
|
|
}
|
|
}];
|
|
|
|
exp1 = [self expectationWithDescription:@""];
|
|
exp2 = [self expectationWithDescription:@""];
|
|
|
|
[RLMRealm.defaultRealm transactionWithBlock:^{
|
|
[IntObject createInDefaultRealmWithValue:@[@0]];
|
|
} error:nil];
|
|
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
|
|
XCTAssertEqual(2, firstBlockCalls);
|
|
XCTAssertEqual(2, secondBlockCalls);
|
|
|
|
[token invalidate];
|
|
[token2 invalidate];
|
|
}
|
|
|
|
- (void)testErrorHandling {
|
|
RLMRealm *realm = [RLMRealm defaultRealm];
|
|
XCTestExpectation *exp = [self expectationWithDescription:@""];
|
|
|
|
// Set the max open files to zero so that opening new files will fail
|
|
rlimit oldrl;
|
|
getrlimit(RLIMIT_NOFILE, &oldrl);
|
|
rlimit rl = oldrl;
|
|
rl.rlim_cur = 0;
|
|
setrlimit(RLIMIT_NOFILE, &rl);
|
|
|
|
// Will try to open another copy of the file for the pin SG
|
|
__block bool called = false;
|
|
auto token = [IntObject.allObjects addNotificationBlock:^(RLMResults *results, RLMCollectionChange *change, NSError *error) {
|
|
XCTAssertNil(results);
|
|
RLMValidateRealmError(error, RLMErrorFileAccess, @"Too many open files", nil);
|
|
called = true;
|
|
[exp fulfill];
|
|
}];
|
|
|
|
// Restore the old open file limit now so that we can make commits
|
|
setrlimit(RLIMIT_NOFILE, &oldrl);
|
|
|
|
// Block should still be called asynchronously
|
|
XCTAssertFalse(called);
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
XCTAssertTrue(called);
|
|
|
|
// Neither adding a new async query nor commiting a write transaction should
|
|
// cause it to resend the error
|
|
XCTestExpectation *exp2 = [self expectationWithDescription:@""];
|
|
auto token2 = [IntObject.allObjects addNotificationBlock:^(RLMResults *results, RLMCollectionChange *change, NSError *error) {
|
|
XCTAssertNil(results);
|
|
RLMValidateRealmError(error, RLMErrorFileAccess, @"Too many open files", nil);
|
|
[exp2 fulfill];
|
|
}];
|
|
[realm beginWriteTransaction];
|
|
[IntObject createInDefaultRealmWithValue:@[@0]];
|
|
[realm commitWriteTransaction];
|
|
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
|
|
[token invalidate];
|
|
[token2 invalidate];
|
|
}
|
|
|
|
- (void)testRLMResultsInstanceIsReused {
|
|
__weak __block RLMResults *prev;
|
|
__block bool first = true;
|
|
|
|
XCTestExpectation *expectation = [self expectationWithDescription:@""];
|
|
auto token = [IntObject.allObjects addNotificationBlock:^(RLMResults *results, RLMCollectionChange *change, NSError *e) {
|
|
if (first) {
|
|
prev = results;
|
|
first = false;
|
|
}
|
|
else {
|
|
XCTAssertEqual(prev, results); // deliberately not EqualObjects
|
|
}
|
|
[expectation fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
XCTAssertNotNil(prev);
|
|
[token invalidate];
|
|
}
|
|
|
|
- (void)testCancellationTokenKeepsSubscriptionAlive {
|
|
__block XCTestExpectation *expectation = [self expectationWithDescription:@""];
|
|
RLMNotificationToken *token;
|
|
@autoreleasepool {
|
|
token = [IntObject.allObjects addNotificationBlock:^(RLMResults *results, RLMCollectionChange *change, NSError *err) {
|
|
XCTAssertNotNil(results);
|
|
XCTAssertNil(err);
|
|
[expectation fulfill];
|
|
}];
|
|
}
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
|
|
// at this point there are no strong references to anything other than the
|
|
// token, so verify that things haven't magically gone away
|
|
// this would be better as a multi-process tests with the commit done
|
|
// from a different process
|
|
|
|
expectation = [self expectationWithDescription:@""];
|
|
@autoreleasepool {
|
|
[RLMRealm.defaultRealm transactionWithBlock:^{
|
|
[IntObject createInDefaultRealmWithValue:@[@0]];
|
|
}];
|
|
}
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
}
|
|
|
|
- (void)testCancellationTokenPreventsOpeningRealmWithMismatchedConfig {
|
|
__block XCTestExpectation *expectation = [self expectationWithDescription:@""];
|
|
RLMNotificationToken *token;
|
|
@autoreleasepool {
|
|
token = [IntObject.allObjects addNotificationBlock:^(RLMResults *results, RLMCollectionChange *change, NSError *err) {
|
|
XCTAssertNotNil(results);
|
|
XCTAssertNil(err);
|
|
[expectation fulfill];
|
|
}];
|
|
}
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
|
|
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
|
|
config.readOnly = true;
|
|
@autoreleasepool {
|
|
XCTAssertThrows([RLMRealm realmWithConfiguration:config error:nil]);
|
|
}
|
|
|
|
[token invalidate];
|
|
XCTAssertNoThrow([RLMRealm realmWithConfiguration:config error:nil]);
|
|
}
|
|
|
|
- (void)testAddAndRemoveQueries {
|
|
RLMRealm *realm = [RLMRealm defaultRealm];
|
|
@autoreleasepool {
|
|
RLMResults *results = IntObject.allObjects;
|
|
[[self subscribeAndWaitForInitial:results block:^(RLMResults *r) {
|
|
XCTFail(@"results delivered after removal");
|
|
}] invalidate];
|
|
|
|
// Readd same results at same version
|
|
[[self subscribeAndWaitForInitial:results block:^(RLMResults *r) {
|
|
XCTFail(@"results delivered after removal");
|
|
}] invalidate];
|
|
|
|
// Add different results at same version
|
|
[[self subscribeAndWaitForInitial:IntObject.allObjects block:^(RLMResults *r) {
|
|
XCTFail(@"results delivered after removal");
|
|
}] invalidate];
|
|
|
|
[self waitForNotification:RLMRealmDidChangeNotification realm:RLMRealm.defaultRealm block:^{
|
|
[RLMRealm.defaultRealm transactionWithBlock:^{ }];
|
|
}];
|
|
|
|
// Readd at later version
|
|
[[self subscribeAndWaitForInitial:results block:^(RLMResults *r) {
|
|
XCTFail(@"results delivered after removal");
|
|
}] invalidate];
|
|
|
|
// Add different results at later version
|
|
[[self subscribeAndWaitForInitial:[IntObject allObjectsInRealm:realm] block:^(RLMResults *r) {
|
|
XCTFail(@"results delivered after removal");
|
|
}] invalidate];
|
|
}
|
|
|
|
// Add different results after all of the previous async queries have been
|
|
// removed entirely
|
|
[[self subscribeAndWaitForInitial:[IntObject allObjectsInRealm:realm] block:^(RLMResults *r) {
|
|
XCTFail(@"results delivered after removal");
|
|
}] invalidate];
|
|
}
|
|
|
|
- (void)testMultipleSourceVersionsForAsyncQueries {
|
|
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
|
|
config.cache = false;
|
|
|
|
// Create ten RLMRealm instances, each with a different read version
|
|
RLMRealm *realms[10];
|
|
for (int i = 0; i < 10; ++i) {
|
|
RLMRealm *realm = realms[i] = [RLMRealm realmWithConfiguration:config error:nil];
|
|
[realm transactionWithBlock:^{
|
|
[IntObject createInRealm:realm withValue:@[@(i)]];
|
|
}];
|
|
}
|
|
|
|
// Each Realm should see a different number of objects as they're on different versions
|
|
for (NSUInteger i = 0; i < 10; ++i) {
|
|
XCTAssertEqual(i + 1, [IntObject allObjectsInRealm:realms[i]].count);
|
|
}
|
|
|
|
RLMNotificationToken *tokens[10];
|
|
|
|
// asyncify them in reverse order so that the version pin has to go backwards
|
|
for (int i = 9; i >= 0; --i) {
|
|
XCTestExpectation *exp = [self expectationWithDescription:@(i).stringValue];
|
|
tokens[i] = [[IntObject allObjectsInRealm:realms[i]] addNotificationBlock:^(RLMResults *results, RLMCollectionChange *change, NSError *error) {
|
|
XCTAssertEqual(10U, results.count);
|
|
XCTAssertNil(error);
|
|
[exp fulfill];
|
|
}];
|
|
}
|
|
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
|
|
for (int i = 0; i < 10; ++i) {
|
|
[tokens[i] invalidate];
|
|
}
|
|
}
|
|
|
|
- (void)testMultipleSourceVersionsWithNotifiersRemovedBeforeRunning {
|
|
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
|
|
config.cache = false;
|
|
config.config.automatic_change_notifications = false;
|
|
|
|
// Create ten RLMRealm instances, each with a different read version
|
|
RLMRealm *realms[10];
|
|
for (int i = 0; i < 10; ++i) {
|
|
RLMRealm *realm = realms[i] = [ManualRefreshRealm realmWithConfiguration:config error:nil];
|
|
[realm transactionWithBlock:^{
|
|
[IntObject createInRealm:realm withValue:@[@(i)]];
|
|
}];
|
|
}
|
|
|
|
__block int calls = 0;
|
|
RLMNotificationToken *tokens[10];
|
|
@autoreleasepool {
|
|
for (int i = 0; i < 10; ++i) {
|
|
tokens[i] = [[IntObject allObjectsInRealm:realms[i]]
|
|
addNotificationBlock:^(RLMResults *, RLMCollectionChange *, NSError *) {
|
|
++calls;
|
|
}];
|
|
}
|
|
|
|
// Each Realm should see a different number of objects as they're on different versions
|
|
for (NSUInteger i = 0; i < 10; ++i) {
|
|
XCTAssertEqual(i + 1, [IntObject allObjectsInRealm:realms[i]].count);
|
|
}
|
|
|
|
// remove all but the last two so that the version pin is for a version
|
|
// that doesn't have a notifier anymore
|
|
for (int i = 0; i < 7; ++i) {
|
|
[tokens[i] invalidate];
|
|
}
|
|
}
|
|
|
|
// Let the background job run now
|
|
auto coord = realm::_impl::RealmCoordinator::get_existing_coordinator(config.config.path);
|
|
coord->on_change();
|
|
|
|
for (int i = 7; i < 10; ++i) {
|
|
realms[i]->_realm->notify();
|
|
XCTAssertEqual(calls, i - 6);
|
|
}
|
|
|
|
for (int i = 7; i < 10; ++i) {
|
|
[tokens[i] invalidate];
|
|
}
|
|
}
|
|
|
|
- (void)testMultipleCallbacksForOneQuery {
|
|
RLMResults *results = IntObject.allObjects;
|
|
|
|
__block int calls1 = 0;
|
|
auto token1 = [self subscribeAndWaitForInitial:results block:^(RLMResults *results) {
|
|
++calls1;
|
|
}];
|
|
XCTAssertEqual(calls1, 0);
|
|
|
|
__block int calls2 = 0;
|
|
auto token2 = [self subscribeAndWaitForInitial:results block:^(RLMResults *results) {
|
|
++calls2;
|
|
}];
|
|
XCTAssertEqual(calls1, 0);
|
|
XCTAssertEqual(calls2, 0);
|
|
|
|
[self waitForNotification:RLMRealmDidChangeNotification realm:results.realm block:^{
|
|
[self createObject:0];
|
|
}];
|
|
|
|
XCTAssertEqual(calls1, 1);
|
|
XCTAssertEqual(calls2, 1);
|
|
|
|
[token1 invalidate];
|
|
|
|
[self waitForNotification:RLMRealmDidChangeNotification realm:results.realm block:^{
|
|
[self createObject:0];
|
|
}];
|
|
|
|
XCTAssertEqual(calls1, 1);
|
|
XCTAssertEqual(calls2, 2);
|
|
|
|
[token2 invalidate];
|
|
|
|
[self waitForNotification:RLMRealmDidChangeNotification realm:results.realm block:^{
|
|
[self createObject:0];
|
|
}];
|
|
|
|
XCTAssertEqual(calls1, 1);
|
|
XCTAssertEqual(calls2, 2);
|
|
}
|
|
|
|
- (void)testRemovingBlockFromWithinNotificationBlock {
|
|
RLMResults *results = IntObject.allObjects;
|
|
|
|
__block int calls = 0;
|
|
__block RLMNotificationToken *token1, *token2;
|
|
token1 = [self subscribeAndWaitForInitial:results block:^(RLMResults *results) {
|
|
[token1 invalidate];
|
|
++calls;
|
|
}];
|
|
token2 = [self subscribeAndWaitForInitial:results block:^(RLMResults *results) {
|
|
[token2 invalidate];
|
|
++calls;
|
|
}];
|
|
|
|
[self waitForNotification:RLMRealmDidChangeNotification realm:results.realm block:^{
|
|
[self createObject:0];
|
|
}];
|
|
|
|
[self waitForNotification:RLMRealmDidChangeNotification realm:results.realm block:^{
|
|
[self createObject:0];
|
|
}];
|
|
XCTAssertEqual(calls, 2);
|
|
}
|
|
|
|
- (void)testAddingBlockFromWithinNotificationBlock {
|
|
RLMResults *results = IntObject.allObjects;
|
|
|
|
__block int calls = 0;
|
|
__block RLMNotificationToken *token1, *token2;
|
|
token1 = [self subscribeAndWaitForInitial:results block:^(RLMResults *results) {
|
|
if (++calls == 1) {
|
|
token2 = [results addNotificationBlock:^(RLMResults *results, RLMCollectionChange *change, NSError *error) {
|
|
++calls;
|
|
}];
|
|
}
|
|
}];
|
|
|
|
// Triggers one call on each block. Nested call is deferred until next refresh.
|
|
[self waitForNotification:RLMRealmDidChangeNotification realm:results.realm block:^{
|
|
[self createObject:0];
|
|
}];
|
|
XCTAssertEqual(calls, 1);
|
|
[results.realm refresh];
|
|
XCTAssertEqual(calls, 2);
|
|
|
|
// Triggers one call on each block
|
|
[self waitForNotification:RLMRealmDidChangeNotification realm:results.realm block:^{
|
|
[self createObject:0];
|
|
}];
|
|
XCTAssertEqual(calls, 4);
|
|
|
|
[token1 invalidate];
|
|
[token2 invalidate];
|
|
}
|
|
|
|
- (void)testAddingNewQueryWithinNotificationBlock {
|
|
RLMResults *results1 = IntObject.allObjects;
|
|
RLMResults *results2 = IntObject.allObjects;
|
|
|
|
__block int calls = 0;
|
|
__block RLMNotificationToken *token1, *token2;
|
|
token1 = [self subscribeAndWaitForInitial:results1 block:^(RLMResults *results) {
|
|
++calls;
|
|
if (calls == 1) {
|
|
CFRunLoopStop(CFRunLoopGetCurrent());
|
|
token2 = [results2 addNotificationBlock:^(RLMResults *results, RLMCollectionChange *change, NSError *error) {
|
|
CFRunLoopStop(CFRunLoopGetCurrent());
|
|
++calls;
|
|
}];
|
|
}
|
|
}];
|
|
|
|
// Triggers one call on outer block, but inner does not get a chance to deliver
|
|
[self dispatchAsync:^{ [self createObject:0]; }];
|
|
CFRunLoopRun();
|
|
XCTAssertEqual(calls, 1);
|
|
|
|
// Pick up the initial run of the inner block
|
|
CFRunLoopRun();
|
|
assert(calls == 2);
|
|
XCTAssertEqual(calls, 2);
|
|
|
|
// Triggers a call on each block
|
|
[self dispatchAsync:^{ [self createObject:0]; }];
|
|
CFRunLoopRun();
|
|
XCTAssertEqual(calls, 4);
|
|
|
|
[token1 invalidate];
|
|
[token2 invalidate];
|
|
}
|
|
|
|
- (void)testAddingNewQueryWithinRealmNotificationBlock {
|
|
__block RLMNotificationToken *queryToken;
|
|
__block XCTestExpectation *exp;
|
|
auto realmToken = [RLMRealm.defaultRealm addNotificationBlock:^(RLMNotification notification, RLMRealm *realm) {
|
|
CFRunLoopStop(CFRunLoopGetCurrent());
|
|
exp = [self expectationWithDescription:@"query notification"];
|
|
queryToken = [IntObject.allObjects addNotificationBlock:^(RLMResults *results, RLMCollectionChange *change, NSError *e) {
|
|
[exp fulfill];
|
|
}];
|
|
}];
|
|
|
|
// Make a background commit to trigger a Realm notification
|
|
[self dispatchAsync:^{ [RLMRealm.defaultRealm transactionWithBlock:^{}]; }];
|
|
|
|
// Wait for the notification
|
|
CFRunLoopRun();
|
|
[realmToken invalidate];
|
|
|
|
// Wait for the initial async query results created within the notification
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
[queryToken invalidate];
|
|
}
|
|
|
|
- (void)testBlockedThreadWithNotificationsDoesNotPreventDeliveryOnOtherThreads {
|
|
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
|
dispatch_semaphore_t sema2 = dispatch_semaphore_create(0);
|
|
[self dispatchAsync:^{
|
|
// Add a notification block on a background thread, run the runloop
|
|
// until the initial results are ready, and then block the thread without
|
|
// running the runloop until the main thread is done testing things
|
|
__block RLMNotificationToken *token;
|
|
CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, ^{
|
|
token = [IntObject.allObjects addNotificationBlock:^(RLMResults *results, RLMCollectionChange *change, NSError *error) {
|
|
dispatch_semaphore_signal(sema);
|
|
CFRunLoopStop(CFRunLoopGetCurrent());
|
|
dispatch_semaphore_wait(sema2, DISPATCH_TIME_FOREVER);
|
|
}];
|
|
});
|
|
CFRunLoopRun();
|
|
[token invalidate];
|
|
}];
|
|
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
|
|
|
|
__block int calls = 0;
|
|
auto token = [self subscribeAndWaitForInitial:IntObject.allObjects block:^(RLMResults *results) {
|
|
++calls;
|
|
}];
|
|
XCTAssertEqual(calls, 0);
|
|
|
|
[self waitForNotification:RLMRealmDidChangeNotification realm:RLMRealm.defaultRealm block:^{
|
|
[self createObject:0];
|
|
}];
|
|
XCTAssertEqual(calls, 1);
|
|
|
|
[token invalidate];
|
|
dispatch_semaphore_signal(sema2);
|
|
}
|
|
|
|
- (void)testAddNotificationBlockFromWrongThread {
|
|
RLMResults *results = [IntObject allObjects];
|
|
[self dispatchAsyncAndWait:^{
|
|
CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, ^{
|
|
XCTAssertThrows([results addNotificationBlock:^(RLMResults *results, RLMCollectionChange *change, NSError *error) {
|
|
XCTFail(@"should not be called");
|
|
}]);
|
|
CFRunLoopStop(CFRunLoopGetCurrent());
|
|
});
|
|
CFRunLoopRun();
|
|
}];
|
|
}
|
|
|
|
- (void)testRemoveNotificationBlockFromWrongThread {
|
|
// Unlike adding this is allowed, because it can happen due to capturing
|
|
// tokens in blocks and users are very confused by errors from deallocation
|
|
// on the wrong thread
|
|
RLMResults *results = [IntObject allObjects];
|
|
auto token = [results addNotificationBlock:^(RLMResults *results, RLMCollectionChange *change, NSError *error) {
|
|
XCTFail(@"should not be called");
|
|
}];
|
|
[self dispatchAsyncAndWait:^{
|
|
[token invalidate];
|
|
}];
|
|
}
|
|
|
|
- (void)testSimultaneouslyRemoveCallbacksFromCallbacksForOtherResults {
|
|
dispatch_semaphore_t sema1 = dispatch_semaphore_create(0);
|
|
dispatch_semaphore_t sema2 = dispatch_semaphore_create(0);
|
|
__block RLMNotificationToken *token1, *token2;
|
|
|
|
[self dispatchAsync:^{
|
|
CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, ^{
|
|
__block bool first = true;
|
|
token1 = [IntObject.allObjects addNotificationBlock:^(RLMResults *results, RLMCollectionChange *change, NSError *error) {
|
|
XCTAssertTrue(first);
|
|
first = false;
|
|
dispatch_semaphore_signal(sema1);
|
|
dispatch_semaphore_wait(sema2, DISPATCH_TIME_FOREVER);
|
|
[token2 invalidate];
|
|
CFRunLoopStop(CFRunLoopGetCurrent());
|
|
}];
|
|
});
|
|
CFRunLoopRun();
|
|
}];
|
|
|
|
CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, ^{
|
|
__block bool first = true;
|
|
token2 = [IntObject.allObjects addNotificationBlock:^(RLMResults *results, RLMCollectionChange *change, NSError *error) {
|
|
XCTAssertTrue(first);
|
|
first = false;
|
|
dispatch_semaphore_signal(sema2);
|
|
dispatch_semaphore_wait(sema1, DISPATCH_TIME_FOREVER);
|
|
[token1 invalidate];
|
|
CFRunLoopStop(CFRunLoopGetCurrent());
|
|
}];
|
|
});
|
|
CFRunLoopRun();
|
|
}
|
|
|
|
- (void)testAsyncNotSupportedForReadOnlyRealms {
|
|
@autoreleasepool { [RLMRealm defaultRealm]; }
|
|
|
|
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
|
|
config.readOnly = true;
|
|
RLMRealm *realm = [RLMRealm realmWithConfiguration:config error:nil];
|
|
|
|
XCTAssertThrows([[IntObject allObjectsInRealm:realm] addNotificationBlock:^(RLMResults *results, RLMCollectionChange *change, NSError *error) {
|
|
XCTFail(@"should not be called");
|
|
}]);
|
|
}
|
|
|
|
- (void)testAsyncNotSupportedInWriteTransactions {
|
|
[RLMRealm.defaultRealm transactionWithBlock:^{
|
|
XCTAssertThrows([IntObject.allObjects addNotificationBlock:^(RLMResults *results, RLMCollectionChange *change, NSError *error) {
|
|
XCTFail(@"should not be called");
|
|
}]);
|
|
}];
|
|
}
|
|
|
|
- (void)testTransactionsAfterDeletingLinkView {
|
|
RLMRealm *realm = [RLMRealm defaultRealm];
|
|
[realm beginWriteTransaction];
|
|
IntObject *io = [IntObject createInRealm:realm withValue:@[@5]];
|
|
ArrayPropertyObject *apo = [ArrayPropertyObject createInRealm:realm withValue:@[@"", @[], @[io]]];
|
|
[realm commitWriteTransaction];
|
|
|
|
RLMNotificationToken *token1 = [self subscribeAndWaitForInitial:apo.intArray block:^(RLMArray *array) {
|
|
XCTAssertTrue(array.invalidated);
|
|
}];
|
|
RLMResults *asResults = [apo.intArray objectsWhere:@"intCol = 5"];
|
|
RLMNotificationToken *token2 = [self subscribeAndWaitForInitial:asResults block:^(RLMResults *results) {
|
|
XCTAssertEqual(results.count, 0U);
|
|
}];
|
|
|
|
// Delete the object containing the RLMArray with notifiers
|
|
[self waitForNotification:RLMRealmDidChangeNotification realm:realm block:^{
|
|
RLMRealm *realm = [RLMRealm defaultRealm];
|
|
[realm transactionWithBlock:^{
|
|
[realm deleteObject:[ArrayPropertyObject allObjectsInRealm:realm].firstObject];
|
|
}];
|
|
}];
|
|
|
|
// Perform another transaction while the notifiers are still alive as
|
|
// transactions deleting the RLMArray and transactions with the RLMArray
|
|
// already deleted hit different code paths
|
|
[self waitForNotification:RLMRealmDidChangeNotification realm:realm block:^{
|
|
RLMRealm *realm = [RLMRealm defaultRealm];
|
|
[realm transactionWithBlock:^{
|
|
[ArrayPropertyObject createInRealm:realm withValue:@[@"", @[], @[]]];
|
|
}];
|
|
}];
|
|
|
|
[token1 invalidate];
|
|
[token2 invalidate];
|
|
}
|
|
|
|
- (void)testInitialResultDiscardsChanges {
|
|
auto token = [IntObject.allObjects addNotificationBlock:^(RLMResults *results, RLMCollectionChange *changes, NSError *) {
|
|
XCTAssertEqual(results.count, 1U);
|
|
XCTAssertNil(changes);
|
|
CFRunLoopStop(CFRunLoopGetCurrent());
|
|
}];
|
|
|
|
// Make a write on a background thread, and then wait for the notification
|
|
// for that write to be delivered to ensure that the notification we get on
|
|
// the main thread actually would include changes
|
|
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
|
[self dispatchAsync:^{
|
|
CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, ^{
|
|
auto token = [IntObject.allObjects addNotificationBlock:^(RLMResults *results, RLMCollectionChange *changes, NSError *) {
|
|
if (changes) {
|
|
dispatch_semaphore_signal(sema);
|
|
CFRunLoopStop(CFRunLoopGetCurrent());
|
|
}
|
|
}];
|
|
|
|
[RLMRealm.defaultRealm transactionWithBlock:^{
|
|
[IntObject createInDefaultRealmWithValue:@[@0]];
|
|
}];
|
|
|
|
CFRunLoopRun();
|
|
[token invalidate];
|
|
CFRunLoopStop(CFRunLoopGetCurrent());
|
|
});
|
|
CFRunLoopRun();
|
|
}];
|
|
|
|
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
|
|
CFRunLoopRun();
|
|
[token invalidate];
|
|
}
|
|
|
|
@end
|
|
|