libs-base/Tests/base/NSKVOSupport/kvoToMany.m
2024-11-16 13:34:56 +00:00

1245 lines
42 KiB
Objective-C

/**
kvoToMany.m
Copyright (C) 2024 Free Software Foundation, Inc.
Written by: Hugo Melder <hugo@algoriddim.com>
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 <scottc@net-community.com> 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 <Foundation/Foundation.h>
#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<void (^)(NSString *, id, NSDictionary *, void *)> *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<void (^)(NSString *, id, NSDictionary *, void *)> *)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<void (^)(NSString *, id, NSDictionary *, void *)> *)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<void (^)(NSString *, id, NSDictionary *, void *)> *)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 *o1 = AUTORELEASE([Observee new]);
NSArray *o2 = AUTORELEASE([Observee new]);
NSArray *test = [NSArray arrayWithObjects: o1, o2, 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 (add<key>Object, 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 (add<key>Object, 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