- Release 0.1.1

This commit is contained in:
giuseppenuc committed 2015-07-09 12:15:17 +02:00
1 parent 9203c1c6ae
commit c38e3a1657
55 files changed
+2621 -567

No files matched your search

@@ -0,0 +1,127 @@
/*
* Copyright (c) 2013, 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/FBSnapshotTestCasePlatform.h>
#import <QuartzCore/QuartzCore.h>
#import <UIKit/UIKit.h>
#import <XCTest/XCTest.h>
/**
Similar to our much-loved XCTAssert() macros. Use this to perform your test. No need to write an explanation, though.
@param view The view to snapshot
@param identifier An optional identifier, used if there are multiple snapshot tests in a given -test method.
@param suffixes An NSOrderedSet of strings for the different suffixes
@param tolerance The percentage of pixels that can differ and still count as an 'identical' view
*/
#define FBSnapshotVerifyViewWithOptions(view__, identifier__, suffixes__, tolerance__) \
{ \
NSString *envReferenceImageDirectory = [NSProcessInfo processInfo].environment[@"FB_REFERENCE_IMAGE_DIR"]; \
NSError *error__ = nil; \
BOOL comparisonSuccess__; \
XCTAssertTrue((suffixes__.count > 0), @"Suffixes set cannot be empty %@", suffixes__); \
XCTAssertNotNil(envReferenceImageDirectory, @"Missing value for referenceImagesDirectory - Set FB_REFERENCE_IMAGE_DIR as Environment variable in your scheme.");\
for (NSString *suffix__ in suffixes__) { \
NSString *referenceImagesDirectory__ = [NSString stringWithFormat:@"%@%@", envReferenceImageDirectory, suffix__]; \
comparisonSuccess__ = [self compareSnapshotOfView:(view__) referenceImagesDirectory:referenceImagesDirectory__ identifier:(identifier__) tolerance:(tolerance__) error:&error__]; \
if (comparisonSuccess__ || self.recordMode) break; \
} \
XCTAssertTrue(comparisonSuccess__, @"Snapshot comparison failed: %@", error__); \
XCTAssertFalse(self.recordMode, @"Test ran in record mode. Reference image is now saved. Disable record mode to perform an actual snapshot comparison!"); \
}
#define FBSnapshotVerifyView(view__, identifier__) \
{ \
FBSnapshotVerifyViewWithOptions(view__, identifier__, FBSnapshotTestCaseDefaultSuffixes(), 0); \
}
/**
Similar to our much-loved XCTAssert() macros. Use this to perform your test. No need to write an explanation, though.
@param layer The layer to snapshot
@param identifier An optional identifier, used is there are multiple snapshot tests in a given -test method.
@param suffixes An NSOrderedSet of strings for the different suffixes
@param tolerance The percentage of pixels that can differ and still count as an 'identical' layer
*/
#define FBSnapshotVerifyLayerWithOptions(layer__, identifier__, suffixes__, tolerance__) \
{ \
NSString *envReferenceImageDirectory = [NSProcessInfo processInfo].environment[@"FB_REFERENCE_IMAGE_DIR"]; \
NSError *error__ = nil; \
BOOL comparisonSuccess__; \
XCTAssertTrue((suffixes__.count > 0), @"Suffixes set cannot be empty %@", suffixes__); \
XCTAssertNotNil(envReferenceImageDirectory, @"Missing value for referenceImagesDirectory - Set FB_REFERENCE_IMAGE_DIR as Environment variable in your scheme.");\
for (NSString *suffix__ in suffixes__) { \
NSString *referenceImagesDirectory__ = [NSString stringWithFormat:@"%@%@", envReferenceImageDirectory, suffix__]; \
comparisonSuccess__ = [self compareSnapshotOfLayer:(layer__) referenceImagesDirectory:referenceImagesDirectory__ identifier:(identifier__) tolerance:(tolerance__) error:&error__]; \
if (comparisonSuccess__ || self.recordMode) break; \
} \
XCTAssertTrue(comparisonSuccess__, @"Snapshot comparison failed: %@", error__); \
XCTAssertFalse(self.recordMode, @"Test ran in record mode. Reference image is now saved. Disable record mode to perform an actual snapshot comparison!"); \
}
#define FBSnapshotVerifyLayer(layer__, identifier__) \
{ \
FBSnapshotVerifyLayerWithOptions(layer__, identifier__, FBSnapshotTestCaseDefaultSuffixes(), 0); \
}
/**
The base class of view snapshotting tests. If you have small UI component, it's often easier to configure it in a test
and compare an image of the view to a reference image that write lots of complex layout-code tests.
In order to flip the tests in your subclass to record the reference images set @c recordMode to @c YES.
For example:
@code
- (void)setUp
{
[super setUp];
self.recordMode = YES;
}
@endcode
*/
@interface FBSnapshotTestCase : XCTestCase
/**
When YES, the test macros will save reference images, rather than performing an actual test.
*/
@property (readwrite, nonatomic, assign) BOOL recordMode;
/**
Performs the comparison or records a snapshot of the layer if recordMode is YES.
@param layer The Layer to snapshot
@param referenceImagesDirectory The directory in which reference images are stored.
@param identifier An optional identifier, used if there are multiple snapshot tests in a given -test method.
@param tolerance The percentage difference to still count as identical - 0 mean pixel perfect, 1 means I don't care
@param errorPtr An error to log in an XCTAssert() macro if the method fails (missing reference image, images differ, etc).
@returns YES if the comparison (or saving of the reference image) succeeded.
*/
- (BOOL)compareSnapshotOfLayer:(CALayer *)layer
referenceImagesDirectory:(NSString *)referenceImagesDirectory
identifier:(NSString *)identifier
tolerance:(CGFloat)tolerance
error:(NSError **)errorPtr;
/**
Performs the comparison or records a snapshot of the view if recordMode is YES.
@param view The view to snapshot
@param referenceImagesDirectory The directory in which reference images are stored.
@param identifier An optional identifier, used if there are multiple snapshot tests in a given -test method.
@param tolerance The percentage difference to still count as identical - 0 mean pixel perfect, 1 means I don't care
@param errorPtr An error to log in an XCTAssert() macro if the method fails (missing reference image, images differ, etc).
@returns YES if the comparison (or saving of the reference image) succeeded.
*/
- (BOOL)compareSnapshotOfView:(UIView *)view
referenceImagesDirectory:(NSString *)referenceImagesDirectory
identifier:(NSString *)identifier
tolerance:(CGFloat)tolerance
error:(NSError **)errorPtr;
@end
@@ -0,0 +1,86 @@
/*
* Copyright (c) 2013, 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.h"
#import "FBSnapshotTestController.h"
@implementation FBSnapshotTestCase
{
FBSnapshotTestController *_snapshotController;
}
- (void)setUp
{
[super setUp];
_snapshotController = [[FBSnapshotTestController alloc] initWithTestName:NSStringFromClass([self class])];
}
- (void)tearDown
{
_snapshotController = nil;
[super tearDown];
}
- (BOOL)recordMode
{
return _snapshotController.recordMode;
}
- (void)setRecordMode:(BOOL)recordMode
{
NSAssert1(_snapshotController, @"%s cannot be called before [super setUp]", __FUNCTION__);
_snapshotController.recordMode = recordMode;
}
- (BOOL)compareSnapshotOfLayer:(CALayer *)layer
referenceImagesDirectory:(NSString *)referenceImagesDirectory
identifier:(NSString *)identifier
tolerance:(CGFloat)tolerance
error:(NSError **)errorPtr
{
return [self _compareSnapshotOfViewOrLayer:layer
referenceImagesDirectory:referenceImagesDirectory
identifier:identifier
tolerance:tolerance
error:errorPtr];
}
- (BOOL)compareSnapshotOfView:(UIView *)view
referenceImagesDirectory:(NSString *)referenceImagesDirectory
identifier:(NSString *)identifier
tolerance:(CGFloat)tolerance
error:(NSError **)errorPtr
{
return [self _compareSnapshotOfViewOrLayer:view
referenceImagesDirectory:referenceImagesDirectory
identifier:identifier
tolerance:tolerance
error:errorPtr];
}
#pragma mark -
#pragma mark Private API
- (BOOL)_compareSnapshotOfViewOrLayer:(id)viewOrLayer
referenceImagesDirectory:(NSString *)referenceImagesDirectory
identifier:(NSString *)identifier
tolerance:(CGFloat)tolerance
error:(NSError **)errorPtr
{
_snapshotController.referenceImagesDirectory = referenceImagesDirectory;
return [_snapshotController compareSnapshotOfViewOrLayer:viewOrLayer
selector:self.invocation.selector
identifier:identifier
tolerance:tolerance
error:errorPtr];
}
@end
@@ -0,0 +1,36 @@
/*
* 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 <Foundation/Foundation.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
Returns a Boolean value that indicates whether the snapshot test is running in 64Bit.
This method is a convenience for creating the suffixes set based on the architecture
that the test is running.
@returns @c YES if the test is running in 64bit, otherwise @c NO.
*/
BOOL FBSnapshotTestCaseIs64Bit(void);
/**
Returns a default set of strings that is used to append a suffix based on the architectures.
@warning Do not modify this function, you can create your own and use it with @c FBSnapshotVerifyViewWithOptions()
@returns An @c NSOrderedSet object containing strings that are appended to the reference images directory.
*/
NSOrderedSet *FBSnapshotTestCaseDefaultSuffixes(void);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,31 @@
/*
* 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/FBSnapshotTestCasePlatform.h>
BOOL FBSnapshotTestCaseIs64Bit(void)
{
#if __LP64__
return YES;
#else
return NO;
#endif
}
NSOrderedSet *FBSnapshotTestCaseDefaultSuffixes(void)
{
NSMutableOrderedSet *suffixesSet = [[NSMutableOrderedSet alloc] init];
[suffixesSet addObject:@"_32"];
[suffixesSet addObject:@"_64"];
if (FBSnapshotTestCaseIs64Bit()) {
return [suffixesSet reversedOrderedSet];
}
return [suffixesSet copy];
}
@@ -0,0 +1,154 @@
/*
* Copyright (c) 2013, 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 <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
typedef NS_ENUM(NSInteger, FBSnapshotTestControllerErrorCode) {
FBSnapshotTestControllerErrorCodeUnknown,
FBSnapshotTestControllerErrorCodeNeedsRecord,
FBSnapshotTestControllerErrorCodePNGCreationFailed,
FBSnapshotTestControllerErrorCodeImagesDifferentSizes,
FBSnapshotTestControllerErrorCodeImagesDifferent,
};
/**
Errors returned by the methods of FBSnapshotTestController use this domain.
*/
extern NSString *const FBSnapshotTestControllerErrorDomain;
/**
Errors returned by the methods of FBSnapshotTestController sometimes contain this key in the `userInfo` dictionary.
*/
extern NSString *const FBReferenceImageFilePathKey;
/**
Provides the heavy-lifting for FBSnapshotTestCase. It loads and saves images, along with performing the actual pixel-
by-pixel comparison of images.
Instances are initialized with the test class, and directories to read and write to.
*/
@interface FBSnapshotTestController : NSObject
/**
Record snapshots.
**/
@property (readwrite, nonatomic, assign) BOOL recordMode;
/**
@param testClass The subclass of FBSnapshotTestCase that is using this controller.
@returns An instance of FBSnapshotTestController.
*/
- (instancetype)initWithTestClass:(Class)testClass;
/**
Designated initializer.
@param testName The name of the tests.
@returns An instance of FBSnapshotTestController.
*/
- (instancetype)initWithTestName:(NSString *)testName;
/**
Performs the comparison of the layer.
@param layer The Layer to snapshot.
@param selector The test method being run.
@param identifier An optional identifier, used is there are muliptle snapshot tests in a given -test method.
@param error An error to log in an XCTAssert() macro if the method fails (missing reference image, images differ, etc).
@returns YES if the comparison (or saving of the reference image) succeeded.
*/
- (BOOL)compareSnapshotOfLayer:(CALayer *)layer
selector:(SEL)selector
identifier:(NSString *)identifier
error:(NSError **)errorPtr;
/**
Performs the comparison of the view.
@param view The view to snapshot.
@param selector The test method being run.
@param identifier An optional identifier, used is there are muliptle snapshot tests in a given -test method.
@param error An error to log in an XCTAssert() macro if the method fails (missing reference image, images differ, etc).
@returns YES if the comparison (or saving of the reference image) succeeded.
*/
- (BOOL)compareSnapshotOfView:(UIView *)view
selector:(SEL)selector
identifier:(NSString *)identifier
error:(NSError **)errorPtr;
/**
Performs the comparison of a view or layer.
@param view The view or layer to snapshot.
@param selector The test method being run.
@param identifier An optional identifier, used is there are muliptle snapshot tests in a given -test method.
@param tolerance The percentage of pixels that can differ and still be considered 'identical'
@param error An error to log in an XCTAssert() macro if the method fails (missing reference image, images differ, etc).
@returns YES if the comparison (or saving of the reference image) succeeded.
*/
- (BOOL)compareSnapshotOfViewOrLayer:(id)viewOrLayer
selector:(SEL)selector
identifier:(NSString *)identifier
tolerance:(CGFloat)tolerance
error:(NSError **)errorPtr;
/**
The directory in which referfence images are stored.
*/
@property (readwrite, nonatomic, copy) NSString *referenceImagesDirectory;
/**
Loads a reference image.
@param selector The test method being run.
@param identifier The optional identifier, used when multiple images are tested in a single -test method.
@param errorPtr An error, if this methods returns nil, the error will be something useful.
@returns An image.
*/
- (UIImage *)referenceImageForSelector:(SEL)selector
identifier:(NSString *)identifier
error:(NSError **)errorPtr;
/**
Saves a reference image.
@param selector The test method being run.
@param identifier The optional identifier, used when multiple images are tested in a single -test method.
@param errorPtr An error, if this methods returns NO, the error will be something useful.
@returns An image.
*/
- (BOOL)saveReferenceImage:(UIImage *)image
selector:(SEL)selector
identifier:(NSString *)identifier
error:(NSError **)errorPtr;
/**
Performs a pixel-by-pixel comparison of the two images with an allowable margin of error.
@param referenceImage The reference (correct) image.
@param image The image to test against the reference.
@param tolerance The percentage of pixels that can differ and still be considered 'identical'
@param errorPtr An error that indicates why the comparison failed if it does.
@returns YES if the comparison succeeded and the images are the same(ish).
*/
- (BOOL)compareReferenceImage:(UIImage *)referenceImage
toImage:(UIImage *)image
tolerance:(CGFloat)tolerance
error:(NSError **)errorPtr;
/**
Saves the reference image and the test image to `failedOutputDirectory`.
@param referenceImage The reference (correct) image.
@param testImage The image to test against the reference.
@param selector The test method being run.
@param identifier The optional identifier, used when multiple images are tested in a single -test method.
@param errorPtr An error that indicates why the comparison failed if it does.
@returns YES if the save succeeded.
*/
- (BOOL)saveFailedReferenceImage:(UIImage *)referenceImage
testImage:(UIImage *)testImage
selector:(SEL)selector
identifier:(NSString *)identifier
error:(NSError **)errorPtr;
@end
@@ -0,0 +1,384 @@
/*
* Copyright (c) 2013, 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 "FBSnapshotTestController.h"
#import "UIImage+Compare.h"
#import "UIImage+Diff.h"
#import <UIKit/UIKit.h>
NSString *const FBSnapshotTestControllerErrorDomain = @"FBSnapshotTestControllerErrorDomain";
NSString *const FBReferenceImageFilePathKey = @"FBReferenceImageFilePathKey";
@implementation FBSnapshotTestController
{
NSString *_testName;
NSFileManager *_fileManager;
}
#pragma mark -
#pragma mark Lifecycle
- (instancetype)initWithTestClass:(Class)testClass;
{
return [self initWithTestName:NSStringFromClass(testClass)];
}
- (instancetype)initWithTestName:(NSString *)testName
{
if ((self = [super init])) {
_testName = [testName copy];
_fileManager = [[NSFileManager alloc] init];
}
return self;
}
#pragma mark -
#pragma mark Properties
- (NSString *)description
{
return [NSString stringWithFormat:@"%@ %@", [super description], _referenceImagesDirectory];
}
#pragma mark -
#pragma mark Public API
- (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)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;
}
- (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 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;
}
- (BOOL)compareReferenceImage:(UIImage *)referenceImage
toImage:(UIImage *)image
tolerance:(CGFloat)tolerance
error:(NSError **)errorPtr {
if (CGSizeEqualToSize(referenceImage.size, image.size)) {
BOOL imagesEqual = [referenceImage compareWithImage:image tolerance:tolerance];
if (NULL != errorPtr) {
*errorPtr = [NSError errorWithDomain:FBSnapshotTestControllerErrorDomain
code:FBSnapshotTestControllerErrorCodeImagesDifferent
userInfo:@{
NSLocalizedDescriptionKey: @"Images different",
}];
}
return imagesEqual;
}
if (NULL != errorPtr) {
*errorPtr = [NSError errorWithDomain:FBSnapshotTestControllerErrorDomain
code:FBSnapshotTestControllerErrorCodeImagesDifferentSizes
userInfo:@{
NSLocalizedDescriptionKey: @"Images different sizes",
NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:@"referenceImage:%@, image:%@",
NSStringFromCGSize(referenceImage.size),
NSStringFromCGSize(image.size)],
}];
}
return NO;
}
#pragma mark -
#pragma mark Private API
typedef NS_ENUM(NSUInteger, FBTestSnapshotFileNameType) {
FBTestSnapshotFileNameTypeReference,
FBTestSnapshotFileNameTypeFailedReference,
FBTestSnapshotFileNameTypeFailedTest,
FBTestSnapshotFileNameTypeFailedTestDiff,
};
- (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 ([[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)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];
}
}
#pragma mark -
#pragma mark Private API
- (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 _snapshotViewOrLayer:viewOrLayer];
BOOL imagesSame = [self compareReferenceImage:referenceImage toImage:snapshot tolerance:tolerance error:errorPtr];
if (!imagesSame) {
[self saveFailedReferenceImage:referenceImage
testImage:snapshot
selector:selector
identifier:identifier
error:errorPtr];
}
return imagesSame;
}
return NO;
}
- (BOOL)_recordSnapshotOfViewOrLayer:(id)viewOrLayer
selector:(SEL)selector
identifier:(NSString *)identifier
error:(NSError **)errorPtr
{
UIImage *snapshot = [self _snapshotViewOrLayer:viewOrLayer];
return [self saveReferenceImage:snapshot selector:selector identifier:identifier error:errorPtr];
}
- (UIImage *)_snapshotViewOrLayer:(id)viewOrLayer
{
CALayer *layer = nil;
if ([viewOrLayer isKindOfClass:[UIView class]]) {
return [self _renderView:viewOrLayer];
} else if ([viewOrLayer isKindOfClass:[CALayer class]]) {
layer = (CALayer *)viewOrLayer;
[layer layoutIfNeeded];
return [self _renderLayer:layer];
} else {
[NSException raise:@"Only UIView and CALayer classes can be snapshotted" format:@"%@", viewOrLayer];
}
return nil;
}
- (UIImage *)_renderLayer:(CALayer *)layer
{
CGRect bounds = layer.bounds;
NSAssert1(CGRectGetWidth(bounds), @"Zero width for layer %@", layer);
NSAssert1(CGRectGetHeight(bounds), @"Zero height for layer %@", layer);
UIGraphicsBeginImageContextWithOptions(bounds.size, NO, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
NSAssert1(context, @"Could not generate context for layer %@", layer);
CGContextSaveGState(context);
{
[layer renderInContext:context];
}
CGContextRestoreGState(context);
UIImage *snapshot = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return snapshot;
}
- (UIImage *)_renderView:(UIView *)view
{
[view layoutIfNeeded];
return [self _renderLayer:view.layer];
}
@end
@@ -0,0 +1,58 @@
/*
* 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.
*
*/
public extension FBSnapshotTestCase {
public func FBSnapshotVerifyView(view: UIView, identifier: String = "", suffixes: NSOrderedSet = FBSnapshotTestCaseDefaultSuffixes(), file: String = __FILE__, line: UInt = __LINE__) {
let envReferenceImageDirectory = NSProcessInfo.processInfo().environment["FB_REFERENCE_IMAGE_DIR"] as? String
var error: NSError?
if let envReferenceImageDirectory = envReferenceImageDirectory {
for suffix in suffixes {
let referenceImagesDirectory = "\(envReferenceImageDirectory)\(suffix)"
let comparisonSuccess = compareSnapshotOfView(view, referenceImagesDirectory: referenceImagesDirectory, identifier: identifier, tolerance: 0, error: &error)
if comparisonSuccess || recordMode {
break
}
assert(comparisonSuccess, message: "Snapshot comparison failed: \(error)", file: file, line: line)
assert(recordMode == false, message: "Test ran in record mode. Reference image is now saved. Disable record mode to perform an actual snapshot comparison!", file: file, line: line)
}
} else {
assert(false, message: "Missing value for referenceImagesDirectory - Set FB_REFERENCE_IMAGE_DIR as Environment variable in your scheme.", file: file, line: line)
}
}
public func FBSnapshotVerifyLayer(layer: CALayer, identifier: String = "", suffixes: NSOrderedSet = FBSnapshotTestCaseDefaultSuffixes(), file: String = __FILE__, line: UInt = __LINE__) {
let envReferenceImageDirectory = NSProcessInfo.processInfo().environment["FB_REFERENCE_IMAGE_DIR"] as? String
var error: NSError?
var comparisonSuccess = false
if let envReferenceImageDirectory = envReferenceImageDirectory {
for suffix in suffixes {
let referenceImagesDirectory = "\(envReferenceImageDirectory)\(suffix)"
comparisonSuccess = compareSnapshotOfLayer(layer, referenceImagesDirectory: referenceImagesDirectory, identifier: identifier, tolerance: 0, error: &error)
if comparisonSuccess || recordMode {
break
}
assert(comparisonSuccess, message: "Snapshot comparison failed: \(error)", file: file, line: line)
assert(recordMode == false, message: "Test ran in record mode. Reference image is now saved. Disable record mode to perform an actual snapshot comparison!", file: file, line: line)
}
} else {
XCTFail("Missing value for referenceImagesDirectory - Set FB_REFERENCE_IMAGE_DIR as Environment variable in your scheme.")
}
}
func assert(assertion: Bool, message: String, file: String, line: UInt) {
if !assertion {
XCTFail(message, file: file, line: line)
}
}
}
@@ -0,0 +1,37 @@
//
// Created by Gabriel Handford on 3/1/09.
// Copyright 2009-2013. All rights reserved.
// Created by John Boiles on 10/20/11.
// Copyright (c) 2011. All rights reserved
// Modified by Felix Schulze on 2/11/13.
// Copyright 2013. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
//
#import <UIKit/UIKit.h>
@interface UIImage (Compare)
- (BOOL)compareWithImage:(UIImage *)image tolerance:(CGFloat)tolerance;
@end
@@ -0,0 +1,134 @@
//
// Created by Gabriel Handford on 3/1/09.
// Copyright 2009-2013. All rights reserved.
// Created by John Boiles on 10/20/11.
// Copyright (c) 2011. All rights reserved
// Modified by Felix Schulze on 2/11/13.
// Copyright 2013. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
//
#import "UIImage+Compare.h"
// This makes debugging much more fun
typedef union {
uint32_t raw;
unsigned char bytes[4];
struct {
char red;
char green;
char blue;
char alpha;
} __attribute__ ((packed)) pixels;
} FBComparePixel;
@implementation UIImage (Compare)
- (BOOL)compareWithImage:(UIImage *)image tolerance:(CGFloat)tolerance
{
NSAssert(CGSizeEqualToSize(self.size, image.size), @"Images must be same size.");
CGSize referenceImageSize = CGSizeMake(CGImageGetWidth(self.CGImage), CGImageGetHeight(self.CGImage));
CGSize imageSize = CGSizeMake(CGImageGetWidth(image.CGImage), CGImageGetHeight(image.CGImage));
// The images have the equal size, so we could use the smallest amount of bytes because of byte padding
size_t minBytesPerRow = MIN(CGImageGetBytesPerRow(self.CGImage), CGImageGetBytesPerRow(image.CGImage));
size_t referenceImageSizeBytes = referenceImageSize.height * minBytesPerRow;
void *referenceImagePixels = calloc(1, referenceImageSizeBytes);
void *imagePixels = calloc(1, referenceImageSizeBytes);
if (!referenceImagePixels || !imagePixels) {
free(referenceImagePixels);
free(imagePixels);
return NO;
}
CGContextRef referenceImageContext = CGBitmapContextCreate(referenceImagePixels,
referenceImageSize.width,
referenceImageSize.height,
CGImageGetBitsPerComponent(self.CGImage),
minBytesPerRow,
CGImageGetColorSpace(self.CGImage),
(CGBitmapInfo)kCGImageAlphaPremultipliedLast
);
CGContextRef imageContext = CGBitmapContextCreate(imagePixels,
imageSize.width,
imageSize.height,
CGImageGetBitsPerComponent(image.CGImage),
minBytesPerRow,
CGImageGetColorSpace(image.CGImage),
(CGBitmapInfo)kCGImageAlphaPremultipliedLast
);
if (!referenceImageContext || !imageContext) {
CGContextRelease(referenceImageContext);
CGContextRelease(imageContext);
free(referenceImagePixels);
free(imagePixels);
return NO;
}
CGContextDrawImage(referenceImageContext, CGRectMake(0, 0, referenceImageSize.width, referenceImageSize.height), self.CGImage);
CGContextDrawImage(imageContext, CGRectMake(0, 0, imageSize.width, imageSize.height), image.CGImage);
CGContextRelease(referenceImageContext);
CGContextRelease(imageContext);
BOOL imageEqual = YES;
// Do a fast compare if we can
if (tolerance == 0) {
imageEqual = (memcmp(referenceImagePixels, imagePixels, referenceImageSizeBytes) == 0);
} else {
// Go through each pixel in turn and see if it is different
const NSInteger pixelCount = self.size.width * self.size.height;
FBComparePixel *p1 = referenceImagePixels;
FBComparePixel *p2 = imagePixels;
NSInteger numDiffPixels = 0;
for (int n = 0; n < pixelCount; ++n) {
// If this pixel is different, increment the pixel diff count and see
// if we have hit our limit.
if (p1->raw != p2->raw) {
numDiffPixels ++;
CGFloat percent = (CGFloat)numDiffPixels / pixelCount;
if (percent > tolerance) {
imageEqual = NO;
break;
}
}
p1++;
p2++;
}
}
free(referenceImagePixels);
free(imagePixels);
return imageEqual;
}
@end
@@ -0,0 +1,37 @@
//
// Created by Gabriel Handford on 3/1/09.
// Copyright 2009-2013. All rights reserved.
// Created by John Boiles on 10/20/11.
// Copyright (c) 2011. All rights reserved
// Modified by Felix Schulze on 2/11/13.
// Copyright 2013. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
//
#import <UIKit/UIKit.h>
@interface UIImage (Diff)
- (UIImage *)diffWithImage:(UIImage *)image;
@end
@@ -0,0 +1,56 @@
//
// Created by Gabriel Handford on 3/1/09.
// Copyright 2009-2013. All rights reserved.
// Created by John Boiles on 10/20/11.
// Copyright (c) 2011. All rights reserved
// Modified by Felix Schulze on 2/11/13.
// Copyright 2013. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
//
#import "UIImage+Diff.h"
@implementation UIImage (Diff)
- (UIImage *)diffWithImage:(UIImage *)image
{
if (!image) {
return nil;
}
CGSize imageSize = CGSizeMake(MAX(self.size.width, image.size.width), MAX(self.size.height, image.size.height));
UIGraphicsBeginImageContextWithOptions(imageSize, YES, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
[self drawInRect:CGRectMake(0, 0, self.size.width, self.size.height)];
CGContextSetAlpha(context, 0.5);
CGContextBeginTransparencyLayer(context, NULL);
[image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)];
CGContextSetBlendMode(context, kCGBlendModeDifference);
CGContextSetFillColorWithColor(context,[UIColor whiteColor].CGColor);
CGContextFillRect(context, CGRectMake(0, 0, self.size.width, self.size.height));
CGContextEndTransparencyLayer(context);
UIImage *returnImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return returnImage;
}
@end