An amazing project that generates micro reports from tournament results
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.

2077 lines
67 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 "RLMObjectSchema_Private.hpp"
#import "RLMObjectStore.h"
#import "RLMObject_Private.hpp"
#import "RLMRealmConfiguration_Private.hpp"
#import "RLMRealm_Private.hpp"
#import "RLMSchema_Private.h"
#import "shared_realm.hpp"
#import <realm/descriptor.hpp>
#import <realm/group.hpp>
#import <atomic>
#import <memory>
#import <objc/runtime.h>
#import <vector>
RLM_ARRAY_TYPE(KVOObject)
RLM_ARRAY_TYPE(KVOLinkObject1)
@interface KVOObject : RLMObject
@property int pk; // Primary key for isEqual:
@property int ignored;
@property BOOL boolCol;
@property int16_t int16Col;
@property int32_t int32Col;
@property int64_t int64Col;
@property float floatCol;
@property double doubleCol;
@property bool cBoolCol;
@property NSString *stringCol;
@property NSData *binaryCol;
@property NSDate *dateCol;
@property KVOObject *objectCol;
@property RLMArray<RLMBool> *boolArray;
@property RLMArray<RLMInt> *intArray;
@property RLMArray<RLMFloat> *floatArray;
@property RLMArray<RLMDouble> *doubleArray;
@property RLMArray<RLMString> *stringArray;
@property RLMArray<RLMData> *dataArray;
@property RLMArray<RLMDate> *dateArray;
@property RLMArray<KVOObject> *objectArray;
@property NSNumber<RLMInt> *optIntCol;
@property NSNumber<RLMFloat> *optFloatCol;
@property NSNumber<RLMDouble> *optDoubleCol;
@property NSNumber<RLMBool> *optBoolCol;
@end
@implementation KVOObject
+ (NSString *)primaryKey {
return @"pk";
}
+ (NSArray *)ignoredProperties {
return @[@"ignored"];
}
@end
@interface KVOLinkObject1 : RLMObject
@property int pk; // Primary key for isEqual:
@property KVOObject *obj;
@property RLMArray<KVOObject> *array;
@end
@implementation KVOLinkObject1
+ (NSString *)primaryKey {
return @"pk";
}
@end
@interface KVOLinkObject2 : RLMObject
@property int pk; // Primary key for isEqual:
@property KVOLinkObject1 *obj;
@property RLMArray<KVOLinkObject1> *array;
@end
@implementation KVOLinkObject2
+ (NSString *)primaryKey {
return @"pk";
}
@end
@interface PlainKVOObject : NSObject
@property int ignored;
@property BOOL boolCol;
@property int16_t int16Col;
@property int32_t int32Col;
@property int64_t int64Col;
@property float floatCol;
@property double doubleCol;
@property bool cBoolCol;
@property NSString *stringCol;
@property NSData *binaryCol;
@property NSDate *dateCol;
@property PlainKVOObject *objectCol;
@property NSMutableArray *boolArray;
@property NSMutableArray *intArray;
@property NSMutableArray *floatArray;
@property NSMutableArray *doubleArray;
@property NSMutableArray *stringArray;
@property NSMutableArray *dataArray;
@property NSMutableArray *dateArray;
@property NSMutableArray *objectArray;
@property NSNumber<RLMInt> *optIntCol;
@property NSNumber<RLMFloat> *optFloatCol;
@property NSNumber<RLMDouble> *optDoubleCol;
@property NSNumber<RLMBool> *optBoolCol;
@end
@implementation PlainKVOObject
@end
@interface PlainLinkObject1 : NSObject
@property PlainKVOObject *obj;
@property NSMutableArray *array;
@end
@implementation PlainLinkObject1
@end
@interface PlainLinkObject2 : NSObject
@property PlainLinkObject1 *obj;
@property NSMutableArray *array;
@end
@implementation PlainLinkObject2
@end
// Tables with no links (or backlinks) preserve the order of rows on
// insertion/deletion, while tables with links do not, so we need an object
// class known to have no links to test the ordered case
@interface ObjectWithNoLinksToOrFrom : RLMObject
@property int value;
@end
@implementation ObjectWithNoLinksToOrFrom
@end
// An object which removes a KVO registration when it's deallocated, for use
// as an associated object
@interface KVOUnregisterHelper : NSObject
@end
@implementation KVOUnregisterHelper {
__unsafe_unretained id _obj;
__unsafe_unretained id _observer;
NSString *_keyPath;
}
+ (void)automaticallyUnregister:(id)observer object:(id)obj keyPath:(NSString *)keyPath {
KVOUnregisterHelper *helper = [self new];
helper->_observer = observer;
helper->_obj = obj;
helper->_keyPath = keyPath;
objc_setAssociatedObject(obj, (__bridge void *)helper, helper, OBJC_ASSOCIATION_RETAIN);
}
- (void)dealloc {
[_obj removeObserver:_observer forKeyPath:_keyPath];
}
@end
// A KVO observer which retains the given object until it observes a change
@interface ReleaseOnObservation : NSObject
@property (strong) id object;
@end
@implementation ReleaseOnObservation
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(__unused NSDictionary *)change
context:(void *)context
{
[object removeObserver:self forKeyPath:keyPath context:context];
_object = nil;
}
@end
@interface KVOTests : RLMTestCase
// get an object that should be observed for the given object being mutated
// used by some of the subclasses to observe a different accessor for the same row
- (id)observableForObject:(id)obj;
@end
// subscribes to kvo notifications on the passed object on creation, records
// all change notifications sent and makes them available in `notifications`,
// and automatically unsubscribes on destruction
class KVORecorder {
id _observer;
id _obj;
NSString *_keyPath;
RLMRealm *_mutationRealm;
RLMRealm *_observationRealm;
NSMutableArray *_notifications;
public:
// construct a new recorder for the given `keyPath` on `obj`, using `observer`
// as the NSObject helper to actually add as an observer
KVORecorder(id observer, id obj, NSString *keyPath,
int options = NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew)
: _observer(observer)
, _obj([observer observableForObject:obj])
, _keyPath(keyPath)
, _mutationRealm([obj respondsToSelector:@selector(realm)] ? (RLMRealm *)[obj realm] : nil)
, _observationRealm([_obj respondsToSelector:@selector(realm)] ? (RLMRealm *)[_obj realm] : nil)
, _notifications([NSMutableArray new])
{
[_obj addObserver:observer forKeyPath:keyPath options:options context:this];
}
~KVORecorder() {
id self = _observer;
@try {
[_obj removeObserver:_observer forKeyPath:_keyPath context:this];
}
@catch (NSException *e) {
XCTFail(@"%@", e.description);
}
XCTAssertEqual(0U, _notifications.count);
}
// record a single notification
void operator()(NSString *key, id obj, NSDictionary *changeDictionary) {
id self = _observer;
XCTAssertEqual(obj, _obj);
XCTAssertEqualObjects(key, _keyPath);
[_notifications addObject:changeDictionary.copy];
}
// ensure that the observed object is updated for any changes made to the
// object being mutated if they are different
void refresh() {
if (_mutationRealm != _observationRealm) {
[_mutationRealm commitWriteTransaction];
[_observationRealm refresh];
[_mutationRealm beginWriteTransaction];
}
}
NSDictionary *pop_front() {
NSDictionary *value = [_notifications firstObject];
if (value) {
[_notifications removeObjectAtIndex:0U];
}
return value;
}
NSUInteger size() const {
return _notifications.count;
}
bool empty() const {
return _notifications.count == 0;
}
};
// Assert that `recorder` has a notification at `index` and return it if so
#define AssertNotification(recorder) ([&]{ \
(recorder).refresh(); \
NSDictionary *value = recorder.pop_front(); \
XCTAssertNotNil(value, @"Did not get a notification when expected"); \
return value; \
})()
// Validate that `recorder` has at least one notification, and that the first
// notification is the expected one
#define AssertChanged(recorder, from, to) do { \
if (NSDictionary *note = AssertNotification((recorder))) { \
XCTAssertEqualObjects(@(NSKeyValueChangeSetting), note[NSKeyValueChangeKindKey]); \
XCTAssertEqualObjects((from), note[NSKeyValueChangeOldKey]); \
XCTAssertEqualObjects((to), note[NSKeyValueChangeNewKey]); \
} \
else { \
return; \
} \
} while (false)
// Validate that `r` has a notification with the given kind and changed indexes,
// remove it, and verify that there are no more notifications
#define AssertIndexChange(kind, indexes) do { \
if (NSDictionary *note = AssertNotification(r)) { \
XCTAssertEqual([note[NSKeyValueChangeKindKey] intValue], kind); \
XCTAssertEqualObjects(note[NSKeyValueChangeIndexesKey], indexes); \
} \
XCTAssertTrue(r.empty()); \
} while (0)
// Tests for plain Foundation key-value observing to verify that we correctly
// match the standard semantics. Each of the subclasses of KVOTests runs the
// same set of tests on RLMObjects in difference scenarios
@implementation KVOTests
// forward a KVO notification to the KVORecorder stored in the context
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
(*static_cast<KVORecorder *>(context))(keyPath, object, change);
}
// overridden in the multiple accessors, one realm and multiple realms cases
- (id)observableForObject:(id)obj {
return obj;
}
// overridden in the multiple realms case because `-refresh` does not send
// notifications for intermediate states
- (bool)collapsesNotifications {
return false;
}
// overridden in all subclases to return the appropriate object
// base class runs the tests on a plain NSObject using stock KVO to ensure that
// the tests are actually covering the correct behavior, since there's a great
// deal that the documentation doesn't specify
- (id)createObject {
PlainKVOObject *obj = [PlainKVOObject new];
obj.int16Col = 1;
obj.int32Col = 2;
obj.int64Col = 3;
obj.binaryCol = NSData.data;
obj.stringCol = @"";
obj.dateCol = [NSDate dateWithTimeIntervalSinceReferenceDate:0];
obj.boolArray = [NSMutableArray array];
obj.intArray = [NSMutableArray array];
obj.floatArray = [NSMutableArray array];
obj.doubleArray = [NSMutableArray array];
obj.stringArray = [NSMutableArray array];
obj.dataArray = [NSMutableArray array];
obj.dateArray = [NSMutableArray array];
obj.objectArray = [NSMutableArray array];
return obj;
}
- (id)createLinkObject {
PlainLinkObject1 *obj1 = [PlainLinkObject1 new];
obj1.obj = [self createObject];
obj1.array = [NSMutableArray new];
PlainLinkObject2 *obj2 = [PlainLinkObject2 new];
obj2.obj = obj1;
obj2.array = [NSMutableArray new];
return obj2;
}
// actual tests follow
- (void)testRegisterForUnknownProperty {
KVOObject *obj = [self createObject];
XCTAssertNoThrow([obj addObserver:self forKeyPath:@"non-existent" options:0 context:nullptr]);
XCTAssertNoThrow([obj removeObserver:self forKeyPath:@"non-existent"]);
XCTAssertNoThrow([obj addObserver:self forKeyPath:@"non-existent" options:NSKeyValueObservingOptionOld context:nullptr]);
XCTAssertNoThrow([obj removeObserver:self forKeyPath:@"non-existent"]);
XCTAssertNoThrow([obj addObserver:self forKeyPath:@"non-existent" options:NSKeyValueObservingOptionPrior context:nullptr]);
XCTAssertNoThrow([obj removeObserver:self forKeyPath:@"non-existent"]);
}
- (void)testRemoveObserver {
KVOObject *obj = [self createObject];
XCTAssertThrowsSpecificNamed([obj removeObserver:self forKeyPath:@"int32Col"], NSException, NSRangeException);
XCTAssertThrowsSpecificNamed([obj removeObserver:self forKeyPath:@"int32Col" context:nullptr], NSException, NSRangeException);
XCTAssertNoThrow([obj addObserver:self forKeyPath:@"int32Col" options:0 context:nullptr]);
XCTAssertNoThrow([obj removeObserver:self forKeyPath:@"int32Col"]);
XCTAssertThrowsSpecificNamed([obj removeObserver:self forKeyPath:@"int32Col"], NSException, NSRangeException);
// `context` parameter must match if it's passed, but the overload that doesn't
// take one will unregister any context
void *context1 = (void *)1;
void *context2 = (void *)2;
XCTAssertNoThrow([obj addObserver:self forKeyPath:@"int32Col" options:0 context:context1]);
XCTAssertThrows([obj removeObserver:self forKeyPath:@"int32Col" context:context2]);
XCTAssertNoThrow([obj removeObserver:self forKeyPath:@"int32Col" context:context1]);
XCTAssertNoThrow([obj addObserver:self forKeyPath:@"int32Col" options:0 context:context2]);
XCTAssertNoThrow([obj removeObserver:self forKeyPath:@"int32Col" context:context2]);
XCTAssertNoThrow([obj addObserver:self forKeyPath:@"int32Col" options:0 context:context2]);
XCTAssertNoThrow([obj removeObserver:self forKeyPath:@"int32Col"]);
XCTAssertThrows([obj removeObserver:self forKeyPath:@"int32Col"]);
XCTAssertNoThrow([obj addObserver:self forKeyPath:@"int32Col" options:0 context:context1]);
XCTAssertNoThrow([obj addObserver:self forKeyPath:@"int32Col" options:0 context:context2]);
XCTAssertNoThrow([obj removeObserver:self forKeyPath:@"int32Col" context:context1]);
XCTAssertThrows([obj removeObserver:self forKeyPath:@"int32Col" context:context1]);
XCTAssertNoThrow([obj removeObserver:self forKeyPath:@"int32Col" context:context2]);
XCTAssertThrows([obj removeObserver:self forKeyPath:@"int32Col" context:context2]);
XCTAssertNoThrow([obj addObserver:self forKeyPath:@"int32Col" options:0 context:context1]);
XCTAssertNoThrow([obj addObserver:self forKeyPath:@"int32Col" options:0 context:context2]);
XCTAssertNoThrow([obj removeObserver:self forKeyPath:@"int32Col" context:context2]);
XCTAssertThrows([obj removeObserver:self forKeyPath:@"int32Col" context:context2]);
XCTAssertNoThrow([obj removeObserver:self forKeyPath:@"int32Col" context:context1]);
XCTAssertThrows([obj removeObserver:self forKeyPath:@"int32Col" context:context1]);
// no context version should only unregister one (unspecified) observer
XCTAssertNoThrow([obj addObserver:self forKeyPath:@"int32Col" options:0 context:context1]);
XCTAssertNoThrow([obj addObserver:self forKeyPath:@"int32Col" options:0 context:context2]);
XCTAssertNoThrow([obj removeObserver:self forKeyPath:@"int32Col"]);
XCTAssertNoThrow([obj removeObserver:self forKeyPath:@"int32Col"]);
XCTAssertThrows([obj removeObserver:self forKeyPath:@"int32Col"]);
}
- (void)testRemoveObserverInObservation {
auto helper = [ReleaseOnObservation new];
__unsafe_unretained id obj;
__weak id weakObj;
@autoreleasepool {
obj = weakObj = helper.object = [self createObject];
[obj addObserver:helper forKeyPath:@"int32Col" options:NSKeyValueObservingOptionOld context:nullptr];
}
[obj setInt32Col:0];
XCTAssertNil(helper.object);
XCTAssertNil(weakObj);
}
- (void)testSimple {
KVOObject *obj = [self createObject];
{
KVORecorder r(self, obj, @"int32Col");
obj.int32Col = 10;
AssertChanged(r, @2, @10);
}
{
KVORecorder r(self, obj, @"int32Col");
obj.int32Col = 1;
AssertChanged(r, @10, @1);
}
}
- (void)testSelfAssignmentNotifies {
KVOObject *obj = [self createObject];
{
KVORecorder r(self, obj, @"int32Col");
obj.int32Col = obj.int32Col;
AssertChanged(r, @2, @2);
}
}
- (void)testMultipleObserversAreNotified {
KVOObject *obj = [self createObject];
{
KVORecorder r1(self, obj, @"int32Col");
KVORecorder r2(self, obj, @"int32Col");
KVORecorder r3(self, obj, @"int32Col");
obj.int32Col = 10;
AssertChanged(r1, @2, @10);
AssertChanged(r2, @2, @10);
AssertChanged(r3, @2, @10);
}
}
- (void)testOnlyObserversForTheCorrectPropertyAreNotified {
KVOObject *obj = [self createObject];
{
KVORecorder r16(self, obj, @"int16Col");
KVORecorder r32(self, obj, @"int32Col");
KVORecorder r64(self, obj, @"int64Col");
obj.int16Col = 2;
AssertChanged(r16, @1, @2);
XCTAssertTrue(r16.empty());
XCTAssertTrue(r32.empty());
XCTAssertTrue(r64.empty());
obj.int32Col = 2;
AssertChanged(r32, @2, @2);
XCTAssertTrue(r16.empty());
XCTAssertTrue(r32.empty());
XCTAssertTrue(r64.empty());
obj.int64Col = 2;
AssertChanged(r64, @3, @2);
XCTAssertTrue(r16.empty());
XCTAssertTrue(r32.empty());
XCTAssertTrue(r64.empty());
}
}
- (void)testMultipleChangesWithSingleObserver {
KVOObject *obj = [self createObject];
KVORecorder r(self, obj, @"int32Col");
obj.int32Col = 1;
obj.int32Col = 2;
obj.int32Col = 3;
obj.int32Col = 3;
if (self.collapsesNotifications) {
AssertChanged(r, @2, @3);
}
else {
AssertChanged(r, @2, @1);
AssertChanged(r, @1, @2);
AssertChanged(r, @2, @3);
AssertChanged(r, @3, @3);
}
}
- (void)testOnlyObserversForTheCorrectObjectAreNotified {
KVOObject *obj1 = [self createObject];
KVOObject *obj2 = [self createObject];
KVORecorder r1(self, obj1, @"int32Col");
KVORecorder r2(self, obj2, @"int32Col");
obj1.int32Col = 10;
AssertChanged(r1, @2, @10);
XCTAssertEqual(0U, r2.size());
obj2.int32Col = 5;
AssertChanged(r2, @2, @5);
}
- (void)testOptionsInitial {
KVOObject *obj = [self createObject];
{
KVORecorder r(self, obj, @"int32Col", 0);
XCTAssertEqual(0U, r.size());
}
{
KVORecorder r(self, obj, @"int32Col", NSKeyValueObservingOptionInitial);
r.pop_front();
}
}
- (void)testOptionsOld {
KVOObject *obj = [self createObject];
{
KVORecorder r(self, obj, @"int32Col", 0);
obj.int32Col = 0;
if (NSDictionary *note = AssertNotification(r)) {
XCTAssertNil(note[NSKeyValueChangeOldKey]);
}
}
{
KVORecorder r(self, obj, @"int32Col", NSKeyValueObservingOptionOld);
obj.int32Col = 0;
if (NSDictionary *note = AssertNotification(r)) {
XCTAssertNotNil(note[NSKeyValueChangeOldKey]);
}
}
}
- (void)testOptionsNew {
KVOObject *obj = [self createObject];
{
KVORecorder r(self, obj, @"int32Col", 0);
obj.int32Col = 0;
if (NSDictionary *note = AssertNotification(r)) {
XCTAssertNil(note[NSKeyValueChangeNewKey]);
}
}
{
KVORecorder r(self, obj, @"int32Col", NSKeyValueObservingOptionNew);
obj.int32Col = 0;
if (NSDictionary *note = AssertNotification(r)) {
XCTAssertNotNil(note[NSKeyValueChangeNewKey]);
}
}
}
- (void)testOptionsPrior {
KVOObject *obj = [self createObject];
KVORecorder r(self, obj, @"int32Col", NSKeyValueObservingOptionNew|NSKeyValueObservingOptionPrior);
obj.int32Col = 0;
r.refresh();
XCTAssertEqual(2U, r.size());
if (NSDictionary *note = AssertNotification(r)) {
XCTAssertNil(note[NSKeyValueChangeNewKey]);
XCTAssertEqualObjects(@YES, note[NSKeyValueChangeNotificationIsPriorKey]);
}
if (NSDictionary *note = AssertNotification(r)) {
XCTAssertNotNil(note[NSKeyValueChangeNewKey]);
XCTAssertNil(note[NSKeyValueChangeNotificationIsPriorKey]);
}
}
- (void)testAllPropertyTypes {
KVOObject *obj = [self createObject];
{
KVORecorder r(self, obj, @"boolCol");
obj.boolCol = YES;
AssertChanged(r, @NO, @YES);
}
{
KVORecorder r(self, obj, @"int16Col");
obj.int16Col = 0;
AssertChanged(r, @1, @0);
}
{
KVORecorder r(self, obj, @"int32Col");
obj.int32Col = 0;
AssertChanged(r, @2, @0);
}
{
KVORecorder r(self, obj, @"int64Col");
obj.int64Col = 0;
AssertChanged(r, @3, @0);
}
{
KVORecorder r(self, obj, @"floatCol");
obj.floatCol = 1.0f;
AssertChanged(r, @0, @1);
}
{
KVORecorder r(self, obj, @"doubleCol");
obj.doubleCol = 1.0;
AssertChanged(r, @0, @1);
}
{
KVORecorder r(self, obj, @"cBoolCol");
obj.cBoolCol = YES;
AssertChanged(r, @NO, @YES);
}
{
KVORecorder r(self, obj, @"stringCol");
obj.stringCol = @"abc";
AssertChanged(r, @"", @"abc");
obj.stringCol = nil;
AssertChanged(r, @"abc", NSNull.null);
}
{
KVORecorder r(self, obj, @"binaryCol");
NSData *data = [@"abc" dataUsingEncoding:NSUTF8StringEncoding];
obj.binaryCol = data;
AssertChanged(r, NSData.data, data);
obj.binaryCol = nil;
AssertChanged(r, data, NSNull.null);
}
{
KVORecorder r(self, obj, @"dateCol");
NSDate *date = [NSDate dateWithTimeIntervalSinceReferenceDate:1];
obj.dateCol = date;
AssertChanged(r, [NSDate dateWithTimeIntervalSinceReferenceDate:0], date);
obj.dateCol = nil;
AssertChanged(r, date, NSNull.null);
}
{
KVORecorder r(self, obj, @"objectCol");
obj.objectCol = obj;
AssertChanged(r, NSNull.null, [self observableForObject:obj]);
obj.objectCol = nil;
AssertChanged(r, [self observableForObject:obj], NSNull.null);
}
{
KVORecorder r(self, obj, @"intArray");
obj.intArray = obj.intArray;
r.refresh();
r.pop_front(); // asserts that there's something to pop
}
{
KVORecorder r(self, obj, @"boolArray");
obj.boolArray = obj.boolArray;
r.refresh();
r.pop_front(); // asserts that there's something to pop
}
{
KVORecorder r(self, obj, @"floatArray");
obj.floatArray = obj.floatArray;
r.refresh();
r.pop_front(); // asserts that there's something to pop
}
{
KVORecorder r(self, obj, @"doubleArray");
obj.doubleArray = obj.doubleArray;
r.refresh();
r.pop_front(); // asserts that there's something to pop
}
{
KVORecorder r(self, obj, @"stringArray");
obj.stringArray = obj.stringArray;
r.refresh();
r.pop_front(); // asserts that there's something to pop
}
{
KVORecorder r(self, obj, @"dataArray");
obj.dataArray = obj.dataArray;
r.refresh();
r.pop_front(); // asserts that there's something to pop
}
{
KVORecorder r(self, obj, @"dateArray");
obj.dateArray = obj.dateArray;
r.refresh();
r.pop_front(); // asserts that there's something to pop
}
{
KVORecorder r(self, obj, @"objectArray");
obj.objectArray = obj.objectArray;
r.refresh();
r.pop_front(); // asserts that there's something to pop
}
{
KVORecorder r(self, obj, @"optIntCol");
obj.optIntCol = @1;
AssertChanged(r, NSNull.null, @1);
obj.optIntCol = nil;
AssertChanged(r, @1, NSNull.null);
}
{
KVORecorder r(self, obj, @"optFloatCol");
obj.optFloatCol = @1.1f;
AssertChanged(r, NSNull.null, @1.1f);
obj.optFloatCol = nil;
AssertChanged(r, @1.1f, NSNull.null);
}
{
KVORecorder r(self, obj, @"optDoubleCol");
obj.optDoubleCol = @1.1;
AssertChanged(r, NSNull.null, @1.1);
obj.optDoubleCol = nil;
AssertChanged(r, @1.1, NSNull.null);
}
{
KVORecorder r(self, obj, @"optBoolCol");
obj.optBoolCol = @YES;
AssertChanged(r, NSNull.null, @YES);
obj.optBoolCol = nil;
AssertChanged(r, @YES, NSNull.null);
}
}
- (void)testAllPropertyTypesKVC {
KVOObject *obj = [self createObject];
{
KVORecorder r(self, obj, @"boolCol");
[obj setValue:@YES forKey:@"boolCol"];
AssertChanged(r, @NO, @YES);
}
{
KVORecorder r(self, obj, @"int16Col");
[obj setValue:@0 forKey:@"int16Col"];
AssertChanged(r, @1, @0);
}
{
KVORecorder r(self, obj, @"int32Col");
[obj setValue:@0 forKey:@"int32Col"];
AssertChanged(r, @2, @0);
}
{
KVORecorder r(self, obj, @"int64Col");
[obj setValue:@0 forKey:@"int64Col"];
AssertChanged(r, @3, @0);
}
{
KVORecorder r(self, obj, @"floatCol");
[obj setValue:@1.0f forKey:@"floatCol"];
AssertChanged(r, @0, @1);
}
{
KVORecorder r(self, obj, @"doubleCol");
[obj setValue:@1.0 forKey:@"doubleCol"];
AssertChanged(r, @0, @1);
}
{
KVORecorder r(self, obj, @"cBoolCol");
[obj setValue:@YES forKey:@"cBoolCol"];
AssertChanged(r, @NO, @YES);
}
{
KVORecorder r(self, obj, @"stringCol");
[obj setValue:@"abc" forKey:@"stringCol"];
AssertChanged(r, @"", @"abc");
[obj setValue:nil forKey:@"stringCol"];
AssertChanged(r, @"abc", NSNull.null);
}
{
KVORecorder r(self, obj, @"binaryCol");
NSData *data = [@"abc" dataUsingEncoding:NSUTF8StringEncoding];
[obj setValue:data forKey:@"binaryCol"];
AssertChanged(r, NSData.data, data);
[obj setValue:nil forKey:@"binaryCol"];
AssertChanged(r, data, NSNull.null);
}
{
KVORecorder r(self, obj, @"dateCol");
NSDate *date = [NSDate dateWithTimeIntervalSinceReferenceDate:1];
[obj setValue:date forKey:@"dateCol"];
AssertChanged(r, [NSDate dateWithTimeIntervalSinceReferenceDate:0], date);
[obj setValue:nil forKey:@"dateCol"];
AssertChanged(r, date, NSNull.null);
}
{
KVORecorder r(self, obj, @"objectCol");
[obj setValue:obj forKey:@"objectCol"];
AssertChanged(r, NSNull.null, [self observableForObject:obj]);
[obj setValue:nil forKey:@"objectCol"];
AssertChanged(r, [self observableForObject:obj], NSNull.null);
}
{
KVORecorder r(self, obj, @"objectArray");
[obj setValue:obj.objectArray forKey:@"objectArray"];
r.refresh();
r.pop_front(); // asserts that there's something to pop
}
{
KVORecorder r(self, obj, @"optIntCol");
[obj setValue:@1 forKey:@"optIntCol"];
AssertChanged(r, NSNull.null, @1);
[obj setValue:nil forKey:@"optIntCol"];
AssertChanged(r, @1, NSNull.null);
}
{
KVORecorder r(self, obj, @"optFloatCol");
[obj setValue:@1.1f forKey:@"optFloatCol"];
AssertChanged(r, NSNull.null, @1.1f);
[obj setValue:nil forKey:@"optFloatCol"];
AssertChanged(r, @1.1f, NSNull.null);
}
{
KVORecorder r(self, obj, @"optDoubleCol");
[obj setValue:@1.1 forKey:@"optDoubleCol"];
AssertChanged(r, NSNull.null, @1.1);
[obj setValue:nil forKey:@"optDoubleCol"];
AssertChanged(r, @1.1, NSNull.null);
}
{
KVORecorder r(self, obj, @"optBoolCol");
[obj setValue:@YES forKey:@"optBoolCol"];
AssertChanged(r, NSNull.null, @YES);
[obj setValue:nil forKey:@"optBoolCol"];
AssertChanged(r, @YES, NSNull.null);
}
}
- (void)testAllPropertyTypesDynamic {
KVOObject *obj = [self createObject];
if (![obj respondsToSelector:@selector(setObject:forKeyedSubscript:)]) {
return;
}
{
KVORecorder r(self, obj, @"boolCol");
obj[@"boolCol"] = @YES;
AssertChanged(r, @NO, @YES);
}
{
KVORecorder r(self, obj, @"int16Col");
obj[@"int16Col"] = @0;
AssertChanged(r, @1, @0);
}
{
KVORecorder r(self, obj, @"int32Col");
obj[@"int32Col"] = @0;
AssertChanged(r, @2, @0);
}
{
KVORecorder r(self, obj, @"int64Col");
obj[@"int64Col"] = @0;
AssertChanged(r, @3, @0);
}
{
KVORecorder r(self, obj, @"floatCol");
obj[@"floatCol"] = @1.0f;
AssertChanged(r, @0, @1);
}
{
KVORecorder r(self, obj, @"doubleCol");
obj[@"doubleCol"] = @1.0;
AssertChanged(r, @0, @1);
}
{
KVORecorder r(self, obj, @"cBoolCol");
obj[@"cBoolCol"] = @YES;
AssertChanged(r, @NO, @YES);
}
{
KVORecorder r(self, obj, @"stringCol");
obj[@"stringCol"] = @"abc";
AssertChanged(r, @"", @"abc");
obj[@"stringCol"] = nil;
AssertChanged(r, @"abc", NSNull.null);
}
{
KVORecorder r(self, obj, @"binaryCol");
NSData *data = [@"abc" dataUsingEncoding:NSUTF8StringEncoding];
obj[@"binaryCol"] = data;
AssertChanged(r, NSData.data, data);
obj[@"binaryCol"] = nil;
AssertChanged(r, data, NSNull.null);
}
{
KVORecorder r(self, obj, @"dateCol");
NSDate *date = [NSDate dateWithTimeIntervalSinceReferenceDate:1];
obj[@"dateCol"] = date;
AssertChanged(r, [NSDate dateWithTimeIntervalSinceReferenceDate:0], date);
obj[@"dateCol"] = nil;
AssertChanged(r, date, NSNull.null);
}
{
KVORecorder r(self, obj, @"objectCol");
obj[@"objectCol"] = obj;
AssertChanged(r, NSNull.null, [self observableForObject:obj]);
obj[@"objectCol"] = nil;
AssertChanged(r, [self observableForObject:obj], NSNull.null);
}
{
KVORecorder r(self, obj, @"objectArray");
obj[@"objectArray"] = obj.objectArray;
r.refresh();
r.pop_front(); // asserts that there's something to pop
}
{
KVORecorder r(self, obj, @"optIntCol");
obj[@"optIntCol"] = @1;
AssertChanged(r, NSNull.null, @1);
obj[@"optIntCol"] = nil;
AssertChanged(r, @1, NSNull.null);
}
{
KVORecorder r(self, obj, @"optFloatCol");
obj[@"optFloatCol"] = @1.1f;
AssertChanged(r, NSNull.null, @1.1f);
obj[@"optFloatCol"] = nil;
AssertChanged(r, @1.1f, NSNull.null);
}
{
KVORecorder r(self, obj, @"optDoubleCol");
obj[@"optDoubleCol"] = @1.1;
AssertChanged(r, NSNull.null, @1.1);
obj[@"optDoubleCol"] = nil;
AssertChanged(r, @1.1, NSNull.null);
}
{
KVORecorder r(self, obj, @"optBoolCol");
obj[@"optBoolCol"] = @YES;
AssertChanged(r, NSNull.null, @YES);
obj[@"optBoolCol"] = nil;
AssertChanged(r, @YES, NSNull.null);
}
}
- (void)testArrayDiffs {
KVOLinkObject2 *obj = [self createLinkObject];
KVORecorder r(self, obj, @"array");
id mutator = [obj mutableArrayValueForKey:@"array"];
[mutator addObject:obj.obj];
AssertIndexChange(NSKeyValueChangeInsertion, [NSIndexSet indexSetWithIndex:0]);
[mutator addObject:obj.obj];
AssertIndexChange(NSKeyValueChangeInsertion, [NSIndexSet indexSetWithIndex:1]);
[mutator removeObjectAtIndex:0];
AssertIndexChange(NSKeyValueChangeRemoval, [NSIndexSet indexSetWithIndex:0]);
[mutator replaceObjectAtIndex:0 withObject:obj.obj];
AssertIndexChange(NSKeyValueChangeReplacement, [NSIndexSet indexSetWithIndex:0]);
NSMutableIndexSet *indexes = [NSMutableIndexSet new];
[indexes addIndex:0];
[indexes addIndex:2];
[mutator insertObjects:@[obj.obj, obj.obj] atIndexes:indexes];
AssertIndexChange(NSKeyValueChangeInsertion, indexes);
[mutator removeObjectsAtIndexes:indexes];
AssertIndexChange(NSKeyValueChangeRemoval, indexes);
if (![obj.array isKindOfClass:[NSArray class]]) {
// We deliberately diverge from NSMutableArray for `removeAllObjects` and
// `addObjectsFromArray:`, because generating a separate notification for
// each object added or removed is needlessly pessimal.
[mutator addObjectsFromArray:@[obj.obj, obj.obj]];
AssertIndexChange(NSKeyValueChangeInsertion, [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1, 2)]);
// NSArray sends multiple notifications for exchange, which we can't do
// on refresh
[mutator exchangeObjectAtIndex:0 withObjectAtIndex:1];
AssertIndexChange(NSKeyValueChangeReplacement, [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, 2)]);
// NSArray doesn't have move
[mutator moveObjectAtIndex:1 toIndex:0];
AssertIndexChange(NSKeyValueChangeReplacement, [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, 2)]);
[mutator removeLastObject];
AssertIndexChange(NSKeyValueChangeRemoval, [NSIndexSet indexSetWithIndex:2]);
[mutator removeAllObjects];
AssertIndexChange(NSKeyValueChangeRemoval, [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, 2)]);
}
}
- (void)testPrimitiveArrayDiffs {
KVOObject *obj = [self createObject];
KVORecorder r(self, obj, @"intArray");
id mutator = [obj mutableArrayValueForKey:@"intArray"];
[mutator addObject:@1];
AssertIndexChange(NSKeyValueChangeInsertion, [NSIndexSet indexSetWithIndex:0]);
[mutator addObject:@2];
AssertIndexChange(NSKeyValueChangeInsertion, [NSIndexSet indexSetWithIndex:1]);
[mutator removeObjectAtIndex:0];
AssertIndexChange(NSKeyValueChangeRemoval, [NSIndexSet indexSetWithIndex:0]);
[mutator replaceObjectAtIndex:0 withObject:@3];
AssertIndexChange(NSKeyValueChangeReplacement, [NSIndexSet indexSetWithIndex:0]);
NSMutableIndexSet *indexes = [NSMutableIndexSet new];
[indexes addIndex:0];
[indexes addIndex:2];
[mutator insertObjects:@[@4, @5] atIndexes:indexes];
AssertIndexChange(NSKeyValueChangeInsertion, indexes);
[mutator removeObjectsAtIndexes:indexes];
AssertIndexChange(NSKeyValueChangeRemoval, indexes);
if (![obj.intArray isKindOfClass:[NSArray class]]) {
// We deliberately diverge from NSMutableArray for `removeAllObjects` and
// `addObjectsFromArray:`, because generating a separate notification for
// each object added or removed is needlessly pessimal.
[mutator addObjectsFromArray:@[@6, @7]];
AssertIndexChange(NSKeyValueChangeInsertion, [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1, 2)]);
// NSArray sends multiple notifications for exchange, which we can't do
// on refresh
[mutator exchangeObjectAtIndex:0 withObjectAtIndex:1];
AssertIndexChange(NSKeyValueChangeReplacement, [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, 2)]);
// NSArray doesn't have move
[mutator moveObjectAtIndex:1 toIndex:0];
AssertIndexChange(NSKeyValueChangeReplacement, [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, 2)]);
[mutator removeLastObject];
AssertIndexChange(NSKeyValueChangeRemoval, [NSIndexSet indexSetWithIndex:2]);
[mutator removeAllObjects];
AssertIndexChange(NSKeyValueChangeRemoval, [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, 2)]);
}
}
- (void)testIgnoredProperty {
KVOObject *obj = [self createObject];
KVORecorder r(self, obj, @"ignored");
obj.ignored = 10;
AssertChanged(r, @0, @10);
}
- (void)testChangeEndOfKeyPath {
KVOLinkObject2 *obj = [self createLinkObject];
std::unique_ptr<KVORecorder> r;
@autoreleasepool {
r = std::make_unique<KVORecorder>(self, obj, @"obj.obj.boolCol");
}
obj.obj.obj.boolCol = YES;
AssertChanged(*r, @NO, @YES);
}
- (void)testChangeMiddleOfKeyPath {
KVOLinkObject2 *obj = [self createLinkObject];
KVOObject *oldObj = obj.obj.obj;
KVOObject *newObj = [self createObject];
newObj.boolCol = YES;
KVORecorder r(self, obj, @"obj.obj.boolCol");
obj.obj.obj = newObj;
AssertChanged(r, @NO, @YES);
newObj.boolCol = NO;
AssertChanged(r, @YES, @NO);
oldObj.boolCol = YES;
}
- (void)testNullifyMiddleOfKeyPath {
KVOLinkObject2 *obj = [self createLinkObject];
KVORecorder r(self, obj, @"obj.obj.boolCol");
obj.obj = nil;
AssertChanged(r, @NO, NSNull.null);
}
- (void)testChangeMiddleOfKeyPathToNonNil {
KVOLinkObject2 *obj = [self createLinkObject];
KVOLinkObject1 *obj2 = obj.obj;
obj.obj = nil;
obj2.obj.boolCol = YES;
KVORecorder r(self, obj, @"obj.obj.boolCol");
obj.obj = obj2;
AssertChanged(r, NSNull.null, @YES);
}
- (void)testArrayKVC {
KVOObject *obj = [self createObject];
[obj.objectArray addObject:obj];
KVORecorder r(self, obj, @"boolCol");
[obj.objectArray setValue:@YES forKey:@"boolCol"];
AssertChanged(r, @NO, @YES);
}
// RLMArray doesn't support @count at all
//- (void)testObserveArrayCount {
// KVOObject *obj = [self createObject];
// KVORecorder r(self, obj, @"objectArray.@count");
// id mutator = [obj mutableArrayValueForKey:@"objectArray"];
// [mutator addObject:obj];
// AssertChanged(r, @0, @1);
//}
@end
// Run tests on an unmanaged RLMObject instance
@interface KVOUnmanagedObjectTests : KVOTests
@end
@implementation KVOUnmanagedObjectTests
- (id)createObject {
static int pk = 0;
KVOObject *obj = [KVOObject new];
obj.pk = pk++;
obj.int16Col = 1;
obj.int32Col = 2;
obj.int64Col = 3;
obj.binaryCol = NSData.data;
obj.stringCol = @"";
obj.dateCol = [NSDate dateWithTimeIntervalSinceReferenceDate:0];
return obj;
}
- (id)createLinkObject {
static int pk = 0;
KVOLinkObject1 *obj1 = [KVOLinkObject1 new];
obj1.pk = pk++;
obj1.obj = [self createObject];
KVOLinkObject2 *obj2 = [KVOLinkObject2 new];
obj2.pk = pk++;
obj2.obj = obj1;
return obj2;
}
- (void)testAddToRealmAfterAddingObservers {
RLMRealm *realm = RLMRealm.defaultRealm;
[realm beginWriteTransaction];
KVOObject *obj = [self createObject];
{
KVORecorder r(self, obj, @"int32Col");
XCTAssertThrows([realm addObject:obj]);
}
XCTAssertNoThrow([realm addObject:obj]);
[realm cancelWriteTransaction];
}
- (void)testObserveInvalidArrayProperty {
KVOObject *obj = [self createObject];
XCTAssertThrows([obj.objectArray addObserver:self forKeyPath:@"self" options:0 context:0]);
XCTAssertNoThrow([obj.objectArray addObserver:self forKeyPath:RLMInvalidatedKey options:0 context:0]);
XCTAssertNoThrow([obj.objectArray removeObserver:self forKeyPath:RLMInvalidatedKey context:0]);
}
- (void)testUnregisteringViaAnAssociatedObject {
@autoreleasepool {
__attribute__((objc_precise_lifetime)) KVOObject *obj = [self createObject];
[obj addObserver:self forKeyPath:@"boolCol" options:0 context:0];
[KVOUnregisterHelper automaticallyUnregister:self object:obj keyPath:@"boolCol"];
}
// Throws if the unregistration doesn't succeed
}
@end
// Run tests on a managed object, modifying the actual object instance being
// observed
@interface KVOManagedObjectTests : KVOTests
@property (nonatomic, strong) RLMRealm *realm;
@end
@implementation KVOManagedObjectTests
- (void)setUp {
[super setUp];
_realm = [self getRealm];
[_realm beginWriteTransaction];
}
- (void)tearDown {
[self.realm cancelWriteTransaction];
self.realm = nil;
[super tearDown];
}
- (RLMRealm *)getRealm {
RLMRealmConfiguration *configuration = [[RLMRealmConfiguration alloc] init];
configuration.inMemoryIdentifier = @"test";
configuration.schemaMode = realm::SchemaMode::Additive;
return [RLMRealm realmWithConfiguration:configuration error:nil];
}
- (id)createObject {
static std::atomic<int> pk{0};
return [KVOObject createInRealm:_realm withValue:@[@(++pk),
@NO, @1, @2, @3, @0, @0, @NO, @"",
NSData.data, [NSDate dateWithTimeIntervalSinceReferenceDate:0]]];
}
- (id)createLinkObject {
static std::atomic<int> pk{0};
return [KVOLinkObject2 createInRealm:_realm withValue:@[@(++pk), @[@(++pk), [self createObject], @[]], @[]]];
}
- (void)testDeleteObservedObject {
KVOObject *obj = [self createObject];
KVORecorder r1(self, obj, @"boolCol");
KVORecorder r2(self, obj, RLMInvalidatedKey);
[self.realm deleteObject:obj];
AssertChanged(r2, @NO, @YES);
// should not crash
}
- (void)testDeleteMiddleOfKeyPath {
KVOLinkObject2 *obj = [self createLinkObject];
KVORecorder r(self, obj, @"obj.obj.boolCol");
[self.realm deleteObject:obj.obj];
AssertChanged(r, @NO, NSNull.null);
}
- (void)testDeleteParentOfObservedRLMArray {
KVOObject *obj = [self createObject];
KVORecorder r1(self, obj, @"objectArray");
KVORecorder r2(self, obj, @"objectArray.invalidated");
KVORecorder r3(self, obj.objectArray, RLMInvalidatedKey);
[self.realm deleteObject:obj];
AssertChanged(r2, @NO, @YES);
AssertChanged(r3, @NO, @YES);
}
- (void)testDeleteAllObjects {
KVOObject *obj = [self createObject];
KVORecorder r1(self, obj, @"boolCol");
KVORecorder r2(self, obj, RLMInvalidatedKey);
[self.realm deleteAllObjects];
AssertChanged(r2, @NO, @YES);
// should not crash
}
- (void)testClearTable {
KVOObject *obj = [self createObject];
KVORecorder r1(self, obj, @"boolCol");
KVORecorder r2(self, obj, RLMInvalidatedKey);
[self.realm deleteObjects:[KVOObject allObjectsInRealm:self.realm]];
AssertChanged(r2, @NO, @YES);
// should not crash
}
- (void)testClearQuery {
KVOObject *obj = [self createObject];
KVORecorder r1(self, obj, @"boolCol");
KVORecorder r2(self, obj, RLMInvalidatedKey);
[self.realm deleteObjects:[KVOObject objectsInRealm:self.realm where:@"TRUEPREDICATE"]];
AssertChanged(r2, @NO, @YES);
// should not crash
}
- (void)testClearLinkView {
KVOObject *obj = [self createObject];
KVOObject *obj2 = [self createObject];
[obj2.objectArray addObject:obj];
KVORecorder r1(self, obj, @"boolCol");
KVORecorder r2(self, obj, RLMInvalidatedKey);
[self.realm deleteObjects:obj2.objectArray];
AssertChanged(r2, @NO, @YES);
// should not crash
}
- (void)testCreateObserverAfterDealloc {
@autoreleasepool {
KVOObject *obj = [self createObject];
KVORecorder r(self, obj, @"boolCol");
obj.boolCol = YES;
AssertChanged(r, @NO, @YES);
}
@autoreleasepool {
KVOObject *obj = [self createObject];
KVORecorder r(self, obj, @"boolCol");
obj.boolCol = YES;
AssertChanged(r, @NO, @YES);
}
}
- (void)testDirectlyDeleteLinkedToObject {
KVOLinkObject2 *obj = [self createLinkObject];
KVOLinkObject1 *linked = obj.obj;
KVORecorder r(self, obj, @"obj");
KVORecorder r2(self, obj, @"obj.invalidated");
[self.realm deleteObject:linked];
if (NSDictionary *note = AssertNotification(r)) {
XCTAssertTrue([note[NSKeyValueChangeOldKey] isKindOfClass:[RLMObjectBase class]]);
XCTAssertEqualObjects(note[NSKeyValueChangeNewKey], NSNull.null);
}
AssertChanged(r2, @NO, NSNull.null);
}
- (void)testDeleteLinkedToObjectViaTableClear {
KVOLinkObject2 *obj = [self createLinkObject];
KVORecorder r(self, obj, @"obj");
KVORecorder r2(self, obj, @"obj.invalidated");
[self.realm deleteObjects:[KVOLinkObject1 allObjectsInRealm:self.realm]];
if (NSDictionary *note = AssertNotification(r)) {
XCTAssertTrue([note[NSKeyValueChangeOldKey] isKindOfClass:[RLMObjectBase class]]);
XCTAssertEqualObjects(note[NSKeyValueChangeNewKey], NSNull.null);
}
AssertChanged(r2, @NO, NSNull.null);
}
- (void)testDeleteLinkedToObjectViaQueryClear {
KVOLinkObject2 *obj = [self createLinkObject];
KVORecorder r(self, obj, @"obj");
KVORecorder r2(self, obj, @"obj.invalidated");
[self.realm deleteObjects:[KVOLinkObject1 objectsInRealm:self.realm where:@"TRUEPREDICATE"]];
if (NSDictionary *note = AssertNotification(r)) {
XCTAssertTrue([note[NSKeyValueChangeOldKey] isKindOfClass:[RLMObjectBase class]]);
XCTAssertEqualObjects(note[NSKeyValueChangeNewKey], NSNull.null);
}
AssertChanged(r2, @NO, NSNull.null);
}
- (void)testDeleteObjectInArray {
KVOLinkObject2 *obj = [self createLinkObject];
KVOLinkObject1 *linked = obj.obj;
[obj.array addObject:linked];
KVORecorder r(self, obj, @"array");
[self.realm deleteObject:linked];
AssertIndexChange(NSKeyValueChangeRemoval, [NSIndexSet indexSetWithIndex:0]);
}
- (void)testDeleteObjectsInArrayViaTableClear {
KVOLinkObject2 *obj = [self createLinkObject];
KVOLinkObject2 *obj2 = [self createLinkObject];
[obj.array addObject:obj.obj];
[obj.array addObject:obj.obj];
[obj.array addObject:obj2.obj];
KVORecorder r(self, obj, @"array");
[self.realm deleteObjects:[KVOLinkObject1 allObjectsInRealm:self.realm]];
AssertIndexChange(NSKeyValueChangeRemoval, ([NSIndexSet indexSetWithIndexesInRange:{0, 3}]));
}
- (void)testDeleteObjectsInArrayViaTableViewClear {
KVOLinkObject2 *obj = [self createLinkObject];
KVOLinkObject2 *obj2 = [self createLinkObject];
[obj.array addObject:obj.obj];
[obj.array addObject:obj.obj];
[obj.array addObject:obj2.obj];
KVORecorder r(self, obj, @"array");
RLMResults *results = [KVOLinkObject1 objectsInRealm:self.realm where:@"TRUEPREDICATE"];
[results lastObject];
[self.realm deleteObjects:results];
AssertIndexChange(NSKeyValueChangeRemoval, ([NSIndexSet indexSetWithIndexesInRange:{0, 3}]));
}
- (void)testDeleteObjectsInArrayViaQueryClear {
KVOLinkObject2 *obj = [self createLinkObject];
KVOLinkObject2 *obj2 = [self createLinkObject];
[obj.array addObject:obj.obj];
[obj.array addObject:obj.obj];
[obj.array addObject:obj2.obj];
KVORecorder r(self, obj, @"array");
[self.realm deleteObjects:[KVOLinkObject1 objectsInRealm:self.realm where:@"TRUEPREDICATE"]];
AssertIndexChange(NSKeyValueChangeRemoval, ([NSIndexSet indexSetWithIndexesInRange:{0, 3}]));
}
- (void)testObserveInvalidArrayProperty {
KVOObject *obj = [self createObject];
RLMArray *array = obj.objectArray;
XCTAssertThrows([array addObserver:self forKeyPath:@"self" options:0 context:0]);
XCTAssertNoThrow([array addObserver:self forKeyPath:RLMInvalidatedKey options:0 context:0]);
XCTAssertNoThrow([array removeObserver:self forKeyPath:RLMInvalidatedKey context:0]);
}
- (void)testInvalidOperationOnObservedArray {
KVOLinkObject2 *obj = [self createLinkObject];
KVOLinkObject1 *linked = obj.obj;
[obj.array addObject:linked];
KVORecorder r(self, obj, @"array");
XCTAssertThrows([obj.array exchangeObjectAtIndex:2 withObjectAtIndex:3]);
// A KVO notification is still sent to observers on the same thread since we
// can't cancel willChange, but the data is not very meaningful so don't check it
if (!self.collapsesNotifications) {
AssertNotification(r);
}
}
@end
// Mutate a different accessor backed by the same row as the accessor being observed
@interface KVOMultipleAccessorsTests : KVOManagedObjectTests
@end
@implementation KVOMultipleAccessorsTests
- (id)observableForObject:(id)value {
if (RLMObject *obj = RLMDynamicCast<RLMObject>(value)) {
RLMObject *copy = RLMCreateManagedAccessor(obj.objectSchema.accessorClass, obj.realm, obj->_info);
copy->_row = obj->_row;
return copy;
}
else if (RLMArray *array = RLMDynamicCast<RLMArray>(value)) {
return array;
}
else {
XCTFail(@"unsupported type");
return nil;
}
}
- (void)testIgnoredProperty {
// ignored properties do not notify other accessors for the same row
}
- (void)testAddOrUpdate {
KVOObject *obj = [self createObject];
KVOObject *obj2 = [[KVOObject alloc] initWithValue:obj];
KVORecorder r(self, obj, @"boolCol");
obj2.boolCol = true;
XCTAssertTrue(r.empty());
[self.realm addOrUpdateObject:obj2];
AssertChanged(r, @NO, @YES);
}
- (void)testCreateOrUpdate {
KVOObject *obj = [self createObject];
KVOObject *obj2 = [[KVOObject alloc] initWithValue:obj];
KVORecorder r(self, obj, @"boolCol");
obj2.boolCol = true;
XCTAssertTrue(r.empty());
[KVOObject createOrUpdateInRealm:self.realm withValue:obj2];
AssertChanged(r, @NO, @YES);
}
// The following tests aren't really multiple-accessor-specific, but they're
// conceptually similar and don't make sense in the multiple realm instances case
- (void)testCancelWriteTransactionWhileObservingNewObject {
KVOObject *obj = [self createObject];
KVORecorder r(self, obj, RLMInvalidatedKey);
KVORecorder r2(self, obj, @"boolCol");
[self.realm cancelWriteTransaction];
AssertChanged(r, @NO, @YES);
r2.pop_front();
[self.realm beginWriteTransaction];
}
- (void)testCancelWriteTransactionWhileObservingChangedProperty {
KVOObject *obj = [self createObject];
[self.realm commitWriteTransaction];
[self.realm beginWriteTransaction];
obj.boolCol = YES;
KVORecorder r(self, obj, @"boolCol");
[self.realm cancelWriteTransaction];
AssertChanged(r, @YES, @NO);
[self.realm beginWriteTransaction];
}
- (void)testCancelWriteTransactionWhileObservingLinkToExistingObject {
KVOObject *obj = [self createObject];
KVOObject *obj2 = [self createObject];
[self.realm commitWriteTransaction];
[self.realm beginWriteTransaction];
obj.objectCol = obj2;
KVORecorder r(self, obj, @"objectCol");
[self.realm cancelWriteTransaction];
AssertChanged(r, obj2, NSNull.null);
[self.realm beginWriteTransaction];
}
- (void)testCancelWriteTransactionWhileObservingLinkToNewObject {
KVOObject *obj = [self createObject];
[self.realm commitWriteTransaction];
[self.realm beginWriteTransaction];
obj.objectCol = [self createObject];
KVORecorder r(self, obj, @"objectCol");
[self.realm cancelWriteTransaction];
if (NSDictionary *note = AssertNotification(r)) {
XCTAssertTrue([note[NSKeyValueChangeOldKey] isKindOfClass:[RLMObjectBase class]]);
XCTAssertEqualObjects(note[NSKeyValueChangeNewKey], NSNull.null);
}
[self.realm beginWriteTransaction];
}
- (void)testCancelWriteTransactionWhileObservingNewObjectLinkingToNewObject {
KVOObject *obj = [self createObject];
obj.objectCol = [self createObject];
KVORecorder r(self, obj, RLMInvalidatedKey);
KVORecorder r2(self, obj, @"objectCol");
KVORecorder r3(self, obj, @"objectCol.boolCol");
[self.realm cancelWriteTransaction];
AssertChanged(r, @NO, @YES);
[self.realm beginWriteTransaction];
}
- (void)testCancelWriteWithArrayChanges {
KVOObject *obj = [self createObject];
[obj.objectArray addObject:obj];
[self.realm commitWriteTransaction];
[self.realm beginWriteTransaction];
{
[obj.objectArray addObject:obj];
KVORecorder r(self, obj, @"objectArray");
[self.realm cancelWriteTransaction];
[self.realm beginWriteTransaction];
AssertIndexChange(NSKeyValueChangeRemoval, [NSIndexSet indexSetWithIndex:1]);
}
{
[obj.objectArray removeLastObject];
KVORecorder r(self, obj, @"objectArray");
[self.realm cancelWriteTransaction];
[self.realm beginWriteTransaction];
AssertIndexChange(NSKeyValueChangeInsertion, [NSIndexSet indexSetWithIndex:0]);
}
{
obj.objectArray[0] = obj;
KVORecorder r(self, obj, @"objectArray");
[self.realm cancelWriteTransaction];
[self.realm beginWriteTransaction];
AssertIndexChange(NSKeyValueChangeReplacement, [NSIndexSet indexSetWithIndex:0]);
}
// test batching with multiple items changed
[obj.objectArray addObject:obj];
[self.realm commitWriteTransaction];
[self.realm beginWriteTransaction];
{
[obj.objectArray removeAllObjects];
KVORecorder r(self, obj, @"objectArray");
[self.realm cancelWriteTransaction];
[self.realm beginWriteTransaction];
AssertIndexChange(NSKeyValueChangeInsertion, ([NSIndexSet indexSetWithIndexesInRange:{0, 2}]));
}
{
[obj.objectArray removeLastObject];
[obj.objectArray removeLastObject];
KVORecorder r(self, obj, @"objectArray");
[self.realm cancelWriteTransaction];
[self.realm beginWriteTransaction];
AssertIndexChange(NSKeyValueChangeInsertion, ([NSIndexSet indexSetWithIndexesInRange:{0, 2}]));
}
{
[obj.objectArray insertObject:obj atIndex:1];
[obj.objectArray insertObject:obj atIndex:0];
KVORecorder r(self, obj, @"objectArray");
[self.realm cancelWriteTransaction];
[self.realm beginWriteTransaction];
NSMutableIndexSet *expected = [NSMutableIndexSet new];
[expected addIndex:0];
[expected addIndex:2]; // shifted due to inserting at 0 after 1
AssertIndexChange(NSKeyValueChangeRemoval, expected);
}
{
[obj.objectArray insertObject:obj atIndex:0];
[obj.objectArray removeLastObject];
KVORecorder r(self, obj, @"objectArray");
[self.realm cancelWriteTransaction];
[self.realm beginWriteTransaction];
AssertChanged(r, obj.objectArray, obj.objectArray);
}
}
- (void)testCancelWriteWithPrimitiveArrayChanges {
KVOObject *obj = [self createObject];
[obj.intArray addObject:@1];
[self.realm commitWriteTransaction];
[self.realm beginWriteTransaction];
{
[obj.intArray addObject:@2];
KVORecorder r(self, obj, @"intArray");
[self.realm cancelWriteTransaction];
[self.realm beginWriteTransaction];
AssertIndexChange(NSKeyValueChangeRemoval, [NSIndexSet indexSetWithIndex:1]);
}
{
[obj.intArray removeLastObject];
KVORecorder r(self, obj, @"intArray");
[self.realm cancelWriteTransaction];
[self.realm beginWriteTransaction];
AssertIndexChange(NSKeyValueChangeInsertion, [NSIndexSet indexSetWithIndex:0]);
}
{
obj.intArray[0] = @3;
KVORecorder r(self, obj, @"intArray");
[self.realm cancelWriteTransaction];
[self.realm beginWriteTransaction];
AssertIndexChange(NSKeyValueChangeReplacement, [NSIndexSet indexSetWithIndex:0]);
}
// test batching with multiple items changed
[obj.intArray addObject:@4];
[self.realm commitWriteTransaction];
[self.realm beginWriteTransaction];
{
[obj.intArray removeAllObjects];
KVORecorder r(self, obj, @"intArray");
[self.realm cancelWriteTransaction];
[self.realm beginWriteTransaction];
AssertIndexChange(NSKeyValueChangeInsertion, ([NSIndexSet indexSetWithIndexesInRange:{0, 2}]));
}
{
[obj.intArray removeLastObject];
[obj.intArray removeLastObject];
KVORecorder r(self, obj, @"intArray");
[self.realm cancelWriteTransaction];
[self.realm beginWriteTransaction];
AssertIndexChange(NSKeyValueChangeInsertion, ([NSIndexSet indexSetWithIndexesInRange:{0, 2}]));
}
{
[obj.intArray insertObject:@5 atIndex:1];
[obj.intArray insertObject:@6 atIndex:0];
KVORecorder r(self, obj, @"intArray");
[self.realm cancelWriteTransaction];
[self.realm beginWriteTransaction];
NSMutableIndexSet *expected = [NSMutableIndexSet new];
[expected addIndex:0];
[expected addIndex:2]; // shifted due to inserting at 0 after 1
AssertIndexChange(NSKeyValueChangeRemoval, expected);
}
{
[obj.intArray insertObject:@7 atIndex:0];
[obj.intArray removeLastObject];
KVORecorder r(self, obj, @"intArray");
[self.realm cancelWriteTransaction];
[self.realm beginWriteTransaction];
AssertChanged(r, obj.intArray, obj.intArray);
}
}
- (void)testCancelWriteWithLinkedObjectedRemoved {
KVOLinkObject2 *obj = [self createLinkObject];
[obj.array addObject:obj.obj];
[self.realm commitWriteTransaction];
[self.realm beginWriteTransaction];
{
[self.realm deleteObject:obj.obj];
KVORecorder r(self, obj, @"array");
KVORecorder r2(self, obj, @"obj");
[self.realm cancelWriteTransaction];
[self.realm beginWriteTransaction];
AssertIndexChange(NSKeyValueChangeInsertion, [NSIndexSet indexSetWithIndex:0]);
AssertChanged(r2, NSNull.null, [KVOLinkObject1 allObjectsInRealm:self.realm].firstObject);
}
{
[self.realm deleteObjects:[KVOLinkObject1 allObjectsInRealm:self.realm]];
KVORecorder r(self, obj, @"array");
KVORecorder r2(self, obj, @"obj");
[self.realm cancelWriteTransaction];
[self.realm beginWriteTransaction];
AssertIndexChange(NSKeyValueChangeInsertion, [NSIndexSet indexSetWithIndex:0]);
AssertChanged(r2, NSNull.null, [KVOLinkObject1 allObjectsInRealm:self.realm].firstObject);
}
{
[self.realm deleteObjects:obj.array];
KVORecorder r(self, obj, @"array");
KVORecorder r2(self, obj, @"obj");
[self.realm cancelWriteTransaction];
[self.realm beginWriteTransaction];
AssertIndexChange(NSKeyValueChangeInsertion, [NSIndexSet indexSetWithIndex:0]);
AssertChanged(r2, NSNull.null, [KVOLinkObject1 allObjectsInRealm:self.realm].firstObject);
}
}
- (void)testInvalidateRealm {
KVOObject *obj = [self createObject];
[self.realm commitWriteTransaction];
KVORecorder r1(self, obj, RLMInvalidatedKey);
KVORecorder r2(self, obj, @"objectArray.invalidated");
[self.realm invalidate];
[self.realm beginWriteTransaction];
AssertChanged(r1, @NO, @YES);
AssertChanged(r2, @NO, @YES);
}
- (void)testRenamedProperties {
auto obj = [RenamedProperties1 createInRealm:self.realm withValue:@[@1, @"a"]];
[self.realm commitWriteTransaction];
[self.realm beginWriteTransaction];
KVORecorder r(self, obj, @"propA");
obj.propA = 2;
AssertChanged(r, @1, @2);
obj[@"propA"] = @3;
AssertChanged(r, @2, @3);
[obj setValue:@4 forKey:@"propA"];
AssertChanged(r, @3, @4);
// Only rollback will notify objects of different types with the same table,
// not direct modification. Probably not worth fixing this.
RenamedProperties2 *obj2 = [RenamedProperties2 allObjectsInRealm:self.realm].firstObject;
KVORecorder r2(self, obj2, @"propC");
[self.realm cancelWriteTransaction];
[self.realm beginWriteTransaction];
AssertChanged(r, @4, @1);
AssertChanged(r2, @4, @1);
}
@end
// Observing an object from a different RLMRealm instance backed by the same
// row as the managed object being mutated
@interface KVOMultipleRealmsTests : KVOManagedObjectTests
@property RLMRealm *secondaryRealm;
@end
@implementation KVOMultipleRealmsTests
- (void)setUp {
[super setUp];
RLMRealmConfiguration *config = self.realm.configuration;
config.cache = false;
self.secondaryRealm = [RLMRealm realmWithConfiguration:config error:nil];
}
- (void)tearDown {
self.secondaryRealm = nil;
[super tearDown];
}
- (id)observableForObject:(id)value {
[self.realm commitWriteTransaction];
[self.realm beginWriteTransaction];
[self.secondaryRealm refresh];
if (RLMObject *obj = RLMDynamicCast<RLMObject>(value)) {
RLMObject *copy = RLMCreateManagedAccessor(obj.objectSchema.accessorClass, self.secondaryRealm,
&self.secondaryRealm->_info[obj.objectSchema.className]);
copy->_row = (*copy->_info->table())[obj->_row.get_index()];
return copy;
}
else if (RLMArray *array = RLMDynamicCast<RLMArray>(value)) {
return array;
}
else {
XCTFail(@"unsupported type");
return nil;
}
}
- (bool)collapsesNotifications {
return true;
}
- (void)testIgnoredProperty {
// ignored properties do not notify other accessors for the same row
}
- (void)testBatchArrayChanges {
KVOObject *obj = [self createObject];
[obj.objectArray addObject:obj];
[obj.objectArray addObject:obj];
[obj.objectArray addObject:obj];
{
KVORecorder r(self, obj, @"objectArray");
[obj.objectArray insertObject:obj atIndex:1];
[obj.objectArray insertObject:obj atIndex:0];
NSMutableIndexSet *expected = [NSMutableIndexSet new];
[expected addIndex:0];
[expected addIndex:2]; // shifted due to inserting at 0 after 1
AssertIndexChange(NSKeyValueChangeInsertion, expected);
}
{
KVORecorder r(self, obj, @"objectArray");
[obj.objectArray removeObjectAtIndex:3];
[obj.objectArray removeObjectAtIndex:3];
AssertIndexChange(NSKeyValueChangeRemoval, ([NSIndexSet indexSetWithIndexesInRange:{3, 2}]));
}
{
KVORecorder r(self, obj, @"objectArray");
[obj.objectArray removeObjectAtIndex:0];
[obj.objectArray removeAllObjects];
AssertIndexChange(NSKeyValueChangeRemoval, ([NSIndexSet indexSetWithIndexesInRange:{0, 3}]));
}
[obj.objectArray addObject:obj];
{
KVORecorder r(self, obj, @"objectArray");
[obj.objectArray addObject:obj];
[obj.objectArray removeAllObjects];
AssertIndexChange(NSKeyValueChangeRemoval, [NSIndexSet indexSetWithIndex:0]);
}
[obj.objectArray addObject:obj];
{
KVORecorder r(self, obj, @"objectArray");
obj.objectArray[0] = obj;
[obj.objectArray removeAllObjects];
AssertIndexChange(NSKeyValueChangeRemoval, [NSIndexSet indexSetWithIndex:0]);
}
}
- (void)testOrderedErase {
NSMutableArray *objects = [NSMutableArray arrayWithCapacity:10];
for (int i = 0; i < 10; ++i) @autoreleasepool {
[objects addObject:[ObjectWithNoLinksToOrFrom createInRealm:self.realm withValue:@[@(i)]]];
}
// deleteObject: always uses move_last_over(), but TableView::clear() uses
// erase() if there's no links
auto deleteObject = ^(int value) {
[self.realm deleteObjects:[ObjectWithNoLinksToOrFrom objectsInRealm:self.realm where:@"value = %d", value]];
};
{ // delete object before observed, then observed
KVORecorder r(self, objects[2], @"invalidated");
deleteObject(1);
deleteObject(2);
AssertChanged(r, @NO, @YES);
}
{ // delete object after observed, then observed
KVORecorder r(self, objects[3], @"invalidated");
deleteObject(4);
deleteObject(3);
AssertChanged(r, @NO, @YES);
}
{ // delete observed, then object before observed
KVORecorder r(self, objects[6], @"invalidated");
deleteObject(6);
deleteObject(5);
AssertChanged(r, @NO, @YES);
}
{ // delete observed, then object after observed
KVORecorder r(self, objects[7], @"invalidated");
deleteObject(7);
deleteObject(8);
AssertChanged(r, @NO, @YES);
}
}
- (void)testInsertNewTables {
KVOObject *obj = [self createObject];
KVORecorder r1(self, obj, @"boolCol");
KVORecorder r2(self, obj, @"int32Col");
obj.boolCol = YES;
// Add tables before the observed one so that the observed one's index changes
realm::Group &group = self.realm->_realm->read_group();
realm::TableRef table1 = group.insert_table(5, "new table");
realm::TableRef table2 = group.insert_table(0, "new table 2");
table1->add_column(realm::type_Int, "col");
table2->add_column(realm::type_Int, "col");
obj.int32Col = 3;
AssertChanged(r1, @NO, @YES);
AssertChanged(r2, @2, @3);
}
- (void)testInsertNewColumns {
KVOObject *obj = [self createObject];
KVORecorder r1(self, obj, @"boolCol");
KVORecorder r2(self, obj, @"int32Col");
auto ndx = obj->_info->tableColumn(@"int32Col");
// Add a column before the observed one so that the observed one's index changes
obj.boolCol = YES;
auto& table = *obj->_info->table();
table.insert_column(0, realm::type_Binary, "new col");
table.insert_column(ndx, realm::type_Binary, "new col 2");
obj->_row.set_int(ndx + 2, 3); // can't use the accessor after a local schema change
AssertChanged(r1, @NO, @YES);
AssertChanged(r2, @2, @3);
}
- (void)testShiftObservedColumnBeforeChange {
KVOObject *obj = [self createObject];
auto ndx = obj->_info->tableColumn(@"boolCol");
KVORecorder r(self, obj, @"boolCol");
obj->_info->table()->insert_column(0, realm::type_Binary, "new col");
obj->_row.set_bool(ndx + 1, true); // can't use the accessor after a local schema change
AssertChanged(r, @NO, @YES);
}
- (void)testShiftObservedColumnAfterChange {
KVOObject *obj = [self createObject];
KVORecorder r(self, obj, @"boolCol");
obj.boolCol = YES;
obj->_info->table()->insert_column(0, realm::type_Binary, "new col");
AssertChanged(r, @NO, @YES);
}
- (void)testSwapRowsIsNotAChange {
KVOObject *obj = [self createObject];
[self createObject];
KVORecorder r(self, obj, @"boolCol");
obj->_info->table()->swap_rows(0, 1);
r.refresh();
XCTAssertTrue(r.empty());
}
- (void)testSwapRowsBeforeChange {
KVOObject *obj = [self createObject];
[self createObject];
KVORecorder r(self, obj, @"boolCol");
obj->_info->table()->swap_rows(0, 1);
obj.boolCol = YES;
AssertChanged(r, @NO, @YES);
}
- (void)testSwapRowsAfterChange {
KVOObject *obj = [self createObject];
[self createObject];
KVORecorder r(self, obj, @"boolCol");
obj.boolCol = YES;
obj->_info->table()->swap_rows(0, 1);
AssertChanged(r, @NO, @YES);
}
- (void)testSwapRowsBeforeArrayChange {
KVOObject *obj = [self createObject];
[self createObject];
KVORecorder r(self, obj, @"objectArray");
obj->_info->table()->swap_rows(0, 1);
[obj.objectArray addObject:obj];
AssertIndexChange(NSKeyValueChangeInsertion, [NSIndexSet indexSetWithIndex:0]);
}
- (void)testSwapRowsAfterArrayChange {
KVOObject *obj = [self createObject];
[self createObject];
KVORecorder r(self, obj, @"objectArray");
[obj.objectArray addObject:obj];
obj->_info->table()->swap_rows(0, 1);
AssertIndexChange(NSKeyValueChangeInsertion, [NSIndexSet indexSetWithIndex:0]);
}
@end
// Test with the table column order not matching the order of the properties
@interface KVOManagedObjectWithReorderedPropertiesTests : KVOManagedObjectTests
@end
@implementation KVOManagedObjectWithReorderedPropertiesTests
- (RLMRealm *)getRealm {
// Initialize the file with the properties in reverse order, then re-open
// with it in the normal order while the reversed one is still open (as
// otherwise it'll recreate the file due to being in-memory)
RLMSchema *schema = [RLMSchema new];
schema.objectSchema = @[[self reverseProperties:KVOObject.sharedSchema],
[self reverseProperties:KVOLinkObject1.sharedSchema],
[self reverseProperties:KVOLinkObject2.sharedSchema]];
RLMRealmConfiguration *configuration = [[RLMRealmConfiguration alloc] init];
configuration.cache = false;
configuration.inMemoryIdentifier = @"test";
configuration.customSchema = schema;
RLMRealm *reversedRealm = [RLMRealm realmWithConfiguration:configuration error:nil];
configuration.customSchema = nil;
RLMRealm *realm = [RLMRealm realmWithConfiguration:configuration error:nil];
XCTAssertNotEqualObjects(realm.schema, reversedRealm.schema);
return realm;
}
- (RLMObjectSchema *)reverseProperties:(RLMObjectSchema *)source {
RLMObjectSchema *objectSchema = [source copy];
objectSchema.properties = objectSchema.properties.reverseObjectEnumerator.allObjects;
return objectSchema;
}
@end