Expand test coverage on managed object deletion cases and fix bugs. closes #358

This commit is contained in:
Blake Watters
2012-12-16 20:45:01 -05:00
parent b6a94de68a
commit 9f228e0280
5 changed files with 215 additions and 48 deletions

View File

@@ -69,22 +69,18 @@
- (void)visitMapping:(RKMapping *)mapping atKeyPath:(NSString *)keyPath
{
id actualKeyPath = keyPath ?: [NSNull null];
if ([self.keyPaths containsObject:actualKeyPath]) return;
if ([mapping isKindOfClass:[RKEntityMapping class]]) {
[self.mutableKeyPaths addObject:actualKeyPath];
} else {
if ([mapping isKindOfClass:[RKDynamicMapping class]]) {
RKDynamicMapping *dynamicMapping = (RKDynamicMapping *)mapping;
for (RKMapping *nestedMapping in dynamicMapping.objectMappings) {
[self visitMapping:nestedMapping atKeyPath:keyPath];
}
} else if ([mapping isKindOfClass:[RKObjectMapping class]]) {
RKObjectMapping *objectMapping = (RKObjectMapping *)mapping;
for (RKRelationshipMapping *relationshipMapping in objectMapping.relationshipMappings) {
NSString *nestedKeyPath = keyPath ? [@[ keyPath, relationshipMapping.destinationKeyPath ] componentsJoinedByString:@"."] : relationshipMapping.destinationKeyPath;
[self visitMapping:relationshipMapping.mapping atKeyPath:nestedKeyPath];
}
if ([self.keyPaths containsObject:actualKeyPath]) return;
if ([mapping isKindOfClass:[RKEntityMapping class]]) [self.mutableKeyPaths addObject:actualKeyPath];
if ([mapping isKindOfClass:[RKDynamicMapping class]]) {
RKDynamicMapping *dynamicMapping = (RKDynamicMapping *)mapping;
for (RKMapping *nestedMapping in dynamicMapping.objectMappings) {
[self visitMapping:nestedMapping atKeyPath:keyPath];
}
} else if ([mapping isKindOfClass:[RKObjectMapping class]]) {
RKObjectMapping *objectMapping = (RKObjectMapping *)mapping;
for (RKRelationshipMapping *relationshipMapping in objectMapping.relationshipMappings) {
NSString *nestedKeyPath = keyPath ? [@[ keyPath, relationshipMapping.destinationKeyPath ] componentsJoinedByString:@"."] : relationshipMapping.destinationKeyPath;
[self visitMapping:relationshipMapping.mapping atKeyPath:nestedKeyPath];
}
}
}
@@ -102,6 +98,28 @@ NSArray *RKArrayOfFetchRequestFromBlocksWithURL(NSArray *fetchRequestBlocks, NSU
return fetchRequests;
}
/**
Returns the set of keys containing the outermost nesting keypath for all children.
For example, given a set containing: 'this', 'this.that', 'another.one.test', 'another.two.test', 'another.one.test.nested'
would return: 'this, 'another.one', 'another.two'
*/
NSSet *RKSetByRemovingSubkeypathsFromSet(NSSet *setOfKeyPaths);
NSSet *RKSetByRemovingSubkeypathsFromSet(NSSet *setOfKeyPaths)
{
return [setOfKeyPaths objectsPassingTest:^BOOL(NSString *keyPath, BOOL *stop) {
if ([keyPath isEqual:[NSNull null]]) return YES; // Special case the root key path
NSArray *keyPathComponents = [keyPath componentsSeparatedByString:@"."];
NSMutableSet *parentKeyPaths = [NSMutableSet set];
for (NSUInteger index = 0; index < [keyPathComponents count] - 1; index++) {
[parentKeyPaths addObject:[[keyPathComponents subarrayWithRange:NSMakeRange(0, index + 1)] componentsJoinedByString:@"."]];
}
for (NSString *parentKeyPath in parentKeyPaths) {
if ([setOfKeyPaths containsObject:parentKeyPath]) return NO;
}
return YES;
}];
}
// When we map the root object, it is returned under the key `[NSNull null]`
static id RKMappedValueForKeyPathInDictionary(NSString *keyPath, NSDictionary *dictionary)
{
@@ -110,17 +128,33 @@ static id RKMappedValueForKeyPathInDictionary(NSString *keyPath, NSDictionary *d
static void RKSetMappedValueForKeyPathInDictionary(id value, NSString *keyPath, NSMutableDictionary *dictionary)
{
NSCParameterAssert(value);
NSCParameterAssert(keyPath);
NSCParameterAssert(dictionary);
[keyPath isEqual:[NSNull null]] ? [dictionary setObject:value forKey:keyPath] : [dictionary setValue:value forKeyPath:keyPath];
}
// Precondition: Must be called from within the correct context
static NSManagedObject *RKRefetchManagedObjectInContext(NSManagedObject *managedObject, NSManagedObjectContext *managedObjectContext)
{
NSManagedObjectID *managedObjectID = [managedObject objectID];
if (! [managedObject managedObjectContext]) return nil; // Object has been deleted
if ([managedObjectID isTemporaryID]) {
RKLogWarning(@"Unable to refetch managed object %@: the object has a temporary managed object ID.", managedObject);
return managedObject;
}
NSError *error = nil;
NSManagedObject *refetchedObject = [managedObjectContext existingObjectWithID:managedObjectID error:&error];
NSCAssert(refetchedObject, @"Failed to find existing object with ID %@ in context %@: %@", managedObjectID, managedObjectContext, error);
return refetchedObject;
}
// Finds the key paths for all entity mappings in the graph whose parent objects are not other managed objects
static NSDictionary *RKDictionaryFromDictionaryWithManagedObjectsAtKeyPathsRefetchedInContext(NSDictionary *dictionaryOfManagedObjects, NSSet *keyPaths, NSManagedObjectContext *managedObjectContext)
{
if (! [dictionaryOfManagedObjects count]) return dictionaryOfManagedObjects;
NSMutableDictionary *newDictionary = [dictionaryOfManagedObjects mutableCopy];
[managedObjectContext performBlockAndWait:^{
__block NSError *error = nil;
for (NSString *keyPath in keyPaths) {
id value = RKMappedValueForKeyPathInDictionary(keyPath, dictionaryOfManagedObjects);
if (! value) {
@@ -129,51 +163,31 @@ static NSDictionary *RKDictionaryFromDictionaryWithManagedObjectsAtKeyPathsRefet
BOOL isMutable = [value isKindOfClass:[NSMutableArray class]];
NSMutableArray *newValue = [[NSMutableArray alloc] initWithCapacity:[value count]];
for (__strong id object in value) {
if ([object isKindOfClass:[NSManagedObject class]]) {
if (![object managedObjectContext]) continue; // Object was deleted
object = [managedObjectContext existingObjectWithID:[object objectID] error:&error];
NSCAssert(object, @"Failed to find existing object with ID %@ in context %@: %@", [object objectID], managedObjectContext, error);
}
[newValue addObject:object];
if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext);
if (object) [newValue addObject:object];
}
value = (isMutable) ? newValue : [newValue copy];
} else if ([value isKindOfClass:[NSSet class]]) {
BOOL isMutable = [value isKindOfClass:[NSMutableSet class]];
NSMutableSet *newValue = [[NSMutableSet alloc] initWithCapacity:[value count]];
for (__strong id object in value) {
if ([object isKindOfClass:[NSManagedObject class]]) {
if (![object managedObjectContext]) continue; // Object was deleted
object = [managedObjectContext existingObjectWithID:[object objectID] error:&error];
NSCAssert(object, @"Failed to find existing object with ID %@ in context %@: %@", [object objectID], managedObjectContext, error);
}
[newValue addObject:object];
if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext);
if (object) [newValue addObject:object];
}
value = (isMutable) ? newValue : [newValue copy];
} else if ([value isKindOfClass:[NSOrderedSet class]]) {
BOOL isMutable = [value isKindOfClass:[NSMutableOrderedSet class]];
NSMutableOrderedSet *newValue = [NSMutableOrderedSet orderedSet];
[(NSOrderedSet *)value enumerateObjectsUsingBlock:^(id object, NSUInteger index, BOOL *stop) {
if ([object isKindOfClass:[NSManagedObject class]]) {
if ([object managedObjectContext]) {
object = [managedObjectContext existingObjectWithID:[object objectID] error:&error];
NSCAssert(object, @"Failed to find existing object with ID %@ in context %@: %@", [object objectID], managedObjectContext, error);
} else {
// Object was deleted
object = nil;
}
}
if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext);
if (object) [newValue setObject:object atIndex:index];
}];
value = (isMutable) ? newValue : [newValue copy];
} else if ([value isKindOfClass:[NSManagedObject class]]) {
// Object becomes nil if deleted
value = [value managedObjectContext] ? [managedObjectContext existingObjectWithID:[value objectID] error:&error] : nil;
value = RKRefetchManagedObjectInContext(value, managedObjectContext);
}
RKSetMappedValueForKeyPathInDictionary(value, keyPath, newDictionary);
if (value) RKSetMappedValueForKeyPathInDictionary(value, keyPath, newDictionary);
}
}];
@@ -482,7 +496,8 @@ static NSURL *RKRelativeURLFromURLAndResponseDescriptors(NSURL *URL, NSArray *re
// Refetch all managed objects nested at key paths within the results dictionary before returning
if (self.mappingResult) {
NSDictionary *resultsDictionaryFromOriginalContext = RKDictionaryFromDictionaryWithManagedObjectsAtKeyPathsRefetchedInContext([self.mappingResult dictionary], managedObjectMappingResultKeyPaths, self.managedObjectContext);
NSSet *nonNestedKeyPaths = RKSetByRemovingSubkeypathsFromSet(managedObjectMappingResultKeyPaths);
NSDictionary *resultsDictionaryFromOriginalContext = RKDictionaryFromDictionaryWithManagedObjectsAtKeyPathsRefetchedInContext([self.mappingResult dictionary], nonNestedKeyPaths, self.managedObjectContext);
self.mappingResult = [[RKMappingResult alloc] initWithDictionary:resultsDictionaryFromOriginalContext];
}
}

View File

@@ -16,6 +16,7 @@
@interface RKManagedObjectRequestOperation ()
- (NSSet *)localObjectsFromFetchRequestsMatchingRequestURL:(NSError **)error;
@end
NSSet *RKSetByRemovingSubkeypathsFromSet(NSSet *setOfKeyPaths);
@interface RKManagedObjectRequestOperationTest : RKTestCase
@@ -367,6 +368,27 @@
expect([[testUser.friendsOrderedSet firstObject] managedObjectContext]).to.equal(managedObjectStore.persistentStoreManagedObjectContext);
}
- (void)testDeletionOfOrphanedManagedObjects
{
RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore];
RKHuman *orphanedHuman = [NSEntityDescription insertNewObjectForEntityForName:@"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext];
RKEntityMapping *entityMapping = [RKEntityMapping mappingForEntityForName:@"Human" inManagedObjectStore:managedObjectStore];
[entityMapping addAttributeMappingsFromArray:@[ @"name" ]];
RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:entityMapping pathPattern:nil keyPath:@"human" statusCodes:[NSIndexSet indexSetWithIndex:200]];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"/JSON/humans/with_to_one_relationship.json" relativeToURL:[RKTestFactory baseURL]]];
RKManagedObjectRequestOperation *managedObjectRequestOperation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[ responseDescriptor ]];
RKFetchRequestBlock fetchRequestBlock = ^NSFetchRequest * (NSURL *URL) {
return [NSFetchRequest fetchRequestWithEntityName:@"Human"];
};
managedObjectRequestOperation.fetchRequestBlocks = @[ fetchRequestBlock ];
managedObjectRequestOperation.managedObjectContext = managedObjectStore.persistentStoreManagedObjectContext;
[managedObjectRequestOperation start];
expect(managedObjectRequestOperation.error).to.beNil();
expect([managedObjectRequestOperation.mappingResult array]).to.haveCountOf(1);
expect(orphanedHuman.managedObjectContext).to.beNil();
}
- (void)testDeletionOfOrphanedObjectsMappedOnRelationships
{
RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore];
@@ -390,6 +412,131 @@
expect(orphanedHuman.managedObjectContext).to.beNil();
}
// TODO: test deletion of nested objects
- (void)testDeletionOfOrphanedTagsOfPosts
{
RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore];
NSManagedObject *orphanedTag = [NSEntityDescription insertNewObjectForEntityForName:@"Tag" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext];
[orphanedTag setValue:@"orphaned" forKey:@"name"];
RKEntityMapping *postMapping = [RKEntityMapping mappingForEntityForName:@"Post" inManagedObjectStore:managedObjectStore];
[postMapping addAttributeMappingsFromArray:@[ @"title", @"body" ]];
RKEntityMapping *tagMapping = [RKEntityMapping mappingForEntityForName:@"Tag" inManagedObjectStore:managedObjectStore];
[tagMapping addAttributeMappingsFromArray:@[ @"name" ]];
[postMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"tags" toKeyPath:@"tags" withMapping:tagMapping]];
RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:postMapping pathPattern:nil keyPath:@"posts" statusCodes:[NSIndexSet indexSetWithIndex:200]];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"/posts.json" relativeToURL:[RKTestFactory baseURL]]];
RKManagedObjectRequestOperation *managedObjectRequestOperation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[ responseDescriptor ]];
RKFetchRequestBlock fetchRequestBlock = ^NSFetchRequest * (NSURL *URL) {
return [NSFetchRequest fetchRequestWithEntityName:@"Tag"];
};
managedObjectRequestOperation.fetchRequestBlocks = @[ fetchRequestBlock ];
managedObjectRequestOperation.managedObjectContext = managedObjectStore.persistentStoreManagedObjectContext;
[managedObjectRequestOperation start];
expect(managedObjectRequestOperation.error).to.beNil();
expect([managedObjectRequestOperation.mappingResult array]).to.haveCountOf(1);
expect(orphanedTag.managedObjectContext).to.beNil();
// Create 3 tags. Update the post entity so it only points to 2 tags. Tag should be deleted.
// Create 3 tags. Create another post pointing to one of the tags. Update the post entity so it only points to 2 tags. Tag should be deleted.
}
- (void)testThatDeletionOfOrphanedObjectsCanBeSuppressedByPredicate
{
RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore];
NSManagedObject *tagOnDiferentObject = [NSEntityDescription insertNewObjectForEntityForName:@"Tag" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext];
[tagOnDiferentObject setValue:@"orphaned" forKey:@"name"];
NSManagedObject *otherPost = [NSEntityDescription insertNewObjectForEntityForName:@"Post" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext];
[otherPost setValue:[NSSet setWithObject:tagOnDiferentObject] forKey:@"tags"];
RKEntityMapping *postMapping = [RKEntityMapping mappingForEntityForName:@"Post" inManagedObjectStore:managedObjectStore];
[postMapping addAttributeMappingsFromArray:@[ @"title", @"body" ]];
RKEntityMapping *tagMapping = [RKEntityMapping mappingForEntityForName:@"Tag" inManagedObjectStore:managedObjectStore];
[tagMapping addAttributeMappingsFromArray:@[ @"name" ]];
[postMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"tags" toKeyPath:@"tags" withMapping:tagMapping]];
RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:postMapping pathPattern:nil keyPath:@"posts" statusCodes:[NSIndexSet indexSetWithIndex:200]];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"/posts.json" relativeToURL:[RKTestFactory baseURL]]];
RKManagedObjectRequestOperation *managedObjectRequestOperation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[ responseDescriptor ]];
RKFetchRequestBlock fetchRequestBlock = ^NSFetchRequest * (NSURL *URL) {
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Tag"];
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"posts.@count == 0"];
return fetchRequest;
};
managedObjectRequestOperation.fetchRequestBlocks = @[ fetchRequestBlock ];
managedObjectRequestOperation.managedObjectContext = managedObjectStore.persistentStoreManagedObjectContext;
[managedObjectRequestOperation start];
expect(managedObjectRequestOperation.error).to.beNil();
expect([managedObjectRequestOperation.mappingResult array]).to.haveCountOf(1);
expect(tagOnDiferentObject.managedObjectContext).notTo.beNil();
}
- (void)testThatObjectsOrphanedByRequestOperationAreDeletedAppropriately
{
// create tags: development, restkit, orphaned
RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore];
__block NSManagedObject *post = nil;
__block NSManagedObject *orphanedTag;
__block NSManagedObject *anotherTag;
[managedObjectStore.persistentStoreManagedObjectContext performBlockAndWait:^{
NSManagedObject *developmentTag = [NSEntityDescription insertNewObjectForEntityForName:@"Tag" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext];
[developmentTag setValue:@"development" forKey:@"name"];
NSManagedObject *restkitTag = [NSEntityDescription insertNewObjectForEntityForName:@"Tag" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext];
[restkitTag setValue:@"restkit" forKey:@"name"];
orphanedTag = [NSEntityDescription insertNewObjectForEntityForName:@"Tag" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext];
[orphanedTag setValue:@"orphaned" forKey:@"name"];
post = [NSEntityDescription insertNewObjectForEntityForName:@"Post" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext];
[post setValue:@"Post Title" forKey:@"title"];
[post setValue:[NSSet setWithObjects:developmentTag, restkitTag, orphanedTag, nil] forKey:@"tags"];
anotherTag = [NSEntityDescription insertNewObjectForEntityForName:@"Tag" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext];
[anotherTag setValue:@"another" forKey:@"name"];
NSManagedObject *anotherPost = [NSEntityDescription insertNewObjectForEntityForName:@"Post" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext];
[anotherPost setValue:@"Another Post" forKey:@"title"];
[anotherPost setValue:[NSSet setWithObject:anotherTag] forKey:@"tags"];
BOOL success = [managedObjectStore.persistentStoreManagedObjectContext save:nil];
}];
RKEntityMapping *postMapping = [RKEntityMapping mappingForEntityForName:@"Post" inManagedObjectStore:managedObjectStore];
postMapping.identificationAttributes = @[ @"title" ];
[postMapping addAttributeMappingsFromArray:@[ @"title", @"body" ]];
RKEntityMapping *tagMapping = [RKEntityMapping mappingForEntityForName:@"Tag" inManagedObjectStore:managedObjectStore];
tagMapping.identificationAttributes = @[ @"name" ];
[tagMapping addAttributeMappingsFromArray:@[ @"name" ]];
[postMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"tags" toKeyPath:@"tags" withMapping:tagMapping]];
RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:postMapping pathPattern:nil keyPath:@"posts" statusCodes:[NSIndexSet indexSetWithIndex:200]];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"/posts.json" relativeToURL:[RKTestFactory baseURL]]];
RKManagedObjectRequestOperation *managedObjectRequestOperation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[ responseDescriptor ]];
RKFetchRequestManagedObjectCache *managedObjectCache = [RKFetchRequestManagedObjectCache new];
managedObjectRequestOperation.managedObjectCache = managedObjectCache;
RKFetchRequestBlock fetchRequestBlock = ^NSFetchRequest * (NSURL *URL) {
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Tag"];
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"posts.@count == 0"];
return fetchRequest;
};
managedObjectRequestOperation.fetchRequestBlocks = @[ fetchRequestBlock ];
managedObjectRequestOperation.managedObjectContext = managedObjectStore.persistentStoreManagedObjectContext;
[managedObjectRequestOperation start];
expect(managedObjectRequestOperation.error).to.beNil();
expect([managedObjectRequestOperation.mappingResult array]).to.haveCountOf(1);
NSSet *tagNames = [post valueForKeyPath:@"tags.name"];
NSSet *expectedTagNames = [NSSet setWithObjects:@"development", @"restkit", nil ];
expect(tagNames).to.equal(expectedTagNames);
expect([orphanedTag hasBeenDeleted]).to.equal(YES);
expect([anotherTag hasBeenDeleted]).to.equal(NO);
}
- (void)testPruningOfSubkeypathsFromSet
{
NSSet *keyPaths = [NSSet setWithObjects:@"posts", @"posts.tags", @"another", @"something.else.entirely", @"another.this.that", @"somewhere.out.there", @"some.posts", nil];
NSSet *prunedSet = RKSetByRemovingSubkeypathsFromSet(keyPaths);
NSSet *expectedSet = [NSSet setWithObjects:@"posts", @"another", @"something.else.entirely", @"somewhere.out.there", @"some.posts", nil];
expect(prunedSet).to.equal(expectedSet);
}
@end

View File

@@ -98,7 +98,7 @@ class RestKitTestServer < Sinatra::Base
delete '/humans/success' do
status 200
content_type 'application/json'
{:humans => {:status => 'OK'}}.to_json
{:human => {:status => 'OK'}}.to_json
end
post '/echo_params' do
@@ -299,6 +299,11 @@ class RestKitTestServer < Sinatra::Base
content_type 'application/json'
render_fixture('/JSON/ComplexNestedUser.json', :status => 200)
end
get '/posts.json' do
content_type 'application/json'
{ :posts => [{:title => 'Post Title', :body => 'Some body.', :tags => [{ :name => 'development' }, { :name => 'restkit' }] }] }.to_json
end
# start the server if ruby file executed directly
run! if app_file == $0