libs-base/Tests/base/NSKVOSupport/userdefaults.m
Hugo Melder 6681a3da47
Initial port of WinObjC's KVO implementation to GNUstep (#420)
* GSAtomic: Add prefix to macro definitions

* NSKVOSupport: Import

* NSKVOSupport: Add test cases

* NSKVOSwizzling: Ugly C Rewrite

* NSKeyValueObserving: Use old implementation as fallback

* NSKeyValueObserving: Rename TypeEncodingCases header

* NSKVOSupport: Fix new objects not being added to NSKeyValueChangeNew set on set mutation

* NSKeyValueMutableSet: Fix will and didChange notifications for set operations

* NSKeyValueMutableSet: Document Accessor Search Patterns

* NSKVOSupport: Add toMany test

* NSKeyValueCoding: Change notifications when changing value via setValue:forKey:

* NSKVOSupport: Add more tests

* NSKVOSupport: Do not wrap block in try/finally to avoid crash in windows

* NSKVOSwizzling: use _alloca on Windows

* NSKVOSupport: Do not autorelease newWithObservee:

* NSKVOSupport: Do not leak Observee and TestFacade objects

* Improve runtime detection in makefile

* Add file extension of source file in GNUMakefile

* NSKVOSupport: Remove @status comments

* NSKVOSupport: Implement private notify method

* NSUserDefaults: KVO Support and fix macOS incompatibilities

* NSKeyValueObserving: Set old to null if nil

* NSKeyValueObserving: Remove cached new value

* NSMethodSignature: Add signature cache

* NSKVOSupport: Remove ObjC2 features and mark tests failing on GCC as hopeful

* Call class method instead of private _keyPathsForValuesAffectingValueForKey

* Move _keyPathsForValuesAffectingValueForKey body into class method and statically construct empty NSSet

* NSUserDefaults: Change notification should contain old value from other domains aswell

* NSUserDefaults: Fetch new value from all domains

* NSKVOInternal: Fixup filename in header

* NSUserDefaults: Go through search list instead of only one domain in KVO change

* Making indentation a bit less worse

* Add NSUserDefaults KVO tests

* NSKVOSupport: NSUserDefaults test small fixes

* Add autoreleasepool

* NSUserDefaults: Only emit change notifications if value changed

* Avoid compiler warnings and tidy some of the whitespace/formatting

---------

Co-authored-by: Frederik Seiffert <frederik@algoriddim.com>
Co-authored-by: rfm <richardfrithmacdonald@gmail.com>
Co-authored-by: rfm <rfm@gnu.org>
2024-11-10 17:05:23 +01:00

173 lines
6.4 KiB
Objective-C

#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>
#import <Foundation/NSValue.h>
#import <Foundation/NSNull.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSAutoreleasePool.h>
#import <Foundation/NSUserDefaults.h>
#import <Foundation/NSKeyValueObserving.h>
#import <Testing.h>
/* NSUserDefaults KeyValueObserving Tests
*
* Behaviour was validated on macOS 15.0.1 (24A348)
*/
@interface Observer : NSObject
{
@public
NSInteger called;
NSString *lastKeyPath;
id lastObject;
NSDictionary *lastChange;
}
@end
@implementation Observer
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
called++;
ASSIGN(lastKeyPath, keyPath);
ASSIGN(lastObject, object);
ASSIGN(lastChange, change);
}
- (void)dealloc
{
RELEASE(lastKeyPath);
RELEASE(lastObject);
RELEASE(lastChange);
[super dealloc];
}
@end
// NSUserDefaults Domain Search List:
// NSArgumentDomain
// Application Domain
// NSGlobalDomain
// NSRegistrationDomain
//
// Terminology:
// - Entry: An entry is a key value pair.
// - Object and Value: both used interchangeably in the NSUserDefaults API to
// describe the value associated with a given key.
//
// Note that -removeObjectForKey: and -setObject:ForKey: emit only a
// KVO notification when the value has actually changed, meaning
// -objectForKey: would return a different value than before.
//
// Example:
// Assume that a key with the same value is registered in both NSArgumentDomain
// and the application domain. If we remove the value with -removeObjectForKey:,
// we set the value for the key in the application domain to nil, but we stil
// have an entry in the NSArgumentDomain. Thus -objectForKey will return the
// same value as before and no change notification is emitted.
int
main(int argc, char *argv[])
{
NSAutoreleasePool *pool = [NSAutoreleasePool new];
NSUserDefaults *defs = [NSUserDefaults standardUserDefaults];
Observer *obs = [Observer new];
NSString *key1 = @"key1";
NSString *value1 = @"value1";
NSString *key2 = @"key2";
NSString *value2 = @"value2";
NSString *value2Alt = @"value2Alt";
[defs addObserver:obs
forKeyPath:key1
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:NULL];
[defs addObserver:obs
forKeyPath:key2
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:NULL];
// Check if we receive KVO notifications when setting default key in the
// standard application domain
[defs setObject:value1 forKey:key1];
PASS(obs->called == 1, "KVO notification received");
PASS(obs->lastObject != nil, "object is not nil");
PASS(obs->lastChange != nil, "change is not nil");
PASS_EQUAL([obs->lastChange objectForKey:@"kind"],
[NSNumber numberWithInteger:1], "value for 'kind' is 1");
PASS_EQUAL([obs->lastChange objectForKey:@"old"], [NSNull null],
"value for 'old' is [NSNull null]");
PASS_EQUAL([obs->lastChange objectForKey:@"new"], value1,
"value for 'new' is 'value1'");
[defs removeObjectForKey:key1];
PASS(obs->called == 2, "KVO notification received");
PASS(obs->lastObject != nil, "object is not nil");
PASS(obs->lastChange != nil, "change is not nil");
PASS_EQUAL([obs->lastChange objectForKey:@"kind"],
[NSNumber numberWithInteger:1], "value for 'kind' is 1");
PASS_EQUAL([obs->lastChange objectForKey:@"old"], value1,
"value for 'old' is value1");
PASS_EQUAL([obs->lastChange objectForKey:@"new"], [NSNull null],
"value for 'new' is [NSNull null]");
// Test setting two different values for the same key in application domain
// and registration domain. When removing the value in the application domain,
// the value for 'new' in the change dictionary is not nil, but rather the
// value from the registration domain.
[defs setObject:value2 forKey:key2];
PASS(obs->called == 3, "KVO notification received");
PASS(obs->lastObject != nil, "object is not nil");
PASS(obs->lastChange != nil, "change is not nil");
PASS_EQUAL([obs->lastChange objectForKey:@"kind"],
[NSNumber numberWithInteger:1], "value for 'kind' is 1");
PASS_EQUAL([obs->lastChange objectForKey:@"old"], [NSNull null],
"value for 'old' is [NSNull null]");
PASS_EQUAL([obs->lastChange objectForKey:@"new"], value2,
"value for 'new' is 'value2'");
// Set default key in registration domain that is _different_ to the key
// registered in the application domain. This will trigger a change
// notification, when the entry is removed from the application domain.
NSDictionary *registrationDict = [NSDictionary dictionaryWithObject:value2Alt
forKey:key2];
[defs registerDefaults:registrationDict];
[defs removeObjectForKey:key2];
PASS(obs->called == 4, "KVO notification received");
PASS(obs->lastObject != nil, "object is not nil");
PASS(obs->lastChange != nil, "change is not nil");
PASS_EQUAL([obs->lastChange objectForKey:@"kind"],
[NSNumber numberWithInteger:1], "value for 'kind' is 1");
PASS_EQUAL([obs->lastChange objectForKey:@"old"], value2,
"value for 'old' is value2");
// this must not be null in this case
PASS_EQUAL([obs->lastChange objectForKey:@"new"], value2Alt,
"value for 'new' is 'value2Alt'");
// Set default key in registration domain that is _equal_ to the key
// registered in the application domain. This will _not_ trigger a change
// notification, when the entry is removed from the application domain.
registrationDict = [NSDictionary dictionaryWithObject:value1 forKey:key1];
[defs registerDefaults:registrationDict];
// Does not emit a KVO notification as value is not changed
[defs setObject:value1 forKey:key1];
PASS(obs->called == 4,
"KVO notification was not emitted as other domain has the same entry");
// Remove the entry from the application domain.
[defs removeObjectForKey:key1];
PASS(obs->called == 4,
"KVO notification was not emitted as other domain has the same entry");
[defs removeObserver:obs forKeyPath:key1];
[defs removeObserver:obs forKeyPath:key2];
[pool drain];
[obs release];
return 0;
}