diff --git a/Source/GNUmakefile b/Source/GNUmakefile index d7c2fb59a..ccc956b95 100644 --- a/Source/GNUmakefile +++ b/Source/GNUmakefile @@ -254,7 +254,6 @@ NSJSONSerialization.m \ NSKeyedArchiver.m \ NSKeyedUnarchiver.m \ NSKeyValueCoding.m \ -NSKeyValueObserving.m \ NSLengthFormatter.m \ NSLinguisticTagger.m \ NSLocale.m \ @@ -358,6 +357,18 @@ NSZone.m \ externs.m \ objc-load.m +# We have two implementations for Key Value Observing. +# One highly-optimised one that depends on libobjc2 +# and the original implementation. +ifeq ($(OBJC_RUNTIME_LIB), ng) + BASE_MFILES += \ + NSKVOSupport.m \ + NSKVOSwizzling.m +else + BASE_MFILES += \ + NSKeyValueObserving.m +endif + ifeq ($(OBJC_RUNTIME_LIB), ng) BASE_MFILES += \ NSKeyValueCoding+Caching.m diff --git a/Source/GSAtomic.h b/Source/GSAtomic.h index 81687182d..290e55994 100644 --- a/Source/GSAtomic.h +++ b/Source/GSAtomic.h @@ -19,9 +19,9 @@ * Use native C11 atomic operations. _Atomic() should be defined by the * compiler. */ -#define atomic_load_explicit(object, order) \ +#define gs_atomic_load_explicit(object, order) \ __c11_atomic_load(object, order) -#define atomic_store_explicit(object, desired, order) \ +#define gs_atomic_store_explicit(object, desired, order) \ __c11_atomic_store(object, desired, order) #else @@ -33,7 +33,7 @@ #define _Atomic(T) struct { T volatile __val; } #if __has_builtin(__sync_swap) /* Clang provides a full-barrier atomic exchange - use it if available. */ -#define atomic_exchange_explicit(object, desired, order) \ +#define gs_atomic_exchange_explicit(object, desired, order) \ ((void)(order), __sync_swap(&(object)->__val, desired)) #else /* @@ -41,7 +41,7 @@ * practice it is usually a full barrier) so we need an explicit barrier before * it. */ -#define atomic_exchange_explicit(object, desired, order) \ +#define gs_atomic_exchange_explicit(object, desired, order) \ __extension__ ({ \ __typeof__(object) __o = (object); \ __typeof__(desired) __d = (desired); \ @@ -50,10 +50,10 @@ __extension__ ({ \ __sync_lock_test_and_set(&(__o)->__val, __d); \ }) #endif -#define atomic_load_explicit(object, order) \ +#define gs_atomic_load_explicit(object, order) \ ((void)(order), __sync_fetch_and_add(&(object)->__val, 0)) -#define atomic_store_explicit(object, desired, order) \ - ((void)atomic_exchange_explicit(object, desired, order)) +#define gs_atomic_store_explicit(object, desired, order) \ + ((void)gs_atomic_exchange_explicit(object, desired, order)) #endif @@ -64,9 +64,9 @@ __extension__ ({ \ /* * Convenience functions. */ -#define atomic_load(object) \ - atomic_load_explicit(object, __ATOMIC_SEQ_CST) -#define atomic_store(object, desired) \ - atomic_store_explicit(object, desired, __ATOMIC_SEQ_CST) +#define gs_atomic_load(object) \ + gs_atomic_load_explicit(object, __ATOMIC_SEQ_CST) +#define gs_atomic_store(object, desired) \ + gs_atomic_store_explicit(object, desired, __ATOMIC_SEQ_CST) #endif // _GSAtomic_h_ diff --git a/Source/GSPThread.h b/Source/GSPThread.h index 884fbff21..1b82929e2 100644 --- a/Source/GSPThread.h +++ b/Source/GSPThread.h @@ -61,12 +61,18 @@ typedef CONDITION_VARIABLE gs_cond_t; #define GS_COND_BROADCAST(cond) WakeAllConditionVariable(&(cond)) /* Pthread-like locking primitives defined in NSLock.m */ +#ifdef __cplusplus +extern "C" { +#endif void gs_mutex_init(gs_mutex_t *l, gs_mutex_attr_t attr); int gs_mutex_lock(gs_mutex_t *l); int gs_mutex_trylock(gs_mutex_t *l); int gs_mutex_unlock(gs_mutex_t *l); int gs_cond_wait(gs_cond_t *cond, gs_mutex_t *mutex); int gs_cond_timedwait(gs_cond_t *cond, gs_mutex_t *mutex, DWORD millisecs); +#ifdef __cplusplus +} +#endif /* * Threading primitives. diff --git a/Source/NSKVOInternal.h b/Source/NSKVOInternal.h new file mode 100644 index 000000000..7748f0def --- /dev/null +++ b/Source/NSKVOInternal.h @@ -0,0 +1,129 @@ +/** + NSKVOInternal.h + + Copyright (C) 2024 Free Software Foundation, Inc. + + Written by: Hugo Melder + Date: June 2024 + + Based on WinObjC KVO tests by Microsoft Corporation. + + This file is part of GNUStep-base + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + If you are interested in a warranty or support for this source code, + contact Scott Christley for more information. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free + Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110 USA. +*/ +/** + Copyright (c) Microsoft. All rights reserved. + + This code is licensed under the MIT License (MIT). + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +/* This Key Value Observing Implementation is tied to libobjc2 */ + +#import +#import +#import +#import +#import +#import +#import + +#if defined(__OBJC2__) + +#import "GSPThread.h" + +#define NS_COLLECTION_THROW_ILLEGAL_KVO(keyPath) \ + do \ + { \ + [NSException \ + raise: NSInvalidArgumentException \ + format: @"-[%s %s] is not supported. Key path: %@", \ + object_getClassName(self), sel_getName(_cmd), keyPath]; \ + } while (false) + +@class _NSKVOKeypathObserver; + +@interface _NSKVOKeyObserver : NSObject +- (instancetype)initWithObject:(id)object + keypathObserver:(_NSKVOKeypathObserver *)keypathObserver + key:(NSString *)key + restOfKeypath:(NSString *)restOfKeypath + affectedObservers:(NSArray *)affectedObservers; +@property (nonatomic, retain) _NSKVOKeypathObserver *keypathObserver; +@property (nonatomic, retain) _NSKVOKeyObserver *restOfKeypathObserver; +@property (nonatomic, retain) NSArray *dependentObservers; +@property (nonatomic, assign) id object; +@property (nonatomic, copy) NSString *key; +@property (nonatomic, copy) NSString *restOfKeypath; +@property (nonatomic, retain) NSArray *affectedObservers; +@property (nonatomic, assign) BOOL root; +@property (nonatomic, readonly) BOOL isRemoved; +@end + +@interface _NSKVOKeypathObserver : NSObject +- (instancetype)initWithObject:(id)object + observer:(id)observer + keyPath:(NSString *)keypath + options:(NSKeyValueObservingOptions)options + context:(void *)context; +@property (nonatomic, assign) id object; +@property (nonatomic, assign) id observer; +@property (nonatomic, copy) NSString *keypath; +@property (nonatomic, assign) NSKeyValueObservingOptions options; +@property (nonatomic, assign) void *context; + +@property (atomic, retain) NSMutableDictionary *pendingChange; +@end + +@interface _NSKVOObservationInfo : NSObject +{ + NSMutableDictionary *> + *_keyObserverMap; + NSInteger _dependencyDepth; + NSMutableSet *_existingDependentKeys; + gs_mutex_t _lock; +} +- (instancetype)init; +- (NSArray *)observersForKey:(NSString *)key; +- (void)addObserver:(_NSKVOKeyObserver *)observer; +@end + +// From NSKVOSwizzling +void +_NSKVOEnsureKeyWillNotify(id object, NSString *key); + +#endif + +/* Implementation in NSKVOSupport.m for ObjC2 and NSKeyValueObserving + * respectively + */ +@interface +NSObject (NSKeyValueObservingPrivate) +- (void)_notifyObserversOfChangeForKey:(NSString *)key + oldValue:(id)oldValue + newValue:(id)newValue; +@end diff --git a/Source/NSKVOSupport.m b/Source/NSKVOSupport.m new file mode 100644 index 000000000..9dd31562c --- /dev/null +++ b/Source/NSKVOSupport.m @@ -0,0 +1,1256 @@ +/** + NSKVOSupport.m + + Copyright (C) 2024 Free Software Foundation, Inc. + + Written by: Hugo Melder + Date: June 2024 + + Based on WinObjC KVO tests by Microsoft Corporation. + + This file is part of GNUStep-base + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + If you are interested in a warranty or support for this source code, + contact Scott Christley for more information. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free + Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110 USA. +*/ +/** + Copyright (c) Microsoft. All rights reserved. + + This code is licensed under the MIT License (MIT). + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +/* This Key Value Observing Implementation is tied to libobjc2 */ + +#import "common.h" +#import "NSKVOInternal.h" +#import +#import + +#import + +typedef void (^DispatchChangeBlock)(_NSKVOKeyObserver *); + +NSString * +_NSKVCSplitKeypath(NSString *keyPath, NSString *__autoreleasing *pRemainder) +{ + NSRange result = [keyPath rangeOfString:@"."]; + if (keyPath.length > 0 && result.location != NSNotFound) + { + *pRemainder = [keyPath substringFromIndex:result.location + 1]; + return [keyPath substringToIndex:result.location]; + } + *pRemainder = nil; + return keyPath; +} + +#pragma region Key Observer +@interface +_NSKVOKeyObserver () +{ + _Atomic(BOOL) _isRemoved; +} +@end + +@implementation _NSKVOKeyObserver +- (instancetype)initWithObject: (id)object + keypathObserver: (_NSKVOKeypathObserver *)keypathObserver + key: (NSString *)key + restOfKeypath: (NSString *)restOfKeypath + affectedObservers: (NSArray *)affectedObservers +{ + if (self = [super init]) + { + _object = object; + _keypathObserver = [keypathObserver retain]; + _key = [key copy]; + _restOfKeypath = [restOfKeypath copy]; + _affectedObservers = [affectedObservers copy]; + } + return self; +} + +- (void)dealloc +{ + [_keypathObserver release]; + [_key release]; + [_restOfKeypath release]; + [_dependentObservers release]; + [_restOfKeypathObserver release]; + [_affectedObservers release]; + [super dealloc]; +} + +- (BOOL)isRemoved +{ + return _isRemoved; +} + +- (void)setIsRemoved: (BOOL)removed +{ + _isRemoved = removed; +} +@end +#pragma endregion + +#pragma region Keypath Observer +@interface +_NSKVOKeypathObserver () +{ + _Atomic(int) _changeDepth; +} +@end + +@implementation _NSKVOKeypathObserver +- (instancetype) initWithObject: (id)object + observer: (id)observer + keyPath: (NSString *)keypath + options: (NSKeyValueObservingOptions)options + context: (void *)context +{ + if (self = [super init]) + { + _object = object; + _observer = observer; + _keypath = [keypath copy]; + _options = options; + _context = context; + } + return self; +} + +- (void) dealloc +{ + [_keypath release]; + [_pendingChange release]; + [super dealloc]; +} + +- (id) observer +{ + return _observer; +} + +- (BOOL) pushWillChange +{ + return atomic_fetch_add(&_changeDepth, 1) == 0; +} + +- (BOOL) popDidChange +{ + return atomic_fetch_sub(&_changeDepth, 1) == 1; +} +@end +#pragma endregion + +#pragma region Object - level Observation Info +@implementation _NSKVOObservationInfo +- (instancetype) init +{ + if (self = [super init]) + { + _keyObserverMap = [[NSMutableDictionary alloc] initWithCapacity:1]; + GS_MUTEX_INIT(_lock); + } + return self; +} + +- (void) dealloc +{ + if (![self isEmpty]) + { + // We only want to flag for root observers: anything we created internally + // is fair game to be destroyed. + for (NSString *key in [_keyObserverMap keyEnumerator]) + { + for (_NSKVOKeyObserver *keyObserver in + [_keyObserverMap objectForKey:key]) + { + if (keyObserver.root) + { + [NSException + raise:NSInvalidArgumentException + format: + @"Object %@ deallocated with observers still registered.", + keyObserver.object]; + } + } + } + } + [_keyObserverMap release]; + [_existingDependentKeys release]; + + GS_MUTEX_DESTROY(_lock); + + [super dealloc]; +} + +- (void) pushDependencyStack +{ + GS_MUTEX_LOCK(_lock); + if (_dependencyDepth == 0) + { + _existingDependentKeys = [NSMutableSet new]; + } + ++_dependencyDepth; + GS_MUTEX_UNLOCK(_lock); +} + +- (BOOL) lockDependentKeypath: (NSString *)keypath +{ + GS_MUTEX_LOCK(_lock); + if ([_existingDependentKeys containsObject:keypath]) + { + GS_MUTEX_UNLOCK(_lock); + return NO; + } + [_existingDependentKeys addObject:keypath]; + GS_MUTEX_UNLOCK(_lock); + return YES; +} + +- (void) popDependencyStack +{ + GS_MUTEX_LOCK(_lock); + --_dependencyDepth; + if (_dependencyDepth == 0) + { + [_existingDependentKeys release]; + _existingDependentKeys = nil; + } + GS_MUTEX_UNLOCK(_lock); +} + +- (void) addObserver: (_NSKVOKeyObserver *)observer +{ + NSString *key = observer.key; + NSMutableArray *observersForKey = nil; + + GS_MUTEX_LOCK(_lock); + observersForKey = [_keyObserverMap objectForKey:key]; + if (!observersForKey) + { + observersForKey = [NSMutableArray array]; + [_keyObserverMap setObject:observersForKey forKey:key]; + } + [observersForKey addObject:observer]; + GS_MUTEX_UNLOCK(_lock); +} + +- (void) removeObserver: (_NSKVOKeyObserver *)observer +{ + NSString *key; + NSMutableArray *observersForKey; + + GS_MUTEX_LOCK(_lock); + key = observer.key; + observersForKey = [_keyObserverMap objectForKey:key]; + [observersForKey removeObject:observer]; + observer.isRemoved = true; + if (observersForKey.count == 0) + { + [_keyObserverMap removeObjectForKey:key]; + } + GS_MUTEX_UNLOCK(_lock); +} + +- (NSArray *) observersForKey: (NSString *)key +{ + NSArray *result; + + GS_MUTEX_LOCK(_lock); + result = [[[_keyObserverMap objectForKey:key] copy] autorelease]; + GS_MUTEX_UNLOCK(_lock); + return result; +} + +- (bool) isEmpty +{ + BOOL result; + + GS_MUTEX_LOCK(_lock); + result = (_keyObserverMap.count == 0); + GS_MUTEX_UNLOCK(_lock); + return result; +} +@end + +static _NSKVOObservationInfo * +_createObservationInfoForObject(id object) +{ + _NSKVOObservationInfo *observationInfo = [_NSKVOObservationInfo new]; + [object setObservationInfo:observationInfo]; + [observationInfo release]; + return observationInfo; +} +#pragma endregion + +#pragma region Observer / Key Registration +static _NSKVOKeyObserver * +_addKeypathObserver(id object, NSString *keypath, + _NSKVOKeypathObserver *keyPathObserver, + NSArray *affectedObservers); +static void +_removeKeyObserver(_NSKVOKeyObserver *keyObserver); + +// Add all observers with declared dependencies on this one: +// * All keypaths that could trigger a change (keypaths for values affecting +// us). +// * The head of the remaining keypath. +static void +_addNestedObserversAndOptionallyDependents(_NSKVOKeyObserver *keyObserver, + bool dependents) +{ + id object = keyObserver.object; + NSString *key = keyObserver.key; + _NSKVOKeypathObserver *keypathObserver = keyObserver.keypathObserver; + _NSKVOObservationInfo *observationInfo + = (__bridge _NSKVOObservationInfo *) [object observationInfo] + ?: _createObservationInfoForObject(object); + + // Aggregate all keys whose values will affect us. + if (dependents) + { + Class cls = [object class]; + NSSet *valueInfluencingKeys = [cls keyPathsForValuesAffectingValueForKey: key]; + if (valueInfluencingKeys.count > 0) + { + NSArray *affectedKeyObservers; + NSMutableArray *dependentObservers; + + /* affectedKeyObservers is the list of observers that must be notified + * of changes. If we have descendants, we have to add ourselves to the + * growing list of affected keys. If not, we must pass it along + * unmodified. (This is a minor optimization: we don't need to signal + * for our own reconstruction + * if we have no subpath observers.) + */ + affectedKeyObservers = (keyObserver.restOfKeypath + ? ([keyObserver.affectedObservers arrayByAddingObject:keyObserver] + ?: [NSArray arrayWithObject:keyObserver]) + : keyObserver.affectedObservers); + + [observationInfo pushDependencyStack]; + /* Don't allow our own key to be recreated. + */ + [observationInfo lockDependentKeypath:keyObserver.key]; + + dependentObservers = + [NSMutableArray arrayWithCapacity:[valueInfluencingKeys count]]; + for (NSString *dependentKeypath in valueInfluencingKeys) + { + if ([observationInfo lockDependentKeypath:dependentKeypath]) + { + _NSKVOKeyObserver *dependentObserver + = _addKeypathObserver(object, dependentKeypath, + keypathObserver, + affectedKeyObservers); + if (dependentObserver) + { + [dependentObservers addObject:dependentObserver]; + } + } + } + keyObserver.dependentObservers = dependentObservers; + + [observationInfo popDependencyStack]; + } + } + else + { + // Our dependents still exist, but their leaves have been pruned. Give + // them the same treatment as us: recreate their leaves. + for (_NSKVOKeyObserver *dependentKeyObserver in keyObserver + .dependentObservers) + { + _addNestedObserversAndOptionallyDependents(dependentKeyObserver, + false); + } + } + + // If restOfKeypath is non-nil, we have to chain on further observers. + if (keyObserver.restOfKeypath && !keyObserver.restOfKeypathObserver) + { + keyObserver.restOfKeypathObserver + = _addKeypathObserver([object valueForKey:key], + keyObserver.restOfKeypath, keypathObserver, + keyObserver.affectedObservers); + } + + // Back-propagation of changes. + // This is where a value-affecting key signals to its dependent that it should + // be reconstructed. + for (_NSKVOKeyObserver *affectedObserver in keyObserver.affectedObservers) + { + if (!affectedObserver.restOfKeypathObserver) + { + affectedObserver.restOfKeypathObserver + = _addKeypathObserver([affectedObserver.object + valueForKey:affectedObserver.key], + affectedObserver.restOfKeypath, + affectedObserver.keypathObserver, + affectedObserver.affectedObservers); + } + } +} + +static void +_addKeyObserver(_NSKVOKeyObserver *keyObserver) +{ + _NSKVOObservationInfo *observationInfo; + id object = keyObserver.object; + + _NSKVOEnsureKeyWillNotify(object, keyObserver.key); + observationInfo + = (__bridge _NSKVOObservationInfo *) [object observationInfo] + ?: _createObservationInfoForObject(object); + [observationInfo addObserver:keyObserver]; +} + +static _NSKVOKeyObserver * +_addKeypathObserver(id object, NSString *keypath, + _NSKVOKeypathObserver *keyPathObserver, NSArray *affectedObservers) +{ + _NSKVOKeyObserver *keyObserver; + NSString *key; + NSString *restOfKeypath; + + if (!object) + { + return nil; + } + key = _NSKVCSplitKeypath(keypath, &restOfKeypath); + + keyObserver = + [[[_NSKVOKeyObserver alloc] initWithObject:object + keypathObserver:keyPathObserver + key:key + restOfKeypath:restOfKeypath + affectedObservers:affectedObservers] autorelease]; + + if (object) + { + _addNestedObserversAndOptionallyDependents(keyObserver, true); + _addKeyObserver(keyObserver); + } + + return keyObserver; +} +#pragma endregion + +#pragma region Observer / Key Deregistration +static void +_removeNestedObserversAndOptionallyDependents(_NSKVOKeyObserver *keyObserver, + bool dependents) +{ + if (keyObserver.restOfKeypathObserver) + { + // Destroy the subpath observer recursively. + _removeKeyObserver(keyObserver.restOfKeypathObserver); + keyObserver.restOfKeypathObserver = nil; + } + + if (dependents) + { + // Destroy each observer whose value affects ours, recursively. + for (_NSKVOKeyObserver *dependentKeyObserver in keyObserver + .dependentObservers) + { + _removeKeyObserver(dependentKeyObserver); + } + + keyObserver.dependentObservers = nil; + } + else + { + // Our dependents must be kept alive but pruned. + for (_NSKVOKeyObserver *dependentKeyObserver in keyObserver + .dependentObservers) + { + _removeNestedObserversAndOptionallyDependents(dependentKeyObserver, + false); + } + } + + if (keyObserver.affectedObservers) + { + // Begin to reconstruct each observer that depends on our key's value + // (triggers in _addDependentAndNestedObservers). + for (_NSKVOKeyObserver *affectedObserver in keyObserver.affectedObservers) + { + _removeKeyObserver(affectedObserver.restOfKeypathObserver); + affectedObserver.restOfKeypathObserver = nil; + } + } +} + +static void +_removeKeyObserver(_NSKVOKeyObserver *keyObserver) +{ + _NSKVOObservationInfo *observationInfo; + + if (!keyObserver) + { + return; + } + + observationInfo + = (_NSKVOObservationInfo *) [keyObserver.object observationInfo]; + + [keyObserver retain]; + + _removeNestedObserversAndOptionallyDependents(keyObserver, true); + + // These are removed elsewhere; we're probably being cleared as a result of + // their deletion anyway. + keyObserver.affectedObservers = nil; + + [observationInfo removeObserver:keyObserver]; + + [keyObserver release]; +} + +static void +_removeKeypathObserver(id object, NSString *keypath, id observer, void *context) +{ + NSString *key; + NSString *restOfKeypath; + _NSKVOObservationInfo *observationInfo; + + key = _NSKVCSplitKeypath(keypath, &restOfKeypath); + + observationInfo = (_NSKVOObservationInfo *) [object observationInfo]; + for (_NSKVOKeyObserver *keyObserver in [observationInfo observersForKey:key]) + { + _NSKVOKeypathObserver *keypathObserver = keyObserver.keypathObserver; + + if (keypathObserver.observer == observer + && keypathObserver.object == object + && [keypathObserver.keypath isEqual:keypath] + && (!context || keypathObserver.context == context)) + { + _removeKeyObserver(keyObserver); + return; + } + } + + [NSException raise: NSInvalidArgumentException + format: @"Cannot remove observer %@ for keypath \"%@\" from %@" + @" as it is not a registered observer.", + observer, keypath, object]; +} +#pragma endregion + +#pragma region KVO Core Implementation - NSObject category + +@implementation +NSObject (NSKeyValueObserving) + +- (void) observeValueForKeyPath: (NSString *)keyPath + ofObject: (id)object + change: (NSDictionary *)change + context: (void *)context +{ + [NSException raise: NSInternalInconsistencyException + format: @"A key-value observation notification fired, but nobody " + @"responded to it: object %@, keypath %@, change %@.", + object, keyPath, change]; +} + +static void *s_kvoObservationInfoAssociationKey; // has no value; pointer used + // as an association key. + +- (void *) observationInfo +{ + return (__bridge void *) + objc_getAssociatedObject(self, &s_kvoObservationInfoAssociationKey); +} + +- (void) setObservationInfo: (void *)observationInfo +{ + objc_setAssociatedObject(self, &s_kvoObservationInfoAssociationKey, + (__bridge id) observationInfo, + OBJC_ASSOCIATION_RETAIN); +} + ++ (BOOL) automaticallyNotifiesObserversForKey: (NSString *)key +{ + if ([key length] > 0) + { + static const char *const sc_prefix = "automaticallyNotifiesObserversOf"; + static const size_t sc_prefixLength = 32; // strlen(sc_prefix) + const char *rawKey = [key UTF8String]; + size_t keyLength = strlen(rawKey); + size_t bufferSize = sc_prefixLength + keyLength + 1; + char *selectorName = (char *) malloc(bufferSize); + SEL sel; + + memcpy(selectorName, sc_prefix, sc_prefixLength); + selectorName[sc_prefixLength] = toupper(rawKey[0]); + memcpy(&selectorName[sc_prefixLength + 1], &rawKey[1], + keyLength); // copy keyLength characters to include terminating + // NULL from rawKey + sel = sel_registerName(selectorName); + free(selectorName); + if ([self respondsToSelector:sel]) + { + return ((BOOL(*)(id, SEL)) objc_msgSend)(self, sel); + } + } + return YES; +} + ++ (NSSet *) keyPathsForValuesAffectingValueForKey: (NSString *)key +{ + static NSSet *emptySet = nil; + static gs_mutex_t lock = GS_MUTEX_INIT_STATIC; + NSUInteger keyLength; + + if (nil == emptySet) + { + GS_MUTEX_LOCK(lock); + if (nil == emptySet) + { + emptySet = [[NSSet alloc] init]; + [NSObject leakAt: &emptySet]; + } + GS_MUTEX_UNLOCK(lock); + } + + // This function can be a KVO bottleneck, so it will prefer to use c string + // manipulation when safe + keyLength = [key length]; + if (keyLength > 0) + { + static const char *const sc_prefix = "keyPathsForValuesAffecting"; + static const size_t sc_prefixLength = 26; // strlen(sc_prefix) + static const size_t sc_bufferSize = 128; + + // max length of a key that can guaranteed fit in the char buffer, + // even if UTF16->UTF8 conversion causes length to double, or a null + // terminator is needed + static const size_t sc_safeKeyLength + = (sc_bufferSize - sc_prefixLength) / 2 - 1; // 50 + + const char *rawKey; + size_t rawKeyLength; + SEL sel; + + rawKey = [key UTF8String]; + rawKeyLength = strlen(rawKey); + + if (keyLength <= sc_safeKeyLength) + { + // fast path using c string manipulation, will cover most cases, as + // most keyPaths are short + char selectorName[sc_bufferSize]; + + strncpy(selectorName, "keyPathsForValuesAffecting", 26); + + selectorName[sc_prefixLength] = toupper(rawKey[0]); + // Copy the rest of the key, including the null terminator + memcpy(&selectorName[sc_prefixLength + 1], &rawKey[1], rawKeyLength); + sel = sel_registerName(selectorName); + } + else // Guaranteed path for long keyPaths + { + size_t keyLength; + size_t bufferSize; + char *selectorName; + + keyLength = strlen(rawKey); + bufferSize = sc_prefixLength + keyLength + 1; + selectorName = (char *) malloc(bufferSize); + memcpy(selectorName, sc_prefix, sc_prefixLength); + + selectorName[sc_prefixLength] = toupper(rawKey[0]); + // Copy the rest of the key, including the null terminator + memcpy(&selectorName[sc_prefixLength + 1], &rawKey[1], keyLength); + + sel = sel_registerName(selectorName); + free(selectorName); + } + + if ([self respondsToSelector:sel]) + { + return [self performSelector:sel]; + } + } + return emptySet; +} + +- (void) addObserver: (id)observer + forKeyPath: (NSString *)keyPath + options: (NSKeyValueObservingOptions)options + context: (void *)context +{ + _NSKVOKeypathObserver *keypathObserver = + [[[_NSKVOKeypathObserver alloc] initWithObject:self + observer:observer + keyPath:keyPath + options:options + context:context] autorelease]; + _NSKVOKeyObserver *rootObserver + = _addKeypathObserver(self, keyPath, keypathObserver, nil); + rootObserver.root = true; + + if ((options & NSKeyValueObservingOptionInitial)) + { + NSMutableDictionary *change = [NSMutableDictionary + dictionaryWithObjectsAndKeys:@(NSKeyValueChangeSetting), + NSKeyValueChangeKindKey, nil]; + + if ((options & NSKeyValueObservingOptionNew)) + { + id newValue = [self valueForKeyPath:keyPath] ?: [NSNull null]; + [change setObject:newValue forKey:NSKeyValueChangeNewKey]; + } + + [observer observeValueForKeyPath:keyPath + ofObject:self + change:change + context:context]; + } +} + +- (void) removeObserver: (id)observer + forKeyPath: (NSString *)keyPath + context: (void *)context +{ + _NSKVOObservationInfo *observationInfo; + + _removeKeypathObserver(self, keyPath, observer, context); + observationInfo = (__bridge _NSKVOObservationInfo *) [self observationInfo]; + if ([observationInfo isEmpty]) + { + // TODO: was nullptr prior + [self setObservationInfo:nil]; + } +} + +- (void) removeObserver: (id)observer forKeyPath:(NSString *)keyPath +{ + [self removeObserver:observer forKeyPath:keyPath context:NULL]; +} + +// Reference platform does not provide the Set Mutation Kind in the changes +// dictionary, just shows which elements were inserted/removed/replaced +static inline NSKeyValueChange +_changeFromSetMutationKind(NSKeyValueSetMutationKind kind) +{ + switch (kind) + { + case NSKeyValueUnionSetMutation: + return NSKeyValueChangeInsertion; + case NSKeyValueMinusSetMutation: + case NSKeyValueIntersectSetMutation: + return NSKeyValueChangeRemoval; + default: + return NSKeyValueChangeReplacement; + } +} + +static inline id +_valueForPendingChangeAtIndexes(id notifyingObject, NSString *key, + NSString *keypath, id rootObject, + _NSKVOKeyObserver *keyObserver, + NSDictionary *pendingChange) +{ + id value = nil; + NSIndexSet *indexes = pendingChange[NSKeyValueChangeIndexesKey]; + if (indexes) + { + NSArray *collection = [notifyingObject valueForKey:key]; + NSString *restOfKeypath = keyObserver.restOfKeypath; + value = restOfKeypath.length > 0 + ? [collection valueForKeyPath:restOfKeypath] + : collection; + if ([value respondsToSelector:@selector(objectsAtIndexes:)]) + { + value = [value objectsAtIndexes:indexes]; + } + } + else + { + value = [rootObject valueForKeyPath:keypath]; + } + + return value ?: [NSNull null]; +} + +// void TFunc(_NSKVOKeyObserver* keyObserver); +inline static void +_dispatchWillChange(id notifyingObject, NSString *key, + DispatchChangeBlock block) +{ + _NSKVOObservationInfo *observationInfo + = (__bridge _NSKVOObservationInfo *) [notifyingObject observationInfo]; + for (_NSKVOKeyObserver *keyObserver in [observationInfo observersForKey:key]) + { + _NSKVOKeypathObserver *keypathObserver; + + if (keyObserver.isRemoved) + { + continue; + } + + // Skip any keypaths that are in the process of changing. + keypathObserver = keyObserver.keypathObserver; + if ([keypathObserver pushWillChange]) + { + NSKeyValueObservingOptions options; + + // Call into the lambda function, which will do the actual set-up for + // pendingChanges + block(keyObserver); + + options = keypathObserver.options; + if (options & NSKeyValueObservingOptionPrior) + { + NSMutableDictionary *change = keypathObserver.pendingChange; + + [change setObject:@(YES) + forKey:NSKeyValueChangeNotificationIsPriorKey]; + [keypathObserver.observer + observeValueForKeyPath:keypathObserver.keypath + ofObject:keypathObserver.object + change:change + context:keypathObserver.context]; + [change + removeObjectForKey:NSKeyValueChangeNotificationIsPriorKey]; + } + } + + // This must happen regardless of whether we are currently notifying. + _removeNestedObserversAndOptionallyDependents(keyObserver, false); + } +} + +static void +_dispatchDidChange(id notifyingObject, NSString *key, DispatchChangeBlock block) +{ + _NSKVOObservationInfo *observationInfo + = (__bridge _NSKVOObservationInfo *) [notifyingObject observationInfo]; + NSArray<_NSKVOKeyObserver *> *observers = + [observationInfo observersForKey:key]; + for (_NSKVOKeyObserver *keyObserver in [observers reverseObjectEnumerator]) + { + _NSKVOKeypathObserver *keypathObserver; + + if (keyObserver.isRemoved) + { + continue; + } + + // This must happen regardless of whether we are currently notifying. + _addNestedObserversAndOptionallyDependents(keyObserver, false); + + // Skip any keypaths that are in the process of changing. + keypathObserver = keyObserver.keypathObserver; + if ([keypathObserver popDidChange]) + { + id observer; + NSString *keypath; + id rootObject; + NSMutableDictionary *change; + void *context; + + // Call into lambda, which will do set-up for finalizing changes + // dictionary + block(keyObserver); + + observer = keypathObserver.observer; + keypath = keypathObserver.keypath; + rootObject = keypathObserver.object; + change = keypathObserver.pendingChange; + context = keypathObserver.context; + [observer observeValueForKeyPath:keypath + ofObject:rootObject + change:change + context:context]; + keypathObserver.pendingChange = nil; + } + } +} + +- (void) willChangeValueForKey: (NSString *)key +{ + if ([self observationInfo]) + { + _dispatchWillChange(self, key, ^(_NSKVOKeyObserver *keyObserver) { + NSMutableDictionary *change = + [NSMutableDictionary dictionaryWithObject:@(NSKeyValueChangeSetting) + forKey:NSKeyValueChangeKindKey]; + _NSKVOKeypathObserver *keypathObserver = keyObserver.keypathObserver; + NSKeyValueObservingOptions options = keypathObserver.options; + + if (options & NSKeyValueObservingOptionOld) + { + // For to-many mutations, we can't get the old values at indexes + // that have not yet been inserted. + id rootObject = keypathObserver.object; + NSString *keypath = keypathObserver.keypath; + id oldValue = [rootObject valueForKeyPath:keypath] ?: [NSNull null]; + change[NSKeyValueChangeOldKey] = oldValue; + } + + keypathObserver.pendingChange = change; + }); + } +} + +- (void) didChangeValueForKey: (NSString *)key +{ + if ([self observationInfo]) + { + _dispatchDidChange(self, key, ^(_NSKVOKeyObserver *keyObserver) { + _NSKVOKeypathObserver *keypathObserver = keyObserver.keypathObserver; + NSKeyValueObservingOptions options = keypathObserver.options; + NSMutableDictionary *change = keypathObserver.pendingChange; + if ((options & NSKeyValueObservingOptionNew) && + [change[NSKeyValueChangeKindKey] integerValue] + != NSKeyValueChangeRemoval) + { + NSString *keypath = keypathObserver.keypath; + id rootObject = keypathObserver.object; + id newValue = [rootObject valueForKeyPath:keypath] ?: [NSNull null]; + + change[NSKeyValueChangeNewKey] = newValue; + } + }); + } +} + +- (void) willChange: (NSKeyValueChange)changeKind + valuesAtIndexes: (NSIndexSet *)indexes + forKey: (NSString *)key +{ + __block NSKeyValueChange kind = changeKind; + if ([self observationInfo]) + { + _dispatchWillChange(self, key, ^(_NSKVOKeyObserver *keyObserver) { + NSMutableDictionary *change = [NSMutableDictionary + dictionaryWithObjectsAndKeys:@(kind), NSKeyValueChangeKindKey, + indexes, NSKeyValueChangeIndexesKey, + nil]; + _NSKVOKeypathObserver *keypathObserver = keyObserver.keypathObserver; + NSKeyValueObservingOptions options = keypathObserver.options; + id rootObject = keypathObserver.object; + + // The reference platform does not support to-many mutations on nested + // keypaths. We have to treat them as to-one mutations to support + // aggregate functions. + if (kind != NSKeyValueChangeSetting + && keyObserver.restOfKeypathObserver) + { + // This only needs to be done in willChange because didChange + // derives from the existing changeset. + change[NSKeyValueChangeKindKey] = @(kind = NSKeyValueChangeSetting); + + // Make change Old/New values the entire collection rather than a + // to-many change with objectsAtIndexes: + [change removeObjectForKey:NSKeyValueChangeIndexesKey]; + } + + if ((options & NSKeyValueObservingOptionOld) + && kind != NSKeyValueChangeInsertion) + { + // For to-many mutations, we can't get the old values at indexes + // that have not yet been inserted. + NSString *keypath = keypathObserver.keypath; + change[NSKeyValueChangeOldKey] + = _valueForPendingChangeAtIndexes(self, key, keypath, rootObject, + keyObserver, change); + } + + keypathObserver.pendingChange = change; + }); + } +} + +- (void) didChange: (NSKeyValueChange)changeKind + valuesAtIndexes: (NSIndexSet *)indexes + forKey: (NSString *)key +{ + if ([self observationInfo]) + { + _dispatchDidChange(self, key, ^(_NSKVOKeyObserver *keyObserver) { + _NSKVOKeypathObserver *keypathObserver = keyObserver.keypathObserver; + NSKeyValueObservingOptions options = keypathObserver.options; + NSMutableDictionary *change = keypathObserver.pendingChange; + if ((options & NSKeyValueObservingOptionNew) && + [change[NSKeyValueChangeKindKey] integerValue] + != NSKeyValueChangeRemoval) + { + // For to-many mutations, we can't get the new values at indexes + // that have been deleted. + id rootObject = keypathObserver.object; + NSString *keypath = keypathObserver.keypath; + id newValue + = _valueForPendingChangeAtIndexes(self, key, keypath, rootObject, + keyObserver, change); + + change[NSKeyValueChangeNewKey] = newValue; + } + }); + } +} + +// Need to know the previous value for the set if we need to find the values +// added +static const NSString *_NSKeyValueChangeOldSetValue + = @"_NSKeyValueChangeOldSetValue"; + +- (void)willChangeValueForKey: (NSString *)key + withSetMutation: (NSKeyValueSetMutationKind)mutationKind + usingObjects: (NSSet *)objects +{ + if ([self observationInfo]) + { + NSKeyValueChange changeKind = _changeFromSetMutationKind(mutationKind); + _dispatchWillChange(self, key, ^(_NSKVOKeyObserver *keyObserver) { + NSMutableDictionary *change = + [NSMutableDictionary dictionaryWithObject:@(changeKind) + forKey:NSKeyValueChangeKindKey]; + _NSKVOKeypathObserver *keypathObserver = keyObserver.keypathObserver; + NSKeyValueObservingOptions options = keypathObserver.options; + id rootObject = keypathObserver.object; + NSString *keypath = keypathObserver.keypath; + + NSSet *oldValues = [rootObject valueForKeyPath:keypath]; + if ((options & NSKeyValueObservingOptionOld) + && changeKind != NSKeyValueChangeInsertion) + { + // The old value should only contain values which are removed from + // the original dictionary + switch (mutationKind) + { + case NSKeyValueMinusSetMutation: + // The only objects which were removed are those both in + // oldValues and objects + change[NSKeyValueChangeOldKey] = + [oldValues objectsPassingTest:^(id obj, BOOL *stop) { + return [objects containsObject:obj]; + }]; + break; + case NSKeyValueIntersectSetMutation: + case NSKeyValueSetSetMutation: + default: + // The only objects which were removed are those in oldValues + // and NOT in objects + change[NSKeyValueChangeOldKey] = + [oldValues objectsPassingTest:^BOOL(id obj, BOOL *stop) { + return [objects member:obj] ? NO : YES; + }]; + break; + } + } + + if (options & NSKeyValueObservingOptionNew) + { + // Save old value in change dictionary for + // didChangeValueForKey:withSetMutation:usingObjects: to use for + // determining added objects Only needed if observer wants New + // value + change[_NSKeyValueChangeOldSetValue] = + [[oldValues copy] autorelease]; + } + + keypathObserver.pendingChange = change; + }); + } +} + +- (void)didChangeValueForKey: (NSString *)key + withSetMutation: (NSKeyValueSetMutationKind)mutationKind + usingObjects: (NSSet *)objects +{ + if ([self observationInfo]) + { + _dispatchDidChange(self, key, ^(_NSKVOKeyObserver *keyObserver) { + _NSKVOKeypathObserver *keypathObserver = keyObserver.keypathObserver; + NSKeyValueChange changeKind = _changeFromSetMutationKind(mutationKind); + NSKeyValueObservingOptions options = keypathObserver.options; + + if ((options & NSKeyValueObservingOptionNew) + && changeKind != NSKeyValueChangeRemoval) + { + // New values only exist for inserting or replacing, not removing + NSMutableDictionary *change = keypathObserver.pendingChange; + NSSet *oldValues = change[_NSKeyValueChangeOldSetValue]; + // The new value should only contain values which are added to the + // original set The only objects added are those in objects but + // NOT in oldValues + NSSet *newValue = + [objects objectsPassingTest:^BOOL(id obj, BOOL *stop) { + return [oldValues member:obj] ? NO : YES; + }]; + + change[NSKeyValueChangeNewKey] = newValue; + [change removeObjectForKey:_NSKeyValueChangeOldSetValue]; + } + }); + } +} +@end + +#pragma endregion + +#pragma region KVO Core Implementation - Private Access + +@implementation +NSObject (NSKeyValueObservingPrivate) + +- (void)_notifyObserversOfChangeForKey: (NSString *)key + oldValue: (id)oldValue + newValue: (id)newValue +{ + if ([self observationInfo]) + { + _dispatchWillChange(self, key, ^(_NSKVOKeyObserver *keyObserver) { + NSMutableDictionary *change = + [NSMutableDictionary dictionaryWithObject:@(NSKeyValueChangeSetting) + forKey:NSKeyValueChangeKindKey]; + _NSKVOKeypathObserver *keypathObserver = keyObserver.keypathObserver; + NSKeyValueObservingOptions options = keypathObserver.options; + + if (options & NSKeyValueObservingOptionOld) + { + change[NSKeyValueChangeOldKey] = oldValue ? oldValue : [NSNull null]; + } + + keypathObserver.pendingChange = change; + }); + _dispatchDidChange(self, key, ^(_NSKVOKeyObserver *keyObserver) { + _NSKVOKeypathObserver *keypathObserver = keyObserver.keypathObserver; + NSKeyValueObservingOptions options = keypathObserver.options; + NSMutableDictionary *change = keypathObserver.pendingChange; + if ((options & NSKeyValueObservingOptionNew) && + [change[NSKeyValueChangeKindKey] integerValue] + != NSKeyValueChangeRemoval) + { + change[NSKeyValueChangeNewKey] = newValue ? newValue : [NSNull null]; + } + }); + } +} + +@end + +#pragma endregion + +#pragma region KVO Core Implementation - NSArray category + +@implementation +NSArray (NSKeyValueObserving) + +- (void)addObserver: (id)observer + forKeyPath: (NSString *)keyPath + options: (NSKeyValueObservingOptions)options + context: (void *)context +{ + NS_COLLECTION_THROW_ILLEGAL_KVO(keyPath); +} + +- (void)removeObserver: (id)observer + forKeyPath: (NSString *)keyPath + context: (void *)context +{ + NS_COLLECTION_THROW_ILLEGAL_KVO(keyPath); +} + +- (void)removeObserver: (id)observer forKeyPath:(NSString *)keyPath +{ + NS_COLLECTION_THROW_ILLEGAL_KVO(keyPath); +} + +- (void)addObserver: (id)observer + toObjectsAtIndexes: (NSIndexSet *)indexes + forKeyPath: (NSString *)keyPath + options: (NSKeyValueObservingOptions)options + context: (void *)context +{ + [indexes enumerateIndexesUsingBlock:^(NSUInteger index, BOOL *stop) { + [[self objectAtIndex:index] addObserver:observer + forKeyPath:keyPath + options:options + context:context]; + }]; +} + +- (void)removeObserver: (id)observer + fromObjectsAtIndexes: (NSIndexSet *)indexes + forKeyPath: (NSString *)keyPath + context: (void *)context +{ + [indexes enumerateIndexesUsingBlock:^(NSUInteger index, BOOL *stop) { + [[self objectAtIndex:index] removeObserver:observer + forKeyPath:keyPath + context:context]; + }]; +} + +- (void)removeObserver: (NSObject *)observer + fromObjectsAtIndexes: (NSIndexSet *)indexes + forKeyPath: (NSString *)keyPath +{ + [self removeObserver:observer + fromObjectsAtIndexes:indexes + forKeyPath:keyPath + context:NULL]; +} + +@end + +#pragma endregion + +#pragma region KVO Core Implementation - NSSet category + +@implementation +NSSet (NSKeyValueObserving) + +- (void)addObserver: (id)observer + forKeyPath: (NSString *)keyPath + options: (NSKeyValueObservingOptions)options + context: (void *)context +{ + NS_COLLECTION_THROW_ILLEGAL_KVO(keyPath); +} + +- (void)removeObserver: (id)observer + forKeyPath: (NSString *)keyPath + context: (void *)context +{ + NS_COLLECTION_THROW_ILLEGAL_KVO(keyPath); +} + +- (void)removeObserver: (id)observer forKeyPath:(NSString *)keyPath +{ + NS_COLLECTION_THROW_ILLEGAL_KVO(keyPath); +} + +@end + +#pragma endregion diff --git a/Source/NSKVOSwizzling.m b/Source/NSKVOSwizzling.m new file mode 100644 index 000000000..5b9965baa --- /dev/null +++ b/Source/NSKVOSwizzling.m @@ -0,0 +1,695 @@ +/** + NSKVOSwizzling.m + + Copyright (C) 2024 Free Software Foundation, Inc. + + Written by: Hugo Melder + Date: June 2024 + + Based on WinObjC KVO tests by Microsoft Corporation. + + This file is part of GNUStep-base + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + If you are interested in a warranty or support for this source code, + contact Scott Christley for more information. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free + Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110 USA. +*/ +/** + Copyright (c) Microsoft. All rights reserved. + + This code is licensed under the MIT License (MIT). + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +/* This Key Value Observing Implementation is tied to libobjc2 */ + +#import "common.h" +#import "NSKVOInternal.h" + +#import +#import +#import "NSKVOTypeEncodingCases.h" + +#import + +#ifdef WIN32 +#define alloca(x) _alloca(x) +#endif + +/* These are defined by the ABI and the runtime. */ +#define ABI_SUPER(obj) (((Class **) obj)[0][1]) +#define ABI_ISA(obj) (((Class *) obj)[0]) + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-objc-pointer-introspection" +// Small objects, as defined by libobjc2, are tagged pointers. Instead of being +// heap-allocated, the object is stored in the sizeof(id) - +// OBJC_SMALL_OBJECT_BITS bits of the "pointer" itself. +static inline bool +isSmallObject_np(id object) +{ + return ((((uintptr_t) object) & OBJC_SMALL_OBJECT_MASK) != 0); +} +#pragma clang diagnostic pop + +static void +NSKVO$setObject$forKey$(id self, SEL _cmd, id object, NSString *key); +static void +NSKVO$removeObjectForKey$(id self, SEL _cmd, NSString *key); +static void +NSKVO$nilIMP(id self, SEL _cmd) +{} + +static void +_NSKVOEnsureObjectIsKVOAware(id object) +{ + // We have to use ABI_ISA here, because object_getClass will skip the secret + // hidden subclass. + if (class_respondsToSelector(ABI_ISA(object), @selector(_isKVOAware))) + { + // The presence of _isKVOAware signals that we have already mangled this + // object. + return; + } + + /* + A crucial design decision was made here: because object_getClass() skips + autogenerated subclasses, the likes of which are used for associated objects, + the KVO machinery (if implemented using manual subclassing) would delete all + associated objects on an observed instance. That was deemed unacceptable. + + Likewise, this implementation is not free of issues: + - The _np methods are nonportable. + - Anyone using class_getMethodImplementation(object_getClass(...), ...) will + receive the original IMP for any overridden method. + - We have to manually load isa to get at the secret subclass (thus the use of + ABI_ISA/ABI_SUPER.) + - It is dependent upon a libobjc2 implementation detail: + object_addMethod_np creates a hidden subclass for the object's class (one + per object!) + */ + + object_addMethod_np(object, @selector(setObject:forKey:), + (IMP)(NSKVO$setObject$forKey$), "v@:@@"); + object_addMethod_np(object, @selector(removeObjectForKey:), + (IMP)(NSKVO$removeObjectForKey$), "v@:@"); + object_addMethod_np(object, @selector(_isKVOAware), + (IMP)(NSKVO$nilIMP), "v@:"); +} + +#pragma region Method Implementations +// Selector mappings: the class-level mapping from a selector (setX:) to the KVC +// key ("x") to which it corresponds. This is necessary because both "X" and "x" +// map to setX:, but we need to be cognizant of precisely which it was for any +// given observee. The sole reason we can get away with keeping selector<->key +// mappings on the class is that, through the lifetime of said class, it can +// never lose selectors. This mapping will be pertinent to every instance of the +// class. +static inline NSMapTable * +_selectorMappingsForObject(id object) +{ + static char s_selMapKey; + Class cls; + + // here we explicitly want the public + // (non-hidden) class associated with the object. + cls = object_getClass(object); + + @synchronized(cls) + { + NSMapTable *selMappings + = (NSMapTable *) objc_getAssociatedObject(cls, &s_selMapKey); + if (!selMappings) + { + selMappings = [NSMapTable + mapTableWithKeyOptions: NSPointerFunctionsOpaqueMemory + | NSPointerFunctionsOpaquePersonality + valueOptions: NSPointerFunctionsStrongMemory + | NSPointerFunctionsObjectPersonality]; + objc_setAssociatedObject(cls, &s_selMapKey, (id) selMappings, + OBJC_ASSOCIATION_RETAIN); + } + return selMappings; + } +} + +static inline NSString * +_keyForSelector(id object, SEL selector) +{ + return (NSString *) NSMapGet(_selectorMappingsForObject(object), + sel_getName(selector)); +} + +static inline void +_associateSelectorWithKey(id object, SEL selector, NSString *key) +{ + /* this must be insertIfAbsent. otherwise, when calling a setter that itself + * causes observers to be added/removed on this key, this would call this + * method and overwrite the association selector->key with an identical key + * but another object. unfortunately, this would mean that the + * notifyingSetImpl below would then have a dead ```key``` pointer once it + * came time to call didChangeValueForKey (because apparently ARC doesn't take + * care of this properly) + */ + NSMapInsertIfAbsent(_selectorMappingsForObject(object), + sel_getName(selector), key); +} + +static void +notifyingVariadicSetImpl(id self, SEL _cmd, ...) +{ + NSString *key = _keyForSelector(self, _cmd); + + /* [Source: NSInvocation.mm] + * This attempts to flatten the method's arguments (as determined by its type + * encoding) from the stack into a buffer. That buffer is then emitted back + * onto the stack for the imp callthrough. This only works if we assume that + * our calling convention passes variadics and non-variadics in the same way: + * on the stack. For our two supported platforms, this seems to hold true. + */ + NSMethodSignature *sig = [self methodSignatureForSelector: _cmd]; + size_t argSz = objc_sizeof_type([sig getArgumentTypeAtIndex: 2]); + size_t nStackArgs = argSz / sizeof(uintptr_t); + uintptr_t *raw = (uintptr_t *) (calloc(sizeof(uintptr_t), nStackArgs)); + va_list ap; + + va_start(ap, _cmd); + for (uintptr_t i = 0; i < nStackArgs; ++i) + { + raw[i] = va_arg(ap, uintptr_t); + } + va_end(ap); + + [self willChangeValueForKey: key]; + { + struct objc_super super = {self, ABI_SUPER(self)}; + IMP imp = (id(*)(id, SEL, ...)) objc_msg_lookup_super(&super, _cmd); + + // VSO 5955259; NSInvocation informs this implementation and this will need + // to be cleaned up when NSInvocation is. + switch (nStackArgs) + { + case 1: + imp(self, _cmd, raw[0]); + break; + case 2: + imp(self, _cmd, raw[0], raw[1]); + break; + case 3: + imp(self, _cmd, raw[0], raw[1], raw[2]); + break; + case 4: + imp(self, _cmd, raw[0], raw[1], raw[2], raw[3]); + break; + case 5: + imp(self, _cmd, raw[0], raw[1], raw[2], raw[3], raw[4]); + break; + case 6: + imp(self, _cmd, raw[0], raw[1], raw[2], raw[3], raw[4], raw[5]); + break; + default: + NSLog(@"Can't override setter with more than 6" + @" sizeof(long int) stack arguments."); + return; + } + } + [self didChangeValueForKey: key]; + + free(raw); +} + +typedef void (*insertObjectAtIndexIMP)(id, SEL, id, NSUInteger); +typedef void (*removeObjectAtIndexIMP)(id, SEL, NSUInteger); +typedef void (*insertAtIndexesIMP)(id, SEL, id, NSIndexSet *); +typedef void (*removeAtIndexesIMP)(id, SEL, NSIndexSet *); +typedef void (*replaceAtIndexesIMP)(id, SEL, NSIndexSet *, NSArray *); +typedef void (*setObjectForKeyIMP)(id, SEL, id, NSString *); +typedef void (*removeObjectForKeyIMP)(id, SEL, NSString *); +typedef void (*replaceObjectAtIndexWithObjectIMP)(id, SEL, NSUInteger, id); + +static void +NSKVONotifying$insertObject$inXxxAtIndex$(id self, SEL _cmd, id object, + NSUInteger index) +{ + NSString *key = _keyForSelector(self, _cmd); + NSIndexSet *indexes = [NSIndexSet indexSetWithIndex: index]; + + [self willChange: NSKeyValueChangeInsertion + valuesAtIndexes: indexes + forKey: key]; + { + struct objc_super super = {self, ABI_SUPER(self)}; + insertObjectAtIndexIMP imp = (void (*)(id, SEL, id, NSUInteger)) + objc_msg_lookup_super(&super, _cmd); + imp(self, _cmd, object, index); + } + [self didChange: NSKeyValueChangeInsertion + valuesAtIndexes: indexes + forKey: key]; +} + +static void +NSKVONotifying$insertXxx$atIndexes$(id self, SEL _cmd, id object, + NSIndexSet *indexes) +{ + NSString *key = _keyForSelector(self, _cmd); + + [self willChange: NSKeyValueChangeInsertion + valuesAtIndexes: indexes + forKey: key]; + { + struct objc_super super = {self, ABI_SUPER(self)}; + insertAtIndexesIMP imp = (void (*)(id, SEL, id, NSIndexSet *)) + objc_msg_lookup_super(&super, _cmd); + imp(self, _cmd, object, indexes); + } + [self didChange: NSKeyValueChangeInsertion + valuesAtIndexes: indexes + forKey: key]; +} + +static void +NSKVONotifying$removeObjectFromXxxAtIndex$(id self, SEL _cmd, NSUInteger index) +{ + NSString *key = _keyForSelector(self, _cmd); + NSIndexSet *indexes = [NSIndexSet indexSetWithIndex: index]; + + [self willChange: NSKeyValueChangeRemoval + valuesAtIndexes: indexes + forKey: key]; + { + struct objc_super super = {self, ABI_SUPER(self)}; + removeObjectAtIndexIMP imp = (void (*)(id, SEL, NSUInteger)) + objc_msg_lookup_super(&super, _cmd); + imp(self, _cmd, index); + } + [self didChange: NSKeyValueChangeRemoval + valuesAtIndexes: indexes + forKey: key]; +} + +static void +NSKVONotifying$removeXxxAtIndexes$(id self, SEL _cmd, NSIndexSet *indexes) +{ + NSString *key = _keyForSelector(self, _cmd); + + [self willChange: NSKeyValueChangeRemoval + valuesAtIndexes: indexes + forKey: key]; + { + struct objc_super super = {self, ABI_SUPER(self)}; + removeAtIndexesIMP imp = (void (*)(id, SEL, NSIndexSet *)) + objc_msg_lookup_super(&super, _cmd); + imp(self, _cmd, indexes); + } + [self didChange: NSKeyValueChangeRemoval + valuesAtIndexes: indexes + forKey: key]; +} + +static void +NSKVONotifying$replaceObjectInXxxAtIndex$withObject$(id self, SEL _cmd, + NSUInteger index, id object) +{ + NSString *key = _keyForSelector(self, _cmd); + NSIndexSet *indexes = [NSIndexSet indexSetWithIndex: index]; + + [self willChange: NSKeyValueChangeReplacement + valuesAtIndexes: indexes + forKey: key]; + { + struct objc_super super = {self, ABI_SUPER(self)}; + replaceObjectAtIndexWithObjectIMP imp = (void (*)(id, SEL, NSUInteger, id)) + objc_msg_lookup_super(&super, _cmd); + imp(self, _cmd, index, object); + } + [self didChange: NSKeyValueChangeReplacement + valuesAtIndexes: indexes + forKey: key]; +} + +static void +NSKVONotifying$replaceXxxAtIndexes$withXxx$(id self, SEL _cmd, + NSIndexSet *indexes, NSArray *objects) +{ + NSString *key = _keyForSelector(self, _cmd); + + [self willChange: NSKeyValueChangeReplacement + valuesAtIndexes: indexes + forKey: key]; + { + struct objc_super super = {self, ABI_SUPER(self)}; + replaceAtIndexesIMP imp = (void (*)(id, SEL, NSIndexSet *, NSArray *)) + objc_msg_lookup_super(&super, _cmd); + imp(self, _cmd, indexes, objects); + } + [self didChange: NSKeyValueChangeReplacement + valuesAtIndexes: indexes + forKey: key]; +} + +#define GENERATE_NSKVOSetDispatch_IMPL(Kind) \ + static inline void _NSKVOSetDispatch_##Kind(id self, SEL _cmd, NSSet *set) \ + { \ + NSString *key = _keyForSelector(self, _cmd); \ + [self willChangeValueForKey: key withSetMutation: Kind usingObjects: set]; \ + { \ + struct objc_super super = {self, ABI_SUPER(self)}; \ + void (*imp)(id, SEL, NSSet *) \ + = (void (*)(id, SEL, NSSet *)) objc_msg_lookup_super(&super, _cmd); \ + imp(self, _cmd, set); \ + } \ + [self didChangeValueForKey: key withSetMutation: Kind usingObjects: set]; \ + } + +#define GENERATE_NSKVOSetDispatchIndividual_IMPL(Kind) \ + static inline void _NSKVOSetDispatchIndividual_##Kind(id self, SEL _cmd, \ + id obj) \ + { \ + NSSet *set = [NSSet setWithObject: obj]; \ + NSString *key = _keyForSelector(self, _cmd); \ + [self willChangeValueForKey: key withSetMutation: Kind usingObjects: set]; \ + { \ + struct objc_super super = {self, ABI_SUPER(self)}; \ + void (*imp)(id, SEL, id) \ + = (void (*)(id, SEL, id)) objc_msg_lookup_super(&super, _cmd); \ + imp(self, _cmd, obj); \ + } \ + [self didChangeValueForKey: key withSetMutation: Kind usingObjects: set]; \ + } + +GENERATE_NSKVOSetDispatchIndividual_IMPL(NSKeyValueUnionSetMutation); +GENERATE_NSKVOSetDispatchIndividual_IMPL(NSKeyValueMinusSetMutation); +//GENERATE_NSKVOSetDispatchIndividual_IMPL(NSKeyValueIntersectSetMutation); + +GENERATE_NSKVOSetDispatch_IMPL(NSKeyValueUnionSetMutation); +GENERATE_NSKVOSetDispatch_IMPL(NSKeyValueMinusSetMutation); +GENERATE_NSKVOSetDispatch_IMPL(NSKeyValueIntersectSetMutation); + +// - (void)setObject: (id)object forKey: (NSString*)key +static void +NSKVO$setObject$forKey$(id self, SEL _cmd, id object, NSString *key) +{ + [self willChangeValueForKey: key]; +{ + struct objc_super super = {self, ABI_SUPER(self)}; + setObjectForKeyIMP imp; + + imp = (void (*)(id, SEL, id, NSString *)) objc_msg_lookup_super(&super, _cmd); + imp(self, _cmd, object, key); +} + [self didChangeValueForKey: key]; +} + +// - (void)removeObjectForKey: (NSString*)key +static void +NSKVO$removeObjectForKey$(id self, SEL _cmd, NSString *key) +{ + [self willChangeValueForKey: key]; +{ + struct objc_super super = {self, ABI_SUPER(self)}; + removeObjectForKeyIMP imp; + + imp = (void (*)(id, SEL, NSString *)) objc_msg_lookup_super(&super, _cmd); + imp(self, _cmd, key); +} + [self didChangeValueForKey: key]; +} +#pragma endregion + +#define GENERATE_NOTIFYING_SET_IMPL(funcName, type) \ + static void funcName(id self, SEL _cmd, type val) \ + { \ + struct objc_super super = {self, ABI_SUPER(self)}; \ + NSString *key = _keyForSelector(self, _cmd); \ + void (*imp)(id, SEL, type) \ + = (void (*)(id, SEL, type)) objc_msg_lookup_super(&super, _cmd); \ + [self willChangeValueForKey: key]; \ + imp(self, _cmd, val); \ + [self didChangeValueForKey: key]; \ + } + +GENERATE_NOTIFYING_SET_IMPL(notifyingSetImplDouble, double); +GENERATE_NOTIFYING_SET_IMPL(notifyingSetImplFloat, float); + +GENERATE_NOTIFYING_SET_IMPL(notifyingSetImplChar, signed char); +GENERATE_NOTIFYING_SET_IMPL(notifyingSetImplInt, int); +GENERATE_NOTIFYING_SET_IMPL(notifyingSetImplShort, short); +GENERATE_NOTIFYING_SET_IMPL(notifyingSetImplLong, long); +GENERATE_NOTIFYING_SET_IMPL(notifyingSetImplLongLong, long long); + +GENERATE_NOTIFYING_SET_IMPL(notifyingSetImplUnsignedChar, unsigned char); +GENERATE_NOTIFYING_SET_IMPL(notifyingSetImplUnsignedInt, unsigned int); +GENERATE_NOTIFYING_SET_IMPL(notifyingSetImplUnsignedShort, unsigned short); +GENERATE_NOTIFYING_SET_IMPL(notifyingSetImplUnsignedLong, unsigned long); +GENERATE_NOTIFYING_SET_IMPL(notifyingSetImplUnsignedLongLong, + unsigned long long); + +// GENERATE_NOTIFYING_SET_IMPL(notifyingSetImplBool, bool); +GENERATE_NOTIFYING_SET_IMPL(notifyingSetImplObject, id); +GENERATE_NOTIFYING_SET_IMPL(notifyingSetImplPointer, void *); + +#define KVO_SET_IMPL_CASE(type, name, capitalizedName, encodingChar) \ + case encodingChar: { \ + newImpl = (IMP) (¬ifyingSetImpl##capitalizedName); \ + break; \ + } + +SEL +KVCSetterForPropertyName(NSObject *self, const char *key) +{ + SEL sel = nil; + size_t len = strlen(key); + // For the key "example", we must construct the following buffer: + // _ _ _ _ x a m p l e _ \0 + // and fill it with the following characters: + // s e t E x a m p l e : \0 + char *buf = (char *) alloca(3 + len + 2); + memcpy(buf + 4, key + 1, len); + buf[0] = 's'; + buf[1] = 'e'; + buf[2] = 't'; + buf[3] = toupper(key[0]); + buf[3 + len] = ':'; + buf[3 + len + 1] = '\0'; + sel = sel_getUid(buf); + if ([self respondsToSelector: sel]) + { + return sel; + } + + return nil; +} + +// invariant: rawKey has already been capitalized +static inline void +_NSKVOEnsureSimpleKeyWillNotify(id object, NSString *key, const char *rawKey) +{ + Method originalMethod; + const char *valueType; + const char *types; + NSMethodSignature *sig; + SEL sel; + IMP newImpl = NULL; + + sel = KVCSetterForPropertyName(object, rawKey); + originalMethod = class_getInstanceMethod(object_getClass(object), sel); + types = method_getTypeEncoding(originalMethod); + if (!types) + { + return; + } + + sig = [NSMethodSignature signatureWithObjCTypes: types]; + valueType = [sig getArgumentTypeAtIndex: 2]; + + switch (valueType[0]) + { + OBJC_APPLY_ALL_TYPE_ENCODINGS(KVO_SET_IMPL_CASE); + case '{': + case '[': { + size_t valueSize = objc_sizeof_type(valueType); + if (valueSize > 6 * sizeof(uintptr_t)) + { + [NSException raise: NSInvalidArgumentException + format: @"Class %s key %@ has a value size of %u bytes" + @", and cannot currently be KVO compliant.", + class_getName(object_getClass(object)), key, + (unsigned int) (valueSize)]; + } + newImpl = (IMP) (¬ifyingVariadicSetImpl); + break; + } + default: + [NSException raise: NSInvalidArgumentException + format: @"Class %s is not KVO compliant for key %@.", + class_getName(object_getClass(object)), key]; + return; + } + + _associateSelectorWithKey(object, sel, key); + object_addMethod_np(object, sel, newImpl, types); +} + +static void +replaceAndAssociateWithKey(id object, SEL sel, NSString *key, IMP imp) +{ + if ([object respondsToSelector: sel]) + { + const char *selName = sel_getName(sel); + Method method = class_getInstanceMethod(object_getClass(object), sel); + if (!method) + { + NSWarnLog(@"NSObject (NSKeyValueObservation): Unable to find method " + @"for %s on class %s; perhaps it is a forward?", + selName, object_getClassName(object)); + return; + } + + _associateSelectorWithKey(object, sel, key); + object_replaceMethod_np(object, sel, imp, method_getTypeEncoding(method)); + } +} + +static SEL +formatSelector(NSString *format, ...) +{ + NSString *s; + SEL sel; + va_list ap; + + va_start(ap, format); + s = [[NSString alloc] initWithFormat: format arguments: ap]; + sel = NSSelectorFromString([s autorelease]); + va_end(ap); + return sel; +} + +// invariant: rawKey has already been capitalized +static inline void +_NSKVOEnsureOrderedCollectionWillNotify(id object, NSString *key, + const char *rawKey) +{ + SEL insertOne = formatSelector(@"insertObject:in%sAtIndex:", rawKey); + SEL insertMany = formatSelector(@"insert%s:atIndexes:", rawKey); + + if ([object respondsToSelector: insertOne] + || [object respondsToSelector: insertMany]) + { + replaceAndAssociateWithKey(object, insertOne, key, + (IMP)NSKVONotifying$insertObject$inXxxAtIndex$); + replaceAndAssociateWithKey(object, insertMany, key, + (IMP)NSKVONotifying$insertXxx$atIndexes$); + replaceAndAssociateWithKey( + object, formatSelector(@"removeObjectFrom%sAtIndex:", rawKey), key, + (IMP)NSKVONotifying$removeObjectFromXxxAtIndex$); + replaceAndAssociateWithKey(object, + formatSelector(@"remove%sAtIndexes:", rawKey), + key, (IMP)NSKVONotifying$removeXxxAtIndexes$); + replaceAndAssociateWithKey( + object, formatSelector(@"replaceObjectIn%sAtIndex:withObject:", rawKey), + key, (IMP)NSKVONotifying$replaceObjectInXxxAtIndex$withObject$); + replaceAndAssociateWithKey( + object, formatSelector(@"replace%sAtIndexes:with%s:", rawKey, rawKey), + key, (IMP)NSKVONotifying$replaceXxxAtIndexes$withXxx$); + } +} + +// invariant: rawKey has already been capitalized +static inline void +_NSKVOEnsureUnorderedCollectionWillNotify(id object, NSString *key, + const char *rawKey) +{ + SEL addOne = formatSelector(@"add%sObject:", rawKey); + SEL addMany = formatSelector(@"add%s:", rawKey); + SEL removeOne = formatSelector(@"remove%sObject:", rawKey); + SEL removeMany = formatSelector(@"remove%s:", rawKey); + + if (([object respondsToSelector: addOne] + || [object respondsToSelector: addMany]) + && ([object respondsToSelector: removeOne] + || [object respondsToSelector: removeMany])) + { + replaceAndAssociateWithKey( + object, addOne, key, + (IMP)_NSKVOSetDispatchIndividual_NSKeyValueUnionSetMutation); + replaceAndAssociateWithKey( + object, addMany, key, + (IMP)_NSKVOSetDispatch_NSKeyValueUnionSetMutation); + replaceAndAssociateWithKey( + object, removeOne, key, + (IMP)_NSKVOSetDispatchIndividual_NSKeyValueMinusSetMutation); + replaceAndAssociateWithKey( + object, removeMany, key, + (IMP)_NSKVOSetDispatch_NSKeyValueMinusSetMutation); + replaceAndAssociateWithKey( + object, formatSelector(@"intersect%s:", rawKey), key, + (IMP)_NSKVOSetDispatch_NSKeyValueIntersectSetMutation); + } +} + +char * +mutableBufferFromString(NSString *string) +{ + NSUInteger lengthInBytes = [string length] + 1; + char *rawKey = (char *) malloc(lengthInBytes); + + [string getCString: rawKey + maxLength: lengthInBytes + encoding: NSASCIIStringEncoding]; + return rawKey; +} + +// NSKVOEnsureKeyWillNotify is the main entrypoint into the swizzling code. +void +_NSKVOEnsureKeyWillNotify(id object, NSString *key) +{ + char *rawKey; + + // Since we cannot replace the isa of tagged pointer objects, we can't swizzle + // them. + if (isSmallObject_np(object)) + { + return; + } + + // A class is allowed to decline automatic swizzling for any/all of its keys. + if (![[object class] automaticallyNotifiesObserversForKey: key]) + { + return; + } + + rawKey = mutableBufferFromString(key); + rawKey[0] = toupper(rawKey[0]); + + @synchronized(object) + { + _NSKVOEnsureObjectIsKVOAware(object); + _NSKVOEnsureSimpleKeyWillNotify(object, key, rawKey); + _NSKVOEnsureOrderedCollectionWillNotify(object, key, rawKey); + _NSKVOEnsureUnorderedCollectionWillNotify(object, key, rawKey); + } + + free(rawKey); +} diff --git a/Source/NSKVOTypeEncodingCases.h b/Source/NSKVOTypeEncodingCases.h new file mode 100644 index 000000000..f71dbf71a --- /dev/null +++ b/Source/NSKVOTypeEncodingCases.h @@ -0,0 +1,90 @@ +/** + NSKVOSwizzling.m + + Copyright (C) 2024 Free Software Foundation, Inc. + + Written by: Hugo Melder + Date: June 2024 + + Based on WinObjC KVO tests by Microsoft Corporation. + + This file is part of GNUStep-base + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + If you are interested in a warranty or support for this source code, + contact Scott Christley for more information. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free + Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110 USA. +*/ +/** + Copyright (c) Microsoft. All rights reserved. + + This code is licensed under the MIT License (MIT). + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +/* This Key Value Observing Implementation is tied to libobjc2 */ + +#pragma once + +// Each OBJC_APPLY_*_TYPE_ENCODINGS macro expects a single argument: the name +// of a macro to apply for every type encoding. That macro should take the form +// #define name(ctype, objectiveCName, CapitalizedObjectiveCName, +// typeEncodingCharacter) + +#if defined(_Bool) +#define OBJC_APPLY_BOOL_TYPE_ENCODING(_APPLY_TYPE_MACRO) \ + _APPLY_TYPE_MACRO(_Bool, bool, Bool, 'B') +#else +#define OBJC_APPLY_BOOL_TYPE_ENCODING(_APPLY_TYPE_MACRO) // Nothing +#endif + +#define OBJC_APPLY_NUMERIC_TYPE_ENCODINGS(_APPLY_TYPE_MACRO) \ + _APPLY_TYPE_MACRO(double, double, Double, 'd') \ + _APPLY_TYPE_MACRO(float, float, Float, 'f') \ + _APPLY_TYPE_MACRO(signed char, char, Char, 'c') \ + _APPLY_TYPE_MACRO(int, int, Int, 'i') \ + _APPLY_TYPE_MACRO(short, short, Short, 's') \ + _APPLY_TYPE_MACRO(long, long, Long, 'l') \ + _APPLY_TYPE_MACRO(long long, longLong, LongLong, 'q') \ + _APPLY_TYPE_MACRO(unsigned char, unsignedChar, UnsignedChar, 'C') \ + _APPLY_TYPE_MACRO(unsigned short, unsignedShort, UnsignedShort, 'S') \ + _APPLY_TYPE_MACRO(unsigned int, unsignedInt, UnsignedInt, 'I') \ + _APPLY_TYPE_MACRO(unsigned long, unsignedLong, UnsignedLong, 'L') \ + _APPLY_TYPE_MACRO(unsigned long long, unsignedLongLong, UnsignedLongLong, \ + 'Q') \ + OBJC_APPLY_BOOL_TYPE_ENCODING(_APPLY_TYPE_MACRO) + +//APPLY_TYPE(__int128, int128, Int128, 't') \ +//APPLY_TYPE(unsigned __int128, unsignedInt128, UnsignedInt128, 'T') + +#define OBJC_APPLY_OBJECTIVEC_TYPE_ENCODINGS(_APPLY_TYPE_MACRO) \ + _APPLY_TYPE_MACRO(id, object, Object, '@') \ + _APPLY_TYPE_MACRO(Class, class, Pointer, '#') \ + _APPLY_TYPE_MACRO(SEL, selector, Pointer, ':') +#define OBJC_APPLY_POINTER_TYPE_ENCODINGS(_APPLY_TYPE_MACRO) \ + _APPLY_TYPE_MACRO(char *, cString, Pointer, '*') + +#define OBJC_APPLY_ALL_TYPE_ENCODINGS(_APPLY_TYPE_MACRO) \ + OBJC_APPLY_NUMERIC_TYPE_ENCODINGS(_APPLY_TYPE_MACRO) \ + OBJC_APPLY_OBJECTIVEC_TYPE_ENCODINGS(_APPLY_TYPE_MACRO) \ + OBJC_APPLY_POINTER_TYPE_ENCODINGS(_APPLY_TYPE_MACRO) diff --git a/Source/NSKeyValueCoding.m b/Source/NSKeyValueCoding.m index c83758ddc..17a9ab192 100644 --- a/Source/NSKeyValueCoding.m +++ b/Source/NSKeyValueCoding.m @@ -143,6 +143,7 @@ static inline void setupCompat() } } } + GSObjCSetVal(self, key, anObject, sel, type, size, off); } @@ -350,6 +351,8 @@ - (void) setValue: (id)anObject forKey: (NSString*)aKey { unsigned size = [aKey length] * 8; char key[size + 1]; + BOOL shouldNotify = [[self class] automaticallyNotifiesObserversForKey:aKey]; + #ifdef WANT_DEPRECATED_KVC_COMPAT IMP o = [self methodForSelector: @selector(takeValue:forKey:)]; @@ -365,7 +368,18 @@ - (void) setValue: (id)anObject forKey: (NSString*)aKey maxLength: size + 1 encoding: NSUTF8StringEncoding]; size = strlen(key); + + if (shouldNotify) + { + [self willChangeValueForKey: aKey]; + } + SetValueForKey(self, anObject, key, size); + + if (shouldNotify) + { + [self didChangeValueForKey: aKey]; + } } diff --git a/Source/NSKeyValueMutableSet.m b/Source/NSKeyValueMutableSet.m index 0fd15fefc..656e96139 100644 --- a/Source/NSKeyValueMutableSet.m +++ b/Source/NSKeyValueMutableSet.m @@ -103,6 +103,8 @@ + (NSKeyValueMutableSet *) setForKey: (NSString *)aKey ofObject: (id)anObject } + /* Ordering as specified in "Accessor Search Patterns" from Key-Value Coding + * Programming Guide */ proxy = [NSKeyValueFastMutableSet setForKey: aKey ofObject: anObject withCapitalizedKey: keybuf]; @@ -717,14 +719,14 @@ - (void) unionSet: (id)anObject { [object willChangeValueForKey: key withSetMutation: NSKeyValueUnionSetMutation - usingObjects: [NSSet setWithObject: anObject]]; + usingObjects: anObject]; } [set unionSet: anObject]; if (notifiesObservers && !changeInProgress) { [object didChangeValueForKey: key withSetMutation: NSKeyValueUnionSetMutation - usingObjects: [NSSet setWithObject:anObject]]; + usingObjects: anObject]; } } @@ -734,14 +736,14 @@ - (void) minusSet: (id)anObject { [object willChangeValueForKey: key withSetMutation: NSKeyValueMinusSetMutation - usingObjects: [NSSet setWithObject: anObject]]; + usingObjects: anObject]; } [set minusSet: anObject]; if (notifiesObservers && !changeInProgress) { [object didChangeValueForKey: key withSetMutation: NSKeyValueMinusSetMutation - usingObjects: [NSSet setWithObject: anObject]]; + usingObjects: anObject]; } } @@ -751,14 +753,14 @@ - (void) intersectSet: (id)anObject { [object willChangeValueForKey: key withSetMutation: NSKeyValueIntersectSetMutation - usingObjects: [NSSet setWithObject: anObject]]; + usingObjects: anObject]; } [set intersectSet: anObject]; if (notifiesObservers && !changeInProgress) { [object didChangeValueForKey: key withSetMutation: NSKeyValueIntersectSetMutation - usingObjects: [NSSet setWithObject: anObject]]; + usingObjects: anObject]; } } @@ -768,14 +770,14 @@ - (void) setSet: (id)anObject { [object willChangeValueForKey: key withSetMutation: NSKeyValueSetSetMutation - usingObjects: [NSSet setWithObject: anObject]]; + usingObjects: anObject]; } [set setSet: anObject]; if (notifiesObservers && !changeInProgress) { [object didChangeValueForKey: key withSetMutation: NSKeyValueSetSetMutation - usingObjects: [NSSet setWithObject: anObject]]; + usingObjects: anObject]; } } @end diff --git a/Source/NSKeyValueObserving.m b/Source/NSKeyValueObserving.m index 2f1d2f96a..b761a0141 100644 --- a/Source/NSKeyValueObserving.m +++ b/Source/NSKeyValueObserving.m @@ -2071,6 +2071,60 @@ - (void) didChangeValueForKey: (NSString*)aKey @end +@implementation NSObject (NSKeyValueObservingPrivate) + +- (void)_notifyObserversOfChangeForKey:(NSString *)aKey + oldValue:(id)old + newValue:(id)new +{ + GSKVOPathInfo *pathInfo; + GSKVOInfo *info; + + info = (GSKVOInfo *)[self observationInfo]; + if (info == nil) + { + return; + } + + if (new == nil) + { + new = null; + } + if (old == nil) + { + old = null; + } + + pathInfo = [info lockReturningPathInfoForKey: aKey]; + if (pathInfo != nil) + { + if (pathInfo->recursion++ == 0) + { + [pathInfo->change setObject: old + forKey: NSKeyValueChangeOldKey]; + [pathInfo->change removeObjectForKey: NSKeyValueChangeNewKey]; + [pathInfo->change setValue: + [NSNumber numberWithInt: NSKeyValueChangeSetting] + forKey: NSKeyValueChangeKindKey]; + [pathInfo notifyForKey: aKey ofInstance: [info instance] prior: YES]; + + [pathInfo->change setValue: new + forKey: NSKeyValueChangeNewKey]; + [pathInfo notifyForKey: aKey ofInstance: [info instance] prior: NO]; + } + if (pathInfo->recursion > 0) + { + pathInfo->recursion--; + } + [info unlock]; + } + + [self willChangeValueForDependentsOfKey: aKey]; + [self didChangeValueForDependentsOfKey: aKey]; +} + +@end + @implementation NSObject (NSKeyValueObservingCustomization) + (BOOL) automaticallyNotifiesObserversForKey: (NSString*)aKey diff --git a/Source/NSLock.m b/Source/NSLock.m index bcfa51ee9..ae2310a31 100644 --- a/Source/NSLock.m +++ b/Source/NSLock.m @@ -951,12 +951,12 @@ + (id) allocWithZone: (NSZone*)z { assert(mutex->depth == 0); mutex->depth = 1; - atomic_store(&mutex->owner, thisThread); + gs_atomic_store(&mutex->owner, thisThread); return 0; } // needs to be atomic because another thread can concurrently set it - ownerThread = atomic_load(&mutex->owner); + ownerThread = gs_atomic_load(&mutex->owner); if (ownerThread == thisThread) { // this thread already owns this lock @@ -985,7 +985,7 @@ + (id) allocWithZone: (NSZone*)z AcquireSRWLockExclusive(&mutex->lock); assert(mutex->depth == 0); mutex->depth = 1; - atomic_store(&mutex->owner, thisThread); + gs_atomic_store(&mutex->owner, thisThread); return 0; } @@ -999,12 +999,12 @@ + (id) allocWithZone: (NSZone*)z { assert(mutex->depth == 0); mutex->depth = 1; - atomic_store(&mutex->owner, thisThread); + gs_atomic_store(&mutex->owner, thisThread); return 0; } // needs to be atomic because another thread can concurrently set it - ownerThread = atomic_load(&mutex->owner); + ownerThread = gs_atomic_load(&mutex->owner); if (ownerThread == thisThread && mutex->attr == gs_mutex_attr_recursive) { // this thread already owns this lock and it's recursive @@ -1028,7 +1028,7 @@ + (id) allocWithZone: (NSZone*)z case gs_mutex_attr_recursive: { // return error if lock is not held by this thread DWORD thisThread = GetCurrentThreadId(); - DWORD ownerThread = atomic_load(&mutex->owner); + DWORD ownerThread = gs_atomic_load(&mutex->owner); if (ownerThread != thisThread) { return EPERM; } @@ -1046,7 +1046,7 @@ + (id) allocWithZone: (NSZone*)z { assert(mutex->depth == 1); mutex->depth = 0; - atomic_store(&mutex->owner, 0); + gs_atomic_store(&mutex->owner, 0); ReleaseSRWLockExclusive(&mutex->lock); return 0; } @@ -1060,7 +1060,7 @@ + (id) allocWithZone: (NSZone*)z assert(mutex->depth == 1); mutex->depth = 0; - atomic_store(&mutex->owner, 0); + gs_atomic_store(&mutex->owner, 0); if (!SleepConditionVariableSRW(cond, &mutex->lock, millisecs, 0)) { @@ -1074,7 +1074,7 @@ + (id) allocWithZone: (NSZone*)z assert(mutex->depth == 0); mutex->depth = 1; - atomic_store(&mutex->owner, GetCurrentThreadId()); + gs_atomic_store(&mutex->owner, GetCurrentThreadId()); return retVal; } diff --git a/Source/NSMethodSignature.m b/Source/NSMethodSignature.m index f2612dca0..4277dc64a 100644 --- a/Source/NSMethodSignature.m +++ b/Source/NSMethodSignature.m @@ -38,7 +38,28 @@ #import "Foundation/NSException.h" #import "Foundation/NSCoder.h" + +static inline unsigned int +gs_string_hash(const char *s) +{ + unsigned int val = 0; + while (*s != 0) + { + val = (val << 5) + val + *s++; + } + return val; +} + +#define GSI_MAP_RETAIN_KEY(M, X) +#define GSI_MAP_RELEASE_KEY(M, X) +#define GSI_MAP_HASH(M, X) (gs_string_hash(X.ptr)) +#define GSI_MAP_EQUAL(M, X,Y) (strcmp(X.ptr, Y.ptr) == 0) +#define GSI_MAP_KTYPES GSUNION_PTR +#define GSI_MAP_VTYPES GSUNION_OBJ +#import "GNUstepBase/GSIMap.h" + #import "GSInvocation.h" +#import "GSPThread.h" #ifdef HAVE_MALLOC_H #if !defined(__OpenBSD__) @@ -555,7 +576,29 @@ - (id) _initWithObjCTypes: (const char*)t + (NSMethodSignature*) signatureWithObjCTypes: (const char*)t { - return AUTORELEASE([[[self class] alloc] _initWithObjCTypes: t]); + GSIMapNode node; + NSMethodSignature *sig; + + static GSIMapTable_t cacheTable = {}; + static gs_mutex_t cacheTableLock = GS_MUTEX_INIT_STATIC; + + GS_MUTEX_LOCK(cacheTableLock); + if (cacheTable.zone == 0) + { + GSIMapInitWithZoneAndCapacity(&cacheTable, [self zone], 8); + } + + node = GSIMapNodeForKey(&cacheTable, (GSIMapKey)t); + if (node == 0) + { + sig = [[self alloc] _initWithObjCTypes: t]; + GSIMapAddPair(&cacheTable, (GSIMapKey)t, (GSIMapVal)(id)sig); + } else { + sig = RETAIN(node->value.obj); + } + GS_MUTEX_UNLOCK(cacheTableLock); + + return AUTORELEASE(sig); } - (NSArgumentInfo) argumentInfoAtIndex: (NSUInteger)index diff --git a/Source/NSUserDefaults.m b/Source/NSUserDefaults.m index 8acf311f4..1a0b8ec74 100644 --- a/Source/NSUserDefaults.m +++ b/Source/NSUserDefaults.m @@ -54,6 +54,8 @@ #import "GNUstepBase/NSProcessInfo+GNUstepBase.h" #import "GNUstepBase/NSString+GNUstepBase.h" +#import "NSKVOInternal.h" + #if defined(_WIN32) #import "win32/NSString+Win32Additions.h" @@ -359,15 +361,15 @@ - (BOOL) synchronize; NSMutableArray *names = [NSMutableArray arrayWithCapacity: 10]; #if defined(_WIN32) - NSEnumerator *enumerator; - NSArray *languages; - NSString *locale; - BOOL ret; - + NSEnumerator *enumerator; + NSArray *languages; + NSString *locale; + BOOL ret; unsigned long numberOfLanguages = 0; unsigned long length = 7; unsigned long factor = sizeof(wchar_t); - wchar_t *buffer = malloc(length * factor); + wchar_t *buffer = malloc(length * factor); + if (!buffer) { return names; @@ -377,56 +379,61 @@ - (BOOL) synchronize; * two-letter language code, and CC is the two-letter country code. */ ret = GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &numberOfLanguages, - buffer, &length); + buffer, &length); if (!ret) { length = 0; - if (GetLastError() == ERROR_INSUFFICIENT_BUFFER && - GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &numberOfLanguages, - NULL, &length)) { - wchar_t *oldBuffer = buffer; - buffer = realloc(buffer, length * factor); - if (!buffer) - { - free(oldBuffer); - return names; - } + if (GetLastError() == ERROR_INSUFFICIENT_BUFFER + && GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &numberOfLanguages, + NULL, &length)) + { + wchar_t *oldBuffer = buffer; - ret = GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &numberOfLanguages, buffer, &length); - if (!ret) - { - free(buffer); - return names; - } - } + buffer = realloc(buffer, length * factor); + if (!buffer) + { + free(oldBuffer); + return names; + } + + ret = GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, + &numberOfLanguages, buffer, &length); + if (!ret) + { + free(buffer); + return names; + } + } } - languages = [NSString arrayFromWCharList:buffer length:length]; + languages = [NSString arrayFromWCharList: buffer length: length]; enumerator = [languages objectEnumerator]; free(buffer); - while (nil != (locale = [enumerator nextObject])) - { + while (nil != (locale = [enumerator nextObject])) + { /* Replace "-" Separator with "_" */ - locale = [locale stringByReplacingOccurrencesOfString:@"-" withString:@"_"]; - [names addObjectsFromArray: GSLanguagesFromLocale(locale)]; - } + locale = [locale stringByReplacingOccurrencesOfString: @"-" + withString: @"_"]; + [names addObjectsFromArray: GSLanguagesFromLocale(locale)]; + } #elif defined(__ANDROID__) - // When running on Android, the process must be correctly initialized - // with GSInitializeProcessAndroid (See NSProcessInfo). - // - // If the minimum API level is 24 or higher, the user-prefered locales - // are retrieved from the Android system and passed as GSAndroidLocaleList - // process argument - NSArray *args = [[NSProcessInfo processInfo] arguments]; - NSEnumerator *enumerator = [args objectEnumerator]; - NSString *key = nil; - NSString *localeList = nil; + /* When running on Android, the process must be correctly initialized + * with GSInitializeProcessAndroid (See NSProcessInfo). + * + * If the minimum API level is 24 or higher, the user-prefered locales + * are retrieved from the Android system and passed as GSAndroidLocaleList + * process argument + */ + NSArray *args = [[NSProcessInfo processInfo] arguments]; + NSEnumerator *enumerator = [args objectEnumerator]; + NSString *key = nil; + NSString *localeList = nil; [enumerator nextObject]; // Skip process name. while (nil != (key = [enumerator nextObject])) { - if ([key isEqualToString:@"-GSAndroidLocaleList"]) + if ([key isEqualToString: @"-GSAndroidLocaleList"]) { localeList = [enumerator nextObject]; break; @@ -436,8 +443,8 @@ - (BOOL) synchronize; // The locale list is a comma-separated list of locales of form ll-CC if (localeList != nil) { - NSString *locale; - NSArray *locales = [localeList componentsSeparatedByString: @","]; + NSString *locale; + NSArray *locales = [localeList componentsSeparatedByString: @","]; enumerator = [locales objectEnumerator]; while (nil != (locale = [enumerator nextObject])) @@ -446,16 +453,19 @@ - (BOOL) synchronize; } } #else - // Add the languages listed in the LANGUAGE environment variable - // (a non-POSIX GNU extension) + /* Add the languages listed in the LANGUAGE environment variable + * (a non-POSIX GNU extension) + */ { - NSString *env = [[[NSProcessInfo processInfo] environment] - objectForKey: @"LANGUAGE"]; + NSString *env; + + env = [[[NSProcessInfo processInfo] environment] objectForKey: @"LANGUAGE"]; if (env != nil && [env length] > 0) { - NSArray *array = [env componentsSeparatedByString: @":"]; - NSEnumerator *enumerator = [array objectEnumerator]; - NSString *locale; + NSArray *array = [env componentsSeparatedByString: @":"]; + NSEnumerator *enumerator = [array objectEnumerator]; + NSString *locale; + while (nil != (locale = [enumerator nextObject])) { [names addObjectsFromArray: GSLanguagesFromLocale(locale)]; @@ -656,6 +666,23 @@ + (void) atExit DESTROY(syncLock); } +/* Opt-out off automatic willChange/didChange notifications + * as the KVO behaviour for NSUserDefaults is slightly different. + * + * We do not notify observers of changes that do not actually + * change the value of a key (the value is equal to the old value). + * + * https://developer.apple.com/documentation/foundation/nsuserdefaults#2926902 + * "You can use key-value observing to be notified of any updates to + * a particular default value. You can also register as an observer for + * NSUserDefaultsDidChangeNotification on the defaultCenter notification center + * in order to be notified of all updates to a local defaults database." + */ ++ (BOOL) automaticallyNotifiesObserversForKey: (NSString*)key +{ + return NO; +} + + (void) initialize { if (self == [NSUserDefaults class]) @@ -1494,12 +1521,35 @@ - (void) removeObjectForKey: (NSString*)defaultName NS_DURING { GSPersistentDomain *pd = [_persDomains objectForKey: processName]; + id old = [self objectForKey: defaultName]; if (nil != pd) { - if ([pd setObject: nil forKey: defaultName]) + if ([pd setObject: nil forKey: defaultName]) { + id new; [self _changePersistentDomain: processName]; + new = [self objectForKey: defaultName]; + /* Emit only a KVO notification when the value has actually + * changed, meaning -objectForKey: would return a different + * value than before. + */ + if ([new hash] != [old hash]) + { + [self _notifyObserversOfChangeForKey: defaultName + oldValue: old + newValue: new]; + } + } + else + { + /* We always notify observers of a change, even if the value + * itself is unchanged. + */ + [[NSNotificationCenter defaultCenter] + postNotificationName: NSUserDefaultsDidChangeNotification + object: self]; + } } [_lock unlock]; @@ -1619,6 +1669,7 @@ - (void) setObject: (id)value forKey: (NSString*)defaultName NS_DURING { GSPersistentDomain *pd; + id old; pd = [_persDomains objectForKey: processName]; if (nil == pd) @@ -1628,9 +1679,35 @@ - (void) setObject: (id)value forKey: (NSString*)defaultName [_persDomains setObject: pd forKey: processName]; RELEASE(pd); } + // Make sure to search all domains and not only the process domain + old = [self objectForKey: defaultName]; if ([pd setObject: value forKey: defaultName]) { + id new; + + /* New value must be fetched from all domains, as there might be + * a registered default if value is nil, or the value is + * superseded by GSPrimary or NSArgumentDomain + */ + new = [self objectForKey: defaultName]; [self _changePersistentDomain: processName]; + + // Emit only a KVO notification when the value has actually changed + if ([new hash] != [old hash]) + { + [self _notifyObserversOfChangeForKey: defaultName + oldValue: old + newValue: new]; + } + } + else + { + /* We always notify observers of a change, even if the value + * itself is unchanged. + */ + [[NSNotificationCenter defaultCenter] + postNotificationName: NSUserDefaultsDidChangeNotification + object: self]; } [_lock unlock]; } diff --git a/Tests/base/NSKVOSupport/TestInfo b/Tests/base/NSKVOSupport/TestInfo new file mode 100644 index 000000000..e69de29bb diff --git a/Tests/base/NSKVOSupport/basic.m b/Tests/base/NSKVOSupport/basic.m new file mode 100644 index 000000000..9d6389d76 --- /dev/null +++ b/Tests/base/NSKVOSupport/basic.m @@ -0,0 +1,120 @@ +#import +#import "ObjectTesting.h" + +@interface Foo : NSObject +{ +@public + BOOL a; + NSInteger b; + NSString *c; + NSArray *d; +} +- (void) setA: (BOOL)v; +- (void) setB: (NSInteger)v; +- (void) setC: (NSString *)v; +@end + +@implementation Foo +- (void) setA: (BOOL)v +{ + a = v; +} +- (void) setB: (NSInteger)v +{ + b = v; +} +- (void) setC: (NSString *)v +{ + c = v; +} +@end + +@interface Observer : NSObject +{ + Foo *object; + NSString *expectedKeyPath; + NSInteger receivedCalls; +} +- (NSString*) expectedKeyPath; +- (void) setExpectedKeyPath: (NSString*)s; +- (NSInteger) receivedCalls; +- (void) setReceivedCalls: (NSInteger)i; +@end + +@implementation Observer + +- (id)init +{ + self = [super init]; + if (self) + { + receivedCalls = 0; + } + return self; +} + +static char observerContext; + +- (void)startObserving:(Foo *)target +{ + object = target; + [target addObserver:self forKeyPath:@"a" options:0 context:&observerContext]; + [target addObserver:self forKeyPath:@"b" options:0 context:&observerContext]; + [target addObserver:self forKeyPath:@"c" options:0 context:&observerContext]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)o + change:(NSDictionary *)change + context:(void *)context +{ + PASS(context == &observerContext, "context"); + PASS(o == self->object, "object"); + PASS([keyPath isEqualToString: [self expectedKeyPath]], "key path"); + [self setReceivedCalls: [self receivedCalls] + 1]; +} + +- (NSString*) expectedKeyPath +{ + return expectedKeyPath; +} +- (void) setExpectedKeyPath: (NSString*)s +{ + expectedKeyPath = s; +} +- (NSInteger) receivedCalls +{ + return receivedCalls; +} +- (void) setReceivedCalls: (NSInteger)i +{ + receivedCalls = i; +} + +@end + +int +main(int argc, char *argv[]) +{ + [NSAutoreleasePool new]; + + Foo *foo = [Foo new]; + Observer *obs = [Observer new]; + + [obs startObserving: foo]; + + [obs setExpectedKeyPath: @"a"]; + [foo setA: YES]; + PASS([obs receivedCalls] == 1, "received calls") + + [obs setExpectedKeyPath: @"b"]; + [foo setB: 1]; + PASS([obs receivedCalls] == 2, "received calls") + + [obs setExpectedKeyPath: @"c"]; + [foo setC: @"henlo"]; + PASS([obs receivedCalls] == 3, "received calls") + + return 0; +} + diff --git a/Tests/base/NSKVOSupport/general.m b/Tests/base/NSKVOSupport/general.m new file mode 100644 index 000000000..c215bde32 --- /dev/null +++ b/Tests/base/NSKVOSupport/general.m @@ -0,0 +1,2172 @@ +/** + general.m + + + Copyright (C) 2024 Free Software Foundation, Inc. + + Written by: Hugo Melder + Date: June 2024 + + Based on WinObjC KVO tests by Microsoft Corporation. + + This file is part of GNUStep-base + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free + Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110 USA. +*/ +/** + Copyright (c) Microsoft. All rights reserved. + + This code is licensed under the MIT License (MIT). + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +#import +#import "Testing.h" + +#define BOXF(V) [NSNumber numberWithFloat: (V)] +#define BOXI(V) [NSNumber numberWithInteger: (V)] +#define MPAIR(K,V)\ + [NSMutableDictionary dictionaryWithObjectsAndKeys: V, K, nil] +#define BOXBOOL(V) [NSNumber numberWithBool: (V)] + +#if defined (__OBJC2__) +#define FLAKY_ON_GCC_START +#define FLAKY_ON_GCC_END +#else +#define FLAKY_ON_GCC_START \ + testHopeful = YES; +#define FLAKY_ON_GCC_END \ + testHopeful = NO; +#endif + +@interface TestKVOSelfObserver : NSObject +{ + id _dummy; +} +@end +@implementation TestKVOSelfObserver +- (id)init +{ + self = [super init]; + if (self) + { + [self addObserver:self forKeyPath:@"dummy" options:0 context:nil]; + } + return self; +} +- (void)dealloc +{ + [self removeObserver:self forKeyPath:@"dummy"]; + [super dealloc]; +} +@end + +@interface TestKVOChange : NSObject { + NSString *_keypath; + id _object; + NSDictionary *_info; + void *_context; +} + +- (NSString *)keypath; +- (void)setKeypath:(NSString *)newKeypath; + +- (id)object; +- (void)setObject:(id)newObject; + +- (NSDictionary *)info; +- (void)setInfo:(NSDictionary *)newInfo; + +- (void *)context; +- (void)setContext:(void *)newContext; + +@end + +@implementation TestKVOChange ++ (id)changeWithKeypath:(NSString *)keypath + object:(id)object + info:(NSDictionary *)info + context:(void *)context +{ + TestKVOChange *change = [[[self alloc] init] autorelease]; + [change setKeypath: keypath]; + [change setObject: object]; + [change setInfo: info]; + [change setContext: context]; + return change; +} + +- (NSString *)keypath { + return _keypath; +} + +- (void)setKeypath:(NSString *)newKeypath +{ + if (_keypath != newKeypath) + { + [_keypath release]; + _keypath = [newKeypath copy]; + } +} + +- (id)object +{ + return _object; +} + +- (void)setObject:(id)newObject +{ + ASSIGN(_object, newObject); +} + +- (NSDictionary *)info +{ + return _info; +} + +- (void)setInfo:(NSDictionary *)newInfo +{ + ASSIGN(_info, [newInfo copy]); +} + +- (void *)context +{ + return _context; +} + +- (void)setContext:(void *)newContext +{ + _context = newContext; +} + +- (void)dealloc +{ + [_object release]; + [_keypath release]; + [_info release]; + [super dealloc]; +} + +@end + +@interface TestKVOObserver : NSObject +{ + NSMutableDictionary *_changedKeypaths; + NSLock *_lock; +} +- (void)observeValueForKeyPath:(NSString *)keypath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context; +- (NSSet *)changesForKeypath:(NSString *)keypath; +- (NSInteger)numberOfObservedChanges; +@end + +@implementation TestKVOObserver +- (id)init +{ + self = [super init]; + if (self) + { + _changedKeypaths = [NSMutableDictionary new]; + _lock = [NSLock new]; + } + return self; +} +- (void)observeValueForKeyPath:(NSString *)keypath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context +{ + { + NSMutableSet *changeSet = [_changedKeypaths objectForKey:keypath]; + if (!changeSet) + { + changeSet = [NSMutableSet set]; + [_changedKeypaths setObject: changeSet forKey: keypath]; + } + [changeSet addObject:[TestKVOChange changeWithKeypath:keypath + object:object + info:change + context:context]]; + } +} +- (NSSet *)changesForKeypath:(NSString *)keypath +{ + [_lock lock]; + NSSet *paths = [[_changedKeypaths objectForKey:keypath] copy]; + [_lock unlock]; + return paths; +} +- (void)clear +{ + [_lock lock]; + [_changedKeypaths removeAllObjects]; + [_lock unlock]; +} +- (NSInteger)numberOfObservedChanges +{ + [_lock lock]; + NSInteger accumulator = 0; + for (NSString *keypath in [_changedKeypaths allKeys]) + { + accumulator += [[_changedKeypaths objectForKey:keypath] count]; + } + [_lock unlock]; + return accumulator; +} + +- (void) dealloc { + [_lock release]; + [_changedKeypaths release]; +} +@end + +struct TestKVOStruct +{ + int a, b, c; +}; + +/* +@interface TestKVOObject : NSObject +{ + NSString *_internal_derivedObjectProperty; + NSString *_internal_keyDerivedTwoTimes; + int _manuallyNotifyingIntegerProperty; + int _ivarWithoutSetter; +} + +@property (nonatomic, retain) NSString *nonNotifyingObjectProperty; + +@property (nonatomic, retain) NSString *basicObjectProperty; +@property (nonatomic, assign) uint32_t basicPodProperty; +@property (nonatomic, assign) struct TestKVOStruct structProperty; + +// derivedObjectProperty is derived from basicObjectProperty. +@property (nonatomic, readonly) NSString *derivedObjectProperty; + +@property (nonatomic, retain) TestKVOObject *cascadableKey; +@property (nonatomic, readonly) TestKVOObject *derivedCascadableKey; + +@property (nonatomic, retain) id recursiveDependent1; +@property (nonatomic, retain) id recursiveDependent2; + +@property (nonatomic, retain) NSMutableDictionary *dictionaryProperty; + +@property (nonatomic, retain) id boolTrigger1; +@property (nonatomic, retain) id boolTrigger2; +@property (nonatomic, readonly) bool dependsOnTwoKeys; + +- (void)incrementManualIntegerProperty; +@end +*/ + +@interface TestKVOObject : NSObject { + NSString *_internal_derivedObjectProperty; + NSString *_internal_keyDerivedTwoTimes; + int _manuallyNotifyingIntegerProperty; + int _ivarWithoutSetter; + + NSString *_nonNotifyingObjectProperty; + NSString *_basicObjectProperty; + uint32_t _basicPodProperty; + struct TestKVOStruct _structProperty; + TestKVOObject *_cascadableKey; + id _recursiveDependent1; + id _recursiveDependent2; + NSMutableDictionary *_dictionaryProperty; + id _boolTrigger1; + id _boolTrigger2; +} + +- (NSString *)nonNotifyingObjectProperty; +- (void)setNonNotifyingObjectProperty:(NSString *)newValue; + +- (NSString *)basicObjectProperty; +- (void)setBasicObjectProperty:(NSString *)newValue; + +- (uint32_t)basicPodProperty; +- (void)setBasicPodProperty:(uint32_t)newValue; + +- (struct TestKVOStruct)structProperty; +- (void)setStructProperty:(struct TestKVOStruct)newValue; + +- (NSString *)derivedObjectProperty; + +- (TestKVOObject *)cascadableKey; +- (void)setCascadableKey:(TestKVOObject *)newValue; + +- (TestKVOObject *)derivedCascadableKey; + +- (id)recursiveDependent1; +- (void)setRecursiveDependent1:(id)newValue; + +- (id)recursiveDependent2; +- (void)setRecursiveDependent2:(id)newValue; + +- (NSMutableDictionary *)dictionaryProperty; +- (void)setDictionaryProperty:(NSMutableDictionary *)newValue; + +- (id)boolTrigger1; +- (void)setBoolTrigger1:(id)newValue; + +- (id)boolTrigger2; +- (void)setBoolTrigger2:(id)newValue; + +- (bool)dependsOnTwoKeys; + +// This modifies the internal integer property and notifies about it. +- (void)incrementManualIntegerProperty; + +@end + + +@implementation TestKVOObject +- (void)dealloc +{ + [_cascadableKey release]; + [_nonNotifyingObjectProperty release]; + [_basicObjectProperty release]; + [_recursiveDependent1 release]; + [_recursiveDependent2 release]; + [_dictionaryProperty release]; + [_boolTrigger1 release]; + [_boolTrigger2 release]; + [super dealloc]; +} + ++ (NSSet *)keyPathsForValuesAffectingDerivedObjectProperty +{ + return [NSSet setWithObject:@"basicObjectProperty"]; +} + ++ (NSSet *)keyPathsForValuesAffectingRecursiveDependent1 +{ + return [NSSet setWithObject:@"recursiveDependent2"]; +} + ++ (NSSet *)keyPathsForValuesAffectingRecursiveDependent2 +{ + return [NSSet setWithObject:@"recursiveDependent1"]; +} + ++ (NSSet *)keyPathsForValuesAffectingDerivedCascadableKey +{ + return [NSSet setWithObject:@"cascadableKey"]; +} + ++ (NSSet *)keyPathsForValuesAffectingKeyDependentOnSubKeypath +{ + return [NSSet setWithObject:@"dictionaryProperty.subDictionary"]; +} + ++ (NSSet *)keyPathsForValuesAffectingKeyDerivedTwoTimes +{ + return [NSSet setWithObject:@"derivedObjectProperty"]; +} + ++ (NSSet *)keyPathsForValuesAffectingDependsOnTwoKeys +{ + return [NSSet setWithArray: [NSArray arrayWithObjects: + @"boolTrigger1", @"boolTrigger2", nil] ]; +} + ++ (NSSet *)keyPathsForValuesAffectingDependsOnTwoSubKeys +{ + return [NSSet setWithArray: [NSArray arrayWithObjects: + @"cascadableKey.boolTrigger1", @"cascadableKey.boolTrigger2", nil] ]; +} + +- (bool)dependsOnTwoKeys +{ + return _boolTrigger1 != nil && _boolTrigger2 != nil; +} + +- (bool)dependsOnTwoSubKeys +{ + return _cascadableKey.boolTrigger1 != nil + && _cascadableKey.boolTrigger2 != nil; +} + +- (id)keyDependentOnSubKeypath +{ + return [_dictionaryProperty objectForKey:@"subDictionary"]; +} + ++ (BOOL)automaticallyNotifiesObserversOfManuallyNotifyingIntegerProperty +{ + return NO; +} + ++ (BOOL)automaticallyNotifiesObserversOfNonNotifyingObjectProperty +{ + return NO; +} + +- (NSString *)derivedObjectProperty +{ + return _internal_derivedObjectProperty; +} + +- (void)setBasicObjectProperty:(NSString *)basicObjectProperty +{ + [_basicObjectProperty release]; + _basicObjectProperty = [basicObjectProperty retain]; + _internal_derivedObjectProperty = + [NSString stringWithFormat:@"!!!%@!!!", _basicObjectProperty]; + _internal_keyDerivedTwoTimes = + [NSString stringWithFormat:@"---%@---", [self derivedObjectProperty]]; +} + +- (NSString *)keyDerivedTwoTimes +{ + return _internal_keyDerivedTwoTimes; +} + +- (TestKVOObject *)derivedCascadableKey +{ + return _cascadableKey; +} + +- (void)incrementManualIntegerProperty +{ + [self willChangeValueForKey:@"manuallyNotifyingIntegerProperty"]; + _manuallyNotifyingIntegerProperty++; + [self didChangeValueForKey:@"manuallyNotifyingIntegerProperty"]; +} + +// Accessors + +- (NSString *)nonNotifyingObjectProperty { + return _nonNotifyingObjectProperty; +} + +- (void)setNonNotifyingObjectProperty:(NSString *)newValue { + ASSIGN(_nonNotifyingObjectProperty, newValue); +} + +- (NSString *)basicObjectProperty { + return _basicObjectProperty; +} + +- (uint32_t)basicPodProperty { + return _basicPodProperty; +} + +- (void)setBasicPodProperty:(uint32_t)newValue { + _basicPodProperty = newValue; +} + +- (struct TestKVOStruct)structProperty { + return _structProperty; +} + +- (void)setStructProperty:(struct TestKVOStruct)newValue { + _structProperty = newValue; +} + +- (TestKVOObject *)cascadableKey { + return _cascadableKey; +} + +- (void)setCascadableKey:(TestKVOObject *)newValue { + ASSIGN(_cascadableKey, newValue); +} + +- (id)recursiveDependent1 { + return _recursiveDependent1; +} + +- (void)setRecursiveDependent1:(id)newValue { + ASSIGN(_recursiveDependent1, newValue); +} + +- (id)recursiveDependent2 { + return _recursiveDependent2; +} + +- (void)setRecursiveDependent2:(id)newValue { + ASSIGN(_recursiveDependent2, newValue); +} + +- (NSMutableDictionary *)dictionaryProperty { + return _dictionaryProperty; +} + +- (void)setDictionaryProperty:(NSMutableDictionary *)newValue { + ASSIGN(_dictionaryProperty, newValue); +} + +- (id)boolTrigger1 { + return _boolTrigger1; +} + +- (void)setBoolTrigger1:(id)newValue { + ASSIGN(_boolTrigger1, newValue); +} + +- (id)boolTrigger2 { + return _boolTrigger2; +} + +- (void)setBoolTrigger2:(id)newValue { + ASSIGN(_boolTrigger2, newValue); +} + +@end + +@interface TestKVOObject2 : NSObject +{ + float _someFloat; +} + +@end +@implementation TestKVOObject2 +- (float)someFloat +{ + return _someFloat; +} +- (void)setSomeFloat:(float)newValue +{ + _someFloat = newValue; +} +@end + +static void +BasicChangeNotification() +{ + START_SET("BasicChangeNotification"); + FLAKY_ON_GCC_START + + TestKVOObject *observed = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObserver *observer = [[[TestKVOObserver alloc] init] autorelease]; + + [observed addObserver:observer + forKeyPath:@"basicObjectProperty" + options:NSKeyValueObservingOptionNew + context:NULL]; + observed.basicObjectProperty = @"Hello"; + + PASS_EQUAL([[observer changesForKeypath:@"basicObjectProperty"] count], 1, + "One change on basicObjectProperty should have fired."); + PASS_EQUAL([[observer changesForKeypath:@"basicPodProperty"] count], 0, + "Zero changes on basicPodProperty should have fired."); + PASS_EQUAL([[observer changesForKeypath:@"derivedObjectProperty"] count], 0, + "Zero changes on derivedObjectProperty should have fired."); + + PASS_EQUAL([[[observer changesForKeypath:@"basicObjectProperty"] anyObject] + object], + observed, + "The notification object should match the observed object."); + PASS_EQUAL( + nil, + [[[[observer changesForKeypath:@"basicObjectProperty"] anyObject] info] + objectForKey:NSKeyValueChangeOldKey], + "There should be no old value included in the change notification."); + PASS_EQUAL( + [[[[observer changesForKeypath:@"basicObjectProperty"] anyObject] info] + objectForKey:NSKeyValueChangeNewKey], + @"Hello", + "The new value stored in the change notification should be Hello."); + PASS_RUNS([observed removeObserver:observer + forKeyPath:@"basicObjectProperty"], + "remove observer should not throw"); + + FLAKY_ON_GCC_END + END_SET("BasicChangeNotification"); +} + +static void +ExclusiveChangeNotification() +{ + START_SET("ExclusiveChangeNotification"); + + TestKVOObject *observed = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObserver *observer = [[[TestKVOObserver alloc] init] autorelease]; + TestKVOObserver *observer2 = [[[TestKVOObserver alloc] init] autorelease]; + + [observed addObserver:observer + forKeyPath:@"basicObjectProperty" + options:NSKeyValueObservingOptionNew + context:NULL]; + [observed addObserver:observer2 + forKeyPath:@"basicPodProperty" + options:NSKeyValueObservingOptionNew + context:NULL]; + + [observed setBasicObjectProperty:@"Hello"]; + [observed setBasicPodProperty:1]; + + PASS_EQUAL([[observer changesForKeypath:@"basicObjectProperty"] count], 1, + "One change on basicObjectProperty should have fired."); + PASS_EQUAL( + [[observer2 changesForKeypath:@"basicObjectProperty"] count], 0, + "No changes on basicObjectProperty for second observer should have fired."); + PASS_EQUAL([[observer2 changesForKeypath:@"basicPodProperty"] count], 1, + "One change on basicPodProperty should have fired."); + PASS_EQUAL( + [[observer changesForKeypath:@"basicPodProperty"] count], 0, + "No changes on basicPodProperty for first observer should have fired."); + + PASS_RUNS([observed removeObserver:observer + forKeyPath:@"basicObjectProperty"], + "remove observer should not throw"); + PASS_RUNS([observed removeObserver:observer2 forKeyPath:@"basicPodProperty"], + "remove observer should not throw"); + + END_SET("ExclusiveChangeNotification"); +} + +static void +ManualChangeNotification() +{ + START_SET("ManualChangeNotification"); + + TestKVOObject *observed = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObserver *observer = [[[TestKVOObserver alloc] init] autorelease]; + + [observed addObserver:observer + forKeyPath:@"manuallyNotifyingIntegerProperty" + options:NSKeyValueObservingOptionNew + context:NULL]; + [observed incrementManualIntegerProperty]; + + PASS_EQUAL( + [[observer changesForKeypath:@"manuallyNotifyingIntegerProperty"] count], 1, + "One change on manuallyNotifyingIntegerProperty should have fired."); + PASS_EQUAL( + [[[[observer changesForKeypath:@"manuallyNotifyingIntegerProperty"] + anyObject] info] objectForKey:NSKeyValueChangeNewKey], + BOXI(1), + "The new value stored in the change notification should be a boxed 1."); + + PASS_RUNS([observed removeObserver:observer + forKeyPath:@"manuallyNotifyingIntegerProperty"], + "remove observer should not throw"); + + END_SET("ManualChangeNotification"); +} + +static void +BasicChangeCaptureOld() +{ + START_SET("BasicChangeCaptureOld"); + + TestKVOObject *observed = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObserver *observer = [[[TestKVOObserver alloc] init] autorelease]; + + [observed addObserver:observer + forKeyPath:@"basicObjectProperty" + options:NSKeyValueObservingOptionOld + context:NULL]; + observed.basicObjectProperty = @"Hello"; + + PASS_EQUAL([[observer changesForKeypath:@"basicObjectProperty"] count], 1, + "One change on basicObjectProperty should have fired."); + + PASS_EQUAL([[[[observer changesForKeypath:@"basicObjectProperty"] anyObject] + info] objectForKey:NSKeyValueChangeOldKey], + [NSNull null], + "The old value stored in the change notification should be null."); + + PASS_RUNS([observed removeObserver:observer + forKeyPath:@"basicObjectProperty"], + "remove observer should not throw"); + + END_SET("BasicChangeCaptureOld"); +} + +static void +CascadingNotificationWithEmptyLeaf() +{ + START_SET("CascadingNotificationWithEmptyLeaf"); + FLAKY_ON_GCC_START + + TestKVOObject *observed = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObserver *observer = [[[TestKVOObserver alloc] init] autorelease]; + + [observed + addObserver:observer + forKeyPath:@"cascadableKey.basicObjectProperty" + options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew) + context:NULL]; + + TestKVOObject *subObject = [[[TestKVOObject alloc] init] autorelease]; + subObject.basicObjectProperty = @"Hello"; + observed.cascadableKey = subObject; + + PASS_EQUAL( + [[observer changesForKeypath:@"cascadableKey.basicObjectProperty"] count], + 1, "One change on cascadableKey.basicObjectProperty should have fired."); + + PASS_EQUAL([[[[observer + changesForKeypath:@"cascadableKey.basicObjectProperty"] + anyObject] info] objectForKey:NSKeyValueChangeOldKey], + [NSNull null], + "The old value stored in the change notification should be null."); + + [observer clear]; + + TestKVOObject *subObject2 = [[[TestKVOObject alloc] init] autorelease]; + subObject2.basicObjectProperty = @"Hello"; + observed.cascadableKey = subObject2; + + PASS_EQUAL( + [[observer changesForKeypath:@"cascadableKey.basicObjectProperty"] count], + 1, + "A second change on cascadableKey.basicObjectProperty should have fired."); + + subObject.basicObjectProperty = @"Spurious?"; + + PASS(2 != + [[observer changesForKeypath:@"cascadableKey.basicObjectProperty"] + count], + "A change to the detached subkey should not have triggered a spurious " + "notification."); + + PASS_EQUAL( + [[[[observer changesForKeypath:@"cascadableKey.basicObjectProperty"] + anyObject] info] objectForKey:NSKeyValueChangeOldKey], + @"Hello", + "The old value stored in the change notification should be Hello."); + + PASS_RUNS([observed removeObserver:observer + forKeyPath:@"cascadableKey.basicObjectProperty"], + "remove observer should not throw"); + + FLAKY_ON_GCC_END + END_SET("CascadingNotificationWithEmptyLeaf"); +} + +static void +PriorNotification() +{ + START_SET("PriorNotification"); + + TestKVOObject *observed = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObserver *observer = [[[TestKVOObserver alloc] init] autorelease]; + + [observed + addObserver:observer + forKeyPath:@"basicObjectProperty" + options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionPrior) + context:NULL]; + observed.basicObjectProperty = @"Hello"; + + PASS_EQUAL( + [[observer changesForKeypath:@"basicObjectProperty"] count], 2, + "Two changes on basicObjectProperty should have fired (one prior change)."); + + PASS_EQUAL( + [[[[observer changesForKeypath:@"basicObjectProperty"] anyObject] info] + objectForKey:NSKeyValueChangeOldKey], + [NSNull null], + "The old value stored in the change notification should be null or nil."); + + PASS_RUNS([observed removeObserver:observer + forKeyPath:@"basicObjectProperty"], + "remove observer should not throw"); + + END_SET("PriorNotification"); +} + +static void +DependentKeyNotification() +{ + START_SET("DependentKeyNotification"); + FLAKY_ON_GCC_START + + TestKVOObject *observed = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObserver *observer = [[[TestKVOObserver alloc] init] autorelease]; + + [observed addObserver:observer + forKeyPath:@"derivedObjectProperty" + options:NSKeyValueObservingOptionNew + context:NULL]; + observed.basicObjectProperty = @"Hello"; + + NSSet *basicChanges = [observer changesForKeypath:@"basicObjectProperty"]; + NSSet *derivedChanges = [observer changesForKeypath:@"derivedObjectProperty"]; + + PASS(nil != derivedChanges, "derivedChanges should not be nil."); + + PASS([basicChanges count] == 0, + "No changes on basicObjectProperty should have fired (we did not " + "register for it)."); + PASS([derivedChanges count] == 1, "One change on derivedObjectProperty should have fired."); + + PASS_RUNS([observed removeObserver:observer + forKeyPath:@"derivedObjectProperty"], + "remove observer should not throw"); + + derivedChanges = [observer changesForKeypath:@"derivedObjectProperty"]; + PASS(nil != derivedChanges, "derivedChanges should not be nil."); + PASS_EQUAL([[[derivedChanges anyObject] info] objectForKey:NSKeyValueChangeNewKey], + @"!!!Hello!!!", + "The new value stored in the change notification should be " + "!!!Hello!!! (the derived object)."); + + FLAKY_ON_GCC_END + END_SET("DependentKeyNotification"); +} + +static void +PODNotification() +{ + START_SET("PODNotification"); + + TestKVOObject *observed = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObserver *observer = [[[TestKVOObserver alloc] init] autorelease]; + + [observed addObserver:observer + forKeyPath:@"basicPodProperty" + options:NSKeyValueObservingOptionNew + context:NULL]; + observed.basicPodProperty = 10; + + PASS_EQUAL([[observer changesForKeypath:@"basicPodProperty"] count], 1, + "One change on basicPodProperty should have fired."); + + PASS([[[[[observer changesForKeypath:@"basicPodProperty"] anyObject] info] + objectForKey:NSKeyValueChangeNewKey] isKindOfClass:[NSNumber class]], + "The new value stored in the change notification should be an NSNumber " + "instance."); + PASS_EQUAL( + [[[[observer changesForKeypath:@"basicPodProperty"] anyObject] info] + objectForKey:NSKeyValueChangeNewKey], + BOXI(10), + "The new value stored in the change notification should be a boxed 10."); + + PASS_RUNS([observed removeObserver:observer forKeyPath:@"basicPodProperty"], + "remove observer should not throw"); + + END_SET("PODNotification"); +} + +static void +StructNotification() +{ // Basic change notification on a struct type + START_SET("StructNotification"); + + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + TestKVOObject *observed = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObserver *observer = [[[TestKVOObserver alloc] init] autorelease]; + + PASS_EQUAL([[observer changesForKeypath:@"basicObjectProperty"] count], 0, + "No changes on basicObjectProperty should have fired."); + [observed addObserver:observer + forKeyPath:@"structProperty" + options:NSKeyValueObservingOptionNew + context:NULL]; + struct TestKVOStruct structValue = {1, 2, 3}; + observed.structProperty = structValue; + + PASS_EQUAL([[observer changesForKeypath:@"structProperty"] count], 1, + "One change on structProperty should have fired."); + + PASS(YES == + [[[[[observer changesForKeypath:@"structProperty"] anyObject] info] + objectForKey:NSKeyValueChangeNewKey] isKindOfClass:[NSValue class]], + "The new value stored in the change notification should be " + "an NSValue instance."); + PASS(strcmp([[[[[observer changesForKeypath:@"structProperty"] anyObject] + info] objectForKey:NSKeyValueChangeNewKey] objCType], + @encode(struct TestKVOStruct)) + == 0, + "The new objc type stored in the change notification should have " + "an objc type matching our Struct."); + + PASS_RUNS([observed removeObserver:observer forKeyPath:@"structProperty"], + "remove observer should not throw"); + PASS_RUNS([pool release], "release should not throw"); + + END_SET("StructNotification"); +} + +static void +DisabledNotification() +{ // No notification for non-notifying keypaths. + START_SET("DisabledNotification"); + FLAKY_ON_GCC_START + + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + TestKVOObject *observed = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObserver *observer = [[[TestKVOObserver alloc] init] autorelease]; + + [observed addObserver:observer + forKeyPath:@"nonNotifyingObjectProperty" + options:NSKeyValueObservingOptionNew + context:NULL]; + observed.nonNotifyingObjectProperty = @"Whatever"; + + NSSet *changes = [observer changesForKeypath:@"nonNotifyingObjectProperty"]; + + PASS([changes count] == 0, + "No changes on nonNotifyingObjectProperty should have fired."); + + PASS_RUNS([observed removeObserver:observer + forKeyPath:@"nonNotifyingObjectProperty"], + "remove observer should not throw"); + PASS_RUNS([pool release], "release should not throw"); + + FLAKY_ON_GCC_END + END_SET("DisabledNotification"); +} + +static void +DisabledInitialNotification() +{ // Initial notification for non-notifying keypaths. + START_SET("DisabledInitialNotification"); + FLAKY_ON_GCC_START + + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + TestKVOObject *observed = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObserver *observer = [[[TestKVOObserver alloc] init] autorelease]; + + [observed addObserver:observer + forKeyPath:@"nonNotifyingObjectProperty" + options:NSKeyValueObservingOptionInitial + context:NULL]; + observed.nonNotifyingObjectProperty = @"Whatever"; + + NSSet *changes = [observer changesForKeypath:@"nonNotifyingObjectProperty"]; + + + + PASS(nil != changes, "changes should not be nil."); + PASS([changes count] == 1, + "An INITIAL notification for nonNotifyingObjectProperty should " + "have fired."); + + PASS_EQUAL(BOXF(NSKeyValueChangeSetting), + [[[changes anyObject] info] objectForKey:NSKeyValueChangeKindKey], + "The change kind should be NSKeyValueChangeSetting."); + + PASS_RUNS([observed removeObserver:observer + forKeyPath:@"nonNotifyingObjectProperty"], + "remove observer should not throw"); + PASS_RUNS([pool release], "release should not throw"); + + FLAKY_ON_GCC_END + END_SET("DisabledInitialNotification"); +} + +static void +SetValueForKeyIvarNotification() +{ // Notification of ivar change through setValue:forKey: + START_SET("SetValueForKeyIvarNotification"); + + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + TestKVOObject *observed = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObserver *observer = [[[TestKVOObserver alloc] init] autorelease]; + + [observed addObserver:observer + forKeyPath:@"ivarWithoutSetter" + options:NSKeyValueObservingOptionNew + context:NULL]; + [observed setValue:BOXI(1024) forKey:@"ivarWithoutSetter"]; + + PASS_EQUAL([[observer changesForKeypath:@"ivarWithoutSetter"] count], 1, + "One change on ivarWithoutSetter should have fired (using " + "setValue:forKey:)."); + + PASS_EQUAL( + [[[[observer changesForKeypath:@"ivarWithoutSetter"] anyObject] info] + objectForKey:NSKeyValueChangeNewKey], + BOXI(1024), + "The new value stored in the change notification should a boxed 1024."); + + PASS_RUNS([observed removeObserver:observer forKeyPath:@"ivarWithoutSetter"], + "remove observer should not throw"); + PASS_RUNS([pool release], "release should not throw"); + + END_SET("SetValueForKeyIvarNotification"); +} + +static void +DictionaryNotification() +{ // Basic notification on a dictionary, which does not have properties or + // ivars. + START_SET("DictionaryNotification"); + FLAKY_ON_GCC_START + + NSMutableDictionary *observed = [NSMutableDictionary dictionary]; + TestKVOObserver *observer = [[[TestKVOObserver alloc] init] autorelease]; + + [observed setObject:[[[TestKVOObject alloc] init] autorelease] + forKey:@"subKey"]; + + [observed addObserver:observer + forKeyPath:@"arbitraryValue" + options:NSKeyValueObservingOptionNew + context:NULL]; + [observed addObserver:observer + forKeyPath:@"subKey.basicObjectProperty" + options:NSKeyValueObservingOptionNew + context:NULL]; + + [observed setObject:@"Whatever" forKey:@"arbitraryValue"]; + [observed setValue:@"Whatever2" forKeyPath:@"arbitraryValue"]; + [observed setValue:@"Whatever2" forKeyPath:@"subKey.basicObjectProperty"]; + + NSSet *changes = [observer changesForKeypath:@"arbitraryValue"]; + + PASS(nil != changes, "changes should not be nil."); + PASS([changes count] == 2, + "On a NSMutableDictionary, a change notification for arbitraryValue."); + + changes = [observer changesForKeypath:@"subKey.basicObjectProperty"]; + + PASS(nil != changes, "changes should not be nil."); + PASS([changes count] == 1, + "On a NSMutableDictionary, a change notification for " + "subKey.basicObjectProperty."); + + PASS_RUNS([observed removeObserver:observer forKeyPath:@"arbitraryValue"], + "remove observer should not throw"); + PASS_RUNS([observed removeObserver:observer + forKeyPath:@"subKey.basicObjectProperty"], + "remove observer should not throw"); + + FLAKY_ON_GCC_END + END_SET("DictionaryNotification"); +} + +static void +BasicDeregistration() +{ // Deregistration test + START_SET("BasicDeregistration"); + FLAKY_ON_GCC_START + + TestKVOObject *observed = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObserver *observer = [[[TestKVOObserver alloc] init] autorelease]; + + [observed addObserver:observer + forKeyPath:@"basicObjectProperty" + options:NSKeyValueObservingOptionNew + context:NULL]; + PASS_RUNS([observed removeObserver:observer + forKeyPath:@"basicObjectProperty" + context:NULL], + "remove observer should not throw"); + observed.basicObjectProperty = @"Hello"; + + PASS_EQUAL([[observer changesForKeypath:@"basicObjectProperty"] count], 0, + "No changes on basicObjectProperty should have fired."); + + TestKVOObject *subObject = [[[TestKVOObject alloc] init] autorelease]; + observed.cascadableKey = subObject; + + [observed addObserver:observer + forKeyPath:@"cascadableKey.basicObjectProperty" + options:NSKeyValueObservingOptionNew + context:NULL]; + PASS_RUNS([observed removeObserver:observer + forKeyPath:@"cascadableKey.basicObjectProperty" + context:NULL], + "remove observer should not throw"); + + subObject.basicObjectProperty = @"Hello"; + + NSSet *changes = [observer changesForKeypath:@"cascadableKey.basicObjectProperty"]; + + PASS([changes count] == 0, "No changes on cascadableKey.basicObjectProperty should have fired."); + + FLAKY_ON_GCC_END + END_SET("BasicDeregistration"); +} + +static void +DerivedKeyOnSubpath1() +{ + START_SET("DerivedKeyOnSubpath1"); + + TestKVOObject *observed = [[TestKVOObject alloc] init]; + TestKVOObserver *observer = [[TestKVOObserver alloc] init]; + + [observed addObserver:observer + forKeyPath:@"cascadableKey.derivedObjectProperty.length" + options:NSKeyValueObservingOptionNew + context:NULL]; + + TestKVOObject *subObject = [[TestKVOObject alloc] init]; + subObject.basicObjectProperty = @"Hello"; + observed.cascadableKey = subObject; + + NSSet *changes = [observer changesForKeypath:@"cascadableKey.derivedObjectProperty.length"]; + + PASS(nil != changes, "changes should not be nil."); + PASS([changes count] == 1, "One change on cascade.derived.length should have fired."); + PASS_EQUAL( + [[[changes anyObject] info] objectForKey:NSKeyValueChangeNewKey], + BOXI(11), + "The new value stored in the change notification should a boxed 11."); + + PASS_RUNS([observed + removeObserver:observer + forKeyPath:@"cascadableKey.derivedObjectProperty.length" + context:NULL], + "remove observer should not throw"); + + [observer clear]; + + subObject.basicObjectProperty = @"Whatever"; + + PASS_EQUAL( + [[observer changesForKeypath:@"cascadableKey.derivedObjectProperty.length"] + count], + 0, "No additional changes on cascade.derived.length should have fired."); + + [subObject release]; + [observer release]; + [observed release]; + + END_SET("DerivedKeyOnSubpath1"); +} + +static void +Subpath1() +{ // Test normally-nested observation and value replacement + START_SET("Subpath1"); + + TestKVOObject *observed = [[TestKVOObject alloc] init]; + TestKVOObserver *observer = [[TestKVOObserver alloc] init]; + [observed addObserver:observer + forKeyPath:@"cascadableKey.cascadableKey" + options:0 + context:nil]; + + TestKVOObject *child = [[TestKVOObject alloc] init]; + + [observed setCascadableKey:child]; + [observed setCascadableKey:nil]; + + PASS_EQUAL(2, [observer numberOfObservedChanges], + "Two changes should have been observed."); + + PASS_RUNS([observed removeObserver:observer + forKeyPath:@"cascadableKey.cascadableKey"], + "remove observer should not throw"); + + [child release]; + [observer release]; + [observed release]; + + END_SET("Subpath1"); +} + +static void +SubpathSubpath() +{ // Test deeply-nested observation + START_SET("SubpathSubpath"); + + TestKVOObject *observed = [[TestKVOObject alloc] init]; + TestKVOObserver *observer = [[TestKVOObserver alloc] init]; + [observed addObserver:observer + forKeyPath:@"cascadableKey.cascadableKey.cascadableKey" + options:0 + context:nil]; + + TestKVOObject *child = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObject *childChild = [[[TestKVOObject alloc] init] autorelease]; + + observed.cascadableKey = child; + observed.cascadableKey.cascadableKey = childChild; + observed.cascadableKey.cascadableKey = nil; + observed.cascadableKey = nil; + + PASS_EQUAL(4, [observer numberOfObservedChanges], + "Four changes should have been observed."); + + PASS_RUNS([observed + removeObserver:observer + forKeyPath:@"cascadableKey.cascadableKey.cascadableKey"], + "remove observer should not throw"); + + [observer release]; + [observed release]; + + END_SET("SubpathSubpath"); +} + +static void +SubpathWithHeadReplacement() +{ // Test key value replacement and re-registration (1) + START_SET("SubpathWithHeadReplacement"); + + TestKVOObject *observed = [[TestKVOObject alloc] init]; + TestKVOObserver *observer = [[TestKVOObserver alloc] init]; + + TestKVOObject *child = [[[TestKVOObject alloc] init] autorelease]; + observed.cascadableKey = child; + + [observed addObserver:observer + forKeyPath:@"cascadableKey.cascadableKey" + options:0 + context:nil]; + + [observed setCascadableKey:nil]; + + PASS_EQUAL(1, [observer numberOfObservedChanges], + "One change should have been observed."); + + PASS_RUNS([observed removeObserver:observer + forKeyPath:@"cascadableKey.cascadableKey"], + "remove observer should not throw"); + + [observer release]; + [observed release]; + + END_SET("SubpathWithHeadReplacement"); +} + +static void +SubpathWithTailAndHeadReplacement() +{ // Test key value replacement and re-registration (2) + START_SET("SubpathWithTailAndHeadReplacement"); + + TestKVOObject *observed = [[TestKVOObject alloc] init]; + TestKVOObserver *observer = [[TestKVOObserver alloc] init]; + + TestKVOObject *child = [[[TestKVOObject alloc] init] autorelease]; + observed.cascadableKey = child; + + TestKVOObject *childChild = [[[TestKVOObject alloc] init] autorelease]; + child.cascadableKey = childChild; + + [observed addObserver:observer + forKeyPath:@"cascadableKey.cascadableKey.cascadableKey" + options:0 + context:nil]; + + observed.cascadableKey.cascadableKey = nil; + observed.cascadableKey = nil; + + PASS_EQUAL(2, [observer numberOfObservedChanges], + "Two changes should have been observed."); + + PASS_RUNS([observed + removeObserver:observer + forKeyPath:@"cascadableKey.cascadableKey.cascadableKey"], + "remove observer should not throw"); + + [observer release]; + [observed release]; + + END_SET("SubpathWithTailAndHeadReplacement"); +} + +static void +SubpathWithMultipleReplacement() +{ // Test key value replacement and re-registration (3) + START_SET("SubpathWithMultipleReplacement"); + FLAKY_ON_GCC_START + + TestKVOObject *observed = [[TestKVOObject alloc] init]; + TestKVOObserver *observer = [[TestKVOObserver alloc] init]; + TestKVOObject *child1 = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObject *child2 = [[[TestKVOObject alloc] init] autorelease]; + + [observed addObserver:observer + forKeyPath:@"cascadableKey.cascadableKey" + options:0 + context:nil]; + + observed.cascadableKey = child1; + + observed.cascadableKey = child2; + + observed.cascadableKey = nil; + + PASS_EQUAL(3, [observer numberOfObservedChanges], + "Three changes should have been observed."); + + PASS_RUNS([observed removeObserver:observer + forKeyPath:@"cascadableKey.cascadableKey"], + "remove observer should not throw"); + + [observer release]; + [observed release]; + + FLAKY_ON_GCC_END + END_SET("SubpathWithMultipleReplacement"); +} + +static void +SubpathWithMultipleReplacement2() +{ // Test a more complex nested observation system + START_SET("SubpathWithMultipleReplacement2"); + + TestKVOObject *observed = [[TestKVOObject alloc] init]; + TestKVOObserver *observer = [[TestKVOObserver alloc] init]; + TestKVOObject *child1 = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObject *child2 = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObject *child3 = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObject *child4 = [[[TestKVOObject alloc] init] autorelease]; + + [observed addObserver:observer + forKeyPath:@"cascadableKey.cascadableKey" + options:0 + context:nil]; + + observed.cascadableKey = child1; + + observed.cascadableKey = nil; + + observed.cascadableKey = child2; + + observed.cascadableKey = nil; + + observed.cascadableKey = child3; + child3.cascadableKey = child4; + + observed.cascadableKey = nil; + + PASS_EQUAL(7, [observer numberOfObservedChanges], + "Seven changes should have " + "been observed."); + + PASS_RUNS([observed removeObserver:observer + forKeyPath:@"cascadableKey.cascadableKey"], + "remove observer should not throw"); + + [observer release]; + [observed release]; + + END_SET("SubpathWithMultipleReplacement2"); +} + +static void +SubpathsWithInitialNotification() +{ // Test initial observation on nested keys + START_SET("SubpathsWithInitialNotification"); + + TestKVOObject *observed = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObserver *observer = [[[TestKVOObserver alloc] init] autorelease]; + TestKVOObject *child1 = [[[TestKVOObject alloc] init] autorelease]; + observed.cascadableKey = child1; + + [observed + addObserver:observer + forKeyPath:@"cascadableKey.basicObjectProperty" + options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew + context:nil]; + [observed + addObserver:observer + forKeyPath:@"cascadableKey.basicPodProperty" + options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew + context:nil]; + [observed + addObserver:observer + forKeyPath:@"cascadableKey.derivedObjectProperty" + options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew + context:nil]; + + PASS_EQUAL(3, [observer numberOfObservedChanges], + "Three changes should have " + "been observed."); + PASS_EQUAL([NSNull null], + [[[[observer + changesForKeypath:@"cascadableKey.basicObjectProperty"] + anyObject] info] objectForKey:NSKeyValueChangeNewKey], + "The initial value of basicObjectProperty should be nil."); + PASS_EQUAL(BOXI(0), + [[[[observer changesForKeypath:@"cascadableKey.basicPodProperty"] + anyObject] info] objectForKey:NSKeyValueChangeNewKey], + "The initial value of basicPodProperty should be 0."); + PASS_EQUAL([NSNull null], + [[[[observer + changesForKeypath:@"cascadableKey.derivedObjectProperty"] + anyObject] info] objectForKey:NSKeyValueChangeNewKey], + "The initial value of derivedObjectProperty should be nil."); + + PASS_RUNS([observed removeObserver:observer + forKeyPath:@"cascadableKey.basicObjectProperty"], + "remove observer should not throw"); + PASS_RUNS([observed removeObserver:observer + forKeyPath:@"cascadableKey.basicPodProperty"], + "remove observer should not throw"); + PASS_RUNS([observed removeObserver:observer + forKeyPath:@"cascadableKey.derivedObjectProperty"], + "remove observer should not throw"); + + END_SET("SubpathsWithInitialNotification"); +} + +static void +CyclicDependency() +{ // Make sure that dependency loops don't cause crashes. + START_SET("CyclicDependency"); + FLAKY_ON_GCC_START + + TestKVOObject *observed = [[TestKVOObject alloc] init]; + TestKVOObserver *observer = [[TestKVOObserver alloc] init]; + PASS_RUNS([observed addObserver:observer + forKeyPath:@"recursiveDependent1" + options:1 + context:nil], + "add observer should not throw"); + PASS_RUNS([observed addObserver:observer + forKeyPath:@"recursiveDependent2" + options:1 + context:nil], + "add observer should not throw"); + observed.recursiveDependent1 = @"x"; + observed.recursiveDependent2 = @"y"; + PASS(4 == [observer numberOfObservedChanges], + "Four changes should have " + "been observed."); + PASS_RUNS([observed removeObserver:observer + forKeyPath:@"recursiveDependent1"], + "remove observer should not throw"); + PASS_RUNS([observed removeObserver:observer + forKeyPath:@"recursiveDependent2"], + "remove observer should not throw"); + + [observer release]; + [observed release]; + + FLAKY_ON_GCC_END + END_SET("CyclicDependency"); +} + +static void +ObserveAllProperties() +{ + START_SET("ObserveAllProperties"); + FLAKY_ON_GCC_START + + TestKVOObject *observed = [[TestKVOObject alloc] init]; + TestKVOObserver *observer = [[[TestKVOObserver alloc] init] autorelease]; + + [observed addObserver:observer + forKeyPath:@"basicObjectProperty" + options:NSKeyValueObservingOptionNew + context:NULL]; + [observed addObserver:observer + forKeyPath:@"basicPodProperty" + options:NSKeyValueObservingOptionNew + context:NULL]; + [observed addObserver:observer + forKeyPath:@"structProperty" + options:NSKeyValueObservingOptionNew + context:NULL]; + [observed addObserver:observer + forKeyPath:@"derivedObjectProperty" + options:NSKeyValueObservingOptionNew + context:NULL]; + [observed addObserver:observer + forKeyPath:@"cascadableKey" + options:NSKeyValueObservingOptionNew + context:NULL]; + [observed addObserver:observer + forKeyPath:@"cascadableKey.basicObjectProperty" + options:NSKeyValueObservingOptionNew + context:NULL]; + + struct TestKVOStruct s = {1, 2, 3}; + + observed.basicObjectProperty = @"WHAT"; // 2 here + observed.basicPodProperty = 10; // 1 + observed.structProperty = s; + + TestKVOObject *subObject = [[[TestKVOObject alloc] init] autorelease]; + subObject.basicObjectProperty = @"Hello"; + observed.cascadableKey = subObject; // 2 here + + PASS([observer numberOfObservedChanges] == 6, + "There should have been 6 observed changes on the observer."); + + PASS_RUNS([observed removeObserver:observer + forKeyPath:@"basicObjectProperty"], + "remove observer for keyPath basicObjectProperty should not throw"); + PASS_RUNS([observed removeObserver:observer forKeyPath:@"basicPodProperty"], + "remove observer for keyPath basicPodProperty should not throw"); + PASS_RUNS([observed removeObserver:observer forKeyPath:@"structProperty"], + "remove observer for keyPath structProperty should not throw"); + PASS_RUNS([observed removeObserver:observer + forKeyPath:@"derivedObjectProperty"], + "remove observer should not throw"); + PASS_RUNS([observed removeObserver:observer forKeyPath:@"cascadableKey"], + "remove observer for keyPath cascadableKey should not throw"); + PASS_RUNS([observed removeObserver:observer + forKeyPath:@"cascadableKey.basicObjectProperty"], + "remove observer for keyPath cascadableKey.basicObjectProperty " + "should not throw"); + + FLAKY_ON_GCC_END + END_SET("ObserveAllProperties"); +} + +static void +RemoveWithoutContext() +{ // Test removal without specifying context. + START_SET("RemoveWithoutContext"); + FLAKY_ON_GCC_START + + TestKVOObject *observed = [[TestKVOObject alloc] init]; + TestKVOObserver *observer = [[TestKVOObserver alloc] init]; + + [observed addObserver:observer + forKeyPath:@"basicObjectProperty" + options:NSKeyValueObservingOptionNew + context:(void *) (1)]; + [observed addObserver:observer + forKeyPath:@"basicObjectProperty" + options:NSKeyValueObservingOptionNew + context:(void *) (2)]; + + PASS_RUNS( + [observed removeObserver:observer forKeyPath:@"basicObjectProperty"], + "removing observer forKeyPath=basicObjectProperty should not throw"); + + observed.basicObjectProperty = @""; + + PASS([observer numberOfObservedChanges] == 1, + "There should be only one change notification despite " + "registering two with contexts."); + + PASS_RUNS( + [observed removeObserver:observer forKeyPath:@"basicObjectProperty"], + "removing observer forKeyPath=basicObjectProperty should not throw"); + + [observer release]; + [observed release]; + + FLAKY_ON_GCC_END + END_SET("RemoveWithoutContext"); +} + +static void +RemoveWithDuplicateContext() +{ // Test adding duplicate contexts + START_SET("RemoveWithDuplicateContext"); + FLAKY_ON_GCC_START + + TestKVOObject *observed = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObserver *observer = [[[TestKVOObserver alloc] init] autorelease]; + + [observed addObserver:observer + forKeyPath:@"basicObjectProperty" + options:NSKeyValueObservingOptionNew + context:(void *) (1)]; + [observed addObserver:observer + forKeyPath:@"basicObjectProperty" + options:NSKeyValueObservingOptionNew + context:(void *) (1)]; + + observed.basicObjectProperty = @""; + + PASS([observer numberOfObservedChanges] == 2, + "There should be two observed changes, despite the identical " + "registration."); + + PASS_RUNS( + [observed removeObserver:observer + forKeyPath:@"basicObjectProperty" + context:(void *) (1)], + "removing observer forKeyPath=basicObjectProperty should not throw"); + + observed.basicObjectProperty = @""; + + PASS([observer numberOfObservedChanges] == 3, + "There should be one additional observed change; the removal " + "should have only effected one."); + + PASS_RUNS([observed removeObserver:observer + forKeyPath:@"basicObjectProperty" + context:(void *) (1)], + "removing observer forKeyPath=basicObjectProperty does not throw"); + + FLAKY_ON_GCC_END + END_SET("RemoveWithDuplicateContext"); +} + +static void +RemoveOneOfTwoObservers() +{ // Test adding duplicate contexts + START_SET("RemoveOneOfTwoObservers"); + FLAKY_ON_GCC_START + + TestKVOObject *observed = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObserver *observer = [[[TestKVOObserver alloc] init] autorelease]; + TestKVOObserver *observer2 = [[[TestKVOObserver alloc] init] autorelease]; + + [observed addObserver:observer + forKeyPath:@"basicObjectProperty" + options:NSKeyValueObservingOptionNew + context:NULL]; + [observed addObserver:observer2 + forKeyPath:@"basicObjectProperty" + options:NSKeyValueObservingOptionNew + context:NULL]; + + observed.basicObjectProperty = @""; + + PASS([observer numberOfObservedChanges] == 1, + "There should be one observed change per observer."); + PASS([observer2 numberOfObservedChanges] == 1, + "There should be one observed change per observer."); + + PASS_RUNS([observed removeObserver:observer2 + forKeyPath:@"basicObjectProperty"], + "removing observer2 should not throw"); + + observed.basicObjectProperty = @""; + + PASS([observer numberOfObservedChanges] == 2, + "There should be one additional observed change; the removal " + "should have only removed the second observer."); + + PASS([observer2 numberOfObservedChanges] == 1, + "Observer2 should have only observed one change."); + + PASS_RUNS([observed removeObserver:observer + forKeyPath:@"basicObjectProperty"], + "removing observer should not throw"); + + FLAKY_ON_GCC_END + END_SET("RemoveOneOfTwoObservers"); +} + +static void +RemoveUnregistered() +{ // Test removing an urnegistered observer + START_SET("RemoveUnregistered"); + FLAKY_ON_GCC_START + + TestKVOObject *observed = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObserver *observer = [[[TestKVOObserver alloc] init] autorelease]; + + PASS_EXCEPTION( + [observed removeObserver:observer + forKeyPath:@"basicObjectProperty" + context:(void *) (1)], + (NSString*)nil, + "Removing an unregistered observer should throw an exception.") + + FLAKY_ON_GCC_END + END_SET("RemoveUnregistered"); +} + +static void +SelfObservationDealloc() +{ // Test deallocation of an object that is its own observer + TestKVOSelfObserver *observed = [[TestKVOSelfObserver alloc] init]; + PASS_RUNS([observed release], "deallocating self-observing object should not " + "throw"); +} + +static void +DeepSubpathWithCompleteTree() +{ + START_SET("DeepSubpathWithCompleteTree"); + + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + TestKVOObject2 *floatGuy = [[[TestKVOObject2 alloc] init] autorelease]; + floatGuy.someFloat = 1.234f; + TestKVOObject *observed = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObject *child = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObserver *observer = [[[TestKVOObserver alloc] init] autorelease]; + child.dictionaryProperty = [NSMutableDictionary + dictionaryWithObjectsAndKeys:floatGuy, @"floatGuy", nil]; + observed.cascadableKey = child; + [observed addObserver:observer + forKeyPath:@"cascadableKey.dictionaryProperty.floatGuy.someFloat" + options:0 + context:nil]; + observed.cascadableKey = child; + PASS([observer numberOfObservedChanges] == 1, + "One change should have " + "been observed."); + + PASS_RUNS( + [observed + removeObserver:observer + forKeyPath:@"cascadableKey.dictionaryProperty.floatGuy.someFloat"], + "remove observer should not throw"); + PASS_RUNS([pool release], "release pool should not throw"); + + END_SET("DeepSubpathWithCompleteTree"); +} + +static void +DeepSubpathWithIncompleteTree() +{ + START_SET("DeepSubpathWithIncompleteTree"); + + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + // The same test as above, but testing nil value reconstitution to ensure that + // the keypath is wired up properly. + TestKVOObject *observed = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObserver *observer = [[[TestKVOObserver alloc] init] autorelease]; + [observed addObserver:observer + forKeyPath:@"cascadableKey.dictionaryProperty.floatGuy.someFloat" + options:0 + context:nil]; + + TestKVOObject2 *floatGuy = [[[TestKVOObject2 alloc] init] autorelease]; + floatGuy.someFloat = 1.234f; + TestKVOObject *child = [[[TestKVOObject alloc] init] autorelease]; + child.dictionaryProperty = [NSMutableDictionary + dictionaryWithObjectsAndKeys:floatGuy, @"floatGuy", nil]; + + observed.cascadableKey = child; + observed.cascadableKey = child; + + PASS([observer numberOfObservedChanges] == 2, + "Two changes should have " + "been observed."); + + PASS_RUNS( + [observed + removeObserver:observer + forKeyPath:@"cascadableKey.dictionaryProperty.floatGuy.someFloat"], + "remove observer should not throw"); + PASS_RUNS([pool release], "release pool should not throw"); + + END_SET("DeepSubpathWithIncompleteTree"); +} + +static void +SubpathOnDerivedKey() +{ + START_SET("SubpathOnDerivedKey"); + FLAKY_ON_GCC_START + + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + TestKVOObject *observed = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObject *child = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObject *child2 = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObserver *observer = [[[TestKVOObserver alloc] init] autorelease]; + + observed.cascadableKey = child; + child.dictionaryProperty = + [NSMutableDictionary dictionaryWithDictionary:MPAIR(@"Key1" , @"Value1")]; + + [observed addObserver:observer + forKeyPath:@"derivedCascadableKey.dictionaryProperty.Key1" + options:0 + context:nil]; + + observed.cascadableKey = child2; + child2.dictionaryProperty = + [NSMutableDictionary dictionaryWithDictionary:MPAIR(@"Key1" , @"Value2")]; + + PASS(2 == [observer numberOfObservedChanges], + "Two changes should have " + "been observed."); + + PASS_RUNS([observed + removeObserver:observer + forKeyPath:@"derivedCascadableKey.dictionaryProperty.Key1"], + "remove observer should not throw"); + PASS_RUNS([pool release], "release pool should not throw"); + + FLAKY_ON_GCC_END + END_SET("SubpathOnDerivedKey"); +} + +static void +SubpathWithDerivedKeyBasedOnSubpath() +{ + START_SET("SubpathWithDerivedKeyBasedOnSubpath"); + FLAKY_ON_GCC_START + + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + TestKVOObject *observed = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObserver *observer = [[[TestKVOObserver alloc] init] autorelease]; + + // key dependent on sub keypath is dependent upon + // dictionaryProperty.subDictionary + NSMutableDictionary *mutableDictionary = MPAIR( + @"subDictionary", MPAIR(@"floatGuy" , BOXF(1.234)) + ); + observed.dictionaryProperty = mutableDictionary; + + [observed addObserver:observer + forKeyPath:@"keyDependentOnSubKeypath.floatGuy" + options:0 + context:nil]; + + [mutableDictionary setObject: MPAIR(@"floatGuy" , BOXF(3.456)) forKey: @"subDictionary"]; // 1 notification + + NSMutableDictionary *mutableDictionary2 = MPAIR( + @"subDictionary", MPAIR(@"floatGuy" , BOXF(5.678)) + ); + + observed.dictionaryProperty = mutableDictionary2; // 2nd notification + + [mutableDictionary2 setObject: MPAIR(@"floatGuy" , BOXF(7.890)) forKey: @"subDictionary"]; // 3rd notification + + PASS(3 == [observer numberOfObservedChanges], + "Three changes should have " + "been observed."); + + PASS_RUNS([observed removeObserver:observer + forKeyPath:@"keyDependentOnSubKeypath.floatGuy"], + "remove observer should not throw"); + PASS_RUNS([pool release], "release pool should not throw"); + + FLAKY_ON_GCC_END + END_SET("SubpathWithDerivedKeyBasedOnSubpath"); +} + +static void +MultipleObservers() +{ + START_SET("MultipleObservers"); + FLAKY_ON_GCC_START + + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + TestKVOObject *observed = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObserver *observer = [[[TestKVOObserver alloc] init] autorelease]; + TestKVOObserver *observer2 = [[[TestKVOObserver alloc] init] autorelease]; + + [observed addObserver:observer + forKeyPath:@"basicObjectProperty" + options:NSKeyValueObservingOptionNew + context:NULL]; + observed.basicObjectProperty = @"Hello"; + + PASS_EQUAL([[observer changesForKeypath:@"basicObjectProperty"] count], 1, + "One change on basicObjectProperty should have fired."); + PASS_EQUAL([[observer changesForKeypath:@"basicPodProperty"] count], 0, + "Zero changes on basicPodProperty should have fired."); + PASS_EQUAL([[observer2 changesForKeypath:@"basicObjectProperty"] count], 0, + "Zero changes on basicObjectProperty should have fired (obs 2)."); + PASS_EQUAL([[observer2 changesForKeypath:@"basicPodProperty"] count], 0, + "Zero changes on basicPodProperty should have fired (obs 2)."); + + [observed addObserver:observer2 + forKeyPath:@"basicObjectProperty" + options:NSKeyValueObservingOptionNew + context:NULL]; + observed.basicObjectProperty = @"Goodbye"; + + PASS_EQUAL([[observer changesForKeypath:@"basicObjectProperty"] count], 2, + "Two changes on basicObjectProperty should have fired."); + PASS_EQUAL([[observer changesForKeypath:@"basicPodProperty"] count], 0, + "Zero changes on basicPodProperty should have fired."); + PASS_EQUAL([[observer2 changesForKeypath:@"basicObjectProperty"] count], 1, + "One change on basicObjectProperty should have fired (obs 2)."); + PASS_EQUAL([[observer2 changesForKeypath:@"basicPodProperty"] count], 0, + "Zero changes on basicPodProperty should have fired (obs 2)."); + + PASS_EQUAL([[[observer2 changesForKeypath:@"basicObjectProperty"] anyObject] + object], + observed, + "The notification object should match the observed object."); + PASS_EQUAL( + nil, + [[[[observer2 changesForKeypath:@"basicObjectProperty"] anyObject] info] + objectForKey:NSKeyValueChangeOldKey], + "There should be no old value included in the change notification."); + PASS_EQUAL([[[[observer2 changesForKeypath:@"basicObjectProperty"] anyObject] + info] objectForKey:NSKeyValueChangeNewKey], + @"Goodbye", "The new value should be 'Goodbye'."); + PASS_RUNS([observed removeObserver:observer + forKeyPath:@"basicObjectProperty"], + "remove observer " + "should not throw"); + PASS_RUNS([observed removeObserver:observer2 + forKeyPath:@"basicObjectProperty"], + "remove observer " + "should not throw"); + + PASS_RUNS([pool release], "release pool should not throw"); + + FLAKY_ON_GCC_END + END_SET("MultipleObservers"); +} + +static void +DerivedKeyDependentOnDerivedKey() +{ + START_SET("DerivedKeyDependentOnDerivedKey"); + FLAKY_ON_GCC_START + + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + TestKVOObject *observed = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObserver *observer = [[[TestKVOObserver alloc] init] autorelease]; + + observed.basicObjectProperty = @"Hello"; + + [observed addObserver:observer + forKeyPath:@"keyDerivedTwoTimes" + options:NSKeyValueObservingOptionNew + context:nil]; + + observed.basicObjectProperty = @"KVO"; + + PASS(1 == [observer numberOfObservedChanges], + "One change should have " + "been observed."); + PASS_EQUAL([[[[observer changesForKeypath:@"keyDerivedTwoTimes"] anyObject] + info] objectForKey:NSKeyValueChangeNewKey], + @"---!!!KVO!!!---", "The new value should be '---!!!KVO!!!---'."); + + [observer clear]; + + observed.basicObjectProperty = @"$$$"; + + PASS(1 == [observer numberOfObservedChanges], + "One change should have " + "been observed."); + PASS_EQUAL([[[[observer changesForKeypath:@"keyDerivedTwoTimes"] anyObject] + info] objectForKey:NSKeyValueChangeNewKey], + @"---!!!$$$!!!---", "The new value should be '---!!!$$$!!!---'."); + + PASS_RUNS([observed removeObserver:observer forKeyPath:@"keyDerivedTwoTimes"], + "remove observer " + "should not throw"); + PASS_RUNS([pool release], "release pool should not throw"); + + FLAKY_ON_GCC_END + END_SET("DerivedKeyDependentOnDerivedKey"); +} + +static void +DerivedKeyDependentOnTwoKeys() +{ + START_SET("DerivedKeyDependentOnTwoKeys"); + FLAKY_ON_GCC_START + + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + TestKVOObject *observed = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObserver *observer = [[[TestKVOObserver alloc] init] autorelease]; + + [observed addObserver:observer + forKeyPath:@"dependsOnTwoKeys" + options:NSKeyValueObservingOptionNew + context:nil]; + + observed.boolTrigger1 = @"firstObject"; + + PASS(1 == [observer numberOfObservedChanges], + "One change should have " + "been observed."); + PASS_EQUAL(BOXBOOL(NO), + [[[[observer changesForKeypath:@"dependsOnTwoKeys"] anyObject] + info] objectForKey:NSKeyValueChangeNewKey], + "The new value " + "should be NO."); + + [observer clear]; + observed.boolTrigger2 = @"secondObject"; + + PASS(1 == [observer numberOfObservedChanges], + "One change should have been observed."); + PASS_EQUAL(BOXBOOL(YES), + [[[[observer changesForKeypath:@"dependsOnTwoKeys"] anyObject] + info] objectForKey:NSKeyValueChangeNewKey], + "The new value should be YES."); + + PASS_RUNS([observed removeObserver:observer forKeyPath:@"dependsOnTwoKeys"], + "remove observer should not throw"); + PASS_RUNS([pool release], "release pool should not throw"); + + FLAKY_ON_GCC_END + END_SET("DerivedKeyDependentOnTwoKeys"); +} + +static void +DerivedKeyDependentOnTwoSubKeys() +{ + START_SET("DerivedKeyDependentOnTwoSubKeys"); + FLAKY_ON_GCC_START + + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + TestKVOObject *observed = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObject *child = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObserver *observer = [[[TestKVOObserver alloc] init] autorelease]; + + [observed addObserver:observer + forKeyPath:@"dependsOnTwoSubKeys" + options:NSKeyValueObservingOptionNew + context:nil]; + + observed.cascadableKey = child; + PASS(1 == [observer numberOfObservedChanges], + "One change should have been observed."); + PASS_EQUAL(BOXBOOL(NO), + [[[[observer changesForKeypath:@"dependsOnTwoSubKeys"] anyObject] + info] objectForKey:NSKeyValueChangeNewKey], + "new value should be NO"); + + [observer clear]; + child.boolTrigger1 = @"firstObject"; + + PASS(1 == [observer numberOfObservedChanges], + "One change should have been observed."); + PASS_EQUAL(BOXBOOL(NO), + [[[[observer changesForKeypath:@"dependsOnTwoSubKeys"] anyObject] + info] objectForKey:NSKeyValueChangeNewKey], + "new value should be NO"); + + [observer clear]; + child.boolTrigger2 = @"secondObject"; + + PASS(1 == [observer numberOfObservedChanges], + "One change should have been observed."); + PASS_EQUAL(BOXBOOL(YES), + [[[[observer changesForKeypath:@"dependsOnTwoSubKeys"] anyObject] + info] objectForKey:NSKeyValueChangeNewKey], + "new value should be YES"); + + PASS_RUNS([observed removeObserver:observer + forKeyPath:@"dependsOnTwoSubKeys"], + "remove observer should not throw"); + PASS_RUNS([pool release], "release pool should not throw"); + + FLAKY_ON_GCC_END + END_SET("DerivedKeyDependentOnTwoSubKeys"); +} + +static void +ObserverInfoShouldNotStompOthers() +{ + TestKVOObject *observed = [[[TestKVOObject alloc] init] autorelease]; + TestKVOObject *oldObj = [[[TestKVOObject alloc] init] autorelease]; + observed.cascadableKey = oldObj; + observed.cascadableKey.basicObjectProperty = @"Original"; + TestKVOObserver *observer = [[[TestKVOObserver alloc] init] autorelease]; + + [observed + addObserver:observer + forKeyPath:@"cascadableKey" + options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew) + context:nil]; + [observed + addObserver:observer + forKeyPath:@"cascadableKey.basicObjectProperty" + options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew) + context:nil]; + + TestKVOObject *newObj = [[[TestKVOObject alloc] init] autorelease]; + newObj.basicObjectProperty = @"NewObj"; + observed.cascadableKey = newObj; + + NSDictionary *baseInfo = + [[[observer changesForKeypath:@"cascadableKey"] anyObject] info]; + PASS(nil != baseInfo, "There should be a change notification."); + PASS_EQUAL(oldObj, [baseInfo objectForKey: NSKeyValueChangeOldKey], + "The old value should be the old object."); + PASS_EQUAL(newObj, [baseInfo objectForKey: NSKeyValueChangeNewKey], + "The new value should be the new object."); + + NSDictionary *subInfo = [[[observer + changesForKeypath:@"cascadableKey.basicObjectProperty"] anyObject] info]; + PASS(nil != subInfo, "There should be a change notification."); + PASS_EQUAL(@"Original", [subInfo objectForKey: NSKeyValueChangeOldKey], + "The old value should be the old object's basicObjectProperty."); + PASS_EQUAL(@"NewObj", [subInfo objectForKey: NSKeyValueChangeNewKey], + "The new value should be the new object's basicObjectProperty."); + + PASS_RUNS([observed removeObserver:observer forKeyPath:@"cascadableKey"], + "remove observer should not throw"); + PASS_RUNS([observed removeObserver:observer + forKeyPath:@"cascadableKey.basicObjectProperty"], + "remove observer should not throw"); +} + +static void +SetValueForKeyPropertyNotification() +{ // Notification through setValue:forKey: to make sure that we do + // not get two notifications for the same change. + START_SET("SetValueForKeyPropertyNotification"); + + TestKVOObject *observed = [TestKVOObject new]; + TestKVOObserver *observer = [TestKVOObserver new]; + + [observed addObserver:observer + forKeyPath:@"basicObjectProperty" + options:NSKeyValueObservingOptionNew + context:NULL]; + [observed setValue:BOXI(1024) forKey:@"basicObjectProperty"]; + + PASS_EQUAL([[observer changesForKeypath:@"basicObjectProperty"] count], 1, + "ONLY one change on basicObjectProperty should have fired " + "(using setValue:forKey: should not fire twice)."); + + PASS_EQUAL( + [[[[observer changesForKeypath:@"basicObjectProperty"] anyObject] info] + objectForKey:NSKeyValueChangeNewKey], + BOXI(1024), + "The new value stored in the change notification should a boxed 1024."); + + PASS_RUNS([observed removeObserver:observer + forKeyPath:@"basicObjectProperty"], + "remove observer does not throw"); + + END_SET("SetValueForKeyPropertyNotification"); +} + +int +main(int argc, char *argv[]) +{ + NSAutoreleasePool *arp = [NSAutoreleasePool new]; + + BasicChangeNotification(); + ExclusiveChangeNotification(); + ManualChangeNotification(); + BasicChangeCaptureOld(); + CascadingNotificationWithEmptyLeaf(); + PriorNotification(); + DependentKeyNotification(); + PODNotification(); + StructNotification(); + DisabledNotification(); + DisabledInitialNotification(); + SetValueForKeyIvarNotification(); + SetValueForKeyPropertyNotification(); + DictionaryNotification(); + BasicDeregistration(); + DerivedKeyOnSubpath1(); + Subpath1(); + SubpathSubpath(); + SubpathWithHeadReplacement(); + SubpathWithTailAndHeadReplacement(); + SubpathWithMultipleReplacement(); + SubpathWithMultipleReplacement2(); + SubpathsWithInitialNotification(); + CyclicDependency(); + ObserveAllProperties(); + RemoveWithoutContext(); + RemoveWithDuplicateContext(); + RemoveOneOfTwoObservers(); + RemoveUnregistered(); + SelfObservationDealloc(); + DeepSubpathWithCompleteTree(); + DeepSubpathWithIncompleteTree(); + SubpathOnDerivedKey(); + SubpathWithDerivedKeyBasedOnSubpath(); + MultipleObservers(); + DerivedKeyDependentOnDerivedKey(); + DerivedKeyDependentOnTwoKeys(); + DerivedKeyDependentOnTwoSubKeys(); + ObserverInfoShouldNotStompOthers(); + + DESTROY(arp); + return 0; +} diff --git a/Tests/base/NSKVOSupport/kvoToMany.m b/Tests/base/NSKVOSupport/kvoToMany.m new file mode 100644 index 000000000..b612d1dbc --- /dev/null +++ b/Tests/base/NSKVOSupport/kvoToMany.m @@ -0,0 +1,1242 @@ +/** + kvoToMany.m + + Copyright (C) 2024 Free Software Foundation, Inc. + + Written by: Hugo Melder + Date: June 2024 + + Based on WinObjC KVO tests by Microsoft Corporation. + + This file is part of GNUStep-base + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + If you are interested in a warranty or support for this source code, + contact Scott Christley for more information. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free + Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110 USA. +*/ +/** + Copyright (c) Microsoft. All rights reserved. + + This code is licensed under the MIT License (MIT). + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +#import +#import "Testing.h" + +#define BOXF(V) [NSNumber numberWithFloat: (V)] +#define BOXI(V) [NSNumber numberWithInteger: (V)] + +#if defined(__OBJC2__) + +@interface Observee : NSObject +{ + NSMutableArray *_bareArray; + NSMutableArray *_manualNotificationArray; + NSMutableArray *_kvcMediatedArray; + NSMutableArray *_arrayWithHelpers; + NSMutableSet *_setWithHelpers; + NSMutableSet *_kvcMediatedSet; + NSMutableSet *_manualNotificationSet; + NSSet *_roSet; +} + +- (NSArray *)manualNotificationArray; +- (NSSet *)setWithHelpers; + +@end + +typedef void (^ChangeCallback)(NSString *, id, NSDictionary *, void *); +typedef void (^PerformBlock)(Observee *); + +#define CHANGE_CB \ + ^(NSString * keyPath, id object, NSDictionary * change, void *context) + +@implementation Observee +- (instancetype)init +{ + self = [super init]; + if (self) + { + _bareArray = [NSMutableArray new]; + _manualNotificationArray = [NSMutableArray new]; + _kvcMediatedArray = [NSMutableArray new]; + _arrayWithHelpers = [NSMutableArray new]; + _setWithHelpers = [NSMutableSet new]; + _kvcMediatedSet = [NSMutableSet new]; + _manualNotificationSet = [NSMutableSet new]; + } + return self; +} + +- (void)dealloc +{ + [_bareArray release]; + [_manualNotificationArray release]; + [_kvcMediatedArray release]; + [_arrayWithHelpers release]; + [_setWithHelpers release]; + [_kvcMediatedSet release]; + [_manualNotificationSet release]; + [super dealloc]; +} + +/* Used for testing NSKeyValueFastMutableSet which is used in + * +[NSKeyValueMutableSet setForKey:ofObject:] */ + +- (NSSet *)proxySet +{ + return _kvcMediatedSet; +} + +- (void)addProxySetObject:(id)obj +{ + [_kvcMediatedSet addObject:obj]; +} + +- (void)removeProxySetObject:(id)obj +{ + [_kvcMediatedSet removeObject:obj]; +} + +- (void)addProxySet:(NSSet *)set +{ + [_kvcMediatedSet unionSet:set]; +} + +- (void)removeProxySet:(NSSet *)set +{ + [_kvcMediatedSet minusSet:set]; +} + +/* Used for testing NSKeyValueSlowMutableSet which is used + * when no add or remove method is available. */ +- (NSSet *)proxyRoSet +{ + return _roSet; +} + +- (void)setProxyRoSet:(NSSet *)set +{ + ASSIGN(_roSet, set); +} + +- (void)addObjectToBareArray:(NSObject *)object +{ + [_bareArray addObject:object]; +} + +- (void)addObjectToManualArray:(NSObject *)object +{ + NSIndexSet *indexes = + [NSIndexSet indexSetWithIndex:[_manualNotificationArray count]]; + [self willChange:NSKeyValueChangeInsertion + valuesAtIndexes:indexes + forKey:@"manualNotificationArray"]; + [_manualNotificationArray addObject:object]; + [self didChange:NSKeyValueChangeInsertion + valuesAtIndexes:indexes + forKey:@"manualNotificationArray"]; +} + +- (void)removeObjectFromManualArrayIndex:(NSUInteger)index +{ + NSIndexSet *indexes = [NSIndexSet indexSetWithIndex:index]; + [self willChange:NSKeyValueChangeRemoval + valuesAtIndexes:indexes + forKey:@"manualNotificationArray"]; + [_manualNotificationArray removeObjectAtIndex:index]; + [self didChange:NSKeyValueChangeRemoval + valuesAtIndexes:indexes + forKey:@"manualNotificationArray"]; +} + +- (NSArray *)manualNotificationArray +{ + return _manualNotificationArray; +} + +- (void)insertObject:(NSObject *)object + inArrayWithHelpersAtIndex:(NSUInteger)index +{ + [_arrayWithHelpers insertObject:object atIndex:index]; +} + +- (void)removeObjectFromArrayWithHelpersAtIndex:(NSUInteger)index +{ + [_arrayWithHelpers removeObjectAtIndex:index]; +} + +- (NSSet *)setWithHelpers +{ + return _setWithHelpers; +} + +- (void)addSetWithHelpersObject:(id)obj +{ + [_setWithHelpers addObject:obj]; +} + +- (void)removeSetWithHelpersObject:(id)obj +{ + [_setWithHelpers removeObject:obj]; +} + +- (void)addSetWithHelpers:(NSSet *)set +{ + [_setWithHelpers unionSet:set]; +} + +- (void)removeSetWithHelpers:(NSSet *)set +{ + [_setWithHelpers minusSet:set]; +} + +- (void)intersectSetWithHelpers:(NSSet *)set +{ + [_setWithHelpers intersectSet:set]; +} + +- (void)setSetWithHelpers:(NSSet *)set +{ + [_setWithHelpers setSet:set]; +} + +- (void)manualSetAddObject:(id)obj +{ + NSSet *set = [NSSet setWithObject:obj]; + [self willChangeValueForKey:@"manualNotificationSet" + withSetMutation:NSKeyValueUnionSetMutation + usingObjects:set]; + [_manualNotificationSet addObject:obj]; + [self didChangeValueForKey:@"manualNotificationSet" + withSetMutation:NSKeyValueUnionSetMutation + usingObjects:set]; +} + +- (void)manualSetRemoveObject:(id)obj +{ + NSSet *set = [NSSet setWithObject:obj]; + [self willChangeValueForKey:@"manualNotificationSet" + withSetMutation:NSKeyValueMinusSetMutation + usingObjects:set]; + [_manualNotificationSet removeObject:obj]; + [self didChangeValueForKey:@"manualNotificationSet" + withSetMutation:NSKeyValueMinusSetMutation + usingObjects:set]; +} + +- (void)manualUnionSet:(NSSet *)set +{ + [self willChangeValueForKey:@"manualNotificationSet" + withSetMutation:NSKeyValueUnionSetMutation + usingObjects:set]; + [_manualNotificationSet unionSet:set]; + [self didChangeValueForKey:@"manualNotificationSet" + withSetMutation:NSKeyValueUnionSetMutation + usingObjects:set]; +} + +- (void)manualMinusSet:(NSSet *)set +{ + [self willChangeValueForKey:@"manualNotificationSet" + withSetMutation:NSKeyValueMinusSetMutation + usingObjects:set]; + [_manualNotificationSet minusSet:set]; + [self didChangeValueForKey:@"manualNotificationSet" + withSetMutation:NSKeyValueMinusSetMutation + usingObjects:set]; +} + +- (void)manualIntersectSet:(NSSet *)set +{ + [self willChangeValueForKey:@"manualNotificationSet" + withSetMutation:NSKeyValueIntersectSetMutation + usingObjects:set]; + [_manualNotificationSet intersectSet:set]; + [self didChangeValueForKey:@"manualNotificationSet" + withSetMutation:NSKeyValueIntersectSetMutation + usingObjects:set]; +} + +- (void)manualSetSet:(NSSet *)set +{ + [self willChangeValueForKey:@"manualNotificationSet" + withSetMutation:NSKeyValueSetSetMutation + usingObjects:set]; + [_manualNotificationSet setSet:set]; + [self didChangeValueForKey:@"manualNotificationSet" + withSetMutation:NSKeyValueSetSetMutation + usingObjects:set]; +} + +@end + +@interface TestObserver : NSObject +@property (nonatomic, strong) + NSMutableArray *callbacks; +@property (nonatomic) NSUInteger hits; +@property (nonatomic) NSUInteger callbackIndex; +@end + +@implementation TestObserver +- (instancetype)init +{ + self = [super init]; + if (self) + { + _callbacks = [NSMutableArray new]; + _hits = 0; + _callbackIndex = 0; + } + return self; +} + +- (void)dealloc +{ + [_callbacks release]; + [super dealloc]; +} + +- (void)performBlock:(void (^)(void))block + andExpectChangeCallbacks: + (NSArray *)callbacks +{ + self.hits = 0; + self.callbackIndex = 0; + ASSIGN(_callbacks, callbacks); + + block(); +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context +{ + if (self.callbacks.count > 0) + { + void (^callback)(NSString *, id, NSDictionary *, void *) + = self.callbacks[_callbackIndex]; + _callbackIndex = (_callbackIndex + 1) % [_callbacks count]; + callback(keyPath, object, change, context); + } + self.hits++; +} +@end + +@interface TestFacade : NSObject +@property (nonatomic, strong) Observee *observee; +@property (nonatomic, strong) TestObserver *observer; +@end + +@implementation TestFacade ++ (instancetype)newWithObservee:(Observee *)observee +{ + return [[TestFacade alloc] initWithObservee:observee]; +} + +- (instancetype)initWithObservee:(Observee *)observee +{ + self = [super init]; + if (self) + { + ASSIGN(_observee, observee); + _observer = [TestObserver new]; + } + return self; +} + +- (void)dealloc +{ + [_observee release]; + [_observer release]; + [super dealloc]; +} + +- (void)performBlock:(void (^)(Observee *))block + andExpectChangeCallbacks: + (NSArray *)callbacks +{ + @try + { + [_observer + performBlock:^{ + block(_observee); + } + andExpectChangeCallbacks:callbacks]; + } + @catch (NSException *exception) + { + NSLog(@"Test failed with exception: %@", exception); + } +} + +- (void)observeKeyPath:(NSString *)keyPath + withOptions:(NSKeyValueObservingOptions)options + performingBlock:(void (^)(Observee *))block + andExpectChangeCallbacks: + (NSArray *)callbacks +{ + [self + performBlock:^(Observee *observee) { + [observee addObserver:self.observer + forKeyPath:keyPath + options:options + context:nil]; + block(observee); + [observee removeObserver:self.observer forKeyPath:keyPath]; + } + andExpectChangeCallbacks:callbacks]; +} + +- (NSUInteger)hits +{ + return [_observer hits]; +} +@end + +@interface DummyObject : NSObject +@property (nonatomic, copy) NSString *name; +@property (nonatomic, retain) DummyObject *sub; +@end + +@implementation DummyObject ++ (instancetype)makeDummy +{ + DummyObject *ret = [[DummyObject new] autorelease]; + ret.name = @"Value"; + return ret; +} + +- (void)dealloc +{ + [_name release]; + [_sub release]; + [super dealloc]; +} + +@end + +static void +ToMany_NoNotificationOnBareArray() +{ + START_SET("ToMany_NoNotificationOnBareArray"); + + Observee *observee = [Observee new]; + TestFacade *facade = [TestFacade newWithObservee:observee]; + + [facade observeKeyPath:@"bareArray" + withOptions:0 + performingBlock:^(Observee *observee) { + [observee addObjectToBareArray:@"hello"]; + } + andExpectChangeCallbacks: [NSArray arrayWithObjects: + ^(NSString *keyPath, id object, NSDictionary *change, + void *context) { // Any notification here is illegal. + PASS(NO, "Any notification here is illegal."); + }, nil]]; + PASS([facade hits] == 0, "No notifications were sent"); + + [facade release]; + [observee release]; + + END_SET("ToMany_NoNotificationOnBareArray"); +} + +static void +ToMany_NotifyingArray() +{ + START_SET("ToMany_NotifyingArray"); + + ChangeCallback firstInsertCallback; + ChangeCallback secondInsertCallback; + ChangeCallback removalCallback; + ChangeCallback illegalChangeNotification; + + /* Callback Setup */ + + firstInsertCallback = CHANGE_CB + { + NSIndexSet *indexes; + + PASS_EQUAL(BOXI(NSKeyValueChangeInsertion), change[NSKeyValueChangeKindKey], + "firstInsertCallback: Change is an insertion"); + + indexes = change[NSKeyValueChangeIndexesKey]; + + PASS(indexes != nil, "firstInsertCallback: Indexes are not nil"); + PASS([indexes firstIndex] == 0, "firstInsertCallback: Index is 0"); + + if (![change[NSKeyValueChangeNotificationIsPriorKey] boolValue]) + { + PASS_EQUAL(@"object1", [change[NSKeyValueChangeNewKey] objectAtIndex:0], + "firstInsertCallback: New object is 'object1'"); + } + }; + + secondInsertCallback = CHANGE_CB + { + NSIndexSet *indexes; + + // We should get an add on index 1 of "object2" + PASS_EQUAL(BOXI(NSKeyValueChangeInsertion), change[NSKeyValueChangeKindKey], + "secondInsertCallback: Change is an insertion"); + + indexes = change[NSKeyValueChangeIndexesKey]; + + PASS(indexes != nil, "secondInsertCallback: Indexes are not nil"); + PASS([indexes firstIndex] == 1, "secondInsertCallback: Index is 1"); + + if (![change[NSKeyValueChangeNotificationIsPriorKey] boolValue]) + { + PASS_EQUAL(@"object2", [change[NSKeyValueChangeNewKey] objectAtIndex:0], + "secondInsertCallback: New object is 'object2'"); + } + }; + + removalCallback = CHANGE_CB + { + NSIndexSet *indexes; + + PASS_EQUAL(BOXI(NSKeyValueChangeRemoval), change[NSKeyValueChangeKindKey], + "removalCallback: Change is a removal"); + + indexes = change[NSKeyValueChangeIndexesKey]; + + PASS(indexes != nil, "removalCallback: Indexes are not nil"); + PASS([indexes firstIndex] == 0, "removalCallback: Index is 0"); + if ([change[NSKeyValueChangeNotificationIsPriorKey] boolValue]) + { + PASS_EQUAL(@"object1", [change[NSKeyValueChangeOldKey] objectAtIndex:0], + "removalCallback: Old object is 'object1'"); + } + }; + + illegalChangeNotification + = CHANGE_CB{PASS(NO, "illegalChangeNotification: was called")}; + + /* Testing manually notifiying array (utilizes add and remove meths in + * Observee) */ + + Observee *observee; + TestFacade *facade; + + observee = [Observee new]; + facade = [TestFacade newWithObservee:observee]; + + // This test expects one change for each key; any more than that is a failure. + [facade observeKeyPath:@"manualNotificationArray" + withOptions:NSKeyValueObservingOptionOld + | NSKeyValueObservingOptionNew + performingBlock:^(Observee *observee) { + [observee addObjectToManualArray:@"object1"]; + [observee addObjectToManualArray:@"object2"]; + [observee removeObjectFromManualArrayIndex:0]; + } + andExpectChangeCallbacks: [NSArray arrayWithObjects: + firstInsertCallback, secondInsertCallback, removalCallback, + illegalChangeNotification, nil]]; + PASS([facade hits] == 3, "Three notifications were sent"); + + [facade release]; + [observee release]; + + observee = [Observee new]; + facade = [TestFacade newWithObservee:observee]; + // This test expects two change notifications for each key; any more than that + // is a failure. + [facade observeKeyPath:@"manualNotificationArray" + withOptions:NSKeyValueObservingOptionPrior + | NSKeyValueObservingOptionOld + | NSKeyValueObservingOptionNew + performingBlock:^(Observee *observee) { + [observee addObjectToManualArray:@"object1"]; + [observee addObjectToManualArray:@"object2"]; + [observee removeObjectFromManualArrayIndex:0]; + } + andExpectChangeCallbacks: [NSArray arrayWithObjects: + firstInsertCallback, firstInsertCallback, secondInsertCallback, + secondInsertCallback, removalCallback, removalCallback, + illegalChangeNotification, nil]]; + + PASS([facade hits] == 6, "Six notifications were sent"); + PASS_EQUAL(([NSArray arrayWithObjects: @"object2", nil]), + [observee manualNotificationArray], + "Final array is 'object2'"); + + // This test expects one change notification: the initial one. Any more than + // that is a failure. + ChangeCallback initialNotificationCallback = CHANGE_CB + { + NSArray *expectedArray = [NSArray arrayWithObjects: @"object2", nil]; + PASS_EQUAL(expectedArray, change[NSKeyValueChangeNewKey], + "Initial notification: New array is 'object2'"); + NSLog(@"Initial notification: New array is %@", + change[NSKeyValueChangeNewKey]); + }; + + [facade observeKeyPath:@"manualNotificationArray" + withOptions:NSKeyValueObservingOptionInitial + | NSKeyValueObservingOptionNew + performingBlock:^(Observee *observee) { + } + andExpectChangeCallbacks: [NSArray arrayWithObjects: + initialNotificationCallback, illegalChangeNotification, nil + ]]; + PASS([facade hits] == 1, "One notification was sent"); + + /* Testing mediated array */ + [facade observeKeyPath:@"kvcMediatedArray" + withOptions:NSKeyValueObservingOptionOld + | NSKeyValueObservingOptionNew + performingBlock:^(Observee *observee) { + // This array is not assisted with setter functions and should go + // through the get/mutate/set codepath. + NSMutableArray *mediatedVersionOfArray = + [observee mutableArrayValueForKey:@"kvcMediatedArray"]; + [mediatedVersionOfArray addObject:@"object1"]; + [mediatedVersionOfArray addObject:@"object2"]; + [mediatedVersionOfArray removeObjectAtIndex:0]; + } + andExpectChangeCallbacks: [NSArray arrayWithObjects: + firstInsertCallback, secondInsertCallback, removalCallback, + illegalChangeNotification, nil]]; + PASS([facade hits] == 3, "Three notifications were sent"); + + [facade release]; + [observee release]; + + /* Testing array with helpers */ + observee = [Observee new]; + facade = [TestFacade newWithObservee:observee]; + [facade observeKeyPath:@"arrayWithHelpers" + withOptions:NSKeyValueObservingOptionOld + | NSKeyValueObservingOptionNew + performingBlock:^(Observee *observee) { + // This array is assisted by setter functions, and should also + // dispatch one notification per change. + NSMutableArray *mediatedVersionOfArray = + [observee mutableArrayValueForKey:@"arrayWithHelpers"]; + [mediatedVersionOfArray addObject:@"object1"]; + [mediatedVersionOfArray addObject:@"object2"]; + [mediatedVersionOfArray removeObjectAtIndex:0]; + } + andExpectChangeCallbacks: [NSArray arrayWithObjects: + firstInsertCallback, secondInsertCallback, removalCallback, + illegalChangeNotification, nil]]; + PASS([facade hits] == 3, "Three notifications were sent"); + + [facade release]; + [observee release]; + + observee = [Observee new]; + facade = [TestFacade newWithObservee:observee]; + // In this test, we use the same arrayWithHelpers as above, but interact with + // it manually. + [facade observeKeyPath:@"arrayWithHelpers" + withOptions:NSKeyValueObservingOptionOld + | NSKeyValueObservingOptionNew + performingBlock:^(Observee *observee) { + // This array is assisted by setter functions, and should also + // dispatch one notification per change. + [observee insertObject:@"object1" inArrayWithHelpersAtIndex:0]; + [observee insertObject:@"object2" inArrayWithHelpersAtIndex:1]; + [observee removeObjectFromArrayWithHelpersAtIndex:0]; + } + andExpectChangeCallbacks: [NSArray arrayWithObjects: + firstInsertCallback, secondInsertCallback, removalCallback, + illegalChangeNotification, nil]]; + PASS([facade hits] == 3, "Three notifications were sent"); + + [facade release]; + [observee release]; + + END_SET("ToMany_NotifyingArray"); +} + +static void +ToMany_KVCMediatedArrayWithHelpers_AggregateFunction() +{ + START_SET("ToMany_KVCMediatedArrayWithHelpers_AggregateFunction"); + + ChangeCallback insertCallbackPost; + ChangeCallback illegalChangeNotification; + + insertCallbackPost = CHANGE_CB + { + PASS(change[NSKeyValueChangeNotificationIsPriorKey] == nil, "Post change"); + PASS_EQUAL(BOXI(NSKeyValueChangeSetting), change[NSKeyValueChangeKindKey], + "Change is a setting"); + PASS_EQUAL(BOXI(0), change[NSKeyValueChangeOldKey], "Old value is 0"); + PASS_EQUAL(BOXI(1), change[NSKeyValueChangeNewKey], "New value is 1"); + + NSIndexSet *indexes = change[NSKeyValueChangeIndexesKey]; + PASS(indexes == nil, "Indexes are nil"); + }; + + illegalChangeNotification = CHANGE_CB + { + PASS(NO, "illegalChangeNotification"); + }; + + Observee *observee = [Observee new]; + TestFacade *facade = [TestFacade newWithObservee:observee]; + [facade observeKeyPath:@"arrayWithHelpers.@count" + withOptions:NSKeyValueObservingOptionOld + | NSKeyValueObservingOptionNew + performingBlock:^(Observee *observee) { + // This array is assisted by setter functions, and should also + // dispatch one notification per change. + NSMutableArray *mediatedVersionOfArray = + [observee mutableArrayValueForKey:@"arrayWithHelpers"]; + [mediatedVersionOfArray addObject:@"object1"]; + } + andExpectChangeCallbacks: [NSArray arrayWithObjects: + insertCallbackPost, illegalChangeNotification, nil]]; + PASS([facade hits] == 1, "One notification was sent"); + + [facade release]; + [observee release]; + + observee = [Observee new]; + facade = [TestFacade newWithObservee:observee]; + // In this test, we use the same arrayWithHelpers as above, but interact with + // it manually. + [facade observeKeyPath:@"arrayWithHelpers.@count" + withOptions:NSKeyValueObservingOptionOld + | NSKeyValueObservingOptionNew + performingBlock:^(Observee *observee) { + // This array is assisted by setter functions, and should also + // dispatch one notification per change. + [observee insertObject:@"object1" inArrayWithHelpersAtIndex:0]; + } + andExpectChangeCallbacks: [NSArray arrayWithObjects: + insertCallbackPost, illegalChangeNotification, nil]]; + PASS([facade hits] == 1, "One notification was sent"); + + [facade release]; + [observee release]; + + END_SET("ToMany_KVCMediatedArrayWithHelpers_AggregateFunction"); +} + +static void +ToMany_ToOne_ShouldDowngradeForOrderedObservation() +{ + START_SET("ToMany_ToOne_ShouldDowngradeForOrderedObservation"); + + ChangeCallback insertCallbackPost; + ChangeCallback illegalChangeNotification; + + insertCallbackPost = CHANGE_CB + { + PASS(change[NSKeyValueChangeNotificationIsPriorKey] == nil, "Post change"); + PASS_EQUAL(BOXI(NSKeyValueChangeSetting), change[NSKeyValueChangeKindKey], + "Change is a setting"); + NSArray *expectedOld = [NSArray arrayWithObjects: @"Value", nil]; + PASS_EQUAL(expectedOld, change[NSKeyValueChangeOldKey], + "Old value is correct"); + NSArray *expectedNew = [NSArray arrayWithObjects: @"Value", @"Value", nil]; + PASS_EQUAL(expectedNew, change[NSKeyValueChangeNewKey], + "New value is correct"); + NSIndexSet *indexes = change[NSKeyValueChangeIndexesKey]; + PASS(indexes == nil, "Indexes are nil"); + }; + + illegalChangeNotification = CHANGE_CB + { + PASS(NO, "illegalChangeNotification"); + }; + + Observee *observee = [Observee new]; + [observee insertObject:[DummyObject makeDummy] inArrayWithHelpersAtIndex:0]; + + TestFacade *facade = [TestFacade newWithObservee:observee]; + [facade observeKeyPath:@"arrayWithHelpers.name" + withOptions:NSKeyValueObservingOptionOld + | NSKeyValueObservingOptionNew + performingBlock:^(Observee *observee) { + // This array is assisted by setter functions, and should also + // dispatch one notification per change. + [observee insertObject:[DummyObject makeDummy] + inArrayWithHelpersAtIndex:0]; + } + andExpectChangeCallbacks: [NSArray arrayWithObjects: + insertCallbackPost, illegalChangeNotification, nil]]; + PASS([facade hits] == 1, "One notification was sent"); + + [facade release]; + [observee release]; + + END_SET("ToMany_ToOne_ShouldDowngradeForOrderedObservation"); +} + +static void +ObserverInformationShouldNotLeak() +{ + START_SET("ObserverInformationShouldNotLeak"); + + ChangeCallback onlyNewCallback; + ChangeCallback illegalChangeNotification; + + onlyNewCallback = CHANGE_CB + { + PASS(change[NSKeyValueChangeNewKey] != nil, "New key is not nil"); + PASS(change[NSKeyValueChangeOldKey] == nil, "Old key is nil"); + }; + + illegalChangeNotification = CHANGE_CB + { + PASS(NO, "illegalChangeNotification"); + }; + + Observee *observee = [Observee new]; + TestFacade *firstFacade = [TestFacade newWithObservee:observee]; + [observee + addObserver:firstFacade.observer + forKeyPath:@"manualNotificationArray" + options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew) + context:nil]; + + TestFacade *facade = [TestFacade newWithObservee:observee]; + [facade observeKeyPath:@"manualNotificationArray" + withOptions:NSKeyValueObservingOptionNew + performingBlock:^(Observee *observee) { + [observee addObjectToManualArray:@"object1"]; + } + andExpectChangeCallbacks: [NSArray arrayWithObjects: onlyNewCallback, illegalChangeNotification, nil]]; + + [observee removeObserver:firstFacade.observer + forKeyPath:@"manualNotificationArray"]; + + PASS([facade hits] == 1, "One notification was sent"); + + [facade release]; + [firstFacade release]; + [observee release]; + + END_SET("ObserverInformationShouldNotLeak"); +} + +static void +NSArrayShouldNotBeObservable() +{ + START_SET("NSArrayShouldNotBeObservable"); + + NSArray *test = [NSArray arrayWithObjects: BOXI(1), BOXI(2), BOXI(3), nil]; + TestObserver *observer = [TestObserver new]; + PASS_EXCEPTION([test addObserver:observer + forKeyPath:@"count" + options:0 + context:nil], + (NSString*)nil, + "NSArray is not observable"); + + // These would throw anyways because there should be no observer for the key + // path, but test anyways + PASS_EXCEPTION([test removeObserver:observer forKeyPath:@"count"], + (NSString*)nil, + "Check removing non-existent observer"); + PASS_EXCEPTION([test removeObserver:observer forKeyPath:@"count" context:nil], + (NSString*)nil, + "Check removing non-existent observer"); + + [observer release]; + + END_SET("NSArrayShouldNotBeObservable"); +} + +static void +NSArrayShouldThrowWhenTryingToObserveIndexesOutOfRange() +{ + START_SET("NSArrayShouldThrowWhenTryingToObserveIndexesOutOfRange"); + + NSArray *test = [NSArray arrayWithObjects: [Observee new], [Observee new], nil]; + TestObserver *observer = [TestObserver new]; + PASS_EXCEPTION([test addObserver:observer + toObjectsAtIndexes:[NSIndexSet indexSetWithIndex:4] + forKeyPath:@"bareArray" + options:0 + context:nil], + (NSString*)nil, + "Observe index out of range"); + + [observer release]; + + END_SET("NSArrayShouldThrowWhenTryingToObserveIndexesOutOfRange"); +} + +static void +NSArrayObserveElements() +{ + START_SET("NSArrayObserveElements"); + + Observee *observee1 = [Observee new]; + Observee *observee2 = [Observee new]; + Observee *observee3 = [Observee new]; + + NSArray *observeeArray = [NSArray arrayWithObjects: observee1, observee2, observee3, nil]; + TestObserver *observer = [TestObserver new]; + PASS_RUNS([observeeArray + addObserver:observer + toObjectsAtIndexes:[NSIndexSet + indexSetWithIndexesInRange:NSMakeRange(0, 2)] + forKeyPath:@"manualNotificationArray" + options:(NSKeyValueObservingOptionOld + | NSKeyValueObservingOptionNew) + context:nil], + "Observe first two elements"); + + // First two elements in range for observation so observer will receive + // changes + [observeeArray[0] addObjectToManualArray:@"object1"]; + [observeeArray[0] addObjectToManualArray:@"object2"]; + PASS([observer hits] == 2, "First two elements in range for observation"); + + [observeeArray[1] addObjectToManualArray:@"object1"]; + PASS([observer hits] == 3, "Second element in range for observation"); + + // But the third element is not so observer will not receive changes + [observeeArray[2] addObjectToManualArray:@"object1"]; + PASS([observer hits] == 3, "Third element not in range for observation"); + + PASS_RUNS([observeeArray + removeObserver:observer + fromObjectsAtIndexes:[NSIndexSet + indexSetWithIndexesInRange:NSMakeRange(0, + 1)] + forKeyPath:@"manualNotificationArray"], + "remove observer from first element"); + + // Removed observer from first element, so modifying it will not report a + // change + [observeeArray[0] addObjectToManualArray:@"object3"]; + PASS([observer hits] == 3, "First element observer removed"); + + // But the second element is still being observed + [observeeArray[1] addObjectToManualArray:@"object2"]; + PASS([observer hits] == 4, "Second element still being observed"); + + PASS_RUNS([observeeArray + removeObserver:observer + fromObjectsAtIndexes:[NSIndexSet + indexSetWithIndexesInRange:NSMakeRange(1, + 1)] + forKeyPath:@"manualNotificationArray"], + "remove observer from second element"); + + [observeeArray[1] addObjectToManualArray:@"object3"]; + PASS([observer hits] == 4, "Second element observer removed"); + + [observer release]; + [observee1 release]; + [observee2 release]; + [observee3 release]; + + END_SET("NSArrayObserveElements"); +} + +static void +NSSetShouldNotBeObservable() +{ + START_SET("NSSetShouldNotBeObservable"); + + NSSet *test = [NSSet setWithObjects:BOXI(1), BOXI(2), BOXI(3), nil]; + TestObserver *observer = [TestObserver new]; + PASS_EXCEPTION([test addObserver:observer + forKeyPath:@"count" + options:0 + context:nil], + (NSString*)nil, + "NSSet is not observable"); + + // These would throw anyways because there should be no observer for the key + // path, but test anyways + PASS_EXCEPTION([test removeObserver:observer forKeyPath:@"count"], + (NSString*)nil, + "Check removing non-existent observer"); + PASS_EXCEPTION([test removeObserver:observer forKeyPath:@"count" context:nil], + (NSString*)nil, + "Check removing non-existent observer"); + + [observer release]; + + END_SET("NSSetShouldNotBeObservable"); +} + +static void +NSSetMutationMethods() +{ + START_SET("NSSetMutationMethods"); + + __block BOOL setSetChanged = NO; + + // Union with @({@(1), @(2), @(3)}) to get @({@(1), @(2), @(3)}) + ChangeCallback unionCallback = CHANGE_CB + { + PASS_EQUAL(BOXI(NSKeyValueChangeInsertion), change[NSKeyValueChangeKindKey], + "Union change is an insertion"); + NSSet *expected = [NSSet setWithObjects:BOXI(1), BOXI(2), BOXI(3), nil]; + PASS_EQUAL(change[NSKeyValueChangeNewKey], expected, + "Union new key is correct"); + PASS(change[NSKeyValueChangeOldKey] == nil, "Union old key is nil"); + }; + + // Minus with @({@(1)}) to get @({@(2), @(3)}) + ChangeCallback minusCallback = CHANGE_CB + { + PASS_EQUAL(change[NSKeyValueChangeKindKey], BOXI(NSKeyValueChangeRemoval), + "Minus change is a removal"); + PASS_EQUAL(change[NSKeyValueChangeOldKey], [NSSet setWithObject:BOXI(1)], + "Minus old key is correct"); + PASS(change[NSKeyValueChangeNewKey] == nil, "Minus new key is nil"); + }; + + // Add @(1) to @({@(2), @(3)}) to get @({@(1), @(2), @(3)}) + ChangeCallback addCallback = CHANGE_CB + { + PASS_EQUAL(BOXI(NSKeyValueChangeInsertion), change[NSKeyValueChangeKindKey], + "Add change is an insertion"); + NSLog(@"Change %@", change); + PASS_EQUAL([NSSet setWithObject:BOXI(1)], change[NSKeyValueChangeNewKey], + "Add new key is correct"); + PASS(change[NSKeyValueChangeOldKey] == nil, "Add old key is nil"); + }; + + // Remove @(1) from @({@(1), @(2), @(3)}) to get @({@(2), @(3)}) + ChangeCallback removeCallback = CHANGE_CB + { + PASS_EQUAL(BOXI(NSKeyValueChangeRemoval), change[NSKeyValueChangeKindKey], + "Remove change is a removal"); + PASS_EQUAL([NSSet setWithObject:BOXI(1)], change[NSKeyValueChangeOldKey], + "Remove old key is correct"); + PASS(change[NSKeyValueChangeNewKey] == nil, "Remove new key is nil"); + }; + + // Intersect with @({@(2)}) to get @({2}) + ChangeCallback intersectCallback = CHANGE_CB + { + PASS_EQUAL(BOXI(NSKeyValueChangeRemoval), change[NSKeyValueChangeKindKey], + "Intersect change is a removal"); + NSSet *expected = [NSSet setWithObject:BOXI(3)]; + PASS_EQUAL(expected, change[NSKeyValueChangeOldKey], + "Intersect old key is correct"); + PASS(change[NSKeyValueChangeNewKey] == nil, "Intersect new key is nil"); + }; + + // Set with @({@(3)}) to get @({@(3)}) + ChangeCallback setCallback = CHANGE_CB + { + if (setSetChanged) + { + PASS_EQUAL(BOXI(NSKeyValueChangeReplacement), + change[NSKeyValueChangeKindKey], + "Set change is a replacement"); + PASS_EQUAL([NSSet setWithObject:BOXI(2)], change[NSKeyValueChangeOldKey], + "Set old key is correct"); + PASS_EQUAL([NSSet setWithObject:BOXI(3)], change[NSKeyValueChangeNewKey], + "Set new key is correct"); + } + // setXxx method is not automatically swizzled for observation + else + { + PASS_EQUAL(BOXI(NSKeyValueChangeSetting), change[NSKeyValueChangeKindKey], + "Set change is a setting"); + PASS_EQUAL([NSSet setWithObject:BOXI(3)], change[NSKeyValueChangeOldKey], + "Set old key is correct"); + PASS_EQUAL([NSSet setWithObject:BOXI(3)], change[NSKeyValueChangeNewKey], + "Set new key is correct"); + } + }; + + ChangeCallback illegalChangeNotification = CHANGE_CB + { + PASS(NO, "illegalChangeNotification"); + }; + + Observee *observee = [Observee new]; + TestFacade *facade = [TestFacade newWithObservee:observee]; + + [facade observeKeyPath:@"setWithHelpers" + withOptions:NSKeyValueObservingOptionNew + | NSKeyValueObservingOptionOld + performingBlock:^(Observee *observee) { + // This set is assisted by setter functions, and should also + // dispatch one notification per change. + [observee + addSetWithHelpers:[NSSet setWithObjects:BOXI(1), BOXI(2), BOXI(3), nil]]; + [observee removeSetWithHelpers:[NSSet setWithObject:BOXI(1)]]; + [observee addSetWithHelpersObject:BOXI(1)]; + [observee removeSetWithHelpersObject:BOXI(1)]; + [observee intersectSetWithHelpers:[NSSet setWithObject:BOXI(2)]]; + [observee setSetWithHelpers:[NSSet setWithObject:BOXI(3)]]; + } + andExpectChangeCallbacks: [NSArray arrayWithObjects: + unionCallback, minusCallback, addCallback, removeCallback, + intersectCallback, setCallback, illegalChangeNotification, nil + ]]; + PASS([facade hits] == 6, "All six notifications were sent (setWithHelpers)"); + + setSetChanged = YES; + + [observee release]; + [facade release]; + + observee = [Observee new]; + facade = [TestFacade newWithObservee:observee]; + + [facade observeKeyPath:@"kvcMediatedSet" + withOptions:NSKeyValueObservingOptionNew + | NSKeyValueObservingOptionOld + performingBlock:^(Observee *observee) { + // Proxy mutable set should dispatch one notification per change + // The proxy set is a NSKeyValueIvarMutableSet + NSMutableSet *proxySet = + [observee mutableSetValueForKey:@"kvcMediatedSet"]; + [proxySet unionSet:[NSSet setWithObjects:BOXI(1), BOXI(2), BOXI(3), nil]]; + [proxySet minusSet:[NSSet setWithObject:BOXI(1)]]; + [proxySet addObject:BOXI(1)]; + [proxySet removeObject:BOXI(1)]; + [proxySet intersectSet:[NSSet setWithObject:BOXI(2)]]; + [proxySet setSet:[NSSet setWithObject:BOXI(3)]]; + } + andExpectChangeCallbacks: [NSArray arrayWithObjects: + unionCallback, minusCallback, addCallback, removeCallback, + intersectCallback, setCallback, illegalChangeNotification, nil]]; + PASS([facade hits] == 6, "All six notifications were sent (kvcMediatedSet)"); + + [observee release]; + [facade release]; + + observee = [Observee new]; + facade = [TestFacade newWithObservee:observee]; + + [facade observeKeyPath:@"manualNotificationSet" + withOptions:NSKeyValueObservingOptionNew + | NSKeyValueObservingOptionOld + performingBlock:^(Observee *observee) { + // Manually should dispatch one notification per change + [observee manualUnionSet:[NSSet setWithObjects:BOXI(1), BOXI(2), BOXI(3), nil]]; + [observee manualMinusSet:[NSSet setWithObject:BOXI(1)]]; + [observee manualSetAddObject:BOXI(1)]; + [observee manualSetRemoveObject:BOXI(1)]; + [observee manualIntersectSet:[NSSet setWithObject:BOXI(2)]]; + [observee manualSetSet:[NSSet setWithObject:BOXI(3)]]; + } + andExpectChangeCallbacks: [NSArray arrayWithObjects: + unionCallback, minusCallback, addCallback, removeCallback, + intersectCallback, setCallback, illegalChangeNotification, nil]]; + PASS([facade hits] == 6, + "All six notifications were sent (manualNotificationSet)"); + + /* Indirect proxy (addObject, etc.) to test + * NSKeyValueFastMutableSet */ + [facade observeKeyPath:@"proxySet" + withOptions:NSKeyValueObservingOptionNew + | NSKeyValueObservingOptionOld + performingBlock:^(Observee *observee) { + // Proxy mutable set should dispatch one notification per change + // The proxy set is a NSKeyValueIvarMutableSet + NSMutableSet *proxySet = + [observee mutableSetValueForKey:@"proxySet"]; + [proxySet unionSet:[NSSet setWithObjects:BOXI(1), BOXI(2), BOXI(3), nil]]; + [proxySet minusSet:[NSSet setWithObject:BOXI(1)]]; + [proxySet addObject:BOXI(1)]; + [proxySet removeObject:BOXI(1)]; + [proxySet intersectSet:[NSSet setWithObject:BOXI(2)]]; + [proxySet setSet:[NSSet setWithObject:BOXI(3)]]; + } + andExpectChangeCallbacks: [NSArray arrayWithObjects: + unionCallback, minusCallback, addCallback, removeCallback, + intersectCallback, setCallback, illegalChangeNotification, nil]]; + PASS([facade hits] == 6, "All six notifications were sent (proxySet)"); + + /* Indirect slow proxy via NSInvocation to test NSKeyValueSlowMutableSet */ + /* Indirect proxy (addObject, etc.) to test + * NSKeyValueFastMutableSet */ + [facade observeKeyPath:@"proxyRoSet" + withOptions:NSKeyValueObservingOptionNew + | NSKeyValueObservingOptionOld + performingBlock:^(Observee *observee) { + NSMutableSet *proxySet = + [observee mutableSetValueForKey:@"proxyRoSet"]; + [proxySet unionSet:[NSSet setWithObjects:BOXI(1), BOXI(2), BOXI(3), nil]]; + [proxySet minusSet:[NSSet setWithObject:BOXI(1)]]; + [proxySet addObject:BOXI(1)]; + [proxySet removeObject:BOXI(1)]; + [proxySet intersectSet:[NSSet setWithObject:BOXI(2)]]; + [proxySet setSet:[NSSet setWithObject:BOXI(3)]]; + } + andExpectChangeCallbacks: [NSArray arrayWithObjects: + unionCallback, minusCallback, addCallback, removeCallback, + intersectCallback, setCallback, illegalChangeNotification, nil]]; + PASS([facade hits] == 6, "All six notifications were sent (proxySet)"); + + [observee release]; + [facade release]; + + END_SET("NSSetMutationMethods"); +} + +int +main(int argc, char *argv[]) +{ + NSAutoreleasePool *pool = [NSAutoreleasePool new]; + + ToMany_NoNotificationOnBareArray(); + ToMany_NotifyingArray(); + ToMany_KVCMediatedArrayWithHelpers_AggregateFunction(); + + ToMany_ToOne_ShouldDowngradeForOrderedObservation(); + ObserverInformationShouldNotLeak(); + + // NSArrayShouldNotBeObservable(); + NSArrayShouldThrowWhenTryingToObserveIndexesOutOfRange(); + NSArrayObserveElements(); + + NSSetShouldNotBeObservable(); + NSSetMutationMethods(); + + DESTROY(pool); + + return 0; +} + +#else + +int +main(int argc, const char *argv[]) +{ + NSAutoreleasePool *pool = [NSAutoreleasePool new]; + + NSLog(@"This test requires an Objective-C 2.0 runtime and is not supported " + @"on this platform."); + + DESTROY(pool); + + return 0; +} + +#endif diff --git a/Tests/base/NSKVOSupport/newoldvalues.m b/Tests/base/NSKVOSupport/newoldvalues.m new file mode 100644 index 000000000..c18636edd --- /dev/null +++ b/Tests/base/NSKVOSupport/newoldvalues.m @@ -0,0 +1,239 @@ +#import +#import "ObjectTesting.h" +#import "Testing.h" + +#if defined (__OBJC2__) +#define FLAKY_ON_GCC_START +#define FLAKY_ON_GCC_END +#else +#define FLAKY_ON_GCC_START \ + testHopeful = YES; +#define FLAKY_ON_GCC_END \ + testHopeful = NO; +#endif + +@class Bar; + +@interface Foo : NSObject +{ + Bar *globalBar; + NSInteger a; +} +@end + +@interface Bar : NSObject +{ + NSInteger x; + Foo *firstFoo; + Foo *secondFoo; +} +- (NSInteger) x; +@end + +@implementation Foo + ++ (NSSet *) keyPathsForValuesAffectingB +{ + return [NSSet setWithArray: [NSArray arrayWithObjects: + @"a", @"globalBar.x", nil]]; +} + +- (NSInteger) a +{ + return a; +} +- (void) setA: (NSInteger)v +{ + a = v; +} +- (NSInteger) b +{ + return [self a] + [globalBar x]; +} +- (Bar*) globalBar +{ + return globalBar; +} +- (void) setGlobalBar: (Bar*)v +{ + globalBar = v; +} + +@end + +@implementation Bar + +- (Foo*) firstFoo +{ + return firstFoo; +} +- (void) setFirstFoo: (Foo*)v +{ + firstFoo = v; +} +- (Foo*) secondFoo +{ + return secondFoo; +} +- (void) setSecondFoo: (Foo*)v +{ + secondFoo = v; +} +- (NSInteger) x +{ + return x; +} +- (void) setX: (NSInteger)v +{ + x = v; +} + +- (id)init +{ + self = [super init]; + if (self) + { + [self setFirstFoo: [Foo new]]; + [[self firstFoo] setGlobalBar: self]; + [self setSecondFoo: [Foo new]]; + [[self secondFoo] setGlobalBar: self]; + } + return self; +} + +@end + +@interface Observer : NSObject +{ + Foo *object; + NSInteger expectedOldValue; + NSInteger expectedNewValue; + NSInteger receivedCalls; +} +@end + +@implementation Observer + +- (NSInteger) expectedOldValue +{ + return expectedOldValue; +} +- (void) setExpectedOldValue: (NSInteger)v +{ + expectedOldValue = v; +} +- (NSInteger) expectedNewValue +{ + return expectedNewValue; +} +- (void) setExpectedNewValue: (NSInteger)v +{ + expectedNewValue = v; +} +- (Foo*) object +{ + return object; +} +- (void) setObject: (Foo*)v +{ + object = v; +} +- (NSInteger) receivedCalls +{ + return receivedCalls; +} +- (void) setReceivedCalls: (NSInteger)v +{ + receivedCalls = v; +} + +- (id)init +{ + self = [super init]; + if (self) + { + [self setReceivedCalls: 0]; + } + return self; +} + +static char observerContext; + +- (void) startObserving:(Foo *)target +{ + [self setObject: target]; + [target + addObserver:self + forKeyPath:@"b" + options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld + context:&observerContext]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)o + change:(NSDictionary *)change + context:(void *)context +{ + PASS(context == &observerContext, "context is correct"); + PASS(o == [self object], "object is correct"); + + id newValue = [change objectForKey: NSKeyValueChangeNewKey]; + id oldValue = [change objectForKey: NSKeyValueChangeOldKey]; + + PASS([oldValue integerValue] == self.expectedOldValue, + "new value in change dict"); + PASS([newValue integerValue] == self.expectedNewValue, + "old value in change dict"); + [self setReceivedCalls: [self receivedCalls] + 1]; +} + +@end + +int +main(int argc, char *argv[]) +{ + NSAutoreleasePool *arp = [NSAutoreleasePool new]; + + START_SET("newoldvalues"); + FLAKY_ON_GCC_START + + Bar *bar = [Bar new]; + [bar setX: 0]; + [[bar firstFoo] setA: 1]; + [[bar secondFoo] setA: 2]; + + Observer *obs1 = [Observer new]; + Observer *obs2 = [Observer new]; + [obs1 startObserving: [bar firstFoo]]; + [obs2 startObserving: [bar secondFoo]]; + + [obs1 setExpectedOldValue: 1]; + [obs1 setExpectedNewValue: 2]; + [obs2 setExpectedOldValue: 2]; + [obs2 setExpectedNewValue: 3]; + [bar setX: 1]; + PASS(obs1.receivedCalls == 1, "num observe calls"); + PASS(obs2.receivedCalls == 1, "num observe calls"); + + [obs1 setExpectedOldValue: 2]; + [obs1 setExpectedNewValue: 2]; + [obs2 setExpectedOldValue: 3]; + [obs2 setExpectedNewValue: 3]; + [bar setX: 1]; + PASS([obs1 receivedCalls] == 2, "num observe calls"); + PASS([obs2 receivedCalls] == 2, "num observe calls"); + + [obs1 setExpectedOldValue: 2]; + [obs1 setExpectedNewValue: 3]; + [[bar firstFoo] setA: 2]; + PASS([obs1 receivedCalls] == 3, "num observe calls"); + PASS([obs2 receivedCalls] == 2, "num observe calls"); + + FLAKY_ON_GCC_END + END_SET("newoldvalues"); + + + DESTROY(arp); + + return 0; +} diff --git a/Tests/base/NSKVOSupport/userdefaults.m b/Tests/base/NSKVOSupport/userdefaults.m new file mode 100644 index 000000000..5bcdb168b --- /dev/null +++ b/Tests/base/NSKVOSupport/userdefaults.m @@ -0,0 +1,173 @@ +#import +#import +#import +#import +#import +#import +#import +#import + +#import + +/* NSUserDefaults KeyValueObserving Tests + * + * Behaviour was validated on macOS 15.0.1 (24A348) + */ + +@interface Observer : NSObject +{ +@public + NSInteger called; + NSString *lastKeyPath; + id lastObject; + NSDictionary *lastChange; +} +@end + +@implementation Observer + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context +{ + called++; + ASSIGN(lastKeyPath, keyPath); + ASSIGN(lastObject, object); + ASSIGN(lastChange, change); +} + +- (void)dealloc +{ + RELEASE(lastKeyPath); + RELEASE(lastObject); + RELEASE(lastChange); + [super dealloc]; +} + +@end + +// NSUserDefaults Domain Search List: +// NSArgumentDomain +// Application Domain +// NSGlobalDomain +// NSRegistrationDomain +// +// Terminology: +// - Entry: An entry is a key value pair. +// - Object and Value: both used interchangeably in the NSUserDefaults API to +// describe the value associated with a given key. +// +// Note that -removeObjectForKey: and -setObject:ForKey: emit only a +// KVO notification when the value has actually changed, meaning +// -objectForKey: would return a different value than before. +// +// Example: +// Assume that a key with the same value is registered in both NSArgumentDomain +// and the application domain. If we remove the value with -removeObjectForKey:, +// we set the value for the key in the application domain to nil, but we stil +// have an entry in the NSArgumentDomain. Thus -objectForKey will return the +// same value as before and no change notification is emitted. +int +main(int argc, char *argv[]) +{ + NSAutoreleasePool *pool = [NSAutoreleasePool new]; + NSUserDefaults *defs = [NSUserDefaults standardUserDefaults]; + Observer *obs = [Observer new]; + NSString *key1 = @"key1"; + NSString *value1 = @"value1"; + NSString *key2 = @"key2"; + NSString *value2 = @"value2"; + NSString *value2Alt = @"value2Alt"; + + [defs addObserver:obs + forKeyPath:key1 + options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld + context:NULL]; + [defs addObserver:obs + forKeyPath:key2 + options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld + context:NULL]; + + // Check if we receive KVO notifications when setting default key in the + // standard application domain + [defs setObject:value1 forKey:key1]; + PASS(obs->called == 1, "KVO notification received"); + PASS(obs->lastObject != nil, "object is not nil"); + PASS(obs->lastChange != nil, "change is not nil"); + PASS_EQUAL([obs->lastChange objectForKey:@"kind"], + [NSNumber numberWithInteger:1], "value for 'kind' is 1"); + PASS_EQUAL([obs->lastChange objectForKey:@"old"], [NSNull null], + "value for 'old' is [NSNull null]"); + PASS_EQUAL([obs->lastChange objectForKey:@"new"], value1, + "value for 'new' is 'value1'"); + + [defs removeObjectForKey:key1]; + PASS(obs->called == 2, "KVO notification received"); + PASS(obs->lastObject != nil, "object is not nil"); + PASS(obs->lastChange != nil, "change is not nil"); + PASS_EQUAL([obs->lastChange objectForKey:@"kind"], + [NSNumber numberWithInteger:1], "value for 'kind' is 1"); + PASS_EQUAL([obs->lastChange objectForKey:@"old"], value1, + "value for 'old' is value1"); + PASS_EQUAL([obs->lastChange objectForKey:@"new"], [NSNull null], + "value for 'new' is [NSNull null]"); + + // Test setting two different values for the same key in application domain + // and registration domain. When removing the value in the application domain, + // the value for 'new' in the change dictionary is not nil, but rather the + // value from the registration domain. + [defs setObject:value2 forKey:key2]; + PASS(obs->called == 3, "KVO notification received"); + PASS(obs->lastObject != nil, "object is not nil"); + PASS(obs->lastChange != nil, "change is not nil"); + PASS_EQUAL([obs->lastChange objectForKey:@"kind"], + [NSNumber numberWithInteger:1], "value for 'kind' is 1"); + PASS_EQUAL([obs->lastChange objectForKey:@"old"], [NSNull null], + "value for 'old' is [NSNull null]"); + PASS_EQUAL([obs->lastChange objectForKey:@"new"], value2, + "value for 'new' is 'value2'"); + + // Set default key in registration domain that is _different_ to the key + // registered in the application domain. This will trigger a change + // notification, when the entry is removed from the application domain. + NSDictionary *registrationDict = [NSDictionary dictionaryWithObject:value2Alt + forKey:key2]; + [defs registerDefaults:registrationDict]; + + [defs removeObjectForKey:key2]; + PASS(obs->called == 4, "KVO notification received"); + PASS(obs->lastObject != nil, "object is not nil"); + PASS(obs->lastChange != nil, "change is not nil"); + PASS_EQUAL([obs->lastChange objectForKey:@"kind"], + [NSNumber numberWithInteger:1], "value for 'kind' is 1"); + PASS_EQUAL([obs->lastChange objectForKey:@"old"], value2, + "value for 'old' is value2"); + // this must not be null in this case + PASS_EQUAL([obs->lastChange objectForKey:@"new"], value2Alt, + "value for 'new' is 'value2Alt'"); + + // Set default key in registration domain that is _equal_ to the key + // registered in the application domain. This will _not_ trigger a change + // notification, when the entry is removed from the application domain. + registrationDict = [NSDictionary dictionaryWithObject:value1 forKey:key1]; + [defs registerDefaults:registrationDict]; + + // Does not emit a KVO notification as value is not changed + [defs setObject:value1 forKey:key1]; + PASS(obs->called == 4, + "KVO notification was not emitted as other domain has the same entry"); + + // Remove the entry from the application domain. + [defs removeObjectForKey:key1]; + PASS(obs->called == 4, + "KVO notification was not emitted as other domain has the same entry"); + + [defs removeObserver:obs forKeyPath:key1]; + [defs removeObserver:obs forKeyPath:key2]; + + [pool drain]; + [obs release]; + + return 0; +} diff --git a/Tests/base/NSUserDefaults/general.m b/Tests/base/NSUserDefaults/general.m index cdaa97d6a..b6d6d54ec 100644 --- a/Tests/base/NSUserDefaults/general.m +++ b/Tests/base/NSUserDefaults/general.m @@ -2,79 +2,350 @@ #import #import #import +#import #import +#import #import "ObjectTesting.h" -@interface Observer : NSObject +@interface Observer : NSObject { - unsigned count; + NSInteger count; + NSInteger kvoCount; } -- (NSString*) count; -- (void) notified: (NSNotification*)n; +- (NSInteger)count; +- (NSInteger)kvoCount; +- (void)notified:(NSNotification *)n; @end @implementation Observer -- (NSString*) count +- (NSInteger)count { - return [NSString stringWithFormat: @"%u", count]; + return count; } -- (void) notified: (NSNotification*)n +- (NSInteger)kvoCount +{ + return kvoCount; +} +- (void)notified:(NSNotification *)n { count++; } +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context +{ + id old = [change objectForKey:NSKeyValueChangeOldKey]; + id new = [ change objectForKey : NSKeyValueChangeNewKey ]; + NSKeyValueChange kind = + [[change objectForKey:NSKeyValueChangeKindKey] intValue]; + id isPrior = [change objectForKey:NSKeyValueChangeNotificationIsPriorKey]; + + NSLog(@"KVO: %@: old = %@, new = %@, kind = %ld, isPrior = %@", + keyPath, old, new, kind, isPrior); + + if ([keyPath isEqualToString:@"Test Suite Bool"]) + { + switch (kvoCount) + { + case 0: // Initial + { + PASS_EQUAL( + new, [NSNull null], + "KVO: Initial setting of 'Test Suite Bool' has new = null"); + PASS(kind == NSKeyValueChangeSetting, + "KVO: notification for 'Test Suite Bool' is of kind " + "NSKeyValueChangeSetting (initial)"); + break; + } + case 3: // Prior to [defs setBool:YES forKey:@"Test Suite Bool"]; + { + PASS_EQUAL( + old, [NSNull null], + "KVO: First setting of 'Test Suite Bool' has old = null (prior)"); + PASS(new == nil, + "KVO: First setting of 'Test Suite Bool' has no new (prior)"); + PASS(kind == NSKeyValueChangeSetting, + "KVO: notification for 'Test Suite Bool' is of kind " + "NSKeyValueChangeSetting (prior)"); + PASS_EQUAL(isPrior, [NSNumber numberWithBool:YES], + "KVO: notification for 'Test Suite Bool' is prior"); + break; + } + case 4: // [defs setBool:YES forKey:@"Test Suite Bool"]; + { + PASS_EQUAL( + old, [NSNull null], + "KVO: First setting of 'Test Suite Bool' has old = null"); + PASS([new isKindOfClass:[ NSNumber class ]], + "KVO: New value for 'Test Suite Bool' has NSNumber"); + PASS(YES == [new boolValue], + "KVO: new value for 'Test Suite Bool' is YES"); + PASS(kind == NSKeyValueChangeSetting, + "KVO: notification for 'Test Suite Bool' is of kind " + "NSKeyValueChangeSetting"); + break; + } + case 9: // Prior to [defs removeObjectForKey:@"Test Suite Bool"]; + { + PASS([old isKindOfClass:[NSNumber class]], + "KVO: First setting of 'Test Suite Bool' has old NSNumber"); + PASS(YES == [old boolValue], + "KVO: old value for 'Test Suite Bool' is YES"); + PASS(new == nil, + "KVO: First setting of 'Test Suite Bool' has no new"); + PASS(kind == NSKeyValueChangeSetting, + "KVO: notification for 'Test Suite Bool' is of kind " + "NSKeyValueChangeSetting"); + PASS_EQUAL(isPrior, [NSNumber numberWithBool:YES], + "KVO: notification for 'Test Suite Bool' is prior"); + break; + } + case 10: // [defs removeObjectForKey:@"Test Suite Bool"]; + { + PASS([old isKindOfClass:[NSNumber class]], + "KVO: First setting of 'Test Suite Bool' has old NSNumber"); + PASS(YES == [old boolValue], + "KVO: old value for 'Test Suite Bool' is YES"); + PASS_EQUAL( + new, [NSNull null], + "KVO: First setting of 'Test Suite Bool' has new = null"); + PASS(kind == NSKeyValueChangeSetting, + "KVO: notification for 'Test Suite Bool' is of kind " + "NSKeyValueChangeSetting"); + break; + } + default: { + PASS(NO, "KVO: unexpected count for 'Test Suite Bool'"); + break; + } + } + } + else if ([keyPath isEqualToString:@"Test Suite Int"]) + { + switch (kvoCount) + { + case 1: // Initial + { + PASS_EQUAL( + new, [NSNull null], + "KVO: Initial setting of 'Test Suite Int' has new = null"); + PASS(kind == NSKeyValueChangeSetting, + "KVO: notification for 'Test Suite Int' is of kind " + "NSKeyValueChangeSetting (initial)"); + break; + } + case 5: // Prior to [defs setInteger:34 forKey:@"Test + // Suite Int"]; + { + PASS_EQUAL(old, [NSNull null], + "KVO: First setting of 'Test Suite Int' has old = null"); + PASS(kind == NSKeyValueChangeSetting, + "KVO: notification for 'Test Suite Int' is of kind " + "NSKeyValueChangeSetting"); + PASS_EQUAL(isPrior, [NSNumber numberWithBool:YES], + "KVO: notification for 'Test Suite Int' is prior"); + break; + } + case 6: // [defs setInteger:34 forKey:@"Test Suite Int"]; + { + PASS_EQUAL( + old, [NSNull null], + "KVO: Second setting of 'Test Suite Int' has old = null"); + PASS([new isKindOfClass:[ NSNumber class ]], + "KVO: New value for 'Test Suite Int' has NSNumber"); + PASS(34 == [new intValue], + "KVO: new value for 'Test Suite Int' is 34"); + PASS(kind == NSKeyValueChangeSetting, + "KVO: notification for 'Test Suite Int' is of kind " + "NSKeyValueChangeSetting"); + break; + } + case 11: // Prior to [defs setObject:nil + // forKey:@"Test Suite Int"]; + { + PASS([old isKindOfClass:[NSNumber class]], + "KVO: First setting of 'Test Suite Int' has old NSNumber"); + PASS(34 == [old intValue], + "KVO: old value for 'Test Suite Int' is 34"); + PASS(new == nil, + "KVO: First setting of 'Test Suite Int' has no new"); + PASS(kind == NSKeyValueChangeSetting, + "KVO: notification for 'Test Suite Int' is of kind " + "NSKeyValueChangeSetting"); + PASS_EQUAL(isPrior, [NSNumber numberWithBool:YES], + "KVO: notification for 'Test Suite Int' is prior"); + break; + } + case 12: // [defs setObject:nil forKey:@"Test Suite Int"]; + { + PASS([old isKindOfClass:[NSNumber class]], + "KVO: First setting of 'Test Suite Int' has old NSNumber"); + PASS(34 == [old intValue], + "KVO: old value for 'Test Suite Int' is 34"); + PASS_EQUAL(new, [NSNull null], + "KVO: First setting of 'Test Suite Int' has new = null"); + PASS(kind == NSKeyValueChangeSetting, + "KVO: notification for 'Test Suite Int' is of kind " + "NSKeyValueChangeSetting"); + break; + } + default: { + PASS(NO, "KVO: unexpected count for 'Test Suite Int'"); + break; + } + } + } + else if ([keyPath isEqualToString:@"Test Suite Str"]) + { + switch (kvoCount) + { + case 2: // Initial + { + PASS_EQUAL( + new, [NSNull null], + "KVO: Initial setting of 'Test Suite Str' has new = null"); + PASS(kind == NSKeyValueChangeSetting, + "KVO: notification for 'Test Suite Str' is of kind " + "NSKeyValueChangeSetting (initial)"); + break; + } + case 7: // Prior to [defs setObject:@"SetString" + // forKey:@"Test Suite Str"]; + { + PASS_EQUAL(old, [NSNull null], + "KVO: First setting of 'Test Suite Str' has old = null"); + PASS(kind == NSKeyValueChangeSetting, + "KVO: notification for 'Test Suite Str' is of kind " + "NSKeyValueChangeSetting"); + PASS_EQUAL(isPrior, [NSNumber numberWithBool:YES], + "KVO: notification for 'Test Suite Str' is prior"); + break; + } + case 8: // [defs setObject:@"SetString" + // forKey:@"Test Suite Str"]; + { + PASS_EQUAL( + old, [NSNull null], + "KVO: Second setting of 'Test Suite Str' has old = null"); + PASS([new isKindOfClass:[ NSString class ]], + "KVO: New value for 'Test Suite Str' has NSString"); + PASS([new isEqual:@"SetString"], + "KVO: new value for 'Test Suite Str' is 'SetString'"); + PASS(kind == NSKeyValueChangeSetting, + "KVO: notification for 'Test Suite Str' is of kind " + "NSKeyValueChangeSetting"); + break; + } + default: { + PASS(NO, "KVO: unexpected count for 'Test Suite Str'"); + break; + } + } + } + kvoCount++; +} @end -int main() +int +main() { - NSAutoreleasePool *arp = [NSAutoreleasePool new]; - Observer *obs = [[Observer new] autorelease]; - NSUserDefaults *defs; + NSAutoreleasePool *arp = [NSAutoreleasePool new]; + Observer *obs = [[Observer new] autorelease]; + NSUserDefaults *defs; defs = [NSUserDefaults standardUserDefaults]; - PASS(defs != nil && [defs isKindOfClass: [NSUserDefaults class]], + PASS(defs != nil && [defs isKindOfClass:[NSUserDefaults class]], "NSUserDefaults understands +standardUserDefaults"); - [[NSNotificationCenter defaultCenter] addObserver: obs - selector: @selector(notified:) - name: NSUserDefaultsDidChangeNotification - object: nil]; + /* Reset the defaults */ + [defs removeObjectForKey:@"Test Suite Bool"]; + [defs removeObjectForKey:@"Test Suite Int"]; + [defs removeObjectForKey:@"Test Suite Str"]; + + [[NSNotificationCenter defaultCenter] + addObserver:obs + selector:@selector(notified:) + name:NSUserDefaultsDidChangeNotification + object:nil]; - [defs setBool: YES forKey: @"Test Suite Bool"]; - PASS([defs boolForKey: @"Test Suite Bool"], + [defs addObserver:obs + forKeyPath:@"Test Suite Bool" + options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld + | NSKeyValueObservingOptionPrior + | NSKeyValueObservingOptionInitial + context:NULL]; + + [defs addObserver:obs + forKeyPath:@"Test Suite Int" + options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld + | NSKeyValueObservingOptionPrior + | NSKeyValueObservingOptionInitial + context:NULL]; + + [defs addObserver:obs + forKeyPath:@"Test Suite Str" + options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld + | NSKeyValueObservingOptionPrior + | NSKeyValueObservingOptionInitial + context:NULL]; + PASS([obs kvoCount] == 3, "KVO: initial count is 3"); + + [defs setBool:YES forKey:@"Test Suite Bool"]; + PASS([defs boolForKey:@"Test Suite Bool"], "NSUserDefaults can set/get a BOOL"); - PASS([[defs objectForKey: @"Test Suite Bool"] isKindOfClass:[NSNumber class]], + PASS([[defs objectForKey:@"Test Suite Bool"] isKindOfClass:[NSNumber class]], "NSUserDefaults returns NSNumber for a BOOL"); - PASS_EQUAL([obs count], @"1", "setting a boolean causes notification"); + PASS([obs count] == 1, "setting a boolean causes notification"); + PASS([obs kvoCount] == 5, "KVO: setting boolean caused 2 notifications"); - [defs setInteger: 34 forKey: @"Test Suite Int"]; - PASS([defs integerForKey: @"Test Suite Int"] == 34, + [defs setInteger:34 forKey:@"Test Suite Int"]; + PASS([defs integerForKey:@"Test Suite Int"] == 34, "NSUserDefaults can set/get an int"); - PASS([[defs objectForKey: @"Test Suite Int"] isKindOfClass:[NSNumber class]], + PASS([[defs objectForKey:@"Test Suite Int"] isKindOfClass:[NSNumber class]], "NSUserDefaults returns NSNumber for an int"); - PASS_EQUAL([obs count], @"2", "setting an integer causes notification"); + PASS([obs count] == 2, "setting an integer causes notification"); + PASS([obs kvoCount] == 7, "KVO: setting integer caused 2 notifications"); - [defs setObject: @"SetString" forKey: @"Test Suite Str"]; - PASS([[defs stringForKey: @"Test Suite Str"] isEqual: @"SetString"], + [defs setObject:@"SetString" forKey:@"Test Suite Str"]; + PASS([[defs stringForKey:@"Test Suite Str"] isEqual:@"SetString"], "NSUserDefaults can set/get a string"); - PASS([[defs objectForKey: @"Test Suite Str"] isKindOfClass:[NSString class]], + PASS([[defs objectForKey:@"Test Suite Str"] isKindOfClass:[NSString class]], "NSUserDefaults returns NSString for a string"); - PASS_EQUAL([obs count], @"3", "setting a string causes notification"); + PASS([obs count] == 3, "setting a string causes notification"); + PASS([obs kvoCount] == 9, "KVO: setting integer caused 2 notifications"); - [defs removeObjectForKey: @"Test Suite Bool"]; - PASS(nil == [defs objectForKey: @"Test Suite Bool"], + [defs removeObjectForKey:@"Test Suite Bool"]; + PASS(nil == [defs objectForKey:@"Test Suite Bool"], "NSUserDefaults can use -removeObjectForKey: to remove a bool"); - PASS_EQUAL([obs count], @"4", "removing a key causes notification"); + PASS([obs count] == 4, "removing a key causes notification"); + PASS([obs kvoCount] == 11, "KVO: removing bool caused 2 notifications"); - [defs setObject: nil forKey: @"Test Suite Int"]; - PASS(nil == [defs objectForKey: @"Test Suite Int"], + [defs setObject:nil forKey:@"Test Suite Int"]; + PASS(nil == [defs objectForKey:@"Test Suite Int"], "NSUserDefaults can use -setObject:forKey: to remove an int"); - PASS_EQUAL([obs count], @"5", "setting nil object causes notification"); + PASS([obs count] == 5, "setting nil object causes notification"); + PASS([obs kvoCount] == 13, "KVO: removing int caused 2 notifications"); + + [defs setObject:@"SetString" forKey:@"Test Suite Str"]; + PASS([[defs objectForKey:@"Test Suite Str"] isKindOfClass:[NSString class]], + "NSUserDefaults returns NSString for an updated string"); + + PASS([obs count] == 6, "setting a string causes notification"); + + [defs setObject:nil forKey:@"Test Suite Int"]; + PASS([obs count] == 7, "setting nil object twice causes notification"); + + [defs removeObserver:obs forKeyPath:@"Test Suite Bool" context:NULL]; + [defs removeObserver:obs forKeyPath:@"Test Suite Int" context:NULL]; + [defs removeObserver:obs forKeyPath:@"Test Suite Str" context:NULL]; - [arp release]; arp = nil; + [arp release]; + arp = nil; return 0; }