From 07cd6eb859e7cde51c853363eb7c8b4ae9675f31 Mon Sep 17 00:00:00 2001 From: Joakim Stien Date: Wed, 9 Feb 2022 14:04:19 +0100 Subject: [PATCH 1/8] Added 'pixelDiffThreshold' in image comparison --- .idea/.gitignore | 8 +++++ .idea/misc.xml | 16 +++++++++ .idea/modules.xml | 8 +++++ .idea/swift-snapshot-testing.iml | 2 ++ .idea/vcs.xml | 6 ++++ .../Extensions/UInt8+diff.swift | 11 ++++++ .../Snapshotting/CALayer.swift | 12 ++++--- .../SnapshotTesting/Snapshotting/CGPath.swift | 11 +++--- .../Snapshotting/NSBezierPath.swift | 6 ++-- .../Snapshotting/NSImage.swift | 18 +++++----- .../SnapshotTesting/Snapshotting/NSView.swift | 5 +-- .../Snapshotting/NSViewController.swift | 5 +-- .../Snapshotting/SwiftUIView.swift | 4 ++- .../Snapshotting/UIBezierPath.swift | 5 +-- .../Snapshotting/UIImage.swift | 34 ++++++++++++------- .../SnapshotTesting/Snapshotting/UIView.swift | 4 ++- .../Snapshotting/UIViewController.swift | 9 +++-- 17 files changed, 120 insertions(+), 44 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/swift-snapshot-testing.iml create mode 100644 .idea/vcs.xml create mode 100644 Sources/SnapshotTesting/Extensions/UInt8+diff.swift diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 000000000..13566b81b --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 000000000..7026b534f --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,16 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 000000000..d1a345911 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/swift-snapshot-testing.iml b/.idea/swift-snapshot-testing.iml new file mode 100644 index 000000000..6aab8ae41 --- /dev/null +++ b/.idea/swift-snapshot-testing.iml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 000000000..94a25f7f4 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Sources/SnapshotTesting/Extensions/UInt8+diff.swift b/Sources/SnapshotTesting/Extensions/UInt8+diff.swift new file mode 100644 index 000000000..37caf94a3 --- /dev/null +++ b/Sources/SnapshotTesting/Extensions/UInt8+diff.swift @@ -0,0 +1,11 @@ +import Foundation + +extension UInt8 { + func diff(between other: UInt8) -> UInt8 { + if other > self { + return other - self + } else { + return self - other + } + } +} diff --git a/Sources/SnapshotTesting/Snapshotting/CALayer.swift b/Sources/SnapshotTesting/Snapshotting/CALayer.swift index d11e37f4d..1adde4e9c 100644 --- a/Sources/SnapshotTesting/Snapshotting/CALayer.swift +++ b/Sources/SnapshotTesting/Snapshotting/CALayer.swift @@ -4,14 +4,15 @@ import Cocoa extension Snapshotting where Value == CALayer, Format == NSImage { /// A snapshot strategy for comparing layers based on pixel equality. public static var image: Snapshotting { - return .image(precision: 1) + return .image(precision: 1, pixelDiffThreshold: 0) } /// A snapshot strategy for comparing layers based on pixel equality. /// /// - Parameter precision: The percentage of pixels that must match. - public static func image(precision: Float) -> Snapshotting { - return SimplySnapshotting.image(precision: precision).pullback { layer in + /// - Parameter pixelDiffThreshold: The byte-value threshold at which two pixels are considered different. + public static func image(precision: Float, pixelDiffThreshold: UInt8) -> Snapshotting { + return SimplySnapshotting.image(precision: precision, pixelDiffThreshold: pixelDiffThreshold).pullback { layer in let image = NSImage(size: layer.bounds.size) image.lockFocus() let context = NSGraphicsContext.current!.cgContext @@ -35,9 +36,10 @@ extension Snapshotting where Value == CALayer, Format == UIImage { /// A snapshot strategy for comparing layers based on pixel equality. /// /// - Parameter precision: The percentage of pixels that must match. - public static func image(precision: Float = 1, traits: UITraitCollection = .init()) + /// - Parameter pixelDiffThreshold: The byte-value threshold at which two pixels are considered different. + public static func image(precision: Float = 1, pixelDiffThreshold: UInt8 = 0, traits: UITraitCollection = .init()) -> Snapshotting { - return SimplySnapshotting.image(precision: precision, scale: traits.displayScale).pullback { layer in + return SimplySnapshotting.image(precision: precision, pixelDiffThreshold: pixelDiffThreshold, scale: traits.displayScale).pullback { layer in renderer(bounds: layer.bounds, for: traits).image { ctx in layer.setNeedsLayout() layer.layoutIfNeeded() diff --git a/Sources/SnapshotTesting/Snapshotting/CGPath.swift b/Sources/SnapshotTesting/Snapshotting/CGPath.swift index d7ee7df19..5465f6fdb 100644 --- a/Sources/SnapshotTesting/Snapshotting/CGPath.swift +++ b/Sources/SnapshotTesting/Snapshotting/CGPath.swift @@ -10,8 +10,9 @@ extension Snapshotting where Value == CGPath, Format == NSImage { /// A snapshot strategy for comparing bezier paths based on pixel equality. /// /// - Parameter precision: The percentage of pixels that must match. - public static func image(precision: Float = 1, drawingMode: CGPathDrawingMode = .eoFill) -> Snapshotting { - return SimplySnapshotting.image(precision: precision).pullback { path in + /// - Parameter pixelDiffThreshold: The byte-value threshold at which two pixels are considered different. + public static func image(precision: Float = 1, pixelDiffThreshold: UInt8 = 0, drawingMode: CGPathDrawingMode = .eoFill) -> Snapshotting { + return SimplySnapshotting.image(precision: precision, pixelDiffThreshold: pixelDiffThreshold).pullback { path in let bounds = path.boundingBoxOfPath var transform = CGAffineTransform(translationX: -bounds.origin.x, y: -bounds.origin.y) let path = path.copy(using: &transform)! @@ -39,8 +40,10 @@ extension Snapshotting where Value == CGPath, Format == UIImage { /// A snapshot strategy for comparing bezier paths based on pixel equality. /// /// - Parameter precision: The percentage of pixels that must match. - public static func image(precision: Float = 1, scale: CGFloat = 1, drawingMode: CGPathDrawingMode = .eoFill) -> Snapshotting { - return SimplySnapshotting.image(precision: precision, scale: scale).pullback { path in + /// - Parameter pixelDiffThreshold: The byte-value threshold at which two pixels are considered different. + + public static func image(precision: Float = 1, pixelDiffThreshold: UInt8 = 0, scale: CGFloat = 1, drawingMode: CGPathDrawingMode = .eoFill) -> Snapshotting { + return SimplySnapshotting.image(precision: precision, pixelDiffThreshold: pixelDiffThreshold, scale: scale).pullback { path in let bounds = path.boundingBoxOfPath let format: UIGraphicsImageRendererFormat if #available(iOS 11.0, tvOS 11.0, *) { diff --git a/Sources/SnapshotTesting/Snapshotting/NSBezierPath.swift b/Sources/SnapshotTesting/Snapshotting/NSBezierPath.swift index d9d5defce..90b96208b 100644 --- a/Sources/SnapshotTesting/Snapshotting/NSBezierPath.swift +++ b/Sources/SnapshotTesting/Snapshotting/NSBezierPath.swift @@ -10,8 +10,9 @@ extension Snapshotting where Value == NSBezierPath, Format == NSImage { /// A snapshot strategy for comparing bezier paths based on pixel equality. /// /// - Parameter precision: The percentage of pixels that must match. - public static func image(precision: Float = 1) -> Snapshotting { - return SimplySnapshotting.image(precision: precision).pullback { path in + /// - Parameter pixelDiffThreshold: The byte-value threshold at which two pixels are considered different. + public static func image(precision: Float = 1, pixelDiffThreshold: UInt8 = 0) -> Snapshotting { + return SimplySnapshotting.image(precision: precision, pixelDiffThreshold: pixelDiffThreshold).pullback { path in // Move path info frame: let bounds = path.bounds let transform = AffineTransform(translationByX: -bounds.origin.x, byY: -bounds.origin.y) @@ -90,4 +91,3 @@ private let defaultNumberFormatter: NumberFormatter = { return numberFormatter }() #endif - diff --git a/Sources/SnapshotTesting/Snapshotting/NSImage.swift b/Sources/SnapshotTesting/Snapshotting/NSImage.swift index 625639d8d..5bd075106 100644 --- a/Sources/SnapshotTesting/Snapshotting/NSImage.swift +++ b/Sources/SnapshotTesting/Snapshotting/NSImage.swift @@ -4,18 +4,19 @@ import XCTest extension Diffing where Value == NSImage { /// A pixel-diffing strategy for NSImage's which requires a 100% match. - public static let image = Diffing.image(precision: 1) + public static let image: Diffing = Diffing.image(precision: 1, pixelDiffThreshold: 0) /// A pixel-diffing strategy for NSImage that allows customizing how precise the matching must be. /// /// - Parameter precision: A value between 0 and 1, where 1 means the images must match 100% of their pixels. + /// - Parameter pixelDiffThreshold: The byte-value threshold at which two pixels are considered different. /// - Returns: A new diffing strategy. - public static func image(precision: Float) -> Diffing { + public static func image(precision: Float, pixelDiffThreshold: UInt8) -> Diffing { return .init( toData: { NSImagePNGRepresentation($0)! }, fromData: { NSImage(data: $0)! } ) { old, new in - guard !compare(old, new, precision: precision) else { return nil } + guard !compare(old, new, precision: precision, pixelDiffThreshold: pixelDiffThreshold) else { return nil } let difference = SnapshotTesting.diff(old, new) let message = new.size == old.size ? "Newly-taken snapshot does not match reference." @@ -31,16 +32,17 @@ extension Diffing where Value == NSImage { extension Snapshotting where Value == NSImage, Format == NSImage { /// A snapshot strategy for comparing images based on pixel equality. public static var image: Snapshotting { - return .image(precision: 1) + return .image(precision: 1, pixelDiffThreshold: 0) } /// A snapshot strategy for comparing images based on pixel equality. /// /// - Parameter precision: The percentage of pixels that must match. - public static func image(precision: Float) -> Snapshotting { + /// - Parameter pixelDiffThreshold: The byte-value threshold at which two pixels are considered different. + public static func image(precision: Float, pixelDiffThreshold: UInt8) -> Snapshotting { return .init( pathExtension: "png", - diffing: .image(precision: precision) + diffing: .image(precision: precision, pixelDiffThreshold: pixelDiffThreshold) ) } } @@ -52,7 +54,7 @@ private func NSImagePNGRepresentation(_ image: NSImage) -> Data? { return rep.representation(using: .png, properties: [:]) } -private func compare(_ old: NSImage, _ new: NSImage, precision: Float) -> Bool { +private func compare(_ old: NSImage, _ new: NSImage, precision: Float, pixelDiffThreshold: UInt8) -> Bool { guard let oldCgImage = old.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return false } guard let newCgImage = new.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return false } guard oldCgImage.width != 0 else { return false } @@ -81,7 +83,7 @@ private func compare(_ old: NSImage, _ new: NSImage, precision: Float) -> Bool { let p1: UnsafeMutablePointer = oldRep.bitmapData! let p2: UnsafeMutablePointer = newRep.bitmapData! for offset in 0 ..< pixelCount * 4 { - if p1[offset] != p2[offset] { + if p1[offset].diff(between: p2[offset]) > pixelDiffThreshold { differentPixelCount += 1 } if Float(differentPixelCount) > threshold { return false } diff --git a/Sources/SnapshotTesting/Snapshotting/NSView.swift b/Sources/SnapshotTesting/Snapshotting/NSView.swift index 292570f2c..6e7675c97 100644 --- a/Sources/SnapshotTesting/Snapshotting/NSView.swift +++ b/Sources/SnapshotTesting/Snapshotting/NSView.swift @@ -11,9 +11,10 @@ extension Snapshotting where Value == NSView, Format == NSImage { /// /// - Parameters: /// - precision: The percentage of pixels that must match. + /// - pixelDiffThreshold: The byte-value threshold at which two pixels are considered different. /// - size: A view size override. - public static func image(precision: Float = 1, size: CGSize? = nil) -> Snapshotting { - return SimplySnapshotting.image(precision: precision).asyncPullback { view in + public static func image(precision: Float = 1, pixelDiffThreshold: UInt8 = 0, size: CGSize? = nil) -> Snapshotting { + return SimplySnapshotting.image(precision: precision, pixelDiffThreshold: pixelDiffThreshold).asyncPullback { view in let initialSize = view.frame.size if let size = size { view.frame.size = size } guard view.frame.width > 0, view.frame.height > 0 else { diff --git a/Sources/SnapshotTesting/Snapshotting/NSViewController.swift b/Sources/SnapshotTesting/Snapshotting/NSViewController.swift index 70d972478..d53c3f063 100644 --- a/Sources/SnapshotTesting/Snapshotting/NSViewController.swift +++ b/Sources/SnapshotTesting/Snapshotting/NSViewController.swift @@ -11,9 +11,10 @@ extension Snapshotting where Value == NSViewController, Format == NSImage { /// /// - Parameters: /// - precision: The percentage of pixels that must match. + /// - pixelDiffThreshold: The byte-value threshold at which two pixels are considered different. /// - size: A view size override. - public static func image(precision: Float = 1, size: CGSize? = nil) -> Snapshotting { - return Snapshotting.image(precision: precision, size: size).pullback { $0.view } + public static func image(precision: Float = 1, pixelDiffThreshold: UInt8 = 0, size: CGSize? = nil) -> Snapshotting { + return Snapshotting.image(precision: precision, pixelDiffThreshold: pixelDiffThreshold, size: size).pullback { $0.view } } } diff --git a/Sources/SnapshotTesting/Snapshotting/SwiftUIView.swift b/Sources/SnapshotTesting/Snapshotting/SwiftUIView.swift index ec7bd9103..b46a131df 100644 --- a/Sources/SnapshotTesting/Snapshotting/SwiftUIView.swift +++ b/Sources/SnapshotTesting/Snapshotting/SwiftUIView.swift @@ -28,11 +28,13 @@ extension Snapshotting where Value: SwiftUI.View, Format == UIImage { /// - Parameters: /// - drawHierarchyInKeyWindow: Utilize the simulator's key window in order to render `UIAppearance` and `UIVisualEffect`s. This option requires a host application for your tests and will _not_ work for framework test targets. /// - precision: The percentage of pixels that must match. + /// - pixelDiffThreshold: The byte-value threshold at which two pixels are considered different. /// - size: A view size override. /// - traits: A trait collection override. public static func image( drawHierarchyInKeyWindow: Bool = false, precision: Float = 1, + pixelDiffThreshold: UInt8 = 0, layout: SwiftUISnapshotLayout = .sizeThatFits, traits: UITraitCollection = .init() ) @@ -51,7 +53,7 @@ extension Snapshotting where Value: SwiftUI.View, Format == UIImage { config = .init(safeArea: .zero, size: size, traits: traits) } - return SimplySnapshotting.image(precision: precision, scale: traits.displayScale).asyncPullback { view in + return SimplySnapshotting.image(precision: precision, pixelDiffThreshold: pixelDiffThreshold, scale: traits.displayScale).asyncPullback { view in var config = config let controller: UIViewController diff --git a/Sources/SnapshotTesting/Snapshotting/UIBezierPath.swift b/Sources/SnapshotTesting/Snapshotting/UIBezierPath.swift index 826044f4f..f25481cab 100644 --- a/Sources/SnapshotTesting/Snapshotting/UIBezierPath.swift +++ b/Sources/SnapshotTesting/Snapshotting/UIBezierPath.swift @@ -10,8 +10,9 @@ extension Snapshotting where Value == UIBezierPath, Format == UIImage { /// A snapshot strategy for comparing bezier paths based on pixel equality. /// /// - Parameter precision: The percentage of pixels that must match. - public static func image(precision: Float = 1, scale: CGFloat = 1) -> Snapshotting { - return SimplySnapshotting.image(precision: precision, scale: scale).pullback { path in + /// - Parameter pixelDiffThreshold: The byte-value threshold at which two pixels are considered different. + public static func image(precision: Float = 1, pixelDiffThreshold: UInt8 = 0, scale: CGFloat = 1) -> Snapshotting { + return SimplySnapshotting.image(precision: precision, pixelDiffThreshold: pixelDiffThreshold, scale: scale).pullback { path in let bounds = path.bounds let format: UIGraphicsImageRendererFormat if #available(iOS 11.0, tvOS 11.0, *) { diff --git a/Sources/SnapshotTesting/Snapshotting/UIImage.swift b/Sources/SnapshotTesting/Snapshotting/UIImage.swift index bf87a1c07..1739cf7e4 100644 --- a/Sources/SnapshotTesting/Snapshotting/UIImage.swift +++ b/Sources/SnapshotTesting/Snapshotting/UIImage.swift @@ -4,14 +4,15 @@ import XCTest extension Diffing where Value == UIImage { /// A pixel-diffing strategy for UIImage's which requires a 100% match. - public static let image = Diffing.image(precision: 1, scale: nil) + public static let image = Diffing.image(precision: 1, pixelDiffThreshold: 0, scale: nil) /// A pixel-diffing strategy for UIImage that allows customizing how precise the matching must be. /// - /// - Parameter precision: A value between 0 and 1, where 1 means the images must match 100% of their pixels. + /// - Parameter precision: A value between 0 and 1, where 1 means 100% of the pixels must be within `pixelDiffThreshold`. + /// - Parameter pixelDiffThreshold: If any component (RGB) of a pixel has a greater difference than this value, it is considered different. /// - Parameter scale: Scale to use when loading the reference image from disk. If `nil` or the `UITraitCollection`s default value of `0.0`, the screens scale is used. /// - Returns: A new diffing strategy. - public static func image(precision: Float, scale: CGFloat?) -> Diffing { + public static func image(precision: Float, pixelDiffThreshold: UInt8, scale: CGFloat?) -> Diffing { let imageScale: CGFloat if let scale = scale, scale != 0.0 { imageScale = scale @@ -23,7 +24,7 @@ extension Diffing where Value == UIImage { toData: { $0.pngData() ?? emptyImage().pngData()! }, fromData: { UIImage(data: $0, scale: imageScale)! } ) { old, new in - guard !compare(old, new, precision: precision) else { return nil } + guard !compare(old, new, precision: precision, pixelDiffThreshold: pixelDiffThreshold) else { return nil } let difference = SnapshotTesting.diff(old, new) let message = new.size == old.size ? "Newly-taken snapshot does not match reference." @@ -40,8 +41,8 @@ extension Diffing where Value == UIImage { ) } } - - + + /// Used when the image size has no width or no height to generated the default empty image private static func emptyImage() -> UIImage { let label = UILabel(frame: CGRect(x: 0, y: 0, width: 400, height: 80)) @@ -56,22 +57,23 @@ extension Diffing where Value == UIImage { extension Snapshotting where Value == UIImage, Format == UIImage { /// A snapshot strategy for comparing images based on pixel equality. public static var image: Snapshotting { - return .image(precision: 1, scale: nil) + return .image(precision: 1, pixelDiffThreshold: 0, scale: nil) } /// A snapshot strategy for comparing images based on pixel equality. /// /// - Parameter precision: The percentage of pixels that must match. + /// - Parameter pixelDiffThreshold: The byte-value threshold at which two pixels are considered different. /// - Parameter scale: The scale of the reference image stored on disk. - public static func image(precision: Float, scale: CGFloat?) -> Snapshotting { + public static func image(precision: Float, pixelDiffThreshold: UInt8, scale: CGFloat?) -> Snapshotting { return .init( pathExtension: "png", - diffing: .image(precision: precision, scale: scale) + diffing: .image(precision: precision, pixelDiffThreshold: pixelDiffThreshold, scale: scale) ) } } -private func compare(_ old: UIImage, _ new: UIImage, precision: Float) -> Bool { +private func compare(_ old: UIImage, _ new: UIImage, precision: Float, pixelDiffThreshold: UInt8) -> Bool { guard let oldCgImage = old.cgImage else { return false } guard let newCgImage = new.cgImage else { return false } guard oldCgImage.width != 0 else { return false } @@ -99,12 +101,18 @@ private func compare(_ old: UIImage, _ new: UIImage, precision: Float) -> Bool { guard let newerContext = context(for: newerCgImage, bytesPerRow: minBytesPerRow, data: &newerBytes) else { return false } guard let newerData = newerContext.data else { return false } if memcmp(oldData, newerData, byteCount) == 0 { return true } - if precision >= 1 { return false } + if precision >= 1 && pixelDiffThreshold == 0 { return false } var differentPixelCount = 0 let threshold = 1 - precision for byte in 0.. threshold { return false} + let diff = oldBytes[byte].diff(between: newerBytes[byte]) + if diff > pixelDiffThreshold { + print("[JDBG] diff of \(diff), which exceeds \(pixelDiffThreshold)") + differentPixelCount += 1 + if Float(differentPixelCount) / Float(byteCount) > threshold { + return false + } + } } return true } diff --git a/Sources/SnapshotTesting/Snapshotting/UIView.swift b/Sources/SnapshotTesting/Snapshotting/UIView.swift index fe1e81a5a..1e3998b19 100644 --- a/Sources/SnapshotTesting/Snapshotting/UIView.swift +++ b/Sources/SnapshotTesting/Snapshotting/UIView.swift @@ -12,17 +12,19 @@ extension Snapshotting where Value == UIView, Format == UIImage { /// - Parameters: /// - drawHierarchyInKeyWindow: Utilize the simulator's key window in order to render `UIAppearance` and `UIVisualEffect`s. This option requires a host application for your tests and will _not_ work for framework test targets. /// - precision: The percentage of pixels that must match. + /// - pixelDiffThreshold: The byte-value threshold at which two pixels are considered different. /// - size: A view size override. /// - traits: A trait collection override. public static func image( drawHierarchyInKeyWindow: Bool = false, precision: Float = 1, + pixelDiffThreshold: UInt8 = 0, size: CGSize? = nil, traits: UITraitCollection = .init() ) -> Snapshotting { - return SimplySnapshotting.image(precision: precision, scale: traits.displayScale).asyncPullback { view in + return SimplySnapshotting.image(precision: precision, pixelDiffThreshold: pixelDiffThreshold, scale: traits.displayScale).asyncPullback { view in snapshotView( config: .init(safeArea: .zero, size: size ?? view.frame.size, traits: .init()), drawHierarchyInKeyWindow: drawHierarchyInKeyWindow, diff --git a/Sources/SnapshotTesting/Snapshotting/UIViewController.swift b/Sources/SnapshotTesting/Snapshotting/UIViewController.swift index 45e719cbf..464a57967 100644 --- a/Sources/SnapshotTesting/Snapshotting/UIViewController.swift +++ b/Sources/SnapshotTesting/Snapshotting/UIViewController.swift @@ -12,17 +12,19 @@ extension Snapshotting where Value == UIViewController, Format == UIImage { /// - Parameters: /// - config: A set of device configuration settings. /// - precision: The percentage of pixels that must match. + /// - pixelDiffThreshold: The byte-value threshold at which two pixels are considered different. /// - size: A view size override. /// - traits: A trait collection override. public static func image( on config: ViewImageConfig, precision: Float = 1, + pixelDiffThreshold: UInt8 = 0, size: CGSize? = nil, traits: UITraitCollection = .init() ) -> Snapshotting { - return SimplySnapshotting.image(precision: precision, scale: traits.displayScale).asyncPullback { viewController in + return SimplySnapshotting.image(precision: precision, pixelDiffThreshold: pixelDiffThreshold, scale: traits.displayScale).asyncPullback { viewController in snapshotView( config: size.map { .init(safeArea: config.safeArea, size: $0, traits: config.traits) } ?? config, drawHierarchyInKeyWindow: false, @@ -38,17 +40,18 @@ extension Snapshotting where Value == UIViewController, Format == UIImage { /// - Parameters: /// - drawHierarchyInKeyWindow: Utilize the simulator's key window in order to render `UIAppearance` and `UIVisualEffect`s. This option requires a host application for your tests and will _not_ work for framework test targets. /// - precision: The percentage of pixels that must match. + /// - pixelDiffThreshold: The byte-value threshold at which two pixels are considered different. /// - size: A view size override. /// - traits: A trait collection override. public static func image( drawHierarchyInKeyWindow: Bool = false, precision: Float = 1, + pixelDiffThreshold: UInt8 = 0, size: CGSize? = nil, traits: UITraitCollection = .init() ) -> Snapshotting { - - return SimplySnapshotting.image(precision: precision, scale: traits.displayScale).asyncPullback { viewController in + return SimplySnapshotting.image(precision: precision, pixelDiffThreshold: pixelDiffThreshold, scale: traits.displayScale).asyncPullback { viewController in snapshotView( config: .init(safeArea: .zero, size: size, traits: traits), drawHierarchyInKeyWindow: drawHierarchyInKeyWindow, From 3895720655f634f8ca684937993ab677822e5301 Mon Sep 17 00:00:00 2001 From: Joakim Stien Date: Fri, 7 Jan 2022 10:34:29 +0100 Subject: [PATCH 2/8] Removed debug output --- Sources/SnapshotTesting/Snapshotting/UIImage.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/SnapshotTesting/Snapshotting/UIImage.swift b/Sources/SnapshotTesting/Snapshotting/UIImage.swift index 1739cf7e4..7002efbc5 100644 --- a/Sources/SnapshotTesting/Snapshotting/UIImage.swift +++ b/Sources/SnapshotTesting/Snapshotting/UIImage.swift @@ -107,7 +107,6 @@ private func compare(_ old: UIImage, _ new: UIImage, precision: Float, pixelDiff for byte in 0.. pixelDiffThreshold { - print("[JDBG] diff of \(diff), which exceeds \(pixelDiffThreshold)") differentPixelCount += 1 if Float(differentPixelCount) / Float(byteCount) > threshold { return false From d626b4d168cad96a7191126bc620899fcad06ec3 Mon Sep 17 00:00:00 2001 From: Joakim Stien Date: Wed, 9 Feb 2022 14:13:47 +0100 Subject: [PATCH 3/8] Improved unoptimized loop execution in image comparison --- .../SnapshotTesting/Snapshotting/NSImage.swift | 9 ++++++--- .../SnapshotTesting/Snapshotting/UIImage.swift | 16 +++++++--------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/Sources/SnapshotTesting/Snapshotting/NSImage.swift b/Sources/SnapshotTesting/Snapshotting/NSImage.swift index 5bd075106..b62f0c893 100644 --- a/Sources/SnapshotTesting/Snapshotting/NSImage.swift +++ b/Sources/SnapshotTesting/Snapshotting/NSImage.swift @@ -79,14 +79,17 @@ private func compare(_ old: NSImage, _ new: NSImage, precision: Float, pixelDiff let newRep = NSBitmapImageRep(cgImage: newerCgImage) var differentPixelCount = 0 let pixelCount = oldRep.pixelsWide * oldRep.pixelsHigh - let threshold = (1 - precision) * Float(pixelCount) + let threshold = Int((1 - precision) * Float(pixelCount)) let p1: UnsafeMutablePointer = oldRep.bitmapData! let p2: UnsafeMutablePointer = newRep.bitmapData! - for offset in 0 ..< pixelCount * 4 { + + var offset = 0 + while offset < pixelCount * 4 { if p1[offset].diff(between: p2[offset]) > pixelDiffThreshold { differentPixelCount += 1 } - if Float(differentPixelCount) > threshold { return false } + if differentPixelCount > threshold { return false } + offset += 1 } return true } diff --git a/Sources/SnapshotTesting/Snapshotting/UIImage.swift b/Sources/SnapshotTesting/Snapshotting/UIImage.swift index 7002efbc5..ec805a9ea 100644 --- a/Sources/SnapshotTesting/Snapshotting/UIImage.swift +++ b/Sources/SnapshotTesting/Snapshotting/UIImage.swift @@ -103,15 +103,13 @@ private func compare(_ old: UIImage, _ new: UIImage, precision: Float, pixelDiff if memcmp(oldData, newerData, byteCount) == 0 { return true } if precision >= 1 && pixelDiffThreshold == 0 { return false } var differentPixelCount = 0 - let threshold = 1 - precision - for byte in 0.. pixelDiffThreshold { - differentPixelCount += 1 - if Float(differentPixelCount) / Float(byteCount) > threshold { - return false - } - } + let threshold = Int(1.0 - precision * Float(byteCount)) + + var byte = 0 + while byte < byteCount { + if oldBytes[byte] != newerBytes[byte] { differentPixelCount += 1 } + if differentPixelCount >= threshold { return false } + byte += 1 } return true } From 90db72cb674656c91adb4e5187bfd23d4ef176a0 Mon Sep 17 00:00:00 2001 From: Joakim Stien Date: Wed, 9 Feb 2022 14:28:57 +0100 Subject: [PATCH 4/8] Only checking if we've exceeded threshold if the current value fails --- Sources/SnapshotTesting/Snapshotting/NSImage.swift | 8 +++++--- Sources/SnapshotTesting/Snapshotting/UIImage.swift | 8 ++++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Sources/SnapshotTesting/Snapshotting/NSImage.swift b/Sources/SnapshotTesting/Snapshotting/NSImage.swift index b62f0c893..6c384bf96 100644 --- a/Sources/SnapshotTesting/Snapshotting/NSImage.swift +++ b/Sources/SnapshotTesting/Snapshotting/NSImage.swift @@ -85,10 +85,12 @@ private func compare(_ old: NSImage, _ new: NSImage, precision: Float, pixelDiff var offset = 0 while offset < pixelCount * 4 { - if p1[offset].diff(between: p2[offset]) > pixelDiffThreshold { - differentPixelCount += 1 + if p1[offset] != p2[offset] { + differentPixelCount += 1 + if differentPixelCount > threshold { + return false + } } - if differentPixelCount > threshold { return false } offset += 1 } return true diff --git a/Sources/SnapshotTesting/Snapshotting/UIImage.swift b/Sources/SnapshotTesting/Snapshotting/UIImage.swift index ec805a9ea..010454b8b 100644 --- a/Sources/SnapshotTesting/Snapshotting/UIImage.swift +++ b/Sources/SnapshotTesting/Snapshotting/UIImage.swift @@ -107,8 +107,12 @@ private func compare(_ old: UIImage, _ new: UIImage, precision: Float, pixelDiff var byte = 0 while byte < byteCount { - if oldBytes[byte] != newerBytes[byte] { differentPixelCount += 1 } - if differentPixelCount >= threshold { return false } + if oldBytes[byte] != newerBytes[byte] { + differentPixelCount += 1 + if differentPixelCount >= threshold { + return false + } + } byte += 1 } return true From abf827237242298a637c0c6c90db56c93f33de33 Mon Sep 17 00:00:00 2001 From: Joakim Stien Date: Wed, 9 Feb 2022 14:36:38 +0100 Subject: [PATCH 5/8] Renamed parameter 'pixelDiffThreshold' to 'subpixelThreshold' --- SnapshotTesting.xcodeproj/project.pbxproj | 40 +++++++++++++++---- .../Snapshotting/CALayer.swift | 14 +++---- .../SnapshotTesting/Snapshotting/CGPath.swift | 12 +++--- .../Snapshotting/NSBezierPath.swift | 6 +-- .../Snapshotting/NSImage.swift | 20 +++++----- .../SnapshotTesting/Snapshotting/NSView.swift | 6 +-- .../Snapshotting/NSViewController.swift | 6 +-- .../Snapshotting/SwiftUIView.swift | 6 +-- .../Snapshotting/UIBezierPath.swift | 6 +-- .../Snapshotting/UIImage.swift | 24 +++++------ .../SnapshotTesting/Snapshotting/UIView.swift | 6 +-- .../Snapshotting/UIViewController.swift | 12 +++--- 12 files changed, 91 insertions(+), 67 deletions(-) diff --git a/SnapshotTesting.xcodeproj/project.pbxproj b/SnapshotTesting.xcodeproj/project.pbxproj index f83e1df44..1a8e797a2 100644 --- a/SnapshotTesting.xcodeproj/project.pbxproj +++ b/SnapshotTesting.xcodeproj/project.pbxproj @@ -195,6 +195,9 @@ F3883B573DF4CAFADE5968A9 /* testUpdateSeveralSnapshotsWithLessLines.1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32BC721A155DB7D81BE69683 /* testUpdateSeveralSnapshotsWithLessLines.1.swift */; }; F473E43FAB7DD0C5F4D02437 /* testUpdateSeveralSnapshotsWithMoreLines.1.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECEC001D7DB94ECB4F5A2CF9 /* testUpdateSeveralSnapshotsWithMoreLines.1.swift */; }; F4CB3EC3E5D30B217B4D9699 /* Wait.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10213E0E8B550596F75463C3 /* Wait.swift */; }; + F5B874F327B3FA5F005D3517 /* UInt8+diff.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5B874F227B3FA5F005D3517 /* UInt8+diff.swift */; }; + F5B874F427B3FA5F005D3517 /* UInt8+diff.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5B874F227B3FA5F005D3517 /* UInt8+diff.swift */; }; + F5B874F527B3FA5F005D3517 /* UInt8+diff.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5B874F227B3FA5F005D3517 /* UInt8+diff.swift */; }; F66FB66FCA7B884DA64ADDBE /* Diff.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EEE36D7D9E53E70DABA3996 /* Diff.swift */; }; FBDFF661DB08CFB75DFB12C3 /* SnapshotTesting.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8F9883B60A8B403A39BCF888 /* SnapshotTesting.framework */; }; FE365D5C0F83CE8459CF77DC /* testUpdateSnapshotWithMoreLines.1.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0BC8BF7E37686D31F59B9F6 /* testUpdateSnapshotWithMoreLines.1.swift */; }; @@ -333,6 +336,7 @@ EE91B5904B3F67A3A01610C8 /* testCreateSnapshotWithShorterExtendedDelimiter1.1.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = testCreateSnapshotWithShorterExtendedDelimiter1.1.swift; sourceTree = ""; }; F479E2BD835B9641B85EB51E /* Internal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Internal.swift; sourceTree = ""; }; F4D7D0D81B7E35C6284A3E65 /* testUpdateSnapshotWithExtendedDelimiter1.1.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = testUpdateSnapshotWithExtendedDelimiter1.1.swift; sourceTree = ""; }; + F5B874F227B3FA5F005D3517 /* UInt8+diff.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UInt8+diff.swift"; sourceTree = ""; }; F6D3BC50BC4692DC6D9FAE0F /* InlineSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineSnapshotTests.swift; sourceTree = ""; }; F8F831EAAFB97204ECD0B879 /* Any.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Any.swift; sourceTree = ""; }; FD800FD3282956340AD7706A /* AssertInlineSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssertInlineSnapshot.swift; sourceTree = ""; }; @@ -451,6 +455,7 @@ 5768A55D77F19D897206FF9F /* Extensions */ = { isa = PBXGroup; children = ( + F5B874F227B3FA5F005D3517 /* UInt8+diff.swift */, 10213E0E8B550596F75463C3 /* Wait.swift */, ); path = Extensions; @@ -646,8 +651,6 @@ isa = PBXProject; attributes = { LastUpgradeCheck = 1020; - TargetAttributes = { - }; }; buildConfigurationList = D9A4BF45876C849A308801A2 /* Build configuration list for PBXProject "SnapshotTesting" */; compatibilityVersion = "Xcode 10.0"; @@ -703,6 +706,7 @@ buildActionMask = 2147483647; files = ( 87D3745DBD172E3E6427190F /* Any.swift in Sources */, + F5B874F427B3FA5F005D3517 /* UInt8+diff.swift in Sources */, 6CAB0714112F5AAE5D0FBD23 /* AssertInlineSnapshot.swift in Sources */, 52B7C3800F08E80D7BA8600D /* AssertSnapshot.swift in Sources */, 1420B1BBC10CFF4D03C8AA67 /* Async.swift in Sources */, @@ -743,6 +747,7 @@ buildActionMask = 2147483647; files = ( E2E1BA4E82EBF5D7E7533485 /* Any.swift in Sources */, + F5B874F327B3FA5F005D3517 /* UInt8+diff.swift in Sources */, 90555C6EB17BD465E901043A /* AssertInlineSnapshot.swift in Sources */, AE77649A377D5328DC91D19B /* AssertSnapshot.swift in Sources */, 29602B6DD2A43E1C13DF1D42 /* Async.swift in Sources */, @@ -855,6 +860,7 @@ buildActionMask = 2147483647; files = ( C48A4BD0534BF1817AF91098 /* Any.swift in Sources */, + F5B874F527B3FA5F005D3517 /* UInt8+diff.swift in Sources */, 56551E9E2E7A3DCF41C966F1 /* AssertInlineSnapshot.swift in Sources */, 6C48EAD7C6D1441EE12F907B /* AssertSnapshot.swift in Sources */, 45FAA85BB7198113155FD27F /* Async.swift in Sources */, @@ -1017,7 +1023,10 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_BITCODE = NO; ENABLE_TESTING_SEARCH_PATHS = YES; - FRAMEWORK_SEARCH_PATHS = "$(inherited) $(PLATFORM_DIR)/Developer/Library/Frameworks"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + ); INFOPLIST_FILE = Sources/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( @@ -1065,7 +1074,10 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_BITCODE = NO; ENABLE_TESTING_SEARCH_PATHS = YES; - FRAMEWORK_SEARCH_PATHS = "$(inherited) $(PLATFORM_DIR)/Developer/Library/Frameworks"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + ); INFOPLIST_FILE = Sources/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( @@ -1146,7 +1158,10 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_BITCODE = NO; ENABLE_TESTING_SEARCH_PATHS = YES; - FRAMEWORK_SEARCH_PATHS = "$(inherited) $(PLATFORM_DIR)/Developer/Library/Frameworks"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + ); INFOPLIST_FILE = Sources/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( @@ -1175,7 +1190,10 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_BITCODE = NO; ENABLE_TESTING_SEARCH_PATHS = YES; - FRAMEWORK_SEARCH_PATHS = "$(inherited) $(PLATFORM_DIR)/Developer/Library/Frameworks"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + ); INFOPLIST_FILE = Sources/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( @@ -1204,7 +1222,10 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_BITCODE = NO; ENABLE_TESTING_SEARCH_PATHS = YES; - FRAMEWORK_SEARCH_PATHS = "$(inherited) $(PLATFORM_DIR)/Developer/Library/Frameworks"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + ); INFOPLIST_FILE = Sources/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( @@ -1313,7 +1334,10 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_BITCODE = NO; ENABLE_TESTING_SEARCH_PATHS = YES; - FRAMEWORK_SEARCH_PATHS = "$(inherited) $(PLATFORM_DIR)/Developer/Library/Frameworks"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + ); INFOPLIST_FILE = Sources/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/Sources/SnapshotTesting/Snapshotting/CALayer.swift b/Sources/SnapshotTesting/Snapshotting/CALayer.swift index 1adde4e9c..3d73abd4d 100644 --- a/Sources/SnapshotTesting/Snapshotting/CALayer.swift +++ b/Sources/SnapshotTesting/Snapshotting/CALayer.swift @@ -4,15 +4,15 @@ import Cocoa extension Snapshotting where Value == CALayer, Format == NSImage { /// A snapshot strategy for comparing layers based on pixel equality. public static var image: Snapshotting { - return .image(precision: 1, pixelDiffThreshold: 0) + return .image(precision: 1, subpixelThreshold: 0) } /// A snapshot strategy for comparing layers based on pixel equality. /// /// - Parameter precision: The percentage of pixels that must match. - /// - Parameter pixelDiffThreshold: The byte-value threshold at which two pixels are considered different. - public static func image(precision: Float, pixelDiffThreshold: UInt8) -> Snapshotting { - return SimplySnapshotting.image(precision: precision, pixelDiffThreshold: pixelDiffThreshold).pullback { layer in + /// - Parameter subpixelThreshold: The byte-value threshold at which two subpixels are considered different. + public static func image(precision: Float, subpixelThreshold: UInt8) -> Snapshotting { + return SimplySnapshotting.image(precision: precision, subpixelThreshold: subpixelThreshold).pullback { layer in let image = NSImage(size: layer.bounds.size) image.lockFocus() let context = NSGraphicsContext.current!.cgContext @@ -36,10 +36,10 @@ extension Snapshotting where Value == CALayer, Format == UIImage { /// A snapshot strategy for comparing layers based on pixel equality. /// /// - Parameter precision: The percentage of pixels that must match. - /// - Parameter pixelDiffThreshold: The byte-value threshold at which two pixels are considered different. - public static func image(precision: Float = 1, pixelDiffThreshold: UInt8 = 0, traits: UITraitCollection = .init()) + /// - Parameter subpixelThreshold: The byte-value threshold at which two subpixels are considered different. + public static func image(precision: Float = 1, subpixelThreshold: UInt8 = 0, traits: UITraitCollection = .init()) -> Snapshotting { - return SimplySnapshotting.image(precision: precision, pixelDiffThreshold: pixelDiffThreshold, scale: traits.displayScale).pullback { layer in + return SimplySnapshotting.image(precision: precision, subpixelThreshold: subpixelThreshold, scale: traits.displayScale).pullback { layer in renderer(bounds: layer.bounds, for: traits).image { ctx in layer.setNeedsLayout() layer.layoutIfNeeded() diff --git a/Sources/SnapshotTesting/Snapshotting/CGPath.swift b/Sources/SnapshotTesting/Snapshotting/CGPath.swift index 5465f6fdb..d22eb2a31 100644 --- a/Sources/SnapshotTesting/Snapshotting/CGPath.swift +++ b/Sources/SnapshotTesting/Snapshotting/CGPath.swift @@ -10,9 +10,9 @@ extension Snapshotting where Value == CGPath, Format == NSImage { /// A snapshot strategy for comparing bezier paths based on pixel equality. /// /// - Parameter precision: The percentage of pixels that must match. - /// - Parameter pixelDiffThreshold: The byte-value threshold at which two pixels are considered different. - public static func image(precision: Float = 1, pixelDiffThreshold: UInt8 = 0, drawingMode: CGPathDrawingMode = .eoFill) -> Snapshotting { - return SimplySnapshotting.image(precision: precision, pixelDiffThreshold: pixelDiffThreshold).pullback { path in + /// - Parameter subpixelThreshold: The byte-value threshold at which two subpixels are considered different. + public static func image(precision: Float = 1, subpixelThreshold: UInt8 = 0, drawingMode: CGPathDrawingMode = .eoFill) -> Snapshotting { + return SimplySnapshotting.image(precision: precision, subpixelThreshold: subpixelThreshold).pullback { path in let bounds = path.boundingBoxOfPath var transform = CGAffineTransform(translationX: -bounds.origin.x, y: -bounds.origin.y) let path = path.copy(using: &transform)! @@ -40,10 +40,10 @@ extension Snapshotting where Value == CGPath, Format == UIImage { /// A snapshot strategy for comparing bezier paths based on pixel equality. /// /// - Parameter precision: The percentage of pixels that must match. - /// - Parameter pixelDiffThreshold: The byte-value threshold at which two pixels are considered different. + /// - Parameter subpixelThreshold: The byte-value threshold at which two subpixels are considered different. - public static func image(precision: Float = 1, pixelDiffThreshold: UInt8 = 0, scale: CGFloat = 1, drawingMode: CGPathDrawingMode = .eoFill) -> Snapshotting { - return SimplySnapshotting.image(precision: precision, pixelDiffThreshold: pixelDiffThreshold, scale: scale).pullback { path in + public static func image(precision: Float = 1, subpixelThreshold: UInt8 = 0, scale: CGFloat = 1, drawingMode: CGPathDrawingMode = .eoFill) -> Snapshotting { + return SimplySnapshotting.image(precision: precision, subpixelThreshold: subpixelThreshold, scale: scale).pullback { path in let bounds = path.boundingBoxOfPath let format: UIGraphicsImageRendererFormat if #available(iOS 11.0, tvOS 11.0, *) { diff --git a/Sources/SnapshotTesting/Snapshotting/NSBezierPath.swift b/Sources/SnapshotTesting/Snapshotting/NSBezierPath.swift index 90b96208b..0488ca644 100644 --- a/Sources/SnapshotTesting/Snapshotting/NSBezierPath.swift +++ b/Sources/SnapshotTesting/Snapshotting/NSBezierPath.swift @@ -10,9 +10,9 @@ extension Snapshotting where Value == NSBezierPath, Format == NSImage { /// A snapshot strategy for comparing bezier paths based on pixel equality. /// /// - Parameter precision: The percentage of pixels that must match. - /// - Parameter pixelDiffThreshold: The byte-value threshold at which two pixels are considered different. - public static func image(precision: Float = 1, pixelDiffThreshold: UInt8 = 0) -> Snapshotting { - return SimplySnapshotting.image(precision: precision, pixelDiffThreshold: pixelDiffThreshold).pullback { path in + /// - Parameter subpixelThreshold: The byte-value threshold at which two subpixels are considered different. + public static func image(precision: Float = 1, subpixelThreshold: UInt8 = 0) -> Snapshotting { + return SimplySnapshotting.image(precision: precision, subpixelThreshold: subpixelThreshold).pullback { path in // Move path info frame: let bounds = path.bounds let transform = AffineTransform(translationByX: -bounds.origin.x, byY: -bounds.origin.y) diff --git a/Sources/SnapshotTesting/Snapshotting/NSImage.swift b/Sources/SnapshotTesting/Snapshotting/NSImage.swift index 6c384bf96..133188138 100644 --- a/Sources/SnapshotTesting/Snapshotting/NSImage.swift +++ b/Sources/SnapshotTesting/Snapshotting/NSImage.swift @@ -4,19 +4,19 @@ import XCTest extension Diffing where Value == NSImage { /// A pixel-diffing strategy for NSImage's which requires a 100% match. - public static let image: Diffing = Diffing.image(precision: 1, pixelDiffThreshold: 0) + public static let image: Diffing = Diffing.image(precision: 1, subpixelThreshold: 0) /// A pixel-diffing strategy for NSImage that allows customizing how precise the matching must be. /// /// - Parameter precision: A value between 0 and 1, where 1 means the images must match 100% of their pixels. - /// - Parameter pixelDiffThreshold: The byte-value threshold at which two pixels are considered different. + /// - Parameter subpixelThreshold: The byte-value threshold at which two subpixels are considered different. /// - Returns: A new diffing strategy. - public static func image(precision: Float, pixelDiffThreshold: UInt8) -> Diffing { + public static func image(precision: Float, subpixelThreshold: UInt8) -> Diffing { return .init( toData: { NSImagePNGRepresentation($0)! }, fromData: { NSImage(data: $0)! } ) { old, new in - guard !compare(old, new, precision: precision, pixelDiffThreshold: pixelDiffThreshold) else { return nil } + guard !compare(old, new, precision: precision, subpixelThreshold: subpixelThreshold) else { return nil } let difference = SnapshotTesting.diff(old, new) let message = new.size == old.size ? "Newly-taken snapshot does not match reference." @@ -32,17 +32,17 @@ extension Diffing where Value == NSImage { extension Snapshotting where Value == NSImage, Format == NSImage { /// A snapshot strategy for comparing images based on pixel equality. public static var image: Snapshotting { - return .image(precision: 1, pixelDiffThreshold: 0) + return .image(precision: 1, subpixelThreshold: 0) } /// A snapshot strategy for comparing images based on pixel equality. /// /// - Parameter precision: The percentage of pixels that must match. - /// - Parameter pixelDiffThreshold: The byte-value threshold at which two pixels are considered different. - public static func image(precision: Float, pixelDiffThreshold: UInt8) -> Snapshotting { + /// - Parameter subpixelThreshold: The byte-value threshold at which two subpixels are considered different. + public static func image(precision: Float, subpixelThreshold: UInt8) -> Snapshotting { return .init( pathExtension: "png", - diffing: .image(precision: precision, pixelDiffThreshold: pixelDiffThreshold) + diffing: .image(precision: precision, subpixelThreshold: subpixelThreshold) ) } } @@ -54,7 +54,7 @@ private func NSImagePNGRepresentation(_ image: NSImage) -> Data? { return rep.representation(using: .png, properties: [:]) } -private func compare(_ old: NSImage, _ new: NSImage, precision: Float, pixelDiffThreshold: UInt8) -> Bool { +private func compare(_ old: NSImage, _ new: NSImage, precision: Float, subpixelThreshold: UInt8) -> Bool { guard let oldCgImage = old.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return false } guard let newCgImage = new.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return false } guard oldCgImage.width != 0 else { return false } @@ -85,7 +85,7 @@ private func compare(_ old: NSImage, _ new: NSImage, precision: Float, pixelDiff var offset = 0 while offset < pixelCount * 4 { - if p1[offset] != p2[offset] { + if p1[offset].diff(between: p2[offset]) > subpixelThreshold { differentPixelCount += 1 if differentPixelCount > threshold { return false diff --git a/Sources/SnapshotTesting/Snapshotting/NSView.swift b/Sources/SnapshotTesting/Snapshotting/NSView.swift index 6e7675c97..d9eb450b6 100644 --- a/Sources/SnapshotTesting/Snapshotting/NSView.swift +++ b/Sources/SnapshotTesting/Snapshotting/NSView.swift @@ -11,10 +11,10 @@ extension Snapshotting where Value == NSView, Format == NSImage { /// /// - Parameters: /// - precision: The percentage of pixels that must match. - /// - pixelDiffThreshold: The byte-value threshold at which two pixels are considered different. + /// - subpixelThreshold: The byte-value threshold at which two subpixels are considered different. /// - size: A view size override. - public static func image(precision: Float = 1, pixelDiffThreshold: UInt8 = 0, size: CGSize? = nil) -> Snapshotting { - return SimplySnapshotting.image(precision: precision, pixelDiffThreshold: pixelDiffThreshold).asyncPullback { view in + public static func image(precision: Float = 1, subpixelThreshold: UInt8 = 0, size: CGSize? = nil) -> Snapshotting { + return SimplySnapshotting.image(precision: precision, subpixelThreshold: subpixelThreshold).asyncPullback { view in let initialSize = view.frame.size if let size = size { view.frame.size = size } guard view.frame.width > 0, view.frame.height > 0 else { diff --git a/Sources/SnapshotTesting/Snapshotting/NSViewController.swift b/Sources/SnapshotTesting/Snapshotting/NSViewController.swift index d53c3f063..7f627d244 100644 --- a/Sources/SnapshotTesting/Snapshotting/NSViewController.swift +++ b/Sources/SnapshotTesting/Snapshotting/NSViewController.swift @@ -11,10 +11,10 @@ extension Snapshotting where Value == NSViewController, Format == NSImage { /// /// - Parameters: /// - precision: The percentage of pixels that must match. - /// - pixelDiffThreshold: The byte-value threshold at which two pixels are considered different. + /// - subpixelThreshold: The byte-value threshold at which two subpixels are considered different. /// - size: A view size override. - public static func image(precision: Float = 1, pixelDiffThreshold: UInt8 = 0, size: CGSize? = nil) -> Snapshotting { - return Snapshotting.image(precision: precision, pixelDiffThreshold: pixelDiffThreshold, size: size).pullback { $0.view } + public static func image(precision: Float = 1, subpixelThreshold: UInt8 = 0, size: CGSize? = nil) -> Snapshotting { + return Snapshotting.image(precision: precision, subpixelThreshold: subpixelThreshold, size: size).pullback { $0.view } } } diff --git a/Sources/SnapshotTesting/Snapshotting/SwiftUIView.swift b/Sources/SnapshotTesting/Snapshotting/SwiftUIView.swift index b46a131df..034ce6ecb 100644 --- a/Sources/SnapshotTesting/Snapshotting/SwiftUIView.swift +++ b/Sources/SnapshotTesting/Snapshotting/SwiftUIView.swift @@ -28,13 +28,13 @@ extension Snapshotting where Value: SwiftUI.View, Format == UIImage { /// - Parameters: /// - drawHierarchyInKeyWindow: Utilize the simulator's key window in order to render `UIAppearance` and `UIVisualEffect`s. This option requires a host application for your tests and will _not_ work for framework test targets. /// - precision: The percentage of pixels that must match. - /// - pixelDiffThreshold: The byte-value threshold at which two pixels are considered different. + /// - subpixelThreshold: The byte-value threshold at which two subpixels are considered different. /// - size: A view size override. /// - traits: A trait collection override. public static func image( drawHierarchyInKeyWindow: Bool = false, precision: Float = 1, - pixelDiffThreshold: UInt8 = 0, + subpixelThreshold: UInt8 = 0, layout: SwiftUISnapshotLayout = .sizeThatFits, traits: UITraitCollection = .init() ) @@ -53,7 +53,7 @@ extension Snapshotting where Value: SwiftUI.View, Format == UIImage { config = .init(safeArea: .zero, size: size, traits: traits) } - return SimplySnapshotting.image(precision: precision, pixelDiffThreshold: pixelDiffThreshold, scale: traits.displayScale).asyncPullback { view in + return SimplySnapshotting.image(precision: precision, subpixelThreshold: subpixelThreshold, scale: traits.displayScale).asyncPullback { view in var config = config let controller: UIViewController diff --git a/Sources/SnapshotTesting/Snapshotting/UIBezierPath.swift b/Sources/SnapshotTesting/Snapshotting/UIBezierPath.swift index f25481cab..c45e6d6ed 100644 --- a/Sources/SnapshotTesting/Snapshotting/UIBezierPath.swift +++ b/Sources/SnapshotTesting/Snapshotting/UIBezierPath.swift @@ -10,9 +10,9 @@ extension Snapshotting where Value == UIBezierPath, Format == UIImage { /// A snapshot strategy for comparing bezier paths based on pixel equality. /// /// - Parameter precision: The percentage of pixels that must match. - /// - Parameter pixelDiffThreshold: The byte-value threshold at which two pixels are considered different. - public static func image(precision: Float = 1, pixelDiffThreshold: UInt8 = 0, scale: CGFloat = 1) -> Snapshotting { - return SimplySnapshotting.image(precision: precision, pixelDiffThreshold: pixelDiffThreshold, scale: scale).pullback { path in + /// - Parameter subpixelThreshold: The byte-value threshold at which two subpixels are considered different. + public static func image(precision: Float = 1, subpixelThreshold: UInt8 = 0, scale: CGFloat = 1) -> Snapshotting { + return SimplySnapshotting.image(precision: precision, subpixelThreshold: subpixelThreshold, scale: scale).pullback { path in let bounds = path.bounds let format: UIGraphicsImageRendererFormat if #available(iOS 11.0, tvOS 11.0, *) { diff --git a/Sources/SnapshotTesting/Snapshotting/UIImage.swift b/Sources/SnapshotTesting/Snapshotting/UIImage.swift index 010454b8b..108cdacf0 100644 --- a/Sources/SnapshotTesting/Snapshotting/UIImage.swift +++ b/Sources/SnapshotTesting/Snapshotting/UIImage.swift @@ -4,15 +4,15 @@ import XCTest extension Diffing where Value == UIImage { /// A pixel-diffing strategy for UIImage's which requires a 100% match. - public static let image = Diffing.image(precision: 1, pixelDiffThreshold: 0, scale: nil) + public static let image = Diffing.image(precision: 1, subpixelThreshold: 0, scale: nil) /// A pixel-diffing strategy for UIImage that allows customizing how precise the matching must be. /// - /// - Parameter precision: A value between 0 and 1, where 1 means 100% of the pixels must be within `pixelDiffThreshold`. - /// - Parameter pixelDiffThreshold: If any component (RGB) of a pixel has a greater difference than this value, it is considered different. + /// - Parameter precision: A value between 0 and 1, where 1 means 100% of the pixels must be within `subpixelThreshold`. + /// - Parameter subpixelThreshold: If any component (RGB) of a pixel has a greater difference than this value, it is considered different. /// - Parameter scale: Scale to use when loading the reference image from disk. If `nil` or the `UITraitCollection`s default value of `0.0`, the screens scale is used. /// - Returns: A new diffing strategy. - public static func image(precision: Float, pixelDiffThreshold: UInt8, scale: CGFloat?) -> Diffing { + public static func image(precision: Float, subpixelThreshold: UInt8, scale: CGFloat?) -> Diffing { let imageScale: CGFloat if let scale = scale, scale != 0.0 { imageScale = scale @@ -24,7 +24,7 @@ extension Diffing where Value == UIImage { toData: { $0.pngData() ?? emptyImage().pngData()! }, fromData: { UIImage(data: $0, scale: imageScale)! } ) { old, new in - guard !compare(old, new, precision: precision, pixelDiffThreshold: pixelDiffThreshold) else { return nil } + guard !compare(old, new, precision: precision, subpixelThreshold: subpixelThreshold) else { return nil } let difference = SnapshotTesting.diff(old, new) let message = new.size == old.size ? "Newly-taken snapshot does not match reference." @@ -57,23 +57,23 @@ extension Diffing where Value == UIImage { extension Snapshotting where Value == UIImage, Format == UIImage { /// A snapshot strategy for comparing images based on pixel equality. public static var image: Snapshotting { - return .image(precision: 1, pixelDiffThreshold: 0, scale: nil) + return .image(precision: 1, subpixelThreshold: 0, scale: nil) } /// A snapshot strategy for comparing images based on pixel equality. /// /// - Parameter precision: The percentage of pixels that must match. - /// - Parameter pixelDiffThreshold: The byte-value threshold at which two pixels are considered different. + /// - Parameter subpixelThreshold: The byte-value threshold at which two subpixels are considered different. /// - Parameter scale: The scale of the reference image stored on disk. - public static func image(precision: Float, pixelDiffThreshold: UInt8, scale: CGFloat?) -> Snapshotting { + public static func image(precision: Float, subpixelThreshold: UInt8, scale: CGFloat?) -> Snapshotting { return .init( pathExtension: "png", - diffing: .image(precision: precision, pixelDiffThreshold: pixelDiffThreshold, scale: scale) + diffing: .image(precision: precision, subpixelThreshold: subpixelThreshold, scale: scale) ) } } -private func compare(_ old: UIImage, _ new: UIImage, precision: Float, pixelDiffThreshold: UInt8) -> Bool { +private func compare(_ old: UIImage, _ new: UIImage, precision: Float, subpixelThreshold: UInt8) -> Bool { guard let oldCgImage = old.cgImage else { return false } guard let newCgImage = new.cgImage else { return false } guard oldCgImage.width != 0 else { return false } @@ -101,13 +101,13 @@ private func compare(_ old: UIImage, _ new: UIImage, precision: Float, pixelDiff guard let newerContext = context(for: newerCgImage, bytesPerRow: minBytesPerRow, data: &newerBytes) else { return false } guard let newerData = newerContext.data else { return false } if memcmp(oldData, newerData, byteCount) == 0 { return true } - if precision >= 1 && pixelDiffThreshold == 0 { return false } + if precision >= 1 && subpixelThreshold == 0 { return false } var differentPixelCount = 0 let threshold = Int(1.0 - precision * Float(byteCount)) var byte = 0 while byte < byteCount { - if oldBytes[byte] != newerBytes[byte] { + if oldBytes[byte].diff(between: newerBytes[byte]) > subpixelThreshold { differentPixelCount += 1 if differentPixelCount >= threshold { return false diff --git a/Sources/SnapshotTesting/Snapshotting/UIView.swift b/Sources/SnapshotTesting/Snapshotting/UIView.swift index 1e3998b19..73b111b77 100644 --- a/Sources/SnapshotTesting/Snapshotting/UIView.swift +++ b/Sources/SnapshotTesting/Snapshotting/UIView.swift @@ -12,19 +12,19 @@ extension Snapshotting where Value == UIView, Format == UIImage { /// - Parameters: /// - drawHierarchyInKeyWindow: Utilize the simulator's key window in order to render `UIAppearance` and `UIVisualEffect`s. This option requires a host application for your tests and will _not_ work for framework test targets. /// - precision: The percentage of pixels that must match. - /// - pixelDiffThreshold: The byte-value threshold at which two pixels are considered different. + /// - subpixelThreshold: The byte-value threshold at which two subpixels are considered different. /// - size: A view size override. /// - traits: A trait collection override. public static func image( drawHierarchyInKeyWindow: Bool = false, precision: Float = 1, - pixelDiffThreshold: UInt8 = 0, + subpixelThreshold: UInt8 = 0, size: CGSize? = nil, traits: UITraitCollection = .init() ) -> Snapshotting { - return SimplySnapshotting.image(precision: precision, pixelDiffThreshold: pixelDiffThreshold, scale: traits.displayScale).asyncPullback { view in + return SimplySnapshotting.image(precision: precision, subpixelThreshold: subpixelThreshold, scale: traits.displayScale).asyncPullback { view in snapshotView( config: .init(safeArea: .zero, size: size ?? view.frame.size, traits: .init()), drawHierarchyInKeyWindow: drawHierarchyInKeyWindow, diff --git a/Sources/SnapshotTesting/Snapshotting/UIViewController.swift b/Sources/SnapshotTesting/Snapshotting/UIViewController.swift index 464a57967..dac0ce0bc 100644 --- a/Sources/SnapshotTesting/Snapshotting/UIViewController.swift +++ b/Sources/SnapshotTesting/Snapshotting/UIViewController.swift @@ -12,19 +12,19 @@ extension Snapshotting where Value == UIViewController, Format == UIImage { /// - Parameters: /// - config: A set of device configuration settings. /// - precision: The percentage of pixels that must match. - /// - pixelDiffThreshold: The byte-value threshold at which two pixels are considered different. + /// - subpixelThreshold: The byte-value threshold at which two subpixels are considered different. /// - size: A view size override. /// - traits: A trait collection override. public static func image( on config: ViewImageConfig, precision: Float = 1, - pixelDiffThreshold: UInt8 = 0, + subpixelThreshold: UInt8 = 0, size: CGSize? = nil, traits: UITraitCollection = .init() ) -> Snapshotting { - return SimplySnapshotting.image(precision: precision, pixelDiffThreshold: pixelDiffThreshold, scale: traits.displayScale).asyncPullback { viewController in + return SimplySnapshotting.image(precision: precision, subpixelThreshold: subpixelThreshold, scale: traits.displayScale).asyncPullback { viewController in snapshotView( config: size.map { .init(safeArea: config.safeArea, size: $0, traits: config.traits) } ?? config, drawHierarchyInKeyWindow: false, @@ -40,18 +40,18 @@ extension Snapshotting where Value == UIViewController, Format == UIImage { /// - Parameters: /// - drawHierarchyInKeyWindow: Utilize the simulator's key window in order to render `UIAppearance` and `UIVisualEffect`s. This option requires a host application for your tests and will _not_ work for framework test targets. /// - precision: The percentage of pixels that must match. - /// - pixelDiffThreshold: The byte-value threshold at which two pixels are considered different. + /// - subpixelThreshold: The byte-value threshold at which two subpixels are considered different. /// - size: A view size override. /// - traits: A trait collection override. public static func image( drawHierarchyInKeyWindow: Bool = false, precision: Float = 1, - pixelDiffThreshold: UInt8 = 0, + subpixelThreshold: UInt8 = 0, size: CGSize? = nil, traits: UITraitCollection = .init() ) -> Snapshotting { - return SimplySnapshotting.image(precision: precision, pixelDiffThreshold: pixelDiffThreshold, scale: traits.displayScale).asyncPullback { viewController in + return SimplySnapshotting.image(precision: precision, subpixelThreshold: subpixelThreshold, scale: traits.displayScale).asyncPullback { viewController in snapshotView( config: .init(safeArea: .zero, size: size, traits: traits), drawHierarchyInKeyWindow: drawHierarchyInKeyWindow, From 80bfc60c71a1b83e8c25f0a57d456a6ae0dbd538 Mon Sep 17 00:00:00 2001 From: Joakim Stien Date: Wed, 9 Feb 2022 15:04:50 +0100 Subject: [PATCH 6/8] Updated README --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b5add82d0..04ddb8820 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,17 @@ assertSnapshot(matching: vc, as: .image(on: .iPadMini(.portrait))) assertSnapshot(matching: vc, as: .recursiveDescription(on: .iPadMini(.portrait))) ``` -> ⚠️ Warning: Snapshots must be compared using a simulator with the same OS, device gamut, and scale as the simulator that originally took the reference to avoid discrepancies between images. +> ⚠️ Warning: Snapshots may differ slightly unless compared on the same OS, +> device gamut, and scale as the simulator that originally took the reference. +> If this cannot be avoided, acceptance in differences can be configured by +> setting the `subpixelThreshold`-parameter. +> +> Example: +> ```swift +> // Allow each subpixel to deviate up to 5 byte-values +> assertSnapshot(matching: vc, as: .image(on: .iPhoneX, subpixelThreshold: 5)) +> ``` +> Better yet, SnapshotTesting isn't limited to views and view controllers! There are [a number of available snapshot strategies](Documentation/Available-Snapshot-Strategies.md) to choose from. From 67324cbf53a4416ea1849a2962ce7eb48d5c2a76 Mon Sep 17 00:00:00 2001 From: Joakim Stien Date: Wed, 9 Feb 2022 15:05:16 +0100 Subject: [PATCH 7/8] Removed .idea/ directory --- .idea/.gitignore | 8 -------- .idea/misc.xml | 16 ---------------- .idea/modules.xml | 8 -------- .idea/swift-snapshot-testing.iml | 2 -- .idea/vcs.xml | 6 ------ 5 files changed, 40 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/swift-snapshot-testing.iml delete mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b81b..000000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 7026b534f..000000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index d1a345911..000000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/swift-snapshot-testing.iml b/.idea/swift-snapshot-testing.iml deleted file mode 100644 index 6aab8ae41..000000000 --- a/.idea/swift-snapshot-testing.iml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7f4..000000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file From 0efeef44df913fe60ea868f037f271ac927a8e8c Mon Sep 17 00:00:00 2001 From: Joakim Stien Date: Mon, 14 Feb 2022 07:22:40 +0100 Subject: [PATCH 8/8] Fixed bug in threshold calculation --- Sources/SnapshotTesting/Snapshotting/UIImage.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SnapshotTesting/Snapshotting/UIImage.swift b/Sources/SnapshotTesting/Snapshotting/UIImage.swift index a44908c28..d5e3296c7 100644 --- a/Sources/SnapshotTesting/Snapshotting/UIImage.swift +++ b/Sources/SnapshotTesting/Snapshotting/UIImage.swift @@ -103,7 +103,7 @@ private func compare(_ old: UIImage, _ new: UIImage, precision: Float, subpixelT if memcmp(oldData, newerData, byteCount) == 0 { return true } if precision >= 1 && subpixelThreshold == 0 { return false } var differentPixelCount = 0 - let threshold = Int(1.0 - precision * Float(byteCount)) + let threshold = Int(round((1.0 - precision) * Float(byteCount))) var byte = 0 while byte < byteCount {