/** 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 { if (newInfo != _info) { [_info release]; _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 AUTORELEASE(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; }