From 71e9ba921923f1822583d96125622c22247b1b4c Mon Sep 17 00:00:00 2001 From: DMITRY KULAKOV Date: Tue, 29 Oct 2019 18:56:21 +0300 Subject: [PATCH] Implement different pixels highlighting --- .../Categories/UIImage+Compare.h | 2 +- .../Categories/UIImage+Compare.m | 28 ++++++++++++---- FBSnapshotTestCase/Categories/UIImage+Diff.h | 2 +- FBSnapshotTestCase/Categories/UIImage+Diff.m | 16 +++++++++- FBSnapshotTestCase/FBSnapshotTestCase.h | 5 +++ FBSnapshotTestCase/FBSnapshotTestCase.m | 11 +++++++ FBSnapshotTestCase/FBSnapshotTestController.h | 17 ++++++++-- FBSnapshotTestCase/FBSnapshotTestController.m | 32 +++++++++++++++---- .../FBSnapshotControllerTests.m | 21 ++++++++---- 9 files changed, 108 insertions(+), 26 deletions(-) diff --git a/FBSnapshotTestCase/Categories/UIImage+Compare.h b/FBSnapshotTestCase/Categories/UIImage+Compare.h index fb7d7b0..e932dde 100644 --- a/FBSnapshotTestCase/Categories/UIImage+Compare.h +++ b/FBSnapshotTestCase/Categories/UIImage+Compare.h @@ -42,7 +42,7 @@ NS_ASSUME_NONNULL_BEGIN @param overallTolerance The overall percentage of pixels that are allowed to change from the pixels in the reference image. @return A BOOL which represents if the image is the same or not. */ -- (BOOL)fb_compareWithImage:(UIImage *)image perPixelTolerance:(CGFloat)perPixelTolerance overallTolerance:(CGFloat)overallTolerance; +- (BOOL)fb_compareWithImage:(UIImage *)image perPixelTolerance:(CGFloat)perPixelTolerance overallTolerance:(CGFloat)overallTolerance differentPixels:(NSMutableArray **)differentPixels; @end diff --git a/FBSnapshotTestCase/Categories/UIImage+Compare.m b/FBSnapshotTestCase/Categories/UIImage+Compare.m index a52c965..df38be4 100644 --- a/FBSnapshotTestCase/Categories/UIImage+Compare.m +++ b/FBSnapshotTestCase/Categories/UIImage+Compare.m @@ -44,7 +44,7 @@ @implementation UIImage (Compare) -- (BOOL)fb_compareWithImage:(UIImage *)image perPixelTolerance:(CGFloat)perPixelTolerance overallTolerance:(CGFloat)overallTolerance +- (BOOL)fb_compareWithImage:(UIImage *)image perPixelTolerance:(CGFloat)perPixelTolerance overallTolerance:(CGFloat)overallTolerance differentPixels:(NSMutableArray **)differentPixels { CGSize referenceImageSize = CGSizeMake(CGImageGetWidth(self.CGImage), CGImageGetHeight(self.CGImage)); CGSize imageSize = CGSizeMake(CGImageGetWidth(image.CGImage), CGImageGetHeight(image.CGImage)); @@ -96,7 +96,7 @@ - (BOOL)fb_compareWithImage:(UIImage *)image perPixelTolerance:(CGFloat)perPixel FBComparePixel *p2 = imagePixels; // Do a fast compare if we can - if (overallTolerance == 0 && perPixelTolerance == 0) { + if (overallTolerance == 0 && perPixelTolerance == 0 && !*differentPixels) { imageEqual = (memcmp(referenceImagePixels, imagePixels, referenceImageSizeBytes) == 0); } else { const NSUInteger pixelCount = referenceImageSize.width * referenceImageSize.height; @@ -105,7 +105,8 @@ - (BOOL)fb_compareWithImage:(UIImage *)image perPixelTolerance:(CGFloat)perPixel overallTolerance:overallTolerance pixelCount:pixelCount referencePixels:p1 - imagePixels:p2]; + imagePixels:p2 + differentPixels:differentPixels]; } free(referenceImagePixels); @@ -155,6 +156,7 @@ - (BOOL)_compareAllPixelsWithPerPixelTolerance:(CGFloat)perPixelTolerance pixelCount:(NSUInteger)pixelCount referencePixels:(FBComparePixel *)referencePixel imagePixels:(FBComparePixel *)imagePixel + differentPixels:(NSMutableArray **)differentPixels { NSUInteger numDiffPixels = 0; for (NSUInteger n = 0; n < pixelCount; ++n) { @@ -162,17 +164,29 @@ - (BOOL)_compareAllPixelsWithPerPixelTolerance:(CGFloat)perPixelTolerance // if we have hit our limit. BOOL isIdenticalPixel = [self _comparePixelWithPerPixelTolerance:perPixelTolerance referencePixel:referencePixel imagePixel:imagePixel]; if (!isIdenticalPixel) { + [*differentPixels addObject:[NSNumber numberWithInteger:n]]; + numDiffPixels++; - - CGFloat percent = (CGFloat)numDiffPixels / (CGFloat)pixelCount; - if (percent > overallTolerance) { - return NO; + + if (!*differentPixels) { + CGFloat percent = (CGFloat)numDiffPixels / (CGFloat)pixelCount; + if (percent > overallTolerance) { + return NO; + } } } referencePixel++; imagePixel++; } + + if (*differentPixels) { + CGFloat percent = (CGFloat)numDiffPixels / (CGFloat)pixelCount; + if (percent > overallTolerance) { + return NO; + } + } + return YES; } diff --git a/FBSnapshotTestCase/Categories/UIImage+Diff.h b/FBSnapshotTestCase/Categories/UIImage+Diff.h index 2a97e11..70dda34 100644 --- a/FBSnapshotTestCase/Categories/UIImage+Diff.h +++ b/FBSnapshotTestCase/Categories/UIImage+Diff.h @@ -34,7 +34,7 @@ NS_ASSUME_NONNULL_BEGIN @interface UIImage (Diff) -- (UIImage *)fb_diffWithImage:(UIImage *)image; +- (UIImage *)fb_diffWithImage:(UIImage *)image differentPixels:(NSArray *)differentPixels; @end diff --git a/FBSnapshotTestCase/Categories/UIImage+Diff.m b/FBSnapshotTestCase/Categories/UIImage+Diff.m index 7cb1d40..5921a23 100644 --- a/FBSnapshotTestCase/Categories/UIImage+Diff.m +++ b/FBSnapshotTestCase/Categories/UIImage+Diff.m @@ -32,7 +32,7 @@ @implementation UIImage (Diff) -- (UIImage *)fb_diffWithImage:(UIImage *)image +- (UIImage *)fb_diffWithImage:(UIImage *)image differentPixels:(NSArray *)differentPixels { if (!image) { return nil; @@ -48,6 +48,20 @@ - (UIImage *)fb_diffWithImage:(UIImage *)image CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor); CGContextFillRect(context, CGRectMake(0, 0, self.size.width, self.size.height)); CGContextEndTransparencyLayer(context); + + if (differentPixels) { + CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor); + + CGFloat screenScale = [[UIScreen mainScreen] scale]; + const NSUInteger pixelCount = differentPixels.count; + for (NSUInteger n = 0; n < pixelCount; ++n) { + int pixelNumber = [differentPixels[n] intValue] / screenScale; + CGFloat x = (pixelNumber % ((int) roundf(self.size.width))); + CGFloat y = ((pixelNumber - x) / (self.size.width * screenScale)); + + CGContextFillRect(context, CGRectMake(x, y, 1, 1)); + } + } UIImage *returnImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return returnImage; diff --git a/FBSnapshotTestCase/FBSnapshotTestCase.h b/FBSnapshotTestCase/FBSnapshotTestCase.h index 365b741..bbcaf53 100644 --- a/FBSnapshotTestCase/FBSnapshotTestCase.h +++ b/FBSnapshotTestCase/FBSnapshotTestCase.h @@ -136,6 +136,11 @@ NS_ASSUME_NONNULL_BEGIN */ @property (readwrite, nonatomic, assign) BOOL recordMode; +/** + When YES, will cause hightlighting different fixels on test failure. + */ +@property (readwrite, nonatomic, assign) BOOL highlightDifferentPixels; + /** When set, allows fine-grained control over what you want the file names to include. diff --git a/FBSnapshotTestCase/FBSnapshotTestCase.m b/FBSnapshotTestCase/FBSnapshotTestCase.m index dc10fee..a5f8c8e 100644 --- a/FBSnapshotTestCase/FBSnapshotTestCase.m +++ b/FBSnapshotTestCase/FBSnapshotTestCase.m @@ -39,6 +39,17 @@ - (void)setRecordMode:(BOOL)recordMode _snapshotController.recordMode = recordMode; } +- (BOOL)highlightDifferentPixels +{ + return _snapshotController.highlightDifferentPixels; +} + +- (void)setHighlightDifferentPixels:(BOOL)highlightDifferentPixels +{ + NSAssert1(_snapshotController, @"%s cannot be called before [super setUp]", __FUNCTION__); + _snapshotController.highlightDifferentPixels = highlightDifferentPixels; +} + - (FBSnapshotTestCaseFileNameIncludeOption)fileNameOptions { return _snapshotController.fileNameOptions; diff --git a/FBSnapshotTestCase/FBSnapshotTestController.h b/FBSnapshotTestCase/FBSnapshotTestController.h index 556361f..79a5da3 100644 --- a/FBSnapshotTestCase/FBSnapshotTestController.h +++ b/FBSnapshotTestCase/FBSnapshotTestController.h @@ -59,6 +59,11 @@ extern NSString *const FBDiffedImageKey; */ @property (readwrite, nonatomic, assign) BOOL recordMode; +/** + Highlight Different Pixels. + */ +@property (readwrite, nonatomic, assign) BOOL highlightDifferentPixels; + /** When set, allows fine-grained control over what you want the file names to include. @@ -173,12 +178,15 @@ extern NSString *const FBDiffedImageKey; @param image The image to test against the reference. @param overallTolerance 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. + @param differentPixels An array of different pixel numbers. @returns YES if the comparison succeeded and the images are the same(ish). */ - (BOOL)compareReferenceImage:(UIImage *)referenceImage toImage:(UIImage *)image overallTolerance:(CGFloat)overallTolerance - error:(NSError **)errorPtr; + error:(NSError **)errorPtr + differentPixels:(NSMutableArray **)differentPixels +; /** Performs a pixel-by-pixel comparison of the two images with an allowable margin of error. @@ -187,13 +195,15 @@ extern NSString *const FBDiffedImageKey; @param perPixelTolerance The percentage a given pixel's R,G,B and A components can differ and still be considered 'identical'. @param overallTolerance 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. + @param differentPixels An array of different pixel numbers. @returns YES if the comparison succeeded and the images are the same(ish). */ - (BOOL)compareReferenceImage:(UIImage *)referenceImage toImage:(UIImage *)image perPixelTolerance:(CGFloat)perPixelTolerance overallTolerance:(CGFloat)overallTolerance - error:(NSError **)errorPtr; + error:(NSError **)errorPtr + differentPixels:(NSMutableArray **)differentPixels; /** Saves the reference image and the test image to `failedOutputDirectory`. @@ -208,7 +218,8 @@ extern NSString *const FBDiffedImageKey; testImage:(UIImage *)testImage selector:(SEL)selector identifier:(nullable NSString *)identifier - error:(NSError **)errorPtr; + error:(NSError **)errorPtr + differentPixels:(NSArray *)differentPixels; @end NS_ASSUME_NONNULL_END diff --git a/FBSnapshotTestCase/FBSnapshotTestController.m b/FBSnapshotTestCase/FBSnapshotTestController.m index f4db6e3..1c86ae2 100644 --- a/FBSnapshotTestCase/FBSnapshotTestController.m +++ b/FBSnapshotTestCase/FBSnapshotTestController.m @@ -139,12 +139,14 @@ - (BOOL)compareReferenceImage:(UIImage *)referenceImage toImage:(UIImage *)image overallTolerance:(CGFloat)overallTolerance error:(NSError **)errorPtr + differentPixels:(NSMutableArray **)differentPixels { return [self compareReferenceImage:referenceImage toImage:image perPixelTolerance:0 overallTolerance:overallTolerance - error:errorPtr]; + error:errorPtr + differentPixels:differentPixels]; } - (BOOL)compareReferenceImage:(UIImage *)referenceImage @@ -152,12 +154,13 @@ - (BOOL)compareReferenceImage:(UIImage *)referenceImage perPixelTolerance:(CGFloat)perPixelTolerance overallTolerance:(CGFloat)overallTolerance error:(NSError **)errorPtr + differentPixels:(NSMutableArray **)differentPixels { CGSize referenceImageSize = CGSizeMake(CGImageGetWidth(referenceImage.CGImage), CGImageGetHeight(referenceImage.CGImage)); CGSize imageSize = CGSizeMake(CGImageGetWidth(image.CGImage), CGImageGetHeight(image.CGImage)); BOOL sameImageDimensions = CGSizeEqualToSize(referenceImageSize, imageSize); - if (sameImageDimensions && [referenceImage fb_compareWithImage:image perPixelTolerance:perPixelTolerance overallTolerance:overallTolerance]) { + if (sameImageDimensions && [referenceImage fb_compareWithImage:image perPixelTolerance:perPixelTolerance overallTolerance:overallTolerance differentPixels:differentPixels]) { return YES; } @@ -173,7 +176,7 @@ - (BOOL)compareReferenceImage:(UIImage *)referenceImage NSLocalizedFailureReasonErrorKey : errorReason, FBReferenceImageKey : referenceImage, FBCapturedImageKey : image, - FBDiffedImageKey : [referenceImage fb_diffWithImage:image], + FBDiffedImageKey : [referenceImage fb_diffWithImage:image differentPixels:*differentPixels], }]; } return NO; @@ -184,8 +187,9 @@ - (BOOL)saveFailedReferenceImage:(UIImage *)referenceImage selector:(SEL)selector identifier:(NSString *)identifier error:(NSError **)errorPtr + differentPixels:(NSArray *)differentPixels { - UIImage *diffImage = [referenceImage fb_diffWithImage:testImage]; + UIImage *diffImage = [referenceImage fb_diffWithImage:testImage differentPixels:differentPixels]; [XCTContext runActivityNamed:identifier ?: NSStringFromSelector(selector) block:^(id _Nonnull activity) { XCTAttachment *referenceAttachment = [XCTAttachment attachmentWithImage:referenceImage]; @@ -319,10 +323,26 @@ - (BOOL)_performPixelComparisonWithViewOrLayer:(id)viewOrLayer UIImage *referenceImage = [self referenceImageForSelector:selector identifier:identifier error:errorPtr]; if (referenceImage != nil) { UIImage *snapshot = [self _imageForViewOrLayer:viewOrLayer]; - BOOL imagesSame = [self compareReferenceImage:referenceImage toImage:snapshot perPixelTolerance:perPixelTolerance overallTolerance:overallTolerance error:errorPtr]; + + NSMutableArray *differentPixels = nil; + if (self.highlightDifferentPixels) { + differentPixels = [NSMutableArray array]; + } + + BOOL imagesSame = [self compareReferenceImage:referenceImage + toImage:snapshot + perPixelTolerance:perPixelTolerance + overallTolerance:overallTolerance + error:errorPtr + differentPixels:&differentPixels]; if (!imagesSame) { NSError *saveError = nil; - if ([self saveFailedReferenceImage:referenceImage testImage:snapshot selector:selector identifier:identifier error:&saveError] == NO) { + if ([self saveFailedReferenceImage:referenceImage + testImage:snapshot + selector:selector + identifier:identifier + error:&saveError + differentPixels:differentPixels] == NO) { NSLog(@"Error saving test images: %@", saveError); } } diff --git a/FBSnapshotTestCaseTests/FBSnapshotControllerTests.m b/FBSnapshotTestCaseTests/FBSnapshotControllerTests.m index 600165f..abfe14b 100644 --- a/FBSnapshotTestCaseTests/FBSnapshotControllerTests.m +++ b/FBSnapshotTestCaseTests/FBSnapshotControllerTests.m @@ -29,7 +29,8 @@ - (void)testCompareReferenceImageToImageShouldBeEqual id testClass = nil; FBSnapshotTestController *controller = [[FBSnapshotTestController alloc] initWithTestClass:testClass]; NSError *error = nil; - XCTAssertTrue([controller compareReferenceImage:referenceImage toImage:testImage overallTolerance:0 error:&error]); + NSMutableArray *differentPixels = nil; + XCTAssertTrue([controller compareReferenceImage:referenceImage toImage:testImage overallTolerance:0 error:&error differentPixels:&differentPixels]); XCTAssertNil(error); } @@ -43,7 +44,8 @@ - (void)testCompareReferenceImageToImageShouldNotBeEqual id testClass = nil; FBSnapshotTestController *controller = [[FBSnapshotTestController alloc] initWithTestClass:testClass]; NSError *error = nil; - XCTAssertFalse([controller compareReferenceImage:referenceImage toImage:testImage overallTolerance:0 error:&error]); + NSMutableArray *differentPixels = nil; + XCTAssertFalse([controller compareReferenceImage:referenceImage toImage:testImage overallTolerance:0 error:&error differentPixels:&differentPixels]); XCTAssertNotNil(error); XCTAssertEqual(error.code, FBSnapshotTestControllerErrorCodeImagesDifferent); } @@ -59,7 +61,8 @@ - (void)testCompareReferenceImageWithVeryLowToleranceShouldNotMatch FBSnapshotTestController *controller = [[FBSnapshotTestController alloc] initWithTestClass:testClass]; // With virtually no margin for error, this should fail to be equal NSError *error = nil; - XCTAssertFalse([controller compareReferenceImage:referenceImage toImage:testImage overallTolerance:.0001 error:&error]); + NSMutableArray *differentPixels = nil; + XCTAssertFalse([controller compareReferenceImage:referenceImage toImage:testImage overallTolerance:.0001 error:&error differentPixels:&differentPixels]); XCTAssertNotNil(error); XCTAssertEqual(error.code, FBSnapshotTestControllerErrorCodeImagesDifferent); } @@ -75,7 +78,8 @@ - (void)testCompareReferenceImageWithVeryLowToleranceShouldMatch FBSnapshotTestController *controller = [[FBSnapshotTestController alloc] initWithTestClass:testClass]; // With some tolerance these should be considered the same NSError *error = nil; - XCTAssertTrue([controller compareReferenceImage:referenceImage toImage:testImage overallTolerance:.001 error:&error]); + NSMutableArray *differentPixels = nil; + XCTAssertTrue([controller compareReferenceImage:referenceImage toImage:testImage overallTolerance:.001 error:&error differentPixels:&differentPixels]); XCTAssertNil(error); } @@ -90,7 +94,8 @@ - (void)testCompareReferenceImageWithDifferentSizes FBSnapshotTestController *controller = [[FBSnapshotTestController alloc] initWithTestClass:testClass]; // With some tolerance these should be considered the same NSError *error = nil; - XCTAssertFalse([controller compareReferenceImage:referenceImage toImage:testImage overallTolerance:0 error:&error]); + NSMutableArray *differentPixels = nil; + XCTAssertFalse([controller compareReferenceImage:referenceImage toImage:testImage overallTolerance:0 error:&error differentPixels:&differentPixels]); XCTAssertNotNil(error); XCTAssertEqual(error.code, FBSnapshotTestControllerErrorCodeImagesDifferentSizes); } @@ -170,7 +175,8 @@ - (void)testCompareReferenceImageWithLowPixelToleranceShouldNotMatch FBSnapshotTestController *controller = [[FBSnapshotTestController alloc] initWithTestClass:testClass]; // With virtually no margin for error, this should fail to be equal NSError *error = nil; - XCTAssertFalse([controller compareReferenceImage:referenceImage toImage:testImage perPixelTolerance:.06 overallTolerance:0 error:&error]); + NSMutableArray *differentPixels = nil; + XCTAssertFalse([controller compareReferenceImage:referenceImage toImage:testImage perPixelTolerance:.06 overallTolerance:0 error:&error differentPixels:&differentPixels]); XCTAssertNotNil(error); XCTAssertEqual(error.code, FBSnapshotTestControllerErrorCodeImagesDifferent); } @@ -186,7 +192,8 @@ - (void)testCompareReferenceImageWithLowPixelToleranceShouldMatch FBSnapshotTestController *controller = [[FBSnapshotTestController alloc] initWithTestClass:testClass]; // With some tolerance these should be considered the same NSError *error = nil; - XCTAssertTrue([controller compareReferenceImage:referenceImage toImage:testImage perPixelTolerance:.06 overallTolerance:0 error:&error]); + NSMutableArray *differentPixels = nil; + XCTAssertTrue([controller compareReferenceImage:referenceImage toImage:testImage perPixelTolerance:.06 overallTolerance:0 error:&error differentPixels:&differentPixels]); XCTAssertNil(error); }