Key Value Observing, with test cases.

This commit is contained in:
Ross Boucher
2008-09-25 22:20:21 -07:00
parent 3b39b64e1b
commit 3834751aa3
2 changed files with 469 additions and 81 deletions
+167 -68
View File
@@ -36,17 +36,17 @@ import "CPObject.j"
}
- addObserver:(id)anObserver forKeyPath:(CPString)aPath options:(unsigned)options context:(id)aContext
- (void)addObserver:(id)anObserver forKeyPath:(CPString)aPath options:(unsigned)options context:(id)aContext
{
if (!anObserver || !aPath)
if (!anObserver || !aPath)
return;
[[_CPKVOProxy proxyForObject:self] _addObserver:anObserver forKeyPath:aPath options:options context:aContext];
}
- removeObserver:(id)anObserver forKeyPath:(CPString)aPath
- (void)removeObserver:(id)anObserver forKeyPath:(CPString)aPath
{
if (!anObserver || !aPath)
if (!anObserver || !aPath)
return;
[[KVOProxyMap objectForKey:[self hash]] _removeObserver:anObserver forKeyPath:aPath];
@@ -65,6 +65,9 @@ CPKeyValueObservingOptionOld = 1 << 1;
CPKeyValueObservingOptionInitial = 1 << 2;
CPKeyValueObservingOptionPrior = 1 << 3;
//convenience
var kvoNewAndOld = CPKeyValueObservingOptionNew|CPKeyValueObservingOptionOld;
// KVO Change Dictionary Keys
CPKeyValueChangeKindKey = @"CPKeyValueChangeKindKey";
CPKeyValueChangeNewKey = @"CPKeyValueChangeNewKey";
@@ -95,9 +98,9 @@ var KVOProxyMap = [CPDictionary dictionary];
return proxy;
proxy = [[self alloc] initWithTarget:anObject];
[proxy _replaceSetters];
anObject.isa = proxy.isa;
[KVOProxyMap setObject:proxy forKey:[anObject hash]];
@@ -108,25 +111,25 @@ var KVOProxyMap = [CPDictionary dictionary];
- (id)initWithTarget:(id)aTarget
{
self = [super init];
_targetObject = aTarget;
_nativeClass = [aTarget class];
_replacementMethods = [CPDictionary dictionary];
_observersForKey = [CPDictionary dictionary];
_changesForKey = [CPDictionary dictionary];
return self;
}
- (void)_replaceSetters
{
var currentClass = [_targetObject class];
while (currentClass && currentClass != currentClass.super_class)
{
var methodList = currentClass.method_list,
count = methodList.length;
for (var i=0; i<count; i++)
{
var newMethod = _kvoMethodForMethod(_targetObject, methodList[i]);
@@ -134,7 +137,7 @@ var KVOProxyMap = [CPDictionary dictionary];
if (newMethod)
[_replacementMethods setObject:newMethod forKey:methodList[i].name];
}
currentClass = currentClass.super_class;
}
}
@@ -153,7 +156,7 @@ var KVOProxyMap = [CPDictionary dictionary];
{
var proxy = [_CPKVOProxy proxyForObject:self],
imp = class_getInstanceMethod(proxy._nativeClass, aSelector);
return imp ? YES : NO;
}
@@ -166,10 +169,10 @@ var KVOProxyMap = [CPDictionary dictionary];
- (IMP)methodForSelector:(SEL)aSelector
{
var proxy = [_CPKVOProxy proxyForObject:self],
imp = class_getInstanceMethod(proxy._nativeClass, aSelector),
replacement = [proxy._replacementMethods objectForKey:aSelector];
replacement = [proxy._replacementMethods objectForKey:aSelector],
imp = replacement ? replacement : class_getInstanceMethod(proxy._nativeClass, aSelector);
return replacement ? replacement : imp;
return imp;
}
- (void)forwardInvocation:(CPInvocation)anInvocation
@@ -180,7 +183,7 @@ var KVOProxyMap = [CPDictionary dictionary];
if (method)
method.apply(self, anInvocation._arguments); //FIXME
else
[super forwardInvocation:anInvocation];
class_getInstanceMethod(proxy._nativeClass, @selector(forwardInvocation:)).apply(self, anInvocation.arguments);
}
- (void)_addObserver:(id)anObserver forKeyPath:(CPString)aPath options:(unsigned)options context:(id)aContext
@@ -188,54 +191,58 @@ var KVOProxyMap = [CPDictionary dictionary];
if (!anObserver)
return;
var forwarder = nil;
if (aPath.indexOf('.') != CPNotFound)
return CPLog.error("WHOA, don't go crazy...");
else
forwarder = [[_CPKVOForwardingObserver alloc] initWithKeyPath:aPath object:_targetObject observer:anObserver options:options context:aContext];
var observers = [_observersForKey objectForKey:aPath];
if (!observers)
{
var observers = [_observersForKey objectForKey:aPath];
if (!observers)
{
observers = [CPDictionary dictionary];
[_observersForKey setObject:observers forKey:aPath];
}
[observers setObject:_CPKVOInfoMake(anObserver, options, aContext) forKey:[anObserver hash]];
observers = [CPDictionary dictionary];
[_observersForKey setObject:observers forKey:aPath];
}
var changes = [CPDictionary dictionary];
[changes setObject:[_targetObject valueForKey:aPath] forKey:CPKeyValueChangeNewKey];
[observers setObject:_CPKVOInfoMake(anObserver, options, aContext, forwarder) forKey:[anObserver hash]];
if (options & CPKeyValueObservingOptionInitial)
{
var newValue = [_targetObject valueForKeyPath:aPath];
[_changesForKey setObject:changes forKey:aPath];
if (!newValue && newValue !== "")
newValue = [CPNull null];
if (options & CPKeyValueObservingOptionInitial)
[anObserver observeValueForKeyPath:aPath ofObject:self change:changes context:aContext];
var changes = [CPDictionary dictionaryWithObject:newValue forKey:CPKeyValueChangeNewKey];
[anObserver observeValueForKeyPath:aPath ofObject:self change:changes context:aContext];
}
}
- (void)_removeObserver:(id)anObserver forKeyPath:(CPString)aPath
{
if (aPath.indexOf('.') != CPNotFound)
return CPLog.error("WHOA, don't go crazy...");
else
{
var observers = [_observersForKey objectForKey:aPath];
{
var observers = [_observersForKey objectForKey:aPath];
[observers removeObjectForKey:[anObserver hash]];
if (![observers count])
[_observersForKey removeObjectForKey:aPath];
if (![_observersForKey count])
{
_targetObject.isa = _nativeClass; //restore the original class
[KVOProxyMap removeObjectForKey:[_targetObject hash]];
}
if (aPath.indexOf('.') != CPNotFound)
{
var forwarder = [observers objectForKey:[anObserver hash]].forwarder;
[forwarder finalize];
}
[observers removeObjectForKey:[anObserver hash]];
if (![observers count])
[_observersForKey removeObjectForKey:aPath];
if (![_observersForKey count])
{
_targetObject.isa = _nativeClass; //restore the original class
[KVOProxyMap removeObjectForKey:[_targetObject hash]];
}
}
- (void)willChangeValueForKey:(CPString)aKey
{
if (!aKey)
if (!aKey)
return;
[[_CPKVOProxy proxyForObject:self] _sendNotificationsForKey:aKey isBefore:YES];
@@ -244,7 +251,7 @@ var KVOProxyMap = [CPDictionary dictionary];
- (void)didChangeValueForKey:(CPString)aKey
{
if (!aKey)
if (!aKey)
return;
[[_CPKVOProxy proxyForObject:self] _sendNotificationsForKey:aKey isBefore:NO];
@@ -252,31 +259,28 @@ var KVOProxyMap = [CPDictionary dictionary];
- (void)_sendNotificationsForKey:(CPString)aKey isBefore:(BOOL)isBefore
{
var changes = [_changesForKey objectForKey:aKey],
oldValue = [changes objectForKey:CPKeyValueChangeOldKey],
newValue = [changes objectForKey:CPKeyValueChangeNewKey];
if (!oldValue && oldValue !== "")
oldValue = [CPNull null];
var changes = [_changesForKey objectForKey:aKey];
if (!newValue && newValue !== "")
newValue = [CPNull null];
if (isBefore)
{
changes = [CPDictionary dictionary];
var oldValue = [_targetObject valueForKey:aKey];
if (!oldValue && oldValue !== "")
oldValue = [CPNull null];
[changes setObject:1 forKey:CPKeyValueChangeNotificationIsPriorKey];
[changes setObject:newValue forKey:CPKeyValueChangeOldKey];
[changes setObject:oldValue forKey:CPKeyValueChangeOldKey];
[_changesForKey setObject:changes forKey:aKey];
}
else
{
[changes removeObjectForKey:CPKeyValueChangeNotificationIsPriorKey];
var newValue = [_targetObject valueForKey:aKey];
if (!newValue && newValue !== "")
newValue = [CPNull null];
@@ -285,11 +289,11 @@ var KVOProxyMap = [CPDictionary dictionary];
var observers = [[_observersForKey objectForKey:aKey] allValues],
count = [observers count];
while (count--)
{
var observerInfo = observers[count];
if (isBefore && (observerInfo.options & CPKeyValueObservingOptionPrior))
[observerInfo.observer observeValueForKeyPath:aKey ofObject:_targetObject change:changes context:observerInfo.context];
else if (!isBefore)
@@ -297,14 +301,109 @@ var KVOProxyMap = [CPDictionary dictionary];
}
}
- (BOOL)isKindOfClass:(Class)aClass
{
return [[self class] isSubclassOfClass:aClass];
}
- (BOOL)isMemberOfClass:(Class)aClass
{
return [self class] == aClass;
}
- (CPString)className
{
return [self class].name;
}
@end
var _CPKVOInfoMake = function _CPKVOInfoMake(anObserver, theOptions, aContext)
@implementation _CPKVOForwardingObserver : CPObject
{
id _object;
id _observer;
id _context;
//a.b
CPString _firstPart; //a
CPString _secondPart; //b
id _value;
}
- (id)initWithKeyPath:(CPString)aKeyPath object:(id)anObject observer:(id)anObserver options:(unsigned)options context:(id)aContext
{
self = [super init];
_context = aContext;
_observer = anObserver;
_object = anObject;
//current ignoring options (FIXME?)
var dotIndex = aKeyPath.indexOf('.');
if (dotIndex == CPNotFound)
[CPException raise:CPInvalidArgumentException reason:"Created _CPKVOForwardingObserver without compound key path: "+aKeyPath];
_firstPart = aKeyPath.substring(0, dotIndex);
_secondPart = aKeyPath.substring(dotIndex+1);
//become an observer of the first part of our key (a)
[_object addObserver:self forKeyPath:_firstPart options:kvoNewAndOld context:nil];
//the current value of a (not the value of a.b)
_value = [_object valueForKey:_firstPart];
if (_value)
[_value addObserver:self forKeyPath:_secondPart options:kvoNewAndOld context:nil]; //we're observing b on current a
return self;
}
- (void)observeValueForKeyPath:(CPString)aKeyPath ofObject:(id)anObject change:(CPDictionary)changes context:(id)aContext
{
if (anObject == _object)
{
[_observer observeValueForKeyPath:_firstPart ofObject:_object change:changes context:_context];
//since a has changed, we should remove ourselves as an observer of the old a, and observe the new one
if (_value)
[_value removeObserver:self forKeyPath:_secondPart];
_value = [_object valueForKey:_firstPart];
if (_value)
[_value addObserver:self forKeyPath:_secondPart options:kvoNewAndOld context:nil];
}
else /* if (anObject == _value || !_value) */
{
//a is the same, but a.b has changed -- nothing to do but forward this message along
[_observer observeValueForKeyPath:_firstPart+"."+aKeyPath ofObject:_object change:changes context:_context];
}
}
- (void)finalize
{
if (_value)
[_value removeObserver:self forKeyPath:_secondPart];
[_object removeObserver:self forKeyPath:_firstPart];
_object = nil;
_observer = nil;
_context = nil;
_value = nil;
}
@end
var _CPKVOInfoMake = function _CPKVOInfoMake(anObserver, theOptions, aContext, aForwarder)
{
return {
observer: anObserver,
options: theOptions,
context: aContext
context: aContext,
forwarder: aForwarder
};
}
+302 -13
View File
@@ -5,11 +5,17 @@ import <Foundation/CPKeyValueObserving.j>
{
BOOL _sawInitialObservation;
BOOL _sawPriorObservation;
BOOL _sawObservation;
id bob;
id obj;
id cs101;
id focus;
}
- (void)testAddObserver
{
var bob = [[PersonTester alloc] init];
bob = [[PersonTester alloc] init];
[bob addObserver:self forKeyPath:@"name" options:nil context:"testAddObserver"];
@@ -17,11 +23,24 @@ import <Foundation/CPKeyValueObserving.j>
[self assertTrue: [bob valueForKey:@"name"] == @"set_bob" message: "valueForKey:'name' should be 'set_bob', was: "+[bob valueForKey:@"name"]];
[self assertTrue: bob.name == @"set_bob" message: "bob.name should be 'set_bob', was: "+bob.name];
[self assertTrue: _sawObservation message:"Never recieved an observation"];
}
- (void)testUnobservedKey
{
bob = [[PersonTester alloc] init];
[bob addObserver:self forKeyPath:@"name" options:nil context:"testUnobservedKey"];
[bob setValue:@"555" forKey:@"phoneNumber"];
[self assertTrue: [bob valueForKey:@"phoneNumber"] == @"555" message: "'phoneNumber' should be '555', was: "+[bob valueForKey:@"phoneNumber"]];
[self assertFalse: _sawObservation message:"Should not have recieved an observation"];
}
- (void)testAddTwoObservers
{
var bob = [[PersonTester alloc] init];
bob = [[PersonTester alloc] init];
[bob addObserver:self forKeyPath:@"name" options:nil context:"testAddTwoObservers"];
[bob addObserver:[CPObject new] forKeyPath:@"name" options:nil context:"testAddTwoObservers"];
@@ -30,6 +49,7 @@ import <Foundation/CPKeyValueObserving.j>
[self assertTrue: [bob valueForKey:@"name"] == @"set_bob" message: "valueForKey:'name' should be bob, was: "+[bob valueForKey:@"name"]];
[self assertTrue: bob.name == @"set_bob" message: "bob.name should be 'bob', was: "+bob.name];
[self assertTrue: _sawObservation message:"Never recieved an observation"];
}
- (void)testDirectIVarObservation
@@ -42,11 +62,12 @@ import <Foundation/CPKeyValueObserving.j>
[self assertTrue: [bob valueForKey:@"phoneNumber"] == @"555" message: "valueForKey:'phoneNumber' should be '555', was: "+[bob valueForKey:@"phoneNumber"]];
[self assertTrue: bob.phoneNumber == @"555" message: "bob.phoneNumber should be '555', was: "+bob.phoneNumber];
[self assertTrue: _sawObservation message:"Never recieved an observation"];
}
- (void)testRemoveObserver
{
var bob = [[PersonTester alloc] init];
bob = [[PersonTester alloc] init];
[bob addObserver:self forKeyPath:@"name" options:nil context:"testRemoveObserver"];
[bob addObserver:[CPString new] forKeyPath:@"name" options:nil context:"testRemoveObserver"];
@@ -60,8 +81,8 @@ import <Foundation/CPKeyValueObserving.j>
- (void)testRemoveOtherObserver
{
var bob = [[PersonTester alloc] init],
obj = [CPString new];
bob = [[PersonTester alloc] init];
obj = [CPString new];
[bob addObserver:self forKeyPath:@"name" options:nil context:"testRemoveOtherObserver"];
[bob addObserver:obj forKeyPath:@"name" options:nil context:"testRemoveOtherObserver"];
@@ -71,18 +92,19 @@ import <Foundation/CPKeyValueObserving.j>
[bob setValue:@"bob" forKey:@"name"];
[self assertTrue: [bob valueForKey:@"name"] == @"set_bob" message: "valueForKey:'name' should be bob, was: "+[bob valueForKey:@"name"]];
[self assertTrue: _sawObservation message:"Never recieved an observation"];
}
- (void)testRemoveAllObservers
{
var bob = [[PersonTester alloc] init],
obj1 = [CPArray new],
obj2 = [CPString new];
bob = [[PersonTester alloc] init];
obj = [CPArray new];
obj2 = [CPString new];
[bob addObserver:obj1 forKeyPath:@"name" options:nil context:"testRemoveAllObservers"];
[bob addObserver:obj forKeyPath:@"name" options:nil context:"testRemoveAllObservers"];
[bob addObserver:obj2 forKeyPath:@"name" options:nil context:"testRemoveAllObservers"];
[bob removeObserver:obj1 forKeyPath:@"name"];
[bob removeObserver:obj forKeyPath:@"name"];
[bob removeObserver:obj2 forKeyPath:@"name"];
[bob removeObserver:nil forKeyPath:nil];
@@ -96,7 +118,7 @@ import <Foundation/CPKeyValueObserving.j>
{
_sawPriorObservation = NO;
var bob = [[PersonTester alloc] init];
bob = [[PersonTester alloc] init];
[bob addObserver:self forKeyPath:@"name" options:CPKeyValueObservingOptionPrior context:"testPriorObservationOption"];
@@ -109,7 +131,7 @@ import <Foundation/CPKeyValueObserving.j>
{
_sawInitialObservation = NO;
var bob = [[PersonTester alloc] init];
bob = [[PersonTester alloc] init];
bob.name = "paul";
@@ -121,6 +143,162 @@ import <Foundation/CPKeyValueObserving.j>
[self assertTrue: _sawInitialObservation message: "asked for CPKeyValueObservingOptionInitial but did not recieve corresponding notification"];
}
- (void)testMultipartKey
{
cs101 = [ClassTester new];
bob = [PersonTester new];
[cs101 setTeacher:bob];
[cs101 addObserver:self forKeyPath:@"teacher.name" options:nil context:"testMultipartKey"];
[bob setName:@"bob"];
[self assertTrue:[cs101 valueForKeyPath:@"teacher.name"] == "set_bob" message:"teacher.name should be: set_bob, was: "+[cs101 valueForKeyPath:@"teacher.name"]];
[self assertTrue: _sawObservation message:"Never recieved an observation"];
}
- (void)testThreePartKey
{
focus = [CarTester new];
cs101 = [ClassTester new];
bob = [PersonTester new];
[cs101 setTeacher:bob];
[bob setValue:focus forKey:"car"];
[cs101 addObserver:self forKeyPath:@"teacher.car.model" options:nil context:"testThreePartKey"];
[focus setValue:"ford focus" forKey:"model"];
[self assertTrue: _sawObservation message:"Never recieved an observation"];
}
- (void)testThreePartKeyPart2
{
focus = [CarTester new];
cs101 = [ClassTester new];
bob = [PersonTester new];
[cs101 setTeacher:bob];
[focus setValue:"2000" forKey:"year"];
[cs101 addObserver:self forKeyPath:@"teacher.car.year" options:nil context:"testThreePartKeyPart2"];
[bob setValue:focus forKey:"car"];
[self assertTrue: _sawObservation message:"Never recieved an observation"];
}
- (void)testRemoveMultipartKey
{
cs101 = [ClassTester new];
bob = [PersonTester new];
[cs101 setTeacher:bob];
[cs101 addObserver:self forKeyPath:@"teacher.name" options:nil context:"testRemoveMultipartKey"];
[cs101 removeObserver:self forKeyPath:@"teacher.name"];
[bob setName:@"bob"];
[self assertFalse: _sawObservation message:"Should not have recieved an observation"];
}
- (void)testRemoveThreePartKey
{
focus = [CarTester new];
cs101 = [ClassTester new];
bob = [PersonTester new];
[cs101 setTeacher:bob];
[bob setValue:focus forKey:"car"];
[cs101 addObserver:self forKeyPath:@"teacher.car.model" options:nil context:"testRemoveThreePartKey"];
[cs101 removeObserver:self forKeyPath:@"teacher.car.model"];
[focus setValue:"ford focus" forKey:"model"];
[self assertFalse: _sawObservation message:"Should not have recieved an observation"];
}
- (void)testCrazyKeyPathChanges
{
var a = [A new];
[a setValue:[B new] forKeyPath:"b"];
[a setValue:[C new] forKeyPath:"b.c"];
[a setValue:[D new] forKeyPath:"b.c.d"];
[a setValue:[E new] forKeyPath:"b.c.d.e"];
[a setValue:[F new] forKeyPath:"b.c.d.e.f"];
[a addObserver:self forKeyPath:"b.c.d.e.f" options:nil context:"testCrazyKeyPathChanges"];
[a setValue:[D new] forKeyPath:"b.c.d"];
[self assertTrue: _sawObservation message:"Never recieved an observation"];
}
- (void)testCrazyKeyPathChanges2
{
var a = [A new];
[a setValue:[B new] forKeyPath:"b"];
[a setValue:[C new] forKeyPath:"b.c"];
[a setValue:[D new] forKeyPath:"b.c.d"];
[a setValue:[E new] forKeyPath:"b.c.d.e"];
[a setValue:[F new] forKeyPath:"b.c.d.e.f"];
[a addObserver:self forKeyPath:"b.c.d.e.f" options:nil context:"testCrazyKeyPathChanges2"];
[a setValue:nil forKeyPath:"b.c"];
[self assertTrue: _sawObservation message:"Never recieved an observation"];
}
- (void)testCrazyKeyPathChanges3
{
var a = [A new];
[a setValue:[B new] forKeyPath:"b"];
[a setValue:[C new] forKeyPath:"b.c"];
[a setValue:[D new] forKeyPath:"b.c.d"];
[a setValue:[E new] forKeyPath:"b.c.d.e"];
[a setValue:[F new] forKeyPath:"b.c.d.e.f"];
[a addObserver:self forKeyPath:"b.c.d.e.f" options:nil context:"testCrazyKeyPathChanges3"];
[a setValue:7 forKeyPath:"b.c.d.e.f"];
[self assertTrue: _sawObservation message:"Never recieved an observation"];
}
- (void)testPerformance
{
bob = [PersonTester new];
[bob setValue:"initial bob" forKey:"name"];
var startTime = new Date();
for(var i=0; i<1000; i++)
[bob setValue:i+"bob" forKey:"name"];
var total = new Date() - startTime;
[bob addObserver:[CPObject new] forKeyPath:"name" options:nil context:nil];
startTime = new Date();
for(var i=0; i<1000; i++)
[bob setValue: i+"bob" forKey:"name"];
var secondTotal = new Date() - startTime;
[self assertTrue: (secondTotal < total*2) message: "Overheard of one observer exceeded 100%. first: "+total+" second: "+secondTotal+" %"+FLOOR(secondTotal/total*100)];
}
- (void)observeValueForKeyPath:(CPString)aKeyPath ofObject:(id)anObject change:(CPDictionary)changes context:(id)aContext
{
var oldValue = [changes objectForKey:CPKeyValueChangeOldKey],
@@ -131,8 +309,11 @@ import <Foundation/CPKeyValueObserving.j>
case "testAddObserver":
[self assertTrue: newValue == "set_bob" message: "newValue should be: set_bob was: "+newValue];
[self assertTrue: oldValue == [CPNull null] message: "oldValue should be CPNull was: "+oldValue];
[self assertTrue: anObject == bob message: "anObject should be: "+[bob description]+", was: "+[anObject description]];
break;
case "testUnobservedKey":
[self assertFalse: YES message: "not observing this key, should never get here"];
break;
case "testAddTwoObservers":
[self assertTrue: newValue == "set_bob" message: "newValue should be: set_bob was: "+newValue];
[self assertTrue: oldValue == [CPNull null] message: "oldValue should be CPNull was: "+oldValue];
@@ -184,7 +365,57 @@ import <Foundation/CPKeyValueObserving.j>
[self assertFalse:YES message:"Should never have received this notification"];
break;
case "testMultipartKey":
[self assertTrue: aKeyPath == "teacher.name" message:"Keypath should be: teacher.name, was: "+aKeyPath];
[self assertTrue: newValue == "set_bob" message:"New value should be: set_bob, was: "+newValue];
[self assertTrue: anObject == cs101 message: "anObject should be: "+[cs101 description]+", was: "+[anObject description]];
break;
case "testRemoveMultipartKey":
[self assertFalse:YES message:"Should never have received this notification"];
break;
case "testRemoveThreePartKey":
[self assertFalse:YES message:"Should never have received this notification"];
break;
case "testThreePartKey":
[self assertTrue: aKeyPath == "teacher.car.model" message:"Keypath should be: teacher.car.model, was: "+aKeyPath];
[self assertTrue: newValue == "ford focus" message:"New value should be: ford focus, was: "+newValue];
[self assertTrue: anObject == cs101 message: "anObject should be: "+[cs101 description]+", was: "+[anObject description]];
break;
case "testThreePartKeyPart2":
[self assertTrue: aKeyPath == "teacher.car" message:"Keypath should be: teacher.car, was: "+aKeyPath];
[self assertTrue: newValue.year == "2000" message:"New value should be a car with year: 2000, was: "+[newValue description]];
[self assertTrue: anObject == cs101 message: "anObject should be: "+[cs101 description]+", was: "+[anObject description]];
break;
case "testCrazyKeyPathChanges":
[self assertTrue: [anObject class] == A message:"Should be observing an A class, was: "+[anObject class]];
[self assertTrue: [newValue class] == D message:"Changed class was a D class, got: "+[newValue class]];
[self assertTrue: aKeyPath == "b.c.d" message:"Expected keyPath b.c.d, got: "+aKeyPath];
break;
case "testCrazyKeyPathChanges2":
[self assertTrue: [anObject class] == A message:"Should be observing an A class, was: "+[anObject class]];
[self assertTrue: newValue == [CPNull null] message:"Expected null, got: "+newValue];
[self assertTrue: aKeyPath == "b.c" message:"Expected keyPath b.c, got: "+aKeyPath];
break;
case "testCrazyKeyPathChanges3":
[self assertTrue: [anObject class] == A message:"Should be observing an A class, was: "+[anObject class]];
[self assertTrue: newValue == 7 message:"Expected 7, got: "+newValue];
[self assertTrue: aKeyPath == "b.c.d.e.f" message:"Expected keyPath b.c.d.e.f, got: "+aKeyPath];
break;
default:
[self assertFalse:YES message:"unhandled observation, must be an error"];
return;
}
_sawObservation = YES;
}
@end
@@ -202,6 +433,7 @@ import <Foundation/CPKeyValueObserving.j>
{
CPString name;
CPString phoneNumber;
CarTester car;
}
- (void)setName:(CPString)aName
@@ -209,4 +441,61 @@ import <Foundation/CPKeyValueObserving.j>
name = "set_"+aName;
}
@end
@implementation ClassTester : CPObject
{
PersonTester teacher;
CPArray students;
}
- (void)setTeacher:(PersonTester)aPerson
{
teacher = aPerson;
}
@end
@implementation CarTester : CPObject
{
CPString model;
CPString year;
}
- (void)setModel:(CPString)aModel
{
model = aModel;
}
@end
@implementation A : CPObject
{
id b;
}
@end
@implementation B : CPObject
{
id c;
}
@end
@implementation C : CPObject
{
id d;
}
@end
@implementation D : CPObject
{
id e;
}
@end
@implementation E : CPObject
{
id f;
}
@end
@implementation F : CPObject
{
id g;
}
@end