// // RKMappingTest.m // RestKit // // Created by Blake Watters on 2/17/12. // Copyright (c) 2009-2012 RestKit. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // #import "RKMappingTest.h" #import "RKEntityMapping.h" #import "RKObjectMappingOperationDataSource.h" #import "RKRelationshipMapping.h" #import "RKErrors.h" #import "RKObjectUtilities.h" #import "RKLog.h" // Core Data #import "RKConnectionDescription.h" #import "RKConnectionTestExpectation.h" #import "RKFetchRequestManagedObjectCache.h" #import "RKManagedObjectMappingOperationDataSource.h" // Error Constants NSString * const RKMappingTestErrorDomain = @"org.restkit.RKMappingTest.ErrorDomain"; NSString * const RKMappingTestEventErrorKey = @"RKMappingTestEventErrorKey"; NSString * const RKMappingTestExpectationErrorKey = @"RKMappingTestExpectationErrorKey"; NSString * const RKMappingTestValueErrorKey = @"RKMappingTestValueErrorKey"; NSString * const RKMappingTestVerificationFailureException = @"RKMappingTestVerificationFailureException"; ///----------------------------------------------------------------------------- ///----------------------------------------------------------------------------- @interface RKMappingTestEvent : NSObject @property (nonatomic, strong, readonly) RKPropertyMapping *propertyMapping; @property (nonatomic, strong, readonly) RKConnectionDescription *connection; @property (nonatomic, strong, readonly) id value; @property (weak, nonatomic, readonly) NSString *sourceKeyPath; @property (weak, nonatomic, readonly) NSString *destinationKeyPath; + (RKMappingTestEvent *)eventWithMapping:(RKPropertyMapping *)propertyMapping value:(id)value; + (RKMappingTestEvent *)eventWithConnection:(RKConnectionDescription *)connection value:(id)value; @end @interface RKMappingTestEvent () @property (nonatomic, strong, readwrite) id value; @property (nonatomic, strong, readwrite) RKPropertyMapping *propertyMapping; @property (nonatomic, strong, readwrite) RKConnectionDescription *connection; @end @implementation RKMappingTestEvent + (RKMappingTestEvent *)eventWithMapping:(RKPropertyMapping *)propertyMapping value:(id)value { RKMappingTestEvent *event = [RKMappingTestEvent new]; event.value = value; event.propertyMapping = propertyMapping; return event; } + (RKMappingTestEvent *)eventWithConnection:(RKConnectionDescription *)connection value:(id)value { RKMappingTestEvent *event = [RKMappingTestEvent new]; event.connection = connection; event.value = value; return event; } - (NSString *)sourceKeyPath { return [self.propertyMapping sourceKeyPath]; } - (NSString *)destinationKeyPath { return [self.propertyMapping destinationKeyPath]; } - (NSString *)description { if (self.propertyMapping) { return [NSString stringWithFormat:@"%@ mapped sourceKeyPath '%@' => destinationKeyPath '%@' with value: %@>", [self class], self.sourceKeyPath, self.destinationKeyPath, self.value]; } else if (self.connection) { if ([self.connection isForeignKeyConnection]) { return [NSString stringWithFormat:@"%@ connected Relationship '%@' using attributes '%@' to value: %@>", [self class], [self.connection.relationship name], [self.connection.attributes valueForKey:@"name"], self.value]; } else if ([self.connection isKeyPathConnection]) { return [NSString stringWithFormat:@"%@ connected Relationship '%@' using keyPath '%@' to value: %@>", [self class], [self.connection.relationship name], self.connection.keyPath, self.value]; } } return [super description]; } @end ///----------------------------------------------------------------------------- ///----------------------------------------------------------------------------- @interface RKMappingTest () @property (nonatomic, strong, readwrite) RKMapping *mapping; @property (nonatomic, strong, readwrite) id sourceObject; @property (nonatomic, strong, readwrite) id destinationObject; @property (nonatomic, strong) NSMutableArray *expectations; @property (nonatomic, strong) NSMutableArray *events; @property (nonatomic, assign, getter = hasPerformedMapping) BOOL performedMapping; // Method Definitions for old compilers - (void)performMapping; - (void)verifyExpectation:(RKPropertyMappingTestExpectation *)expectation; @end @implementation RKMappingTest + (RKMappingTest *)testForMapping:(RKMapping *)mapping sourceObject:(id)sourceObject destinationObject:(id)destinationObject { return [[self alloc] initWithMapping:mapping sourceObject:sourceObject destinationObject:destinationObject]; } - (id)initWithMapping:(RKMapping *)mapping sourceObject:(id)sourceObject destinationObject:(id)destinationObject { NSAssert(sourceObject != nil, @"Cannot perform a mapping operation without a sourceObject object"); NSAssert(mapping != nil, @"Cannot perform a mapping operation without a mapping"); self = [super init]; if (self) { self.sourceObject = sourceObject; self.destinationObject = destinationObject; self.mapping = mapping; self.expectations = [NSMutableArray new]; self.events = [NSMutableArray new]; self.performedMapping = NO; } return self; } - (void)addExpectation:(id)expectation { NSParameterAssert(expectation); if (![expectation isKindOfClass:[RKPropertyMappingTestExpectation class]] && ![expectation isKindOfClass:[RKConnectionTestExpectation class]]) { [NSException raise:NSInvalidArgumentException format:@"Invalid expectation: expected an object of type `%@` or `%@`, but instead got a `%@`", [RKPropertyMappingTestExpectation class], [RKConnectionTestExpectation class], expectation]; } [self.expectations addObject:expectation]; } - (RKMappingTestEvent *)eventMatchingExpectation:(id)expectation { for (RKMappingTestEvent *event in [self.events copy]) { if ([expectation isKindOfClass:[RKPropertyMappingTestExpectation class]]) { RKPropertyMappingTestExpectation *propertyExpectation = (RKPropertyMappingTestExpectation *) expectation; if ([event.sourceKeyPath isEqualToString:propertyExpectation.sourceKeyPath] && [event.destinationKeyPath isEqualToString:propertyExpectation.destinationKeyPath]) { return event; } } else if ([expectation isKindOfClass:[RKConnectionTestExpectation class]]) { RKConnectionTestExpectation *connectionExpectation = (RKConnectionTestExpectation *) expectation; if ([[event.connection.relationship name] isEqualToString:connectionExpectation.relationshipName]) { return event; } } } return nil; } - (NSError *)errorForExpectation:(RKPropertyMappingTestExpectation *)expectation withCode:(NSInteger)errorCode userInfo:(NSDictionary *)userInfo description:(NSString *)description reason:(NSString *)reason { NSMutableDictionary *fullUserInfo = [userInfo mutableCopy]; [fullUserInfo setObject:description forKey:NSLocalizedDescriptionKey]; [fullUserInfo setObject:reason forKey:NSLocalizedFailureReasonErrorKey]; return [NSError errorWithDomain:RKMappingTestErrorDomain code:errorCode userInfo:fullUserInfo]; } - (BOOL)event:(RKMappingTestEvent *)event satisfiesExpectation:(id)expectation error:(NSError **)error { BOOL success; NSDictionary *userInfo = @{ RKMappingTestEventErrorKey : event, RKMappingTestExpectationErrorKey : expectation }; if ([expectation isKindOfClass:[RKPropertyMappingTestExpectation class]]) { RKPropertyMappingTestExpectation *propertyExpectation = (RKPropertyMappingTestExpectation *)expectation; if (propertyExpectation.evaluationBlock) { // Let the expectation block evaluate the match NSError *blockError = nil; success = propertyExpectation.evaluationBlock(expectation, event.propertyMapping, event.value, &blockError); if (! success) { if (blockError) { // If the block has given us an error, use the reason NSMutableDictionary *mutableUserInfo = [userInfo mutableCopy]; [mutableUserInfo setValue:blockError forKey:NSUnderlyingErrorKey]; NSString *reason = [NSString stringWithFormat:@"expected to %@ with value %@ '%@', but it did not", expectation, [event.value class], event.value]; *error = [self errorForExpectation:expectation withCode:RKMappingTestEvaluationBlockError userInfo:mutableUserInfo description:[blockError localizedDescription] reason:reason]; *error = blockError; } else { NSString *description = [NSString stringWithFormat:@"evaluation block returned `NO` for %@ value '%@'", [event.value class], event.value]; NSString *reason = [NSString stringWithFormat:@"expected to %@ with value %@ '%@', but it did not", expectation, [event.value class], event.value]; *error = [self errorForExpectation:expectation withCode:RKMappingTestEvaluationBlockError userInfo:userInfo description:description reason:reason]; } } } else if (propertyExpectation.value) { // Use RestKit comparison magic to match values success = RKObjectIsEqualToObject(event.value, propertyExpectation.value); if (! success) { NSString *description = [NSString stringWithFormat:@"mapped to unexpected %@ value '%@'", [event.value class], event.value]; NSString *reason = [NSString stringWithFormat:@"expected to %@, but instead got %@ '%@'", expectation, [event.value class], event.value]; if (error) *error = [self errorForExpectation:expectation withCode:RKMappingTestEvaluationBlockError userInfo:userInfo description:description reason:reason]; } } else if (propertyExpectation.mapping) { if ([event.propertyMapping isKindOfClass:[RKRelationshipMapping class]]) { // Check the mapping that was used to map the relationship RKMapping *relationshipMapping = [(RKRelationshipMapping *)event.propertyMapping mapping]; success = [relationshipMapping isEqualToMapping:propertyExpectation.mapping]; if (! success) { NSString *description = [NSString stringWithFormat:@"mapped using unexpected mapping: %@", relationshipMapping]; NSString *reason = [NSString stringWithFormat:@"expected to %@, but was instead mapped using: %@", expectation, relationshipMapping]; if (error) *error = [self errorForExpectation:expectation withCode:RKMappingTestValueInequalityError userInfo:userInfo description:description reason:reason]; } } else { NSString *description = [NSString stringWithFormat:@"expected a property mapping of type `RKRelationshipMapping` but instead got a `%@`", [propertyExpectation.mapping class]]; NSString *reason = [NSString stringWithFormat:@"expected to %@, but instead of a `RKRelationshipMapping` got a `%@`", expectation, [propertyExpectation.mapping class]]; if (error) *error = [self errorForExpectation:expectation withCode:RKMappingTestMappingMismatchError userInfo:userInfo description:description reason:reason]; // Error message here that a relationship was not mapped!!! return NO; } } else { // We only wanted to know that a mapping occured between the keyPaths success = YES; } } else if ([expectation isKindOfClass:[RKConnectionTestExpectation class]]) { RKConnectionTestExpectation *connectionExpectation = (RKConnectionTestExpectation *)expectation; id expectedValue = connectionExpectation.value; id connectedValue = event.value; // Check that the connection attributes match if (connectionExpectation.attributes) { RKMappingTestCondition([connectionExpectation.attributes isEqualToDictionary:event.connection.attributes], RKMappingTestValueInequalityError, error, @"established connection using unexpected attributes: %@", event.connection.attributes); } // Wrong objects if (expectedValue) { RKMappingTestCondition(connectedValue, RKMappingTestValueInequalityError, error, @"unexpectedly connected to nil object set (%@)", connectedValue); if ([connectedValue isKindOfClass:[NSManagedObject class]] && [connectionExpectation.value isKindOfClass:[NSManagedObject class]]) { // Do a managed object ID comparison RKMappingTestCondition([[connectedValue objectID] isEqual:[expectedValue objectID]], RKMappingTestValueInequalityError, error, @"connected to unexpected managed object: %@", connectedValue); } else { // If we are connecting to a collection of managed objects, do a comparison of object IDs if (RKObjectIsCollectionContainingOnlyManagedObjects(connectedValue) && RKObjectIsCollectionContainingOnlyManagedObjects(expectedValue)) { RKMappingTestCondition(RKObjectIsEqualToObject([connectedValue valueForKeyPath:@"objectID"], [expectedValue valueForKeyPath:@"objectID"]), RKMappingTestValueInequalityError, error, @"connected to unexpected %@ value '%@'", [connectedValue class], connectedValue); } else { RKMappingTestCondition(RKObjectIsEqualToObject(connectedValue, expectedValue), RKMappingTestValueInequalityError, error, @"connected to unexpected %@ value '%@'", [connectedValue class], connectedValue); } } } else { RKMappingTestCondition(connectedValue == nil, RKMappingTestValueInequalityError, error, @"unexpectedly connected to non-nil object set (%@)", connectedValue); } return YES; } return success; } - (id)dataSourceForMappingOperation:(RKMappingOperation *)mappingOperation { // If we have been given an explicit data source, use it if (self.mappingOperationDataSource) return self.mappingOperationDataSource; if ([self.mapping isKindOfClass:[RKEntityMapping class]]) { NSAssert(self.managedObjectContext, @"Cannot test an `RKEntityMapping` with a nil managed object context."); id managedObjectCache = self.managedObjectCache ?: [RKFetchRequestManagedObjectCache new]; RKManagedObjectMappingOperationDataSource *dataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:self.managedObjectContext cache:managedObjectCache]; // Configure an operation queue to enable easy testing of connection operations NSOperationQueue *operationQueue = [NSOperationQueue new]; dataSource.operationQueue = operationQueue; dataSource.parentOperation = mappingOperation; return dataSource; } else { return [RKObjectMappingOperationDataSource new]; } } - (void)performMapping { // Ensure repeated invocations of verify only result in a single mapping operation if (! self.hasPerformedMapping) { id sourceObject = self.rootKeyPath ? [self.sourceObject valueForKeyPath:self.rootKeyPath] : self.sourceObject; RKMappingOperation *mappingOperation = [[RKMappingOperation alloc] initWithSourceObject:sourceObject destinationObject:self.destinationObject mapping:self.mapping]; mappingOperation.dataSource = [self dataSourceForMappingOperation:mappingOperation]; NSError *error = nil; mappingOperation.delegate = self; [mappingOperation start]; if (mappingOperation.error) { [NSException raise:NSInternalInconsistencyException format:@"%p: failed with error: %@\n%@ during mapping from %@ to %@ with mapping %@", self, error, [self description], self.sourceObject, self.destinationObject, self.mapping]; } // Let the connection operations execute to completion if ([mappingOperation.dataSource isKindOfClass:[RKManagedObjectMappingOperationDataSource class]]) { NSOperationQueue *operationQueue = [(RKManagedObjectMappingOperationDataSource *)mappingOperation.dataSource operationQueue]; if (! [operationQueue isEqual:[NSOperationQueue mainQueue]]) { [operationQueue waitUntilAllOperationsAreFinished]; } } self.performedMapping = YES; // Get the destination object from the mapping operation if (! self.destinationObject) self.destinationObject = mappingOperation.destinationObject; } } - (void)verifyExpectation:(RKPropertyMappingTestExpectation *)expectation { RKMappingTestEvent *event = [self eventMatchingExpectation:expectation]; if (event) { // Found a matching event, check if it satisfies the expectation NSError *error = nil; if (! [self event:event satisfiesExpectation:expectation error:&error]) { NSDictionary *userInfo = @{ NSUnderlyingErrorKey: error, RKMappingTestEventErrorKey: event, RKMappingTestExpectationErrorKey: expectation }; [[NSException exceptionWithName:RKMappingTestVerificationFailureException reason:[error localizedDescription] userInfo:userInfo] raise]; } } else { // No match [NSException raise:NSInternalInconsistencyException format:@"%@: expectation not satisfied: %@, but did not.", [self description], [expectation summary]]; } } - (void)verify { [self performMapping]; for (RKPropertyMappingTestExpectation *expectation in self.expectations) { [self verifyExpectation:expectation]; } } #pragma mark - Evaluating Expectations - (BOOL)evaluate { [self performMapping]; for (RKPropertyMappingTestExpectation *expectation in self.expectations) { if (! [self evaluateExpectation:expectation error:nil]) return NO; } return YES; } - (BOOL)evaluateExpectation:(id)expectation error:(NSError **)error { NSParameterAssert(expectation); NSAssert([expectation isKindOfClass:[RKPropertyMappingTestExpectation class]] || [expectation isKindOfClass:[RKConnectionTestExpectation class]], @"Must be an instance of `RKPropertyMappingTestExpectation` or `RKConnectionTestExpectation`"); [self performMapping]; RKMappingTestEvent *event = [self eventMatchingExpectation:expectation]; if (event) { if (! [self event:event satisfiesExpectation:expectation error:error]) { return NO; } } else { if (error) { NSDictionary *userInfo = @{ RKMappingTestExpectationErrorKey : expectation, NSLocalizedDescriptionKey : [NSString stringWithFormat:@"expected to %@, but did not.", [expectation summary]], NSLocalizedFailureReasonErrorKey : [NSString stringWithFormat:@"%@: %@, but did not.", [self description], [expectation summary]] }; *error = [NSError errorWithDomain:RKMappingTestErrorDomain code:RKMappingTestUnsatisfiedExpectationError userInfo:userInfo]; }; return NO; } return YES; } - (NSString *)expectationsDescription { return [self.expectations valueForKey:@"description"]; } - (NSString *)eventsDescription { return [self.events valueForKey:@"description"]; } - (NSString *)description { return [NSString stringWithFormat:@"%@ Expectations: %@\nEvents: %@", [self class], [self expectationsDescription], [self eventsDescription]]; } #pragma mark - RKMappingOperationDelegate - (void)addEvent:(RKMappingTestEvent *)event { @synchronized(self.events) { [self.events addObject:event]; }; } - (void)mappingOperation:(RKMappingOperation *)operation didSetValue:(id)value forKeyPath:(NSString *)keyPath usingMapping:(RKAttributeMapping *)mapping { [self addEvent:[RKMappingTestEvent eventWithMapping:mapping value:value]]; } - (void)mappingOperation:(RKMappingOperation *)operation didNotSetUnchangedValue:(id)value forKeyPath:(NSString *)keyPath usingMapping:(RKAttributeMapping *)mapping { [self addEvent:[RKMappingTestEvent eventWithMapping:mapping value:value]]; } - (void)mappingOperation:(RKMappingOperation *)operation didConnectRelationship:(NSRelationshipDescription *)relationship toValue:(id)value usingConnection:(RKConnectionDescription *)connection { [self addEvent:[RKMappingTestEvent eventWithConnection:connection value:value]]; } @end