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.
1934 lines
72 KiB
1934 lines
72 KiB
////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Copyright 2014 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 "RLMObjectSchema_Private.hpp"
|
|
#import "RLMRealmConfiguration_Private.hpp"
|
|
#import "RLMRealm_Dynamic.h"
|
|
#import "RLMSchema_Private.h"
|
|
#import "RLMRealmUtil.hpp"
|
|
|
|
#import <mach/mach_init.h>
|
|
#import <mach/vm_map.h>
|
|
#import <sys/resource.h>
|
|
#import <thread>
|
|
|
|
#import <realm/util/file.hpp>
|
|
|
|
@interface RLMRealm ()
|
|
+ (BOOL)isCoreDebug;
|
|
@end
|
|
|
|
@interface RLMObjectSchema (Private)
|
|
+ (instancetype)schemaForObjectClass:(Class)objectClass;
|
|
|
|
@property (nonatomic, readwrite, assign) Class objectClass;
|
|
@end
|
|
|
|
@interface RLMSchema (Private)
|
|
@property (nonatomic, readwrite, copy) NSArray *objectSchema;
|
|
@end
|
|
|
|
@interface RealmTests : RLMTestCase
|
|
@end
|
|
|
|
@implementation RealmTests
|
|
|
|
- (void)deleteFiles {
|
|
[super deleteFiles];
|
|
|
|
for (NSString *realmPath in self.pathsFor100Realms) {
|
|
[self deleteRealmFileAtURL:[NSURL fileURLWithPath:realmPath]];
|
|
}
|
|
}
|
|
|
|
#pragma mark - Opening Realms
|
|
|
|
- (void)testOpeningInvalidPathThrows {
|
|
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
|
|
config.fileURL = [NSURL fileURLWithPath:@"/dev/null/foo"];
|
|
RLMAssertThrowsWithCodeMatching([RLMRealm realmWithConfiguration:config error:nil], RLMErrorFileAccess);
|
|
}
|
|
|
|
- (void)testPathCannotBeBothInMemoryAndRegularDurability {
|
|
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
|
|
config.inMemoryIdentifier = @"identifier";
|
|
RLMRealm *inMemoryRealm = [RLMRealm realmWithConfiguration:config error:nil];
|
|
|
|
// make sure we can't open disk-realm at same path
|
|
config.fileURL = [NSURL fileURLWithPath:@(inMemoryRealm.configuration.config.path.c_str())];
|
|
NSError *error; // passing in a reference to assert that this error can't be catched!
|
|
RLMAssertThrowsWithReasonMatching([RLMRealm realmWithConfiguration:config error:&error], @"Realm at path '.*' already opened with different inMemory settings");
|
|
}
|
|
|
|
- (void)testRealmWithPathUsesDefaultConfiguration {
|
|
RLMRealmConfiguration *originalDefaultConfiguration = [RLMRealmConfiguration defaultConfiguration];
|
|
RLMRealmConfiguration *newDefaultConfiguration = [originalDefaultConfiguration copy];
|
|
newDefaultConfiguration.objectClasses = @[];
|
|
[RLMRealmConfiguration setDefaultConfiguration:newDefaultConfiguration];
|
|
XCTAssertEqual([[[[RLMRealm realmWithURL:RLMTestRealmURL()] configuration] objectClasses] count], 0U);
|
|
[RLMRealmConfiguration setDefaultConfiguration:originalDefaultConfiguration];
|
|
}
|
|
|
|
- (void)testReadOnlyFile {
|
|
@autoreleasepool {
|
|
RLMRealm *realm = self.realmWithTestPath;
|
|
[realm beginWriteTransaction];
|
|
[StringObject createInRealm:realm withValue:@[@"a"]];
|
|
[realm commitWriteTransaction];
|
|
}
|
|
|
|
[NSFileManager.defaultManager setAttributes:@{NSFileImmutable: @YES} ofItemAtPath:RLMTestRealmURL().path error:nil];
|
|
|
|
// Should not be able to open read-write
|
|
RLMAssertThrowsWithCodeMatching([self realmWithTestPath], RLMErrorFileAccess);
|
|
|
|
RLMRealm *realm;
|
|
XCTAssertNoThrow(realm = [self readOnlyRealmWithURL:RLMTestRealmURL() error:nil]);
|
|
XCTAssertEqual(1U, [StringObject allObjectsInRealm:realm].count);
|
|
|
|
[NSFileManager.defaultManager setAttributes:@{NSFileImmutable: @NO} ofItemAtPath:RLMTestRealmURL().path error:nil];
|
|
}
|
|
|
|
- (void)testReadOnlyFileInImmutableDirectory {
|
|
@autoreleasepool {
|
|
RLMRealm *realm = self.realmWithTestPath;
|
|
[realm beginWriteTransaction];
|
|
[StringObject createInRealm:realm withValue:@[@"a"]];
|
|
[realm commitWriteTransaction];
|
|
}
|
|
|
|
// Delete '*.lock' and '.note' files to simulate opening Realm in an app bundle
|
|
[[NSFileManager defaultManager] removeItemAtURL:[RLMTestRealmURL() URLByAppendingPathExtension:@"lock"] error:nil];
|
|
[[NSFileManager defaultManager] removeItemAtURL:[RLMTestRealmURL() URLByAppendingPathExtension:@"note"] error:nil];
|
|
|
|
// Make parent directory immutable to simulate opening Realm in an app bundle
|
|
NSURL *parentDirectoryOfTestRealmURL = [RLMTestRealmURL() URLByDeletingLastPathComponent];
|
|
[NSFileManager.defaultManager setAttributes:@{NSFileImmutable: @YES} ofItemAtPath:parentDirectoryOfTestRealmURL.path error:nil];
|
|
|
|
RLMRealm *realm;
|
|
// Read-only Realm should be opened even in immutable directory
|
|
XCTAssertNoThrow(realm = [self readOnlyRealmWithURL:RLMTestRealmURL() error:nil]);
|
|
|
|
[self dispatchAsyncAndWait:^{ XCTAssertNoThrow([self readOnlyRealmWithURL:RLMTestRealmURL() error:nil]); }];
|
|
|
|
[NSFileManager.defaultManager setAttributes:@{NSFileImmutable: @NO} ofItemAtPath:parentDirectoryOfTestRealmURL.path error:nil];
|
|
}
|
|
|
|
- (void)testReadOnlyRealmMustExist {
|
|
RLMAssertThrowsWithCodeMatching([self readOnlyRealmWithURL:RLMTestRealmURL() error:nil], RLMErrorFileNotFound);
|
|
}
|
|
|
|
- (void)testCannotHaveReadOnlyAndReadWriteRealmsAtSamePathAtSameTime {
|
|
NSString *exceptionReason = @"Realm at path '.*' already opened with different read permissions";
|
|
@autoreleasepool {
|
|
XCTAssertNoThrow([self realmWithTestPath]);
|
|
RLMAssertThrowsWithReasonMatching([self readOnlyRealmWithURL:RLMTestRealmURL() error:nil], exceptionReason);
|
|
}
|
|
|
|
@autoreleasepool {
|
|
XCTAssertNoThrow([self readOnlyRealmWithURL:RLMTestRealmURL() error:nil]);
|
|
RLMAssertThrowsWithReasonMatching([self realmWithTestPath], exceptionReason);
|
|
}
|
|
|
|
[self dispatchAsyncAndWait:^{
|
|
XCTAssertNoThrow([self readOnlyRealmWithURL:RLMTestRealmURL() error:nil]);
|
|
RLMAssertThrowsWithReasonMatching([self realmWithTestPath], exceptionReason);
|
|
}];
|
|
}
|
|
|
|
- (void)testCanOpenReadOnlyOnMulitpleThreadsAtOnce {
|
|
@autoreleasepool {
|
|
RLMRealm *realm = self.realmWithTestPath;
|
|
[realm beginWriteTransaction];
|
|
[StringObject createInRealm:realm withValue:@[@"a"]];
|
|
[realm commitWriteTransaction];
|
|
}
|
|
|
|
RLMRealm *realm = [self readOnlyRealmWithURL:RLMTestRealmURL() error:nil];
|
|
XCTAssertEqual(1U, [StringObject allObjectsInRealm:realm].count);
|
|
|
|
[self dispatchAsyncAndWait:^{
|
|
RLMRealm *realm = [self readOnlyRealmWithURL:RLMTestRealmURL() error:nil];
|
|
XCTAssertEqual(1U, [StringObject allObjectsInRealm:realm].count);
|
|
}];
|
|
|
|
// Verify that closing the other RLMRealm didn't manage to break anything
|
|
XCTAssertEqual(1U, [StringObject allObjectsInRealm:realm].count);
|
|
}
|
|
|
|
- (void)testFilePermissionDenied {
|
|
@autoreleasepool {
|
|
XCTAssertNoThrow([self realmWithTestPath]);
|
|
}
|
|
|
|
// Make Realm at test path temporarily unreadable
|
|
NSError *error;
|
|
NSNumber *permissions = [NSFileManager.defaultManager attributesOfItemAtPath:RLMTestRealmURL().path error:&error][NSFilePosixPermissions];
|
|
assert(!error);
|
|
[NSFileManager.defaultManager setAttributes:@{NSFilePosixPermissions: @(0000)} ofItemAtPath:RLMTestRealmURL().path error:&error];
|
|
assert(!error);
|
|
|
|
RLMAssertThrowsWithCodeMatching([self realmWithTestPath], RLMErrorFilePermissionDenied);
|
|
|
|
[NSFileManager.defaultManager setAttributes:@{NSFilePosixPermissions: permissions} ofItemAtPath:RLMTestRealmURL().path error:&error];
|
|
assert(!error);
|
|
}
|
|
|
|
// Check that the data for file was left unchanged when opened with upgrading
|
|
// disabled, but allow expanding the file to the page size
|
|
#define AssertFileUnmodified(oldURL, newURL) do { \
|
|
NSData *oldData = [NSData dataWithContentsOfURL:oldURL]; \
|
|
NSData *newData = [NSData dataWithContentsOfURL:newURL]; \
|
|
if (oldData.length < realm::util::page_size()) { \
|
|
XCTAssertEqual(newData.length, realm::util::page_size()); \
|
|
XCTAssertNotEqual(([newData rangeOfData:oldData options:0 range:{0, oldData.length}]).location, NSNotFound); \
|
|
} \
|
|
else \
|
|
XCTAssertEqualObjects(oldData, newData); \
|
|
} while (0)
|
|
|
|
- (void)testFileFormatUpgradeRequiredDeleteRealmIfNeeded {
|
|
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
|
|
config.deleteRealmIfMigrationNeeded = YES;
|
|
|
|
NSURL *bundledRealmURL = [[NSBundle bundleForClass:[RealmTests class]] URLForResource:@"fileformat-pre-null" withExtension:@"realm"];
|
|
[NSFileManager.defaultManager copyItemAtURL:bundledRealmURL toURL:config.fileURL error:nil];
|
|
|
|
@autoreleasepool {
|
|
XCTAssertTrue([[RLMRealm realmWithConfiguration:config error:nil] isEmpty]);
|
|
}
|
|
|
|
bundledRealmURL = [[NSBundle bundleForClass:[RealmTests class]] URLForResource:@"fileformat-old-date" withExtension:@"realm"];
|
|
[NSFileManager.defaultManager removeItemAtURL:config.fileURL error:nil];
|
|
[NSFileManager.defaultManager copyItemAtURL:bundledRealmURL toURL:config.fileURL error:nil];
|
|
|
|
@autoreleasepool {
|
|
XCTAssertTrue([[RLMRealm realmWithConfiguration:config error:nil] isEmpty]);
|
|
}
|
|
}
|
|
|
|
- (void)testFileFormatUpgradeRequiredButDisabled {
|
|
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
|
|
config.disableFormatUpgrade = true;
|
|
|
|
NSURL *bundledRealmURL = [[NSBundle bundleForClass:[RealmTests class]] URLForResource:@"fileformat-pre-null" withExtension:@"realm"];
|
|
[NSFileManager.defaultManager copyItemAtURL:bundledRealmURL toURL:config.fileURL error:nil];
|
|
|
|
RLMAssertThrowsWithCodeMatching([RLMRealm realmWithConfiguration:config error:nil],
|
|
RLMErrorFileFormatUpgradeRequired);
|
|
AssertFileUnmodified(bundledRealmURL, config.fileURL);
|
|
|
|
bundledRealmURL = [[NSBundle bundleForClass:[RealmTests class]] URLForResource:@"fileformat-old-date" withExtension:@"realm"];
|
|
[NSFileManager.defaultManager removeItemAtURL:config.fileURL error:nil];
|
|
[NSFileManager.defaultManager copyItemAtURL:bundledRealmURL toURL:config.fileURL error:nil];
|
|
|
|
RLMAssertThrowsWithCodeMatching([RLMRealm realmWithConfiguration:config error:nil],
|
|
RLMErrorFileFormatUpgradeRequired);
|
|
AssertFileUnmodified(bundledRealmURL, config.fileURL);
|
|
}
|
|
|
|
- (void)testFileFormatUpgradeRequiredButReadOnly {
|
|
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
|
|
config.readOnly = true;
|
|
|
|
NSURL *bundledRealmURL = [[NSBundle bundleForClass:[RealmTests class]] URLForResource:@"fileformat-pre-null" withExtension:@"realm"];
|
|
[NSFileManager.defaultManager copyItemAtURL:bundledRealmURL toURL:config.fileURL error:nil];
|
|
|
|
RLMAssertThrowsWithCodeMatching([RLMRealm realmWithConfiguration:config error:nil], RLMErrorFileAccess);
|
|
XCTAssertEqualObjects([NSData dataWithContentsOfURL:bundledRealmURL],
|
|
[NSData dataWithContentsOfURL:config.fileURL]);
|
|
|
|
bundledRealmURL = [[NSBundle bundleForClass:[RealmTests class]] URLForResource:@"fileformat-old-date" withExtension:@"realm"];
|
|
[NSFileManager.defaultManager removeItemAtURL:config.fileURL error:nil];
|
|
[NSFileManager.defaultManager copyItemAtURL:bundledRealmURL toURL:config.fileURL error:nil];
|
|
|
|
RLMAssertThrowsWithCodeMatching([RLMRealm realmWithConfiguration:config error:nil], RLMErrorFileAccess);
|
|
XCTAssertEqualObjects([NSData dataWithContentsOfURL:bundledRealmURL],
|
|
[NSData dataWithContentsOfURL:config.fileURL]);
|
|
}
|
|
|
|
#if TARGET_OS_IPHONE && (!TARGET_IPHONE_SIMULATOR || !TARGET_RT_64_BIT)
|
|
- (void)testExceedingVirtualAddressSpace {
|
|
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
|
|
|
|
const NSUInteger stringLength = 1024 * 1024;
|
|
void *mem = calloc(stringLength, '1');
|
|
NSString *largeString = [[NSString alloc] initWithBytesNoCopy:mem
|
|
length:stringLength
|
|
encoding:NSUTF8StringEncoding
|
|
freeWhenDone:YES];
|
|
|
|
@autoreleasepool {
|
|
RLMRealm *realm = [RLMRealm realmWithConfiguration:config error:nil];
|
|
[realm beginWriteTransaction];
|
|
StringObject *stringObj = [StringObject new];
|
|
stringObj.stringCol = largeString;
|
|
[realm addObject:stringObj];
|
|
[realm commitWriteTransaction];
|
|
}
|
|
|
|
struct VirtualMemoryChunk {
|
|
vm_address_t address;
|
|
vm_size_t size;
|
|
};
|
|
|
|
std::vector<VirtualMemoryChunk> allocatedChunks;
|
|
NSUInteger size = 1024 * 1024 * 1024;
|
|
while (size >= stringLength) {
|
|
VirtualMemoryChunk chunk { .size = size };
|
|
kern_return_t ret = vm_allocate(mach_task_self(), &chunk.address, chunk.size,
|
|
VM_FLAGS_ANYWHERE);
|
|
if (ret == KERN_NO_SPACE) {
|
|
size /= 2;
|
|
} else {
|
|
allocatedChunks.push_back(chunk);
|
|
}
|
|
}
|
|
|
|
@autoreleasepool {
|
|
RLMAssertThrowsWithCodeMatching([RLMRealm realmWithConfiguration:config error:nil], RLMErrorAddressSpaceExhausted);
|
|
}
|
|
|
|
for (auto chunk : allocatedChunks) {
|
|
kern_return_t ret = vm_deallocate(mach_task_self(), chunk.address, chunk.size);
|
|
assert(ret == KERN_SUCCESS);
|
|
}
|
|
|
|
@autoreleasepool {
|
|
XCTAssertNoThrow([RLMRealm realmWithConfiguration:config error:nil]);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
- (void)testOpenAsync {
|
|
// Locals
|
|
RLMRealmConfiguration *c = [RLMRealmConfiguration defaultConfiguration];
|
|
XCTestExpectation *ex = [self expectationWithDescription:@"open-async"];
|
|
|
|
// Helpers
|
|
auto assertNoCachedRealm = ^{ XCTAssertNil(RLMGetAnyCachedRealmForPath(c.pathOnDisk.UTF8String)); };
|
|
auto fileExists = ^BOOL() {
|
|
return [[NSFileManager defaultManager] fileExistsAtPath:c.pathOnDisk isDirectory:nil];
|
|
};
|
|
|
|
// Unsuccessful open
|
|
c.readOnly = true;
|
|
[RLMRealm asyncOpenWithConfiguration:c
|
|
callbackQueue:dispatch_get_main_queue()
|
|
callback:^(RLMRealm * _Nullable realm, NSError * _Nullable error) {
|
|
XCTAssertEqual(error.code, RLMErrorFileNotFound);
|
|
XCTAssertNil(realm);
|
|
[ex fulfill];
|
|
}];
|
|
XCTAssertFalse(fileExists());
|
|
assertNoCachedRealm();
|
|
[self waitForExpectationsWithTimeout:1 handler:nil];
|
|
XCTAssertFalse(fileExists());
|
|
assertNoCachedRealm();
|
|
|
|
// Successful open
|
|
c.readOnly = false;
|
|
ex = [self expectationWithDescription:@"open-async"];
|
|
|
|
// Hold exclusive lock on lock file to prevent Realm from being created
|
|
// if the dispatch_async happens too quickly
|
|
NSString *lockFilePath = [c.pathOnDisk stringByAppendingString:@".lock"];
|
|
[[NSFileManager defaultManager] createFileAtPath:lockFilePath contents:[NSData data] attributes:nil];
|
|
int fd = open(lockFilePath.UTF8String, O_RDWR);
|
|
XCTAssertNotEqual(-1, fd);
|
|
int ret = flock(fd, LOCK_SH);
|
|
XCTAssertEqual(0, ret);
|
|
|
|
[RLMRealm asyncOpenWithConfiguration:c
|
|
callbackQueue:dispatch_get_main_queue()
|
|
callback:^(RLMRealm * _Nullable realm, NSError * _Nullable error) {
|
|
XCTAssertNil(error);
|
|
XCTAssertNotNil(realm);
|
|
[ex fulfill];
|
|
}];
|
|
XCTAssertFalse(fileExists());
|
|
flock(fd, LOCK_UN);
|
|
close(fd);
|
|
assertNoCachedRealm();
|
|
[self waitForExpectationsWithTimeout:1 handler:nil];
|
|
XCTAssertTrue(fileExists());
|
|
assertNoCachedRealm();
|
|
}
|
|
|
|
#pragma mark - Adding and Removing Objects
|
|
|
|
- (void)testRealmAddAndRemoveObjects {
|
|
RLMRealm *realm = [self realmWithTestPath];
|
|
[realm beginWriteTransaction];
|
|
[StringObject createInRealm:realm withValue:@[@"a"]];
|
|
[StringObject createInRealm:realm withValue:@[@"b"]];
|
|
[StringObject createInRealm:realm withValue:@[@"c"]];
|
|
XCTAssertEqual([StringObject objectsInRealm:realm withPredicate:nil].count, 3U, @"Expecting 3 objects");
|
|
[realm commitWriteTransaction];
|
|
|
|
// test again after write transaction
|
|
RLMResults *objects = [StringObject allObjectsInRealm:realm];
|
|
XCTAssertEqual(objects.count, 3U, @"Expecting 3 objects");
|
|
XCTAssertEqualObjects([objects.firstObject stringCol], @"a", @"Expecting column to be 'a'");
|
|
|
|
[realm beginWriteTransaction];
|
|
[realm deleteObject:objects[2]];
|
|
[realm deleteObject:objects[0]];
|
|
XCTAssertEqual([StringObject objectsInRealm:realm withPredicate:nil].count, 1U, @"Expecting 1 object");
|
|
[realm commitWriteTransaction];
|
|
|
|
objects = [StringObject allObjectsInRealm:realm];
|
|
XCTAssertEqual(objects.count, 1U, @"Expecting 1 object");
|
|
XCTAssertEqualObjects([objects.firstObject stringCol], @"b", @"Expecting column to be 'b'");
|
|
}
|
|
|
|
- (void)testRemoveUnmanagedObject {
|
|
RLMRealm *realm = [self realmWithTestPath];
|
|
StringObject *obj = [[StringObject alloc] initWithValue:@[@"a"]];
|
|
|
|
[realm beginWriteTransaction];
|
|
XCTAssertThrows([realm deleteObject:obj]);
|
|
obj = [StringObject createInRealm:realm withValue:@[@"b"]];
|
|
[realm commitWriteTransaction];
|
|
|
|
[self waitForNotification:RLMRealmDidChangeNotification realm:realm block:^{
|
|
RLMRealm *realm = [self realmWithTestPath];
|
|
RLMObject *obj = [[StringObject allObjectsInRealm:realm] firstObject];
|
|
[realm beginWriteTransaction];
|
|
[realm deleteObject:obj];
|
|
XCTAssertThrows([realm deleteObject:obj]);
|
|
[realm commitWriteTransaction];
|
|
}];
|
|
|
|
[realm beginWriteTransaction];
|
|
[realm deleteObject:obj];
|
|
[realm commitWriteTransaction];
|
|
}
|
|
|
|
- (void)testRealmBatchRemoveObjects {
|
|
RLMRealm *realm = [self realmWithTestPath];
|
|
[realm beginWriteTransaction];
|
|
StringObject *strObj = [StringObject createInRealm:realm withValue:@[@"a"]];
|
|
[StringObject createInRealm:realm withValue:@[@"b"]];
|
|
[StringObject createInRealm:realm withValue:@[@"c"]];
|
|
[realm commitWriteTransaction];
|
|
|
|
// delete objects
|
|
RLMResults *objects = [StringObject allObjectsInRealm:realm];
|
|
XCTAssertEqual(objects.count, 3U, @"Expecting 3 objects");
|
|
[realm beginWriteTransaction];
|
|
[realm deleteObjects:[StringObject objectsInRealm:realm where:@"stringCol != 'a'"]];
|
|
XCTAssertEqual([[StringObject allObjectsInRealm:realm] count], 1U, @"Expecting 0 objects");
|
|
[realm deleteObjects:objects];
|
|
XCTAssertEqual([[StringObject allObjectsInRealm:realm] count], 0U, @"Expecting 0 objects");
|
|
[realm commitWriteTransaction];
|
|
|
|
XCTAssertEqual([[StringObject allObjectsInRealm:realm] count], 0U, @"Expecting 0 objects");
|
|
XCTAssertThrows(strObj.stringCol, @"Object should be invalidated");
|
|
|
|
// add objects to linkView
|
|
[realm beginWriteTransaction];
|
|
ArrayPropertyObject *obj = [ArrayPropertyObject createInRealm:realm withValue:@[@"name", @[@[@"a"], @[@"b"], @[@"c"]], @[]]];
|
|
[StringObject createInRealm:realm withValue:@[@"d"]];
|
|
[realm commitWriteTransaction];
|
|
|
|
XCTAssertEqual([[StringObject allObjectsInRealm:realm] count], 4U, @"Expecting 4 objects");
|
|
|
|
// remove from linkView
|
|
[realm beginWriteTransaction];
|
|
[realm deleteObjects:obj.array];
|
|
[realm commitWriteTransaction];
|
|
|
|
XCTAssertEqual([[StringObject allObjectsInRealm:realm] count], 1U, @"Expecting 1 object");
|
|
XCTAssertEqual(obj.array.count, 0U, @"Expecting 0 objects");
|
|
|
|
// remove NSArray
|
|
NSArray *arrayOfLastObject = @[[[StringObject allObjectsInRealm:realm] lastObject]];
|
|
[realm beginWriteTransaction];
|
|
[realm deleteObjects:arrayOfLastObject];
|
|
[realm commitWriteTransaction];
|
|
XCTAssertEqual(objects.count, 0U, @"Expecting 0 objects");
|
|
|
|
// add objects to linkView
|
|
[realm beginWriteTransaction];
|
|
[obj.array addObject:[StringObject createInRealm:realm withValue:@[@"a"]]];
|
|
[obj.array addObject:[[StringObject alloc] initWithValue:@[@"b"]]];
|
|
[realm commitWriteTransaction];
|
|
|
|
// remove objects from realm
|
|
XCTAssertEqual(obj.array.count, 2U, @"Expecting 2 objects");
|
|
[realm beginWriteTransaction];
|
|
[realm deleteObjects:[StringObject allObjectsInRealm:realm]];
|
|
[realm commitWriteTransaction];
|
|
XCTAssertEqual(obj.array.count, 0U, @"Expecting 0 objects");
|
|
}
|
|
|
|
- (void)testAddManagedObjectToOtherRealm {
|
|
RLMRealm *realm1 = [self realmWithTestPath];
|
|
RLMRealm *realm2 = [RLMRealm defaultRealm];
|
|
|
|
CircleObject *co1 = [[CircleObject alloc] init];
|
|
co1.data = @"1";
|
|
|
|
CircleObject *co2 = [[CircleObject alloc] init];
|
|
co2.data = @"2";
|
|
co2.next = co1;
|
|
|
|
CircleArrayObject *cao = [[CircleArrayObject alloc] init];
|
|
[cao.circles addObject:co1];
|
|
|
|
[realm1 transactionWithBlock:^{ [realm1 addObject:co1]; }];
|
|
|
|
[realm2 beginWriteTransaction];
|
|
XCTAssertThrows([realm2 addObject:co1], @"should reject already-managed object");
|
|
XCTAssertThrows([realm2 addObject:co2], @"should reject linked managed object");
|
|
XCTAssertThrows([realm2 addObject:cao], @"should reject array containing managed object");
|
|
[realm2 commitWriteTransaction];
|
|
|
|
// The objects are left in an odd state if validation fails (since the
|
|
// exception isn't supposed to be recoverable), so make new objects
|
|
co2 = [[CircleObject alloc] init];
|
|
co2.data = @"2";
|
|
co2.next = co1;
|
|
|
|
cao = [[CircleArrayObject alloc] init];
|
|
[cao.circles addObject:co1];
|
|
|
|
[realm1 beginWriteTransaction];
|
|
XCTAssertNoThrow([realm1 addObject:co2],
|
|
@"should be able to add object which links to object managed by target Realm");
|
|
XCTAssertNoThrow([realm1 addObject:cao],
|
|
@"should be able to add object with an array containing an object managed by target Realm");
|
|
[realm1 commitWriteTransaction];
|
|
}
|
|
|
|
- (void)testCopyObjectsBetweenRealms {
|
|
RLMRealm *realm1 = [self realmWithTestPath];
|
|
RLMRealm *realm2 = [RLMRealm defaultRealm];
|
|
|
|
StringObject *so = [[StringObject alloc] init];
|
|
so.stringCol = @"value";
|
|
|
|
[realm1 beginWriteTransaction];
|
|
[realm1 addObject:so];
|
|
[realm1 commitWriteTransaction];
|
|
|
|
XCTAssertEqual(1U, [StringObject allObjectsInRealm:realm1].count);
|
|
XCTAssertEqual(0U, [StringObject allObjectsInRealm:realm2].count);
|
|
XCTAssertEqualObjects(so.stringCol, @"value");
|
|
|
|
[realm2 beginWriteTransaction];
|
|
StringObject *so2 = [StringObject createInRealm:realm2 withValue:so];
|
|
[realm2 commitWriteTransaction];
|
|
|
|
XCTAssertEqual(1U, [StringObject allObjectsInRealm:realm1].count);
|
|
XCTAssertEqual(1U, [StringObject allObjectsInRealm:realm2].count);
|
|
XCTAssertEqualObjects(so2.stringCol, @"value");
|
|
}
|
|
|
|
- (void)testCopyArrayPropertyBetweenRealms {
|
|
RLMRealm *realm1 = [self realmWithTestPath];
|
|
RLMRealm *realm2 = [RLMRealm defaultRealm];
|
|
|
|
EmployeeObject *eo = [[EmployeeObject alloc] init];
|
|
eo.name = @"name";
|
|
eo.age = 50;
|
|
eo.hired = YES;
|
|
|
|
CompanyObject *co = [[CompanyObject alloc] init];
|
|
co.name = @"company name";
|
|
[co.employees addObject:eo];
|
|
|
|
[realm1 beginWriteTransaction];
|
|
[realm1 addObject:co];
|
|
[realm1 commitWriteTransaction];
|
|
|
|
XCTAssertEqual(1U, [EmployeeObject allObjectsInRealm:realm1].count);
|
|
XCTAssertEqual(1U, [CompanyObject allObjectsInRealm:realm1].count);
|
|
|
|
[realm2 beginWriteTransaction];
|
|
CompanyObject *co2 = [CompanyObject createInRealm:realm2 withValue:co];
|
|
[realm2 commitWriteTransaction];
|
|
|
|
XCTAssertEqual(1U, [EmployeeObject allObjectsInRealm:realm1].count);
|
|
XCTAssertEqual(1U, [CompanyObject allObjectsInRealm:realm1].count);
|
|
XCTAssertEqual(1U, [EmployeeObject allObjectsInRealm:realm2].count);
|
|
XCTAssertEqual(1U, [CompanyObject allObjectsInRealm:realm2].count);
|
|
|
|
XCTAssertEqualObjects(@"name", [co2.employees.firstObject name]);
|
|
}
|
|
|
|
- (void)testCopyLinksBetweenRealms {
|
|
RLMRealm *realm1 = [self realmWithTestPath];
|
|
RLMRealm *realm2 = [RLMRealm defaultRealm];
|
|
|
|
CircleObject *c = [[CircleObject alloc] init];
|
|
c.data = @"1";
|
|
c.next = [[CircleObject alloc] init];
|
|
c.next.data = @"2";
|
|
|
|
[realm1 beginWriteTransaction];
|
|
[realm1 addObject:c];
|
|
[realm1 commitWriteTransaction];
|
|
|
|
XCTAssertEqual(realm1, c.realm);
|
|
XCTAssertEqual(realm1, c.next.realm);
|
|
XCTAssertEqual(2U, [CircleObject allObjectsInRealm:realm1].count);
|
|
|
|
[realm2 beginWriteTransaction];
|
|
CircleObject *c2 = [CircleObject createInRealm:realm2 withValue:c];
|
|
[realm2 commitWriteTransaction];
|
|
|
|
XCTAssertEqualObjects(c2.data, @"1");
|
|
XCTAssertEqualObjects(c2.next.data, @"2");
|
|
|
|
XCTAssertEqual(2U, [CircleObject allObjectsInRealm:realm1].count);
|
|
XCTAssertEqual(2U, [CircleObject allObjectsInRealm:realm2].count);
|
|
}
|
|
|
|
- (void)testCopyObjectsInArrayLiteral {
|
|
RLMRealm *realm1 = [self realmWithTestPath];
|
|
RLMRealm *realm2 = [RLMRealm defaultRealm];
|
|
|
|
CircleObject *c = [[CircleObject alloc] init];
|
|
c.data = @"1";
|
|
|
|
[realm1 beginWriteTransaction];
|
|
[realm1 addObject:c];
|
|
[realm1 commitWriteTransaction];
|
|
|
|
[realm2 beginWriteTransaction];
|
|
CircleObject *c2 = [CircleObject createInRealm:realm2 withValue:@[@"3", @[@"2", c]]];
|
|
[realm2 commitWriteTransaction];
|
|
|
|
XCTAssertEqual(1U, [CircleObject allObjectsInRealm:realm1].count);
|
|
XCTAssertEqual(3U, [CircleObject allObjectsInRealm:realm2].count);
|
|
XCTAssertEqual(realm1, c.realm);
|
|
XCTAssertEqual(realm2, c2.realm);
|
|
|
|
XCTAssertEqualObjects(@"1", c.data);
|
|
XCTAssertEqualObjects(@"3", c2.data);
|
|
XCTAssertEqualObjects(@"2", c2.next.data);
|
|
XCTAssertEqualObjects(@"1", c2.next.next.data);
|
|
}
|
|
|
|
- (void)testAddOrUpdate {
|
|
RLMRealm *realm = [RLMRealm defaultRealm];
|
|
[realm beginWriteTransaction];
|
|
|
|
PrimaryStringObject *obj = [[PrimaryStringObject alloc] initWithValue:@[@"string", @1]];
|
|
[realm addOrUpdateObject:obj];
|
|
RLMResults *objects = [PrimaryStringObject allObjects];
|
|
XCTAssertEqual([objects count], 1U, @"Should have 1 object");
|
|
XCTAssertEqual([(PrimaryStringObject *)objects[0] intCol], 1, @"Value should be 1");
|
|
|
|
PrimaryStringObject *obj2 = [[PrimaryStringObject alloc] initWithValue:@[@"string2", @2]];
|
|
[realm addOrUpdateObject:obj2];
|
|
XCTAssertEqual([objects count], 2U, @"Should have 2 objects");
|
|
|
|
// upsert with new secondary property
|
|
PrimaryStringObject *obj3 = [[PrimaryStringObject alloc] initWithValue:@[@"string", @3]];
|
|
[realm addOrUpdateObject:obj3];
|
|
XCTAssertEqual([objects count], 2U, @"Should have 2 objects");
|
|
XCTAssertEqual([(PrimaryStringObject *)objects[0] intCol], 3, @"Value should be 3");
|
|
|
|
// upsert on non-primary key object should throw
|
|
XCTAssertThrows([realm addOrUpdateObject:[[StringObject alloc] initWithValue:@[@"string"]]]);
|
|
|
|
[realm commitWriteTransaction];
|
|
}
|
|
|
|
- (void)testAddOrUpdateObjectsFromArray {
|
|
RLMRealm *realm = [RLMRealm defaultRealm];
|
|
[realm beginWriteTransaction];
|
|
|
|
PrimaryStringObject *obj = [[PrimaryStringObject alloc] initWithValue:@[@"string1", @1]];
|
|
[realm addObject:obj];
|
|
|
|
PrimaryStringObject *obj2 = [[PrimaryStringObject alloc] initWithValue:@[@"string2", @2]];
|
|
[realm addObject:obj2];
|
|
|
|
PrimaryStringObject *obj3 = [[PrimaryStringObject alloc] initWithValue:@[@"string3", @3]];
|
|
[realm addObject:obj3];
|
|
|
|
RLMResults *objects = [PrimaryStringObject allObjects];
|
|
XCTAssertEqual([objects count], 3U, @"Should have 3 object");
|
|
XCTAssertEqual([(PrimaryStringObject *)objects[0] intCol], 1, @"Value should be 1");
|
|
XCTAssertEqual([(PrimaryStringObject *)objects[1] intCol], 2, @"Value should be 2");
|
|
XCTAssertEqual([(PrimaryStringObject *)objects[2] intCol], 3, @"Value should be 3");
|
|
|
|
// upsert with array of 2 objects. One is to update the existing value, another is added
|
|
NSArray *array = @[[[PrimaryStringObject alloc] initWithValue:@[@"string2", @4]],
|
|
[[PrimaryStringObject alloc] initWithValue:@[@"string4", @5]]];
|
|
[realm addOrUpdateObjects:array];
|
|
XCTAssertEqual([objects count], 4U, @"Should have 4 objects");
|
|
XCTAssertEqual([(PrimaryStringObject *)objects[0] intCol], 1, @"Value should be 1");
|
|
XCTAssertEqual([(PrimaryStringObject *)objects[1] intCol], 4, @"Value should be 4");
|
|
XCTAssertEqual([(PrimaryStringObject *)objects[2] intCol], 3, @"Value should be 3");
|
|
XCTAssertEqual([(PrimaryStringObject *)objects[3] intCol], 5, @"Value should be 5");
|
|
|
|
[realm commitWriteTransaction];
|
|
}
|
|
|
|
- (void)testDelete {
|
|
RLMRealm *realm = [RLMRealm defaultRealm];
|
|
|
|
[realm beginWriteTransaction];
|
|
OwnerObject *obj = [OwnerObject createInDefaultRealmWithValue:@[@"deeter", @[@"barney", @2]]];
|
|
[realm commitWriteTransaction];
|
|
|
|
XCTAssertEqual(1U, OwnerObject.allObjects.count);
|
|
XCTAssertEqual(NO, obj.invalidated);
|
|
|
|
XCTAssertThrows([realm deleteObject:obj]);
|
|
|
|
RLMRealm *testRealm = [self realmWithTestPath];
|
|
[testRealm transactionWithBlock:^{
|
|
XCTAssertThrows([testRealm deleteObject:[[OwnerObject alloc] init]]);
|
|
[realm transactionWithBlock:^{
|
|
XCTAssertThrows([testRealm deleteObject:obj]);
|
|
}];
|
|
}];
|
|
|
|
[realm transactionWithBlock:^{
|
|
[realm deleteObject:obj];
|
|
XCTAssertEqual(YES, obj.invalidated);
|
|
}];
|
|
|
|
XCTAssertEqual(0U, OwnerObject.allObjects.count);
|
|
}
|
|
|
|
- (void)testDeleteObjects {
|
|
RLMRealm *realm = [RLMRealm defaultRealm];
|
|
|
|
[realm beginWriteTransaction];
|
|
CompanyObject *obj = [CompanyObject createInDefaultRealmWithValue:@[@"deeter", @[@[@"barney", @2, @YES]]]];
|
|
NSArray *objects = @[obj];
|
|
[realm commitWriteTransaction];
|
|
|
|
XCTAssertEqual(1U, CompanyObject.allObjects.count);
|
|
|
|
XCTAssertThrows([realm deleteObjects:objects]);
|
|
XCTAssertThrows([realm deleteObjects:[CompanyObject allObjectsInRealm:realm]]);
|
|
XCTAssertThrows([realm deleteObjects:obj.employees]);
|
|
|
|
RLMRealm *testRealm = [self realmWithTestPath];
|
|
[testRealm transactionWithBlock:^{
|
|
[realm transactionWithBlock:^{
|
|
XCTAssertThrows([testRealm deleteObjects:objects]);
|
|
XCTAssertThrows([testRealm deleteObjects:[CompanyObject allObjectsInRealm:realm]]);
|
|
XCTAssertThrows([testRealm deleteObjects:obj.employees]);
|
|
}];
|
|
}];
|
|
|
|
XCTAssertEqual(1U, CompanyObject.allObjects.count);
|
|
}
|
|
|
|
- (void)testDeleteAllObjects {
|
|
RLMRealm *realm = [RLMRealm defaultRealm];
|
|
|
|
[realm beginWriteTransaction];
|
|
OwnerObject *obj = [OwnerObject createInDefaultRealmWithValue:@[@"deeter", @[@"barney", @2]]];
|
|
[realm commitWriteTransaction];
|
|
|
|
XCTAssertEqual(1U, OwnerObject.allObjects.count);
|
|
XCTAssertEqual(1U, DogObject.allObjects.count);
|
|
XCTAssertEqual(NO, obj.invalidated);
|
|
|
|
XCTAssertThrows([realm deleteAllObjects]);
|
|
|
|
[realm transactionWithBlock:^{
|
|
[realm deleteAllObjects];
|
|
XCTAssertEqual(YES, obj.invalidated);
|
|
}];
|
|
|
|
XCTAssertEqual(0U, OwnerObject.allObjects.count);
|
|
XCTAssertEqual(0U, DogObject.allObjects.count);
|
|
}
|
|
|
|
- (void)testAddObjectsFromArray
|
|
{
|
|
RLMRealm *realm = [self realmWithTestPath];
|
|
|
|
[realm beginWriteTransaction];
|
|
XCTAssertThrows(([realm addObjects:@[@[@"Rex", @10]]]),
|
|
@"should reject non-RLMObject in array");
|
|
|
|
DogObject *dog = [DogObject new];
|
|
dog.dogName = @"Rex";
|
|
dog.age = 10;
|
|
XCTAssertNoThrow([realm addObjects:@[dog]], @"should allow RLMObject in array");
|
|
XCTAssertEqual(1U, [[DogObject allObjectsInRealm:realm] count]);
|
|
[realm cancelWriteTransaction];
|
|
}
|
|
|
|
#pragma mark - Transactions
|
|
|
|
- (void)testRealmTransactionBlock {
|
|
RLMRealm *realm = [self realmWithTestPath];
|
|
[realm transactionWithBlock:^{
|
|
[StringObject createInRealm:realm withValue:@[@"b"]];
|
|
}];
|
|
RLMResults *objects = [StringObject allObjectsInRealm:realm];
|
|
XCTAssertEqual(objects.count, 1U, @"Expecting 1 object");
|
|
XCTAssertEqualObjects([objects.firstObject stringCol], @"b", @"Expecting column to be 'b'");
|
|
}
|
|
|
|
- (void)testInWriteTransaction {
|
|
RLMRealm *realm = [self realmWithTestPath];
|
|
XCTAssertFalse(realm.inWriteTransaction);
|
|
[realm beginWriteTransaction];
|
|
XCTAssertTrue(realm.inWriteTransaction);
|
|
[realm cancelWriteTransaction];
|
|
[realm transactionWithBlock:^{
|
|
XCTAssertTrue(realm.inWriteTransaction);
|
|
[realm cancelWriteTransaction];
|
|
XCTAssertFalse(realm.inWriteTransaction);
|
|
}];
|
|
|
|
[realm beginWriteTransaction];
|
|
[realm invalidate];
|
|
XCTAssertFalse(realm.inWriteTransaction);
|
|
}
|
|
|
|
- (void)testAutorefreshAfterBackgroundUpdate {
|
|
RLMRealm *realm = [self realmWithTestPath];
|
|
|
|
XCTAssertEqual(0U, [StringObject allObjectsInRealm:realm].count);
|
|
|
|
[self waitForNotification:RLMRealmDidChangeNotification realm:realm block:^{
|
|
RLMRealm *realm = [self realmWithTestPath];
|
|
[realm beginWriteTransaction];
|
|
[StringObject createInRealm:realm withValue:@[@"string"]];
|
|
[realm commitWriteTransaction];
|
|
}];
|
|
|
|
XCTAssertEqual(1U, [StringObject allObjectsInRealm:realm].count);
|
|
}
|
|
|
|
- (void)testBackgroundUpdateWithoutAutorefresh {
|
|
RLMRealm *realm = [self realmWithTestPath];
|
|
realm.autorefresh = NO;
|
|
|
|
XCTAssertEqual(0U, [StringObject allObjectsInRealm:realm].count);
|
|
|
|
[self waitForNotification:RLMRealmRefreshRequiredNotification realm:realm block:^{
|
|
RLMRealm *realm = [self realmWithTestPath];
|
|
[realm beginWriteTransaction];
|
|
[StringObject createInRealm:realm withValue:@[@"string"]];
|
|
[realm commitWriteTransaction];
|
|
|
|
XCTAssertEqual(1U, [StringObject allObjectsInRealm:realm].count);
|
|
}];
|
|
|
|
XCTAssertEqual(0U, [StringObject allObjectsInRealm:realm].count);
|
|
|
|
[realm refresh];
|
|
XCTAssertEqual(1U, [StringObject allObjectsInRealm:realm].count);
|
|
}
|
|
|
|
- (void)testBeginWriteTransactionsNotifiesWithUpdatedObjects {
|
|
RLMRealm *realm = [self realmWithTestPath];
|
|
realm.autorefresh = NO;
|
|
|
|
XCTAssertEqual(0U, [StringObject allObjectsInRealm:realm].count);
|
|
|
|
// Create an object in a background thread and wait for that to complete,
|
|
// without refreshing the main thread realm
|
|
[self waitForNotification:RLMRealmRefreshRequiredNotification realm:realm block:^{
|
|
RLMRealm *realm = [self realmWithTestPath];
|
|
[realm beginWriteTransaction];
|
|
[StringObject createInRealm:realm withValue:@[@"string"]];
|
|
[realm commitWriteTransaction];
|
|
|
|
XCTAssertEqual(1U, [StringObject allObjectsInRealm:realm].count);
|
|
}];
|
|
|
|
// Verify that the main thread realm still doesn't have any objects
|
|
XCTAssertEqual(0U, [StringObject allObjectsInRealm:realm].count);
|
|
|
|
// Verify that the local notification sent by the beginWriteTransaction
|
|
// below when it advances the realm to the latest version occurs *after*
|
|
// the advance
|
|
__block bool notificationFired = false;
|
|
RLMNotificationToken *token = [realm addNotificationBlock:^(__unused NSString *note, RLMRealm *realm) {
|
|
XCTAssertEqual(1U, [StringObject allObjectsInRealm:realm].count);
|
|
notificationFired = true;
|
|
}];
|
|
|
|
[realm beginWriteTransaction];
|
|
[realm commitWriteTransaction];
|
|
|
|
[token invalidate];
|
|
XCTAssertTrue(notificationFired);
|
|
}
|
|
|
|
- (void)testBeginWriteTransactionsRefreshesRealm {
|
|
// auto refresh on by default
|
|
RLMRealm *realm = [self realmWithTestPath];
|
|
|
|
// Set up notification which will be triggered when calling beginWriteTransaction
|
|
__block bool notificationFired = false;
|
|
RLMNotificationToken *token = [realm addNotificationBlock:^(__unused NSString *note, RLMRealm *realm) {
|
|
XCTAssertEqual(1U, [StringObject allObjectsInRealm:realm].count);
|
|
XCTAssertThrows([realm beginWriteTransaction], @"We should already be in a write transaction");
|
|
notificationFired = true;
|
|
}];
|
|
|
|
// dispatch to background syncronously
|
|
[self dispatchAsyncAndWait:^{
|
|
RLMRealm *realm = [self realmWithTestPath];
|
|
[realm beginWriteTransaction];
|
|
[StringObject createInRealm:realm withValue:@[@"string"]];
|
|
[realm commitWriteTransaction];
|
|
}];
|
|
|
|
// notification shouldnt have fired
|
|
XCTAssertFalse(notificationFired);
|
|
|
|
[realm beginWriteTransaction];
|
|
|
|
// notification should have fired
|
|
XCTAssertTrue(notificationFired);
|
|
|
|
[realm cancelWriteTransaction];
|
|
[token invalidate];
|
|
}
|
|
|
|
- (void)testBeginWriteTransactionFromWithinRefreshRequiredNotification {
|
|
RLMRealm *realm = [RLMRealm defaultRealm];
|
|
realm.autorefresh = NO;
|
|
|
|
auto expectation = [self expectationWithDescription:@""];
|
|
RLMNotificationToken *token = [realm addNotificationBlock:^(NSString *note, RLMRealm *realm) {
|
|
XCTAssertEqual(RLMRealmRefreshRequiredNotification, note);
|
|
XCTAssertEqual(0U, [StringObject allObjectsInRealm:realm].count);
|
|
[realm beginWriteTransaction];
|
|
XCTAssertEqual(1U, [StringObject allObjectsInRealm:realm].count);
|
|
[realm cancelWriteTransaction];
|
|
[expectation fulfill]; // note that this will throw if the notification is incorrectly called twice
|
|
}];
|
|
|
|
[self dispatchAsyncAndWait:^{
|
|
RLMRealm *realm = [RLMRealm defaultRealm];
|
|
[realm beginWriteTransaction];
|
|
[StringObject createInRealm:realm withValue:@[@"string"]];
|
|
[realm commitWriteTransaction];
|
|
}];
|
|
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
[token invalidate];
|
|
}
|
|
|
|
- (void)testBeginWriteTransactionFromWithinRealmChangedNotification {
|
|
RLMRealm *realm = [RLMRealm defaultRealm];
|
|
|
|
auto createObject = ^{
|
|
[self dispatchAsyncAndWait:^{
|
|
RLMRealm *realm = [RLMRealm defaultRealm];
|
|
[realm beginWriteTransaction];
|
|
[StringObject createInRealm:realm withValue:@[@"string"]];
|
|
[realm commitWriteTransaction];
|
|
}];
|
|
};
|
|
|
|
// Test with the triggering transaction on a different thread
|
|
auto expectation = [self expectationWithDescription:@""];
|
|
RLMNotificationToken *token = [realm addNotificationBlock:^(NSString *note, RLMRealm *realm) {
|
|
XCTAssertEqual(RLMRealmDidChangeNotification, note);
|
|
|
|
// We're in DidChange, so the first object is already present
|
|
XCTAssertEqual(1U, [StringObject allObjectsInRealm:realm].count);
|
|
createObject();
|
|
|
|
// Haven't refreshed yet, so still one
|
|
XCTAssertEqual(1U, [StringObject allObjectsInRealm:realm].count);
|
|
|
|
// Refreshes without sending notifications since we're within a notification
|
|
[realm beginWriteTransaction];
|
|
XCTAssertEqual(2U, [StringObject allObjectsInRealm:realm].count);
|
|
[realm cancelWriteTransaction];
|
|
[expectation fulfill]; // note that this will throw if the notification is incorrectly called twice
|
|
}];
|
|
|
|
createObject();
|
|
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
[token invalidate];
|
|
|
|
// Test with the triggering transaction on the same thread
|
|
__block bool first = true;
|
|
token = [realm addNotificationBlock:^(NSString *note, RLMRealm *realm) {
|
|
XCTAssertTrue(first);
|
|
XCTAssertEqual(RLMRealmDidChangeNotification, note);
|
|
XCTAssertEqual(3U, [StringObject allObjectsInRealm:realm].count);
|
|
first = false;
|
|
|
|
[realm beginWriteTransaction]; // should not trigger a notification
|
|
[StringObject createInRealm:realm withValue:@[@"string"]];
|
|
[realm commitWriteTransaction]; // also should not trigger a notification
|
|
}];
|
|
|
|
[realm beginWriteTransaction];
|
|
[StringObject createInRealm:realm withValue:@[@"string"]];
|
|
[realm commitWriteTransaction];
|
|
|
|
XCTAssertFalse(first);
|
|
[token invalidate];
|
|
}
|
|
|
|
- (void)testBeginWriteTransactionFromWithinCollectionChangedNotification {
|
|
RLMRealm *realm = [RLMRealm defaultRealm];
|
|
|
|
auto createObject = ^{
|
|
[self dispatchAsyncAndWait:^{
|
|
RLMRealm *realm = [RLMRealm defaultRealm];
|
|
[realm beginWriteTransaction];
|
|
[StringObject createInRealm:realm withValue:@[@"string"]];
|
|
[realm commitWriteTransaction];
|
|
}];
|
|
};
|
|
|
|
__block auto expectation = [self expectationWithDescription:@""];
|
|
__block RLMNotificationToken *token;
|
|
auto block = ^(RLMResults *results, RLMCollectionChange *changes, NSError *) {
|
|
if (!changes) {
|
|
[expectation fulfill];
|
|
return;
|
|
}
|
|
|
|
XCTAssertEqual(1U, results.count);
|
|
createObject();
|
|
XCTAssertEqual(1U, results.count);
|
|
[realm beginWriteTransaction];
|
|
XCTAssertEqual(2U, results.count);
|
|
[realm cancelWriteTransaction];
|
|
[expectation fulfill];
|
|
[token invalidate];
|
|
};
|
|
token = [StringObject.allObjects addNotificationBlock:block];
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
|
|
createObject();
|
|
expectation = [self expectationWithDescription:@""];
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
}
|
|
|
|
- (void)testReadOnlyRealmIsImmutable
|
|
{
|
|
@autoreleasepool { [self realmWithTestPath]; }
|
|
|
|
RLMRealm *realm = [self readOnlyRealmWithURL:RLMTestRealmURL() error:nil];
|
|
XCTAssertThrows([realm beginWriteTransaction]);
|
|
XCTAssertThrows([realm refresh]);
|
|
}
|
|
|
|
- (void)testRollbackInsert
|
|
{
|
|
RLMRealm *realm = [self realmWithTestPath];
|
|
|
|
[realm beginWriteTransaction];
|
|
IntObject *createdObject = [IntObject createInRealm:realm withValue:@[@0]];
|
|
[realm cancelWriteTransaction];
|
|
|
|
XCTAssertTrue(createdObject.isInvalidated);
|
|
XCTAssertEqual(0U, [IntObject allObjectsInRealm:realm].count);
|
|
}
|
|
|
|
- (void)testRollbackDelete
|
|
{
|
|
RLMRealm *realm = [self realmWithTestPath];
|
|
|
|
[realm beginWriteTransaction];
|
|
IntObject *objectToDelete = [IntObject createInRealm:realm withValue:@[@0]];
|
|
[realm commitWriteTransaction];
|
|
|
|
[realm beginWriteTransaction];
|
|
[realm deleteObject:objectToDelete];
|
|
[realm cancelWriteTransaction];
|
|
|
|
XCTAssertTrue(objectToDelete.isInvalidated);
|
|
XCTAssertEqual(1U, [IntObject allObjectsInRealm:realm].count);
|
|
}
|
|
|
|
- (void)testRollbackModify
|
|
{
|
|
RLMRealm *realm = [self realmWithTestPath];
|
|
|
|
[realm beginWriteTransaction];
|
|
IntObject *objectToModify = [IntObject createInRealm:realm withValue:@[@0]];
|
|
[realm commitWriteTransaction];
|
|
|
|
[realm beginWriteTransaction];
|
|
objectToModify.intCol = 1;
|
|
[realm cancelWriteTransaction];
|
|
|
|
XCTAssertEqual(0, objectToModify.intCol);
|
|
}
|
|
|
|
- (void)testRollbackLink
|
|
{
|
|
RLMRealm *realm = [self realmWithTestPath];
|
|
|
|
[realm beginWriteTransaction];
|
|
CircleObject *obj1 = [CircleObject createInRealm:realm withValue:@[@"1", NSNull.null]];
|
|
CircleObject *obj2 = [CircleObject createInRealm:realm withValue:@[@"2", NSNull.null]];
|
|
[realm commitWriteTransaction];
|
|
|
|
// Link to existing managed
|
|
[realm beginWriteTransaction];
|
|
obj1.next = obj2;
|
|
[realm cancelWriteTransaction];
|
|
|
|
XCTAssertNil(obj1.next);
|
|
|
|
// Link to unmanaged
|
|
[realm beginWriteTransaction];
|
|
CircleObject *obj3 = [[CircleObject alloc] init];
|
|
obj3.data = @"3";
|
|
obj1.next = obj3;
|
|
[realm cancelWriteTransaction];
|
|
|
|
XCTAssertNil(obj1.next);
|
|
XCTAssertEqual(2U, [CircleObject allObjectsInRealm:realm].count);
|
|
|
|
// Remove link
|
|
[realm beginWriteTransaction];
|
|
obj1.next = obj2;
|
|
[realm commitWriteTransaction];
|
|
|
|
[realm beginWriteTransaction];
|
|
obj1.next = nil;
|
|
[realm cancelWriteTransaction];
|
|
|
|
XCTAssertTrue([obj1.next isEqualToObject:obj2]);
|
|
|
|
// Modify link
|
|
[realm beginWriteTransaction];
|
|
CircleObject *obj4 = [CircleObject createInRealm:realm withValue:@[@"4", NSNull.null]];
|
|
[realm commitWriteTransaction];
|
|
|
|
[realm beginWriteTransaction];
|
|
obj1.next = obj4;
|
|
[realm cancelWriteTransaction];
|
|
|
|
XCTAssertTrue([obj1.next isEqualToObject:obj2]);
|
|
}
|
|
|
|
- (void)testRollbackLinkList
|
|
{
|
|
RLMRealm *realm = [self realmWithTestPath];
|
|
|
|
[realm beginWriteTransaction];
|
|
IntObject *obj1 = [IntObject createInRealm:realm withValue:@[@0]];
|
|
IntObject *obj2 = [IntObject createInRealm:realm withValue:@[@1]];
|
|
ArrayPropertyObject *array = [ArrayPropertyObject createInRealm:realm withValue:@[@"", @[], @[obj1]]];
|
|
[realm commitWriteTransaction];
|
|
|
|
// Add existing managed object
|
|
[realm beginWriteTransaction];
|
|
[array.intArray addObject:obj2];
|
|
[realm cancelWriteTransaction];
|
|
|
|
XCTAssertEqual(1U, array.intArray.count);
|
|
|
|
// Add unmanaged object
|
|
[realm beginWriteTransaction];
|
|
[array.intArray addObject:[[IntObject alloc] init]];
|
|
[realm cancelWriteTransaction];
|
|
|
|
XCTAssertEqual(1U, array.intArray.count);
|
|
XCTAssertEqual(2U, [IntObject allObjectsInRealm:realm].count);
|
|
|
|
// Remove
|
|
[realm beginWriteTransaction];
|
|
[array.intArray removeObjectAtIndex:0];
|
|
[realm cancelWriteTransaction];
|
|
|
|
XCTAssertEqual(1U, array.intArray.count);
|
|
|
|
// Modify
|
|
[realm beginWriteTransaction];
|
|
array.intArray[0] = obj2;
|
|
[realm cancelWriteTransaction];
|
|
|
|
XCTAssertEqual(1U, array.intArray.count);
|
|
XCTAssertTrue([array.intArray[0] isEqualToObject:obj1]);
|
|
}
|
|
|
|
- (void)testRollbackTransactionWithBlock
|
|
{
|
|
RLMRealm *realm = [self realmWithTestPath];
|
|
[realm transactionWithBlock:^{
|
|
[IntObject createInRealm:realm withValue:@[@0]];
|
|
[realm cancelWriteTransaction];
|
|
}];
|
|
|
|
XCTAssertEqual(0U, [IntObject allObjectsInRealm:realm].count);
|
|
}
|
|
|
|
- (void)testRollbackTransactionWithoutExplicitCommitOrCancel
|
|
{
|
|
@autoreleasepool {
|
|
RLMRealm *realm = [self realmWithTestPath];
|
|
[realm beginWriteTransaction];
|
|
[IntObject createInRealm:realm withValue:@[@0]];
|
|
}
|
|
|
|
XCTAssertEqual(0U, [IntObject allObjectsInRealm:[self realmWithTestPath]].count);
|
|
}
|
|
|
|
- (void)testCanRestartReadTransactionAfterInvalidate
|
|
{
|
|
RLMRealm *realm = [RLMRealm defaultRealm];
|
|
[realm transactionWithBlock:^{
|
|
[IntObject createInRealm:realm withValue:@[@1]];
|
|
}];
|
|
|
|
[realm invalidate];
|
|
IntObject *obj = [IntObject allObjectsInRealm:realm].firstObject;
|
|
XCTAssertEqual(obj.intCol, 1);
|
|
}
|
|
|
|
- (void)testInvalidateDetachesAccessors
|
|
{
|
|
RLMRealm *realm = [RLMRealm defaultRealm];
|
|
__block IntObject *obj;
|
|
[realm transactionWithBlock:^{
|
|
obj = [IntObject createInRealm:realm withValue:@[@0]];
|
|
}];
|
|
|
|
[realm invalidate];
|
|
XCTAssertTrue(obj.isInvalidated);
|
|
XCTAssertThrows([obj intCol]);
|
|
}
|
|
|
|
- (void)testInvalidateInvalidatesResults
|
|
{
|
|
RLMRealm *realm = [RLMRealm defaultRealm];
|
|
[realm transactionWithBlock:^{
|
|
[IntObject createInRealm:realm withValue:@[@1]];
|
|
}];
|
|
|
|
RLMResults *results = [IntObject objectsInRealm:realm where:@"intCol = 1"];
|
|
XCTAssertEqual([results.firstObject intCol], 1);
|
|
|
|
[realm invalidate];
|
|
XCTAssertThrows([results count]);
|
|
XCTAssertThrows([results firstObject]);
|
|
}
|
|
|
|
- (void)testInvalidateInvalidatesArrays
|
|
{
|
|
RLMRealm *realm = [RLMRealm defaultRealm];
|
|
__block ArrayPropertyObject *arrayObject;
|
|
[realm transactionWithBlock:^{
|
|
arrayObject = [ArrayPropertyObject createInRealm:realm withValue:@[@"", @[], @[@[@1]]]];
|
|
}];
|
|
|
|
RLMArray *array = arrayObject.intArray;
|
|
XCTAssertEqual(1U, array.count);
|
|
|
|
[realm invalidate];
|
|
XCTAssertThrows([array count]);
|
|
}
|
|
|
|
- (void)testInvalidateOnReadOnlyRealmIsError
|
|
{
|
|
@autoreleasepool {
|
|
// Create the file
|
|
[self realmWithTestPath];
|
|
}
|
|
RLMRealm *realm = [self readOnlyRealmWithURL:RLMTestRealmURL() error:nil];
|
|
XCTAssertThrows([realm invalidate]);
|
|
}
|
|
|
|
- (void)testInvalidateBeforeReadDoesNotAssert
|
|
{
|
|
RLMRealm *realm = [RLMRealm defaultRealm];
|
|
[realm invalidate];
|
|
}
|
|
|
|
- (void)testInvalidateDuringWriteRollsBack
|
|
{
|
|
RLMRealm *realm = [RLMRealm defaultRealm];
|
|
[realm beginWriteTransaction];
|
|
@autoreleasepool {
|
|
[IntObject createInRealm:realm withValue:@[@1]];
|
|
}
|
|
[realm invalidate];
|
|
|
|
XCTAssertEqual(0U, [IntObject allObjectsInRealm:realm].count);
|
|
}
|
|
|
|
- (void)testRefreshCreatesAReadTransaction
|
|
{
|
|
RLMRealm *realm = [RLMRealm defaultRealm];
|
|
|
|
[self dispatchAsyncAndWait:^{
|
|
[RLMRealm.defaultRealm transactionWithBlock:^{
|
|
[IntObject createInDefaultRealmWithValue:@[@1]];
|
|
}];
|
|
}];
|
|
|
|
XCTAssertTrue([realm refresh]);
|
|
|
|
[self dispatchAsyncAndWait:^{
|
|
[RLMRealm.defaultRealm transactionWithBlock:^{
|
|
[IntObject createInDefaultRealmWithValue:@[@1]];
|
|
}];
|
|
}];
|
|
|
|
// refresh above should have created a read transaction, so realm should
|
|
// still only see one object
|
|
XCTAssertEqual(1U, [IntObject allObjects].count);
|
|
|
|
// Just a sanity check
|
|
XCTAssertTrue([realm refresh]);
|
|
XCTAssertEqual(2U, [IntObject allObjects].count);
|
|
}
|
|
|
|
- (void)testInWriteTransactionInNotificationFromBeginWrite {
|
|
RLMRealm *realm = RLMRealm.defaultRealm;
|
|
realm.autorefresh = NO;
|
|
|
|
__block bool called = false;
|
|
RLMNotificationToken *token = [realm addNotificationBlock:^(NSString *note, RLMRealm *realm) {
|
|
if (note == RLMRealmDidChangeNotification) {
|
|
called = true;
|
|
XCTAssertTrue(realm.inWriteTransaction);
|
|
}
|
|
}];
|
|
|
|
[self waitForNotification:RLMRealmRefreshRequiredNotification realm:realm block:^{
|
|
[RLMRealm.defaultRealm transactionWithBlock:^{ }];
|
|
}];
|
|
|
|
[realm beginWriteTransaction];
|
|
XCTAssertTrue(called);
|
|
[realm cancelWriteTransaction];
|
|
[token invalidate];
|
|
}
|
|
|
|
- (void)testThrowingFromDidChangeNotificationFromBeginWriteCancelsTransaction {
|
|
RLMRealm *realm = RLMRealm.defaultRealm;
|
|
realm.autorefresh = NO;
|
|
|
|
RLMNotificationToken *token = [realm addNotificationBlock:^(NSString *note, RLMRealm *) {
|
|
if (note == RLMRealmDidChangeNotification) {
|
|
throw 0;
|
|
}
|
|
}];
|
|
|
|
[self waitForNotification:RLMRealmRefreshRequiredNotification realm:realm block:^{
|
|
[RLMRealm.defaultRealm transactionWithBlock:^{ }];
|
|
}];
|
|
|
|
try {
|
|
[realm beginWriteTransaction];
|
|
XCTFail(@"should have thrown");
|
|
}
|
|
catch (int) { }
|
|
[token invalidate];
|
|
|
|
XCTAssertFalse(realm.inWriteTransaction);
|
|
XCTAssertNoThrow([realm beginWriteTransaction]);
|
|
[realm cancelWriteTransaction];
|
|
}
|
|
|
|
- (void)testThrowingFromDidChangeNotificationAfterLocalCommit {
|
|
RLMRealm *realm = RLMRealm.defaultRealm;
|
|
realm.autorefresh = NO;
|
|
|
|
RLMNotificationToken *token = [realm addNotificationBlock:^(NSString *note, RLMRealm *) {
|
|
if (note == RLMRealmDidChangeNotification) {
|
|
throw 0;
|
|
}
|
|
}];
|
|
|
|
[realm beginWriteTransaction];
|
|
try {
|
|
[realm commitWriteTransaction];
|
|
XCTFail(@"should have thrown");
|
|
}
|
|
catch (int) { }
|
|
[token invalidate];
|
|
|
|
XCTAssertFalse(realm.inWriteTransaction);
|
|
XCTAssertNoThrow([realm beginWriteTransaction]);
|
|
[realm cancelWriteTransaction];
|
|
}
|
|
|
|
- (void)testNotificationsFireEvenWithoutReadTransaction {
|
|
RLMRealm *realm = RLMRealm.defaultRealm;
|
|
|
|
XCTestExpectation *notificationFired = [self expectationWithDescription:@"notification fired"];
|
|
__block RLMNotificationToken *token = [realm addNotificationBlock:^(NSString *note, RLMRealm *) {
|
|
if (note == RLMRealmDidChangeNotification) {
|
|
[notificationFired fulfill];
|
|
[token invalidate];
|
|
}
|
|
}];
|
|
|
|
[realm invalidate];
|
|
[self dispatchAsync:^{
|
|
[RLMRealm.defaultRealm transactionWithBlock:^{ }];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
}
|
|
|
|
- (void)testNotificationBlockMustNotBeNil {
|
|
RLMRealm *realm = RLMRealm.defaultRealm;
|
|
XCTAssertThrows([realm addNotificationBlock:self.nonLiteralNil]);
|
|
}
|
|
|
|
- (void)testRefreshInWriteTransactionReturnsFalse {
|
|
RLMRealm *realm = RLMRealm.defaultRealm;
|
|
[realm beginWriteTransaction];
|
|
[IntObject createInRealm:realm withValue:@[@0]];
|
|
XCTAssertFalse([realm refresh]);
|
|
[realm cancelWriteTransaction];
|
|
}
|
|
|
|
- (void)testCancelWriteWhenNotInWrite {
|
|
XCTAssertThrows([RLMRealm.defaultRealm cancelWriteTransaction]);
|
|
}
|
|
|
|
#pragma mark - Threads
|
|
|
|
- (void)testCrossThreadAccess
|
|
{
|
|
RLMRealm *realm = RLMRealm.defaultRealm;
|
|
|
|
[self dispatchAsyncAndWait:^{
|
|
XCTAssertThrows([realm beginWriteTransaction]);
|
|
XCTAssertThrows([IntObject allObjectsInRealm:realm]);
|
|
XCTAssertThrows([IntObject objectsInRealm:realm where:@"intCol = 0"]);
|
|
}];
|
|
}
|
|
|
|
- (void)testHoldRealmAfterSourceThreadIsDestroyed {
|
|
RLMRealm *realm;
|
|
|
|
// Explicitly create a thread so that we can ensure the thread (and thus
|
|
// runloop) is actually destroyed
|
|
std::thread([&] { realm = [RLMRealm defaultRealm]; }).join();
|
|
|
|
[realm.configuration fileURL]; // ensure ARC releases the object after the thread has finished
|
|
}
|
|
|
|
- (void)testBackgroundRealmIsNotified {
|
|
RLMRealm *realm = [self realmWithTestPath];
|
|
|
|
XCTestExpectation *bgReady = [self expectationWithDescription:@"background queue waiting for commit"];
|
|
__block XCTestExpectation *bgDone = nil;
|
|
|
|
[self dispatchAsync:^{
|
|
RLMRealm *realm = [self realmWithTestPath];
|
|
__block bool fulfilled = false;
|
|
|
|
CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, ^{
|
|
__block RLMNotificationToken *token = [realm addNotificationBlock:^(NSString *note, RLMRealm *realm) {
|
|
XCTAssertNotNil(realm, @"Realm should not be nil");
|
|
XCTAssertEqual(note, RLMRealmDidChangeNotification);
|
|
XCTAssertEqual(1U, [StringObject allObjectsInRealm:realm].count);
|
|
fulfilled = true;
|
|
[token invalidate];
|
|
}];
|
|
|
|
// notify main thread that we're ready for it to commit
|
|
[bgReady fulfill];
|
|
});
|
|
|
|
// run for two seconds or until we receive notification
|
|
NSDate *end = [NSDate dateWithTimeIntervalSinceNow:5.0];
|
|
while (!fulfilled) {
|
|
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:end];
|
|
}
|
|
XCTAssertTrue(fulfilled, @"Notification should have been received");
|
|
|
|
[bgDone fulfill];
|
|
}];
|
|
|
|
// wait for background realm to be created
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
bgDone = [self expectationWithDescription:@"background queue done"];;
|
|
|
|
[realm beginWriteTransaction];
|
|
[StringObject createInRealm:realm withValue:@[@"string"]];
|
|
[realm commitWriteTransaction];
|
|
|
|
[self waitForExpectationsWithTimeout:2.0 handler:nil];
|
|
}
|
|
|
|
- (void)testAddingNotificationOutsideOfRunLoopIsAnError {
|
|
[self dispatchAsyncAndWait:^{
|
|
RLMRealm *realm = RLMRealm.defaultRealm;
|
|
XCTAssertThrows([realm addNotificationBlock:^(NSString *, RLMRealm *) { }]);
|
|
|
|
CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, ^{
|
|
RLMNotificationToken *token;
|
|
XCTAssertNoThrow(token = [realm addNotificationBlock:^(NSString *, RLMRealm *) { }]);
|
|
[token invalidate];
|
|
CFRunLoopStop(CFRunLoopGetCurrent());
|
|
});
|
|
|
|
CFRunLoopRun();
|
|
}];
|
|
}
|
|
|
|
#pragma mark - In-memory Realms
|
|
|
|
- (void)testInMemoryRealm {
|
|
@autoreleasepool {
|
|
RLMRealm *inMemoryRealm = [self inMemoryRealmWithIdentifier:@"identifier"];
|
|
|
|
[self waitForNotification:RLMRealmDidChangeNotification realm:inMemoryRealm block:^{
|
|
RLMRealm *inMemoryRealm = [self inMemoryRealmWithIdentifier:@"identifier"];
|
|
[inMemoryRealm beginWriteTransaction];
|
|
[StringObject createInRealm:inMemoryRealm withValue:@[@"a"]];
|
|
[StringObject createInRealm:inMemoryRealm withValue:@[@"b"]];
|
|
[StringObject createInRealm:inMemoryRealm withValue:@[@"c"]];
|
|
XCTAssertEqual(3U, [StringObject allObjectsInRealm:inMemoryRealm].count);
|
|
[inMemoryRealm commitWriteTransaction];
|
|
}];
|
|
|
|
XCTAssertEqual(3U, [StringObject allObjectsInRealm:inMemoryRealm].count);
|
|
|
|
// make sure we can have another
|
|
RLMRealm *anotherInMemoryRealm = [self inMemoryRealmWithIdentifier:@"identifier2"];
|
|
XCTAssertEqual(0U, [StringObject allObjectsInRealm:anotherInMemoryRealm].count);
|
|
}
|
|
|
|
// Should now be empty
|
|
RLMRealm *inMemoryRealm = [self inMemoryRealmWithIdentifier:@"identifier"];
|
|
XCTAssertEqual(0U, [StringObject allObjectsInRealm:inMemoryRealm].count);
|
|
}
|
|
|
|
#pragma mark - Read-only Realms
|
|
|
|
- (void)testReadOnlyRealmWithMissingTables
|
|
{
|
|
// create a realm with only a StringObject table
|
|
@autoreleasepool {
|
|
RLMObjectSchema *objectSchema = [RLMObjectSchema schemaForObjectClass:StringObject.class];
|
|
objectSchema.objectClass = RLMObject.class;
|
|
|
|
RLMSchema *schema = [[RLMSchema alloc] init];
|
|
schema.objectSchema = @[objectSchema];
|
|
RLMRealm *realm = [self realmWithTestPathAndSchema:schema];
|
|
|
|
[realm beginWriteTransaction];
|
|
[realm createObject:StringObject.className withValue:@[@"a"]];
|
|
[realm commitWriteTransaction];
|
|
}
|
|
|
|
RLMRealm *realm = [self readOnlyRealmWithURL:RLMTestRealmURL() error:nil];
|
|
XCTAssertEqual(1U, [StringObject allObjectsInRealm:realm].count);
|
|
XCTAssertNil([PrimaryIntObject objectInRealm:realm forPrimaryKey:@0]);
|
|
|
|
// verify that reading a missing table gives an empty array rather than
|
|
// crashing
|
|
RLMResults *results = [IntObject allObjectsInRealm:realm];
|
|
XCTAssertEqual(0U, results.count);
|
|
XCTAssertEqual(results, [results objectsWhere:@"intCol = 5"]);
|
|
XCTAssertEqual(results, [results sortedResultsUsingKeyPath:@"intCol" ascending:YES]);
|
|
XCTAssertThrows([results objectAtIndex:0]);
|
|
XCTAssertEqual(NSNotFound, [results indexOfObject:self.nonLiteralNil]);
|
|
XCTAssertEqual(NSNotFound, [results indexOfObjectWhere:@"intCol = 5"]);
|
|
XCTAssertNoThrow([realm deleteObjects:results]);
|
|
XCTAssertNil([results maxOfProperty:@"intCol"]);
|
|
XCTAssertNil([results minOfProperty:@"intCol"]);
|
|
XCTAssertNil([results averageOfProperty:@"intCol"]);
|
|
XCTAssertEqualObjects(@0, [results sumOfProperty:@"intCol"]);
|
|
XCTAssertNil([results firstObject]);
|
|
XCTAssertNil([results lastObject]);
|
|
for (__unused id obj in results) {
|
|
XCTFail(@"Got an item in empty results");
|
|
}
|
|
}
|
|
|
|
- (void)testReadOnlyRealmWithMissingColumns
|
|
{
|
|
// create a realm with only a zero-column StringObject table
|
|
@autoreleasepool {
|
|
RLMObjectSchema *objectSchema = [RLMObjectSchema schemaForObjectClass:StringObject.class];
|
|
objectSchema.objectClass = RLMObject.class;
|
|
objectSchema.properties = @[];
|
|
|
|
RLMSchema *schema = [[RLMSchema alloc] init];
|
|
schema.objectSchema = @[objectSchema];
|
|
[self realmWithTestPathAndSchema:schema];
|
|
}
|
|
|
|
XCTAssertThrows([self readOnlyRealmWithURL:RLMTestRealmURL() error:nil],
|
|
@"should reject table missing column");
|
|
}
|
|
#pragma mark - Write Copy to Path
|
|
|
|
- (void)testWriteCopyOfRealm
|
|
{
|
|
RLMRealm *realm = [RLMRealm defaultRealm];
|
|
[realm transactionWithBlock:^{
|
|
[IntObject createInRealm:realm withValue:@[@0]];
|
|
}];
|
|
|
|
NSError *writeError;
|
|
XCTAssertTrue([realm writeCopyToURL:RLMTestRealmURL() encryptionKey:nil error:&writeError]);
|
|
XCTAssertNil(writeError);
|
|
RLMRealm *copy = [self realmWithTestPath];
|
|
XCTAssertEqual(1U, [IntObject allObjectsInRealm:copy].count);
|
|
}
|
|
|
|
- (void)testCannotOverwriteWithWriteCopy
|
|
{
|
|
RLMRealm *realm = [self realmWithTestPath];
|
|
[realm transactionWithBlock:^{
|
|
[IntObject createInRealm:realm withValue:@[@0]];
|
|
}];
|
|
|
|
NSError *writeError;
|
|
// Does not throw when given a nil error out param
|
|
XCTAssertFalse([realm writeCopyToURL:RLMTestRealmURL() encryptionKey:nil error:nil]);
|
|
|
|
NSString *expectedError = [NSString stringWithFormat:@"File at path '%@' already exists.", RLMTestRealmURL().path];
|
|
NSString *expectedUnderlying = [NSString stringWithFormat:@"open(\"%@\") failed: file exists", RLMTestRealmURL().path];
|
|
XCTAssertFalse([realm writeCopyToURL:RLMTestRealmURL() encryptionKey:nil error:&writeError]);
|
|
RLMValidateRealmError(writeError, RLMErrorFileExists, expectedError, expectedUnderlying);
|
|
}
|
|
|
|
- (void)testCannotWriteInNonExistentDirectory
|
|
{
|
|
RLMRealm *realm = [self realmWithTestPath];
|
|
[realm transactionWithBlock:^{
|
|
[IntObject createInRealm:realm withValue:@[@0]];
|
|
}];
|
|
|
|
NSString *badPath = @"/tmp/RLMTestDirMayNotExist/foo";
|
|
|
|
NSString *expectedError = [NSString stringWithFormat:@"Directory at path '%@' does not exist.", badPath];
|
|
NSString *expectedUnderlying = [NSString stringWithFormat:@"open(\"%@\") failed: no such file or directory", badPath];
|
|
NSError *writeError;
|
|
XCTAssertFalse([realm writeCopyToURL:[NSURL fileURLWithPath:badPath] encryptionKey:nil error:&writeError]);
|
|
RLMValidateRealmError(writeError, RLMErrorFileNotFound, expectedError, expectedUnderlying);
|
|
}
|
|
|
|
- (void)testWriteToReadOnlyDirectory
|
|
{
|
|
RLMRealm *realm = [RLMRealm defaultRealm];
|
|
|
|
// Make the parent directory temporarily read-only
|
|
NSString *directory = RLMTestRealmURL().URLByDeletingLastPathComponent.path;
|
|
NSFileManager *fm = NSFileManager.defaultManager;
|
|
NSNumber *oldPermissions = [fm attributesOfItemAtPath:directory error:nil][NSFilePosixPermissions];
|
|
[fm setAttributes:@{NSFilePosixPermissions: @(0100)} ofItemAtPath:directory error:nil];
|
|
|
|
NSString *expectedError = [NSString stringWithFormat:@"Unable to open a Realm at path '%@'. Please use a path where your app has read-write permissions.", RLMTestRealmURL().path];
|
|
NSString *expectedUnderlying = [NSString stringWithFormat:@"open(\"%@\") failed: permission denied", RLMTestRealmURL().path];
|
|
NSError *writeError;
|
|
XCTAssertFalse([realm writeCopyToURL:RLMTestRealmURL() encryptionKey:nil error:&writeError]);
|
|
RLMValidateRealmError(writeError, RLMErrorFilePermissionDenied, expectedError, expectedUnderlying);
|
|
|
|
// Restore old permissions
|
|
[fm setAttributes:@{NSFilePosixPermissions: oldPermissions} ofItemAtPath:directory error:nil];
|
|
}
|
|
|
|
- (void)testWriteWithNonSpecialCasedError
|
|
{
|
|
// Testing an open() error which doesn't have its own exception type and
|
|
// just uses the generic "something failed" error
|
|
RLMRealm *realm = [RLMRealm defaultRealm];
|
|
|
|
// 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);
|
|
|
|
NSString *expectedError = [NSString stringWithFormat:@"Unable to open a Realm at path '%@': open() failed: too many open files",
|
|
RLMTestRealmURL().path];
|
|
NSString *expectedUnderlying = [NSString stringWithFormat:@"open(\"%@\") failed: too many open files", RLMTestRealmURL().path];
|
|
NSError *writeError;
|
|
XCTAssertFalse([realm writeCopyToURL:RLMTestRealmURL() encryptionKey:nil error:&writeError]);
|
|
RLMValidateRealmError(writeError, RLMErrorFileAccess, expectedError, expectedUnderlying);
|
|
|
|
// Restore the old open file limit
|
|
setrlimit(RLIMIT_NOFILE, &oldrl);
|
|
}
|
|
|
|
- (void)testWritingCopyUsesWriteTransactionInProgress
|
|
{
|
|
RLMRealm *realm = [RLMRealm defaultRealm];
|
|
[realm transactionWithBlock:^{
|
|
[IntObject createInRealm:realm withValue:@[@0]];
|
|
|
|
NSError *writeError;
|
|
XCTAssertTrue([realm writeCopyToURL:RLMTestRealmURL() encryptionKey:nil error:&writeError]);
|
|
XCTAssertNil(writeError);
|
|
RLMRealm *copy = [self realmWithTestPath];
|
|
XCTAssertEqual(1U, [IntObject allObjectsInRealm:copy].count);
|
|
}];
|
|
}
|
|
|
|
#pragma mark - Assorted tests
|
|
|
|
- (void)testCoreDebug {
|
|
#if DEBUG
|
|
XCTAssertTrue([RLMRealm isCoreDebug], @"Debug version of Realm should use librealm{-ios}-dbg");
|
|
#else
|
|
XCTAssertFalse([RLMRealm isCoreDebug], @"Release version of Realm should use librealm{-ios}");
|
|
#endif
|
|
}
|
|
|
|
- (void)testIsEmpty {
|
|
RLMRealm *realm = [RLMRealm defaultRealm];
|
|
XCTAssertTrue(realm.isEmpty, @"Realm should be empty on creation.");
|
|
|
|
[realm beginWriteTransaction];
|
|
[StringObject createInRealm:realm withValue:@[@"a"]];
|
|
XCTAssertFalse(realm.isEmpty, @"Realm should not be empty within a write transaction after adding an object.");
|
|
[realm cancelWriteTransaction];
|
|
|
|
XCTAssertTrue(realm.isEmpty, @"Realm should be empty after canceling a write transaction that added an object.");
|
|
|
|
[realm beginWriteTransaction];
|
|
[StringObject createInRealm:realm withValue:@[@"a"]];
|
|
[realm commitWriteTransaction];
|
|
XCTAssertFalse(realm.isEmpty, @"Realm should not be empty after committing a write transaction that added an object.");
|
|
}
|
|
|
|
- (void)testRealmFileAccessNilPath {
|
|
RLMAssertThrowsWithReasonMatching([RLMRealm realmWithURL:self.nonLiteralNil],
|
|
@"Realm path must not be empty", @"nil path");
|
|
}
|
|
|
|
- (void)testRealmFileAccessNoExistingFile
|
|
{
|
|
NSURL *fileURL = [NSURL fileURLWithPath:RLMRealmPathForFile(@"filename.realm")];
|
|
[[NSFileManager defaultManager] removeItemAtPath:fileURL.path error:nil];
|
|
assert(![[NSFileManager defaultManager] fileExistsAtPath:fileURL.path]);
|
|
|
|
NSError *error;
|
|
RLMRealmConfiguration *configuration = [RLMRealmConfiguration defaultConfiguration];
|
|
configuration.fileURL = fileURL;
|
|
XCTAssertNotNil([RLMRealm realmWithConfiguration:configuration error:&error],
|
|
@"Database should have been created");
|
|
XCTAssertNil(error);
|
|
}
|
|
|
|
- (void)testRealmFileAccessInvalidFile
|
|
{
|
|
NSString *content = @"Some content";
|
|
NSData *fileContents = [content dataUsingEncoding:NSUTF8StringEncoding];
|
|
NSURL *fileURL = [NSURL fileURLWithPath:RLMRealmPathForFile(@"filename.realm")];
|
|
[[NSFileManager defaultManager] removeItemAtPath:fileURL.path error:nil];
|
|
assert(![[NSFileManager defaultManager] fileExistsAtPath:fileURL.path]);
|
|
[[NSFileManager defaultManager] createFileAtPath:fileURL.path contents:fileContents attributes:nil];
|
|
|
|
NSError *error;
|
|
RLMRealmConfiguration *configuration = [RLMRealmConfiguration defaultConfiguration];
|
|
configuration.fileURL = fileURL;
|
|
XCTAssertNil([RLMRealm realmWithConfiguration:configuration error:&error], @"Invalid database");
|
|
RLMValidateRealmError(error, RLMErrorFileAccess, @"Unable to open a realm at path", @"Realm file has bad size");
|
|
}
|
|
|
|
- (void)testRealmFileAccessFileIsDirectory
|
|
{
|
|
NSURL *testURL = RLMTestRealmURL();
|
|
[[NSFileManager defaultManager] createDirectoryAtPath:testURL.path
|
|
withIntermediateDirectories:NO
|
|
attributes:nil
|
|
error:nil];
|
|
NSError *error;
|
|
RLMRealmConfiguration *configuration = [RLMRealmConfiguration defaultConfiguration];
|
|
configuration.fileURL = testURL;
|
|
XCTAssertNil([RLMRealm realmWithConfiguration:configuration error:&error], @"Invalid database");
|
|
RLMValidateRealmError(error, RLMErrorFileAccess, @"Unable to open a realm at path", @"Is a directory");
|
|
}
|
|
|
|
#if TARGET_OS_TV
|
|
#else
|
|
- (void)testRealmFifoError
|
|
{
|
|
NSFileManager *manager = [NSFileManager defaultManager];
|
|
NSURL *testURL = RLMTestRealmURL();
|
|
RLMRealmConfiguration *configuration = [RLMRealmConfiguration defaultConfiguration];
|
|
configuration.fileURL = testURL;
|
|
|
|
// Create the expected fifo URL and create a directory.
|
|
// Note that creating a file when a directory with the same name exists produces a different errno, which is good.
|
|
NSURL *fifoURL = [[testURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"realm.note"];
|
|
assert(![manager fileExistsAtPath:fifoURL.path]);
|
|
[manager createDirectoryAtPath:fifoURL.path withIntermediateDirectories:YES attributes:nil error:nil];
|
|
|
|
// Ensure that it doesn't try to fall back to putting it in the temp directory
|
|
auto oldTempDir = realm::SharedGroupOptions::get_sys_tmp_dir();
|
|
realm::SharedGroupOptions::set_sys_tmp_dir("");
|
|
|
|
NSError *error;
|
|
XCTAssertNil([RLMRealm realmWithConfiguration:configuration error:&error], @"Should not have been able to open FIFO");
|
|
XCTAssertNotNil(error);
|
|
RLMValidateRealmError(error, RLMErrorFileAccess, @"Is a directory", nil);
|
|
|
|
realm::SharedGroupOptions::set_sys_tmp_dir(std::move(oldTempDir));
|
|
}
|
|
#endif
|
|
|
|
- (void)testMultipleRealms
|
|
{
|
|
// Create one StringObject in two different realms
|
|
RLMRealm *defaultRealm = [RLMRealm defaultRealm];
|
|
RLMRealm *testRealm = self.realmWithTestPath;
|
|
[defaultRealm beginWriteTransaction];
|
|
[testRealm beginWriteTransaction];
|
|
[StringObject createInRealm:defaultRealm withValue:@[@"a"]];
|
|
[StringObject createInRealm:testRealm withValue:@[@"b"]];
|
|
[testRealm commitWriteTransaction];
|
|
[defaultRealm commitWriteTransaction];
|
|
|
|
// Confirm that objects were added to the correct realms
|
|
RLMResults *defaultObjects = [StringObject allObjectsInRealm:defaultRealm];
|
|
RLMResults *testObjects = [StringObject allObjectsInRealm:testRealm];
|
|
XCTAssertEqual(defaultObjects.count, 1U, @"Expecting 1 object");
|
|
XCTAssertEqual(testObjects.count, 1U, @"Expecting 1 object");
|
|
XCTAssertEqualObjects([defaultObjects.firstObject stringCol], @"a", @"Expecting column to be 'a'");
|
|
XCTAssertEqualObjects([testObjects.firstObject stringCol], @"b", @"Expecting column to be 'b'");
|
|
}
|
|
|
|
|
|
- (void)testInvalidLockFile
|
|
{
|
|
// Create the realm file and lock file
|
|
@autoreleasepool { [RLMRealm defaultRealm]; }
|
|
|
|
int fd = open([RLMRealmConfiguration.defaultConfiguration.fileURL.path stringByAppendingString:@".lock"].UTF8String, O_RDWR);
|
|
XCTAssertNotEqual(-1, fd);
|
|
|
|
// Change the value of the mutex size field in the shared info header
|
|
uint8_t value = 255;
|
|
pwrite(fd, &value, 1, 1);
|
|
|
|
// Ensure that SharedGroup can't get an exclusive lock on the lock file so
|
|
// that it can't just recreate it
|
|
int ret = flock(fd, LOCK_SH);
|
|
XCTAssertEqual(0, ret);
|
|
|
|
NSError *error;
|
|
RLMRealm *realm = [RLMRealm realmWithConfiguration:RLMRealmConfiguration.defaultConfiguration error:&error];
|
|
XCTAssertNil(realm);
|
|
RLMValidateRealmError(error, RLMErrorIncompatibleLockFile, @"Realm file is currently open in another process", nil);
|
|
|
|
flock(fd, LOCK_UN);
|
|
close(fd);
|
|
}
|
|
|
|
- (void)testCannotMigrateRealmWhenRealmIsOpen {
|
|
RLMRealm *realm = [self realmWithTestPath];
|
|
|
|
RLMRealmConfiguration *configuration = [RLMRealmConfiguration defaultConfiguration];
|
|
configuration.fileURL = realm.configuration.fileURL;
|
|
XCTAssertThrows([RLMRealm performMigrationForConfiguration:configuration error:nil]);
|
|
}
|
|
|
|
- (void)testNotificationPipeBufferOverfull {
|
|
RLMRealm *realm = [self inMemoryRealmWithIdentifier:@"test"];
|
|
// pipes have a 8 KB buffer on OS X, so verify we don't block after 8192 commits
|
|
for (int i = 0; i < 9000; ++i) {
|
|
[realm transactionWithBlock:^{}];
|
|
}
|
|
}
|
|
|
|
- (NSArray *)pathsFor100Realms
|
|
{
|
|
NSMutableArray *paths = [NSMutableArray array];
|
|
for (int i = 0; i < 100; ++i) {
|
|
NSString *realmFileName = [NSString stringWithFormat:@"test.%d.realm", i];
|
|
[paths addObject:RLMRealmPathForFile(realmFileName)];
|
|
}
|
|
return paths;
|
|
}
|
|
|
|
- (void)testCanCreate100RealmsWithoutBreakingGCD
|
|
{
|
|
NSMutableArray *realms = [NSMutableArray array];
|
|
for (NSString *realmPath in self.pathsFor100Realms) {
|
|
[realms addObject:[RLMRealm realmWithURL:[NSURL fileURLWithPath:realmPath]]];
|
|
}
|
|
|
|
XCTestExpectation *expectation = [self expectationWithDescription:@"Block dispatched to concurrent queue should be executed"];
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
[expectation fulfill];
|
|
});
|
|
[self waitForExpectationsWithTimeout:1 handler:nil];
|
|
}
|
|
|
|
- (void)testAuxiliaryFilesAreExcludedFromBackup {
|
|
@autoreleasepool { [RLMRealm defaultRealm]; }
|
|
|
|
#if TARGET_OS_TV
|
|
NSArray *auxiliaryFileExtensions = @[@"management", @"lock"]; // tvOS does not support named pipes
|
|
#else
|
|
NSArray *auxiliaryFileExtensions = @[@"management", @"lock", @"note"];
|
|
#endif
|
|
NSURL *fileURL = RLMRealmConfiguration.defaultConfiguration.fileURL;
|
|
for (NSString *pathExtension in auxiliaryFileExtensions) {
|
|
NSNumber *attribute = nil;
|
|
NSError *error = nil;
|
|
BOOL success = [[fileURL URLByAppendingPathExtension:pathExtension] getResourceValue:&attribute forKey:NSURLIsExcludedFromBackupKey error:&error];
|
|
XCTAssertTrue(success);
|
|
XCTAssertNil(error);
|
|
XCTAssertTrue(attribute.boolValue);
|
|
}
|
|
}
|
|
|
|
- (void)testAuxiliaryFilesAreExcludedFromBackupPerformance {
|
|
[self measureBlock:^{
|
|
@autoreleasepool {
|
|
RLMRealm *realm = [RLMRealm defaultRealm];
|
|
realm = [RLMRealm defaultRealm];
|
|
realm = [RLMRealm defaultRealm];
|
|
realm = [RLMRealm defaultRealm];
|
|
realm = [RLMRealm defaultRealm];
|
|
realm = [RLMRealm defaultRealm];
|
|
realm = [RLMRealm defaultRealm];
|
|
realm = [RLMRealm defaultRealm];
|
|
realm = [RLMRealm defaultRealm];
|
|
realm = [RLMRealm defaultRealm];
|
|
realm = [RLMRealm defaultRealm];
|
|
realm = [RLMRealm defaultRealm];
|
|
realm = [RLMRealm defaultRealm];
|
|
realm = [RLMRealm defaultRealm];
|
|
}
|
|
@autoreleasepool { [RLMRealm defaultRealm]; }
|
|
}];
|
|
|
|
// NSURL *fileURL = RLMRealmConfiguration.defaultConfiguration.fileURL;
|
|
// for (NSString *pathExtension in @[@"management", @"lock", @"note"]) {
|
|
// NSNumber *attribute = nil;
|
|
// NSError *error = nil;
|
|
// BOOL success = [[fileURL URLByAppendingPathExtension:pathExtension] getResourceValue:&attribute forKey:NSURLIsExcludedFromBackupKey error:&error];
|
|
// XCTAssertTrue(success);
|
|
// XCTAssertNil(error);
|
|
// XCTAssertTrue(attribute.boolValue);
|
|
// }
|
|
}
|
|
|
|
@end
|
|
|