123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358 |
- /*
- * Copyright (c) 2015, Facebook, Inc.
- * All rights reserved.
- *
- * This source code is licensed under the BSD-style license found in the
- * LICENSE file in the root directory of this source tree. An additional grant
- * of patent rights can be found in the PATENTS file in the same directory.
- *
- */
- #import <FBSnapshotTestCase/FBSnapshotTestController.h>
- #import <FBSnapshotTestCase/FBSnapshotTestCasePlatform.h>
- #import <FBSnapshotTestCase/UIImage+Compare.h>
- #import <FBSnapshotTestCase/UIImage+Diff.h>
- #import <FBSnapshotTestCase/UIImage+Snapshot.h>
- #import <UIKit/UIKit.h>
- NSString *const FBSnapshotTestControllerErrorDomain = @"FBSnapshotTestControllerErrorDomain";
- NSString *const FBReferenceImageFilePathKey = @"FBReferenceImageFilePathKey";
- NSString *const FBReferenceImageKey = @"FBReferenceImageKey";
- NSString *const FBCapturedImageKey = @"FBCapturedImageKey";
- NSString *const FBDiffedImageKey = @"FBDiffedImageKey";
- typedef NS_ENUM(NSUInteger, FBTestSnapshotFileNameType) {
- FBTestSnapshotFileNameTypeReference,
- FBTestSnapshotFileNameTypeFailedReference,
- FBTestSnapshotFileNameTypeFailedTest,
- FBTestSnapshotFileNameTypeFailedTestDiff,
- };
- @implementation FBSnapshotTestController
- {
- NSString *_testName;
- NSFileManager *_fileManager;
- }
- #pragma mark - Initializers
- - (instancetype)initWithTestClass:(Class)testClass;
- {
- return [self initWithTestName:NSStringFromClass(testClass)];
- }
- - (instancetype)initWithTestName:(NSString *)testName
- {
- if (self = [super init]) {
- _testName = [testName copy];
- _deviceAgnostic = NO;
-
- _fileManager = [[NSFileManager alloc] init];
- }
- return self;
- }
- #pragma mark - Overrides
- - (NSString *)description
- {
- return [NSString stringWithFormat:@"%@ %@", [super description], _referenceImagesDirectory];
- }
- #pragma mark - Public API
- - (BOOL)compareSnapshotOfLayer:(CALayer *)layer
- selector:(SEL)selector
- identifier:(NSString *)identifier
- error:(NSError **)errorPtr
- {
- return [self compareSnapshotOfViewOrLayer:layer
- selector:selector
- identifier:identifier
- tolerance:0
- error:errorPtr];
- }
- - (BOOL)compareSnapshotOfView:(UIView *)view
- selector:(SEL)selector
- identifier:(NSString *)identifier
- error:(NSError **)errorPtr
- {
- return [self compareSnapshotOfViewOrLayer:view
- selector:selector
- identifier:identifier
- tolerance:0
- error:errorPtr];
- }
- - (BOOL)compareSnapshotOfViewOrLayer:(id)viewOrLayer
- selector:(SEL)selector
- identifier:(NSString *)identifier
- tolerance:(CGFloat)tolerance
- error:(NSError **)errorPtr
- {
- if (self.recordMode) {
- return [self _recordSnapshotOfViewOrLayer:viewOrLayer selector:selector identifier:identifier error:errorPtr];
- } else {
- return [self _performPixelComparisonWithViewOrLayer:viewOrLayer selector:selector identifier:identifier tolerance:tolerance error:errorPtr];
- }
- }
- - (UIImage *)referenceImageForSelector:(SEL)selector
- identifier:(NSString *)identifier
- error:(NSError **)errorPtr
- {
- NSString *filePath = [self _referenceFilePathForSelector:selector identifier:identifier];
- UIImage *image = [UIImage imageWithContentsOfFile:filePath];
- if (nil == image && NULL != errorPtr) {
- BOOL exists = [_fileManager fileExistsAtPath:filePath];
- if (!exists) {
- *errorPtr = [NSError errorWithDomain:FBSnapshotTestControllerErrorDomain
- code:FBSnapshotTestControllerErrorCodeNeedsRecord
- userInfo:@{
- FBReferenceImageFilePathKey: filePath,
- NSLocalizedDescriptionKey: @"Unable to load reference image.",
- NSLocalizedFailureReasonErrorKey: @"Reference image not found. You need to run the test in record mode",
- }];
- } else {
- *errorPtr = [NSError errorWithDomain:FBSnapshotTestControllerErrorDomain
- code:FBSnapshotTestControllerErrorCodeUnknown
- userInfo:nil];
- }
- }
- return image;
- }
- - (BOOL)compareReferenceImage:(UIImage *)referenceImage
- toImage:(UIImage *)image
- tolerance:(CGFloat)tolerance
- error:(NSError **)errorPtr
- {
- BOOL sameImageDimensions = CGSizeEqualToSize(referenceImage.size, image.size);
- if (sameImageDimensions && [referenceImage fb_compareWithImage:image tolerance:tolerance]) {
- return YES;
- }
-
- if (NULL != errorPtr) {
- NSString *errorDescription = sameImageDimensions ? @"Images different" : @"Images different sizes";
- NSString *errorReason = sameImageDimensions ? [NSString stringWithFormat:@"image pixels differed by more than %.2f%% from the reference image", tolerance * 100]
- : [NSString stringWithFormat:@"referenceImage:%@, image:%@", NSStringFromCGSize(referenceImage.size), NSStringFromCGSize(image.size)];
- FBSnapshotTestControllerErrorCode errorCode = sameImageDimensions ? FBSnapshotTestControllerErrorCodeImagesDifferent : FBSnapshotTestControllerErrorCodeImagesDifferentSizes;
-
- *errorPtr = [NSError errorWithDomain:FBSnapshotTestControllerErrorDomain
- code:errorCode
- userInfo:@{
- NSLocalizedDescriptionKey: errorDescription,
- NSLocalizedFailureReasonErrorKey: errorReason,
- FBReferenceImageKey: referenceImage,
- FBCapturedImageKey: image,
- FBDiffedImageKey: [referenceImage fb_diffWithImage:image],
- }];
- }
- return NO;
- }
- - (BOOL)saveFailedReferenceImage:(UIImage *)referenceImage
- testImage:(UIImage *)testImage
- selector:(SEL)selector
- identifier:(NSString *)identifier
- error:(NSError **)errorPtr
- {
- NSData *referencePNGData = UIImagePNGRepresentation(referenceImage);
- NSData *testPNGData = UIImagePNGRepresentation(testImage);
- NSString *referencePath = [self _failedFilePathForSelector:selector
- identifier:identifier
- fileNameType:FBTestSnapshotFileNameTypeFailedReference];
- NSError *creationError = nil;
- BOOL didCreateDir = [_fileManager createDirectoryAtPath:[referencePath stringByDeletingLastPathComponent]
- withIntermediateDirectories:YES
- attributes:nil
- error:&creationError];
- if (!didCreateDir) {
- if (NULL != errorPtr) {
- *errorPtr = creationError;
- }
- return NO;
- }
- if (![referencePNGData writeToFile:referencePath options:NSDataWritingAtomic error:errorPtr]) {
- return NO;
- }
- NSString *testPath = [self _failedFilePathForSelector:selector
- identifier:identifier
- fileNameType:FBTestSnapshotFileNameTypeFailedTest];
- if (![testPNGData writeToFile:testPath options:NSDataWritingAtomic error:errorPtr]) {
- return NO;
- }
- NSString *diffPath = [self _failedFilePathForSelector:selector
- identifier:identifier
- fileNameType:FBTestSnapshotFileNameTypeFailedTestDiff];
- UIImage *diffImage = [referenceImage fb_diffWithImage:testImage];
- NSData *diffImageData = UIImagePNGRepresentation(diffImage);
- if (![diffImageData writeToFile:diffPath options:NSDataWritingAtomic error:errorPtr]) {
- return NO;
- }
- NSLog(@"If you have Kaleidoscope installed you can run this command to see an image diff:\n"
- @"ksdiff \"%@\" \"%@\"", referencePath, testPath);
- return YES;
- }
- #pragma mark - Private API
- - (NSString *)_fileNameForSelector:(SEL)selector
- identifier:(NSString *)identifier
- fileNameType:(FBTestSnapshotFileNameType)fileNameType
- {
- NSString *fileName = nil;
- switch (fileNameType) {
- case FBTestSnapshotFileNameTypeFailedReference:
- fileName = @"reference_";
- break;
- case FBTestSnapshotFileNameTypeFailedTest:
- fileName = @"failed_";
- break;
- case FBTestSnapshotFileNameTypeFailedTestDiff:
- fileName = @"diff_";
- break;
- default:
- fileName = @"";
- break;
- }
- fileName = [fileName stringByAppendingString:NSStringFromSelector(selector)];
- if (0 < identifier.length) {
- fileName = [fileName stringByAppendingFormat:@"_%@", identifier];
- }
-
- if (self.isDeviceAgnostic) {
- fileName = FBDeviceAgnosticNormalizedFileName(fileName);
- }
-
- if ([[UIScreen mainScreen] scale] > 1) {
- fileName = [fileName stringByAppendingFormat:@"@%.fx", [[UIScreen mainScreen] scale]];
- }
- fileName = [fileName stringByAppendingPathExtension:@"png"];
- return fileName;
- }
- - (NSString *)_referenceFilePathForSelector:(SEL)selector
- identifier:(NSString *)identifier
- {
- NSString *fileName = [self _fileNameForSelector:selector
- identifier:identifier
- fileNameType:FBTestSnapshotFileNameTypeReference];
- NSString *filePath = [_referenceImagesDirectory stringByAppendingPathComponent:_testName];
- filePath = [filePath stringByAppendingPathComponent:fileName];
- return filePath;
- }
- - (NSString *)_failedFilePathForSelector:(SEL)selector
- identifier:(NSString *)identifier
- fileNameType:(FBTestSnapshotFileNameType)fileNameType
- {
- NSString *fileName = [self _fileNameForSelector:selector
- identifier:identifier
- fileNameType:fileNameType];
- NSString *folderPath = NSTemporaryDirectory();
- if (getenv("IMAGE_DIFF_DIR")) {
- folderPath = @(getenv("IMAGE_DIFF_DIR"));
- }
- NSString *filePath = [folderPath stringByAppendingPathComponent:_testName];
- filePath = [filePath stringByAppendingPathComponent:fileName];
- return filePath;
- }
- - (BOOL)_performPixelComparisonWithViewOrLayer:(id)viewOrLayer
- selector:(SEL)selector
- identifier:(NSString *)identifier
- tolerance:(CGFloat)tolerance
- error:(NSError **)errorPtr
- {
- UIImage *referenceImage = [self referenceImageForSelector:selector identifier:identifier error:errorPtr];
- if (nil != referenceImage) {
- UIImage *snapshot = [self _imageForViewOrLayer:viewOrLayer];
- BOOL imagesSame = [self compareReferenceImage:referenceImage toImage:snapshot tolerance:tolerance error:errorPtr];
- if (!imagesSame) {
- NSError *saveError = nil;
- if ([self saveFailedReferenceImage:referenceImage testImage:snapshot selector:selector identifier:identifier error:&saveError] == NO) {
- NSLog(@"Error saving test images: %@", saveError);
- }
- }
- return imagesSame;
- }
- return NO;
- }
- - (BOOL)_recordSnapshotOfViewOrLayer:(id)viewOrLayer
- selector:(SEL)selector
- identifier:(NSString *)identifier
- error:(NSError **)errorPtr
- {
- UIImage *snapshot = [self _imageForViewOrLayer:viewOrLayer];
- return [self _saveReferenceImage:snapshot selector:selector identifier:identifier error:errorPtr];
- }
- - (BOOL)_saveReferenceImage:(UIImage *)image
- selector:(SEL)selector
- identifier:(NSString *)identifier
- error:(NSError **)errorPtr
- {
- BOOL didWrite = NO;
- if (nil != image) {
- NSString *filePath = [self _referenceFilePathForSelector:selector identifier:identifier];
- NSData *pngData = UIImagePNGRepresentation(image);
- if (nil != pngData) {
- NSError *creationError = nil;
- BOOL didCreateDir = [_fileManager createDirectoryAtPath:[filePath stringByDeletingLastPathComponent]
- withIntermediateDirectories:YES
- attributes:nil
- error:&creationError];
- if (!didCreateDir) {
- if (NULL != errorPtr) {
- *errorPtr = creationError;
- }
- return NO;
- }
- didWrite = [pngData writeToFile:filePath options:NSDataWritingAtomic error:errorPtr];
- if (didWrite) {
- NSLog(@"Reference image save at: %@", filePath);
- }
- } else {
- if (nil != errorPtr) {
- *errorPtr = [NSError errorWithDomain:FBSnapshotTestControllerErrorDomain
- code:FBSnapshotTestControllerErrorCodePNGCreationFailed
- userInfo:@{
- FBReferenceImageFilePathKey: filePath,
- }];
- }
- }
- }
- return didWrite;
- }
- - (UIImage *)_imageForViewOrLayer:(id)viewOrLayer
- {
- if ([viewOrLayer isKindOfClass:[UIView class]]) {
- if (_usesDrawViewHierarchyInRect) {
- return [UIImage fb_imageForView:viewOrLayer];
- } else {
- return [UIImage fb_imageForViewLayer:viewOrLayer];
- }
- } else if ([viewOrLayer isKindOfClass:[CALayer class]]) {
- return [UIImage fb_imageForLayer:viewOrLayer];
- } else {
- [NSException raise:@"Only UIView and CALayer classes can be snapshotted" format:@"%@", viewOrLayer];
- }
- return nil;
- }
- @end
|