Skip to content

Commit

Permalink
only support image comparison based on memcmp on the cgImage data
Browse files Browse the repository at this point in the history
  • Loading branch information
ralfebert committed Feb 4, 2025
1 parent c7977a3 commit a49e295
Show file tree
Hide file tree
Showing 9 changed files with 25 additions and 198 deletions.
4 changes: 2 additions & 2 deletions Sources/SnapshotTesting/Snapshotting/CALayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,12 @@
/// human eye.
/// - traits: A trait collection override.
public static func image(
precision: Float = 1, perceptualPrecision: Float = 1, traits: UITraitCollection = .init()
traits: UITraitCollection = .init()
)
-> Snapshotting
{
return SimplySnapshotting.image(
precision: precision, perceptualPrecision: perceptualPrecision, scale: traits.displayScale
scale: traits.displayScale
).pullback { layer in
renderer(bounds: layer.bounds, for: traits).image { ctx in
layer.setNeedsLayout()
Expand Down
4 changes: 2 additions & 2 deletions Sources/SnapshotTesting/Snapshotting/CGPath.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@
/// [the precision](http://zschuessler.github.io/DeltaE/learn/#toc-defining-delta-e) of the
/// human eye.
public static func image(
precision: Float = 1, perceptualPrecision: Float = 1, scale: CGFloat = 1,
scale: CGFloat = 1,
drawingMode: CGPathDrawingMode = .eoFill
) -> Snapshotting {
return SimplySnapshotting.image(
precision: precision, perceptualPrecision: perceptualPrecision, scale: scale
scale: scale
).pullback { path in
let bounds = path.boundingBoxOfPath
let format: UIGraphicsImageRendererFormat
Expand Down
58 changes: 0 additions & 58 deletions Sources/SnapshotTesting/Snapshotting/SceneKit.swift

This file was deleted.

58 changes: 0 additions & 58 deletions Sources/SnapshotTesting/Snapshotting/SpriteKit.swift

This file was deleted.

4 changes: 1 addition & 3 deletions Sources/SnapshotTesting/Snapshotting/SwiftUIView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@
/// - traits: A trait collection override.
public static func image(
drawHierarchyInKeyWindow: Bool = false,
precision: Float = 1,
perceptualPrecision: Float = 1,
layout: SwiftUISnapshotLayout = .sizeThatFits,
traits: UITraitCollection = .init()
)
Expand All @@ -60,7 +58,7 @@
}

return SimplySnapshotting.image(
precision: precision, perceptualPrecision: perceptualPrecision, scale: traits.displayScale
scale: traits.displayScale
).asyncPullback { view in
var config = config

Expand Down
2 changes: 1 addition & 1 deletion Sources/SnapshotTesting/Snapshotting/UIBezierPath.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
precision: Float = 1, perceptualPrecision: Float = 1, scale: CGFloat = 1
) -> Snapshotting {
return SimplySnapshotting.image(
precision: precision, perceptualPrecision: perceptualPrecision, scale: scale
scale: scale
).pullback { path in
let bounds = path.bounds
let format: UIGraphicsImageRendererFormat
Expand Down
81 changes: 16 additions & 65 deletions Sources/SnapshotTesting/Snapshotting/UIImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
/// `UITraitCollection`s default value of `0.0`, the screens scale is used.
/// - Returns: A new diffing strategy.
public static func image(
precision: Float = 1, perceptualPrecision: Float = 1, scale: CGFloat? = nil
precision: Float = 1, scale: CGFloat? = nil
) -> Diffing {
let imageScale: CGFloat
if let scale = scale, scale != 0.0 {
Expand All @@ -33,23 +33,9 @@
) { old, new in
guard
let message = compare(
old, new, precision: precision, perceptualPrecision: perceptualPrecision)
old, new)
else { return nil }
if isSwiftTesting {
return (message, [])
}
let difference = SnapshotTesting.diff(old, new)
let oldAttachment = XCTAttachment(image: old)
oldAttachment.name = "reference"
let isEmptyImage = new.size == .zero
let newAttachment = XCTAttachment(image: isEmptyImage ? emptyImage() : new)
newAttachment.name = "failure"
let differenceAttachment = XCTAttachment(image: difference)
differenceAttachment.name = "difference"
return (
message,
[oldAttachment, newAttachment, differenceAttachment]
)
return (message, [])
}
}

Expand Down Expand Up @@ -80,13 +66,11 @@
/// [the precision](http://zschuessler.github.io/DeltaE/learn/#toc-defining-delta-e) of the
/// human eye.
/// - scale: The scale of the reference image stored on disk.
public static func image(
precision: Float = 1, perceptualPrecision: Float = 1, scale: CGFloat? = nil
public static func image(scale: CGFloat? = nil
) -> Snapshotting {
return .init(
pathExtension: "png",
diffing: .image(
precision: precision, perceptualPrecision: perceptualPrecision, scale: scale)
diffing: .image(scale: scale)
)
}
}
Expand All @@ -96,7 +80,7 @@
private let imageContextBitsPerComponent = 8
private let imageContextBytesPerPixel = 4

private func compare(_ old: UIImage, _ new: UIImage, precision: Float, perceptualPrecision: Float)
private func compare(_ old: UIImage, _ new: UIImage)
-> String?
{
guard let oldCgImage = old.cgImage else {
Expand Down Expand Up @@ -130,36 +114,9 @@
return "Newly-taken snapshot's data could not be loaded."
}
if memcmp(oldData, newerData, byteCount) == 0 { return nil }
if precision >= 1, perceptualPrecision >= 1 {
else {
return "Newly-taken snapshot does not match reference."
}
if perceptualPrecision < 1, #available(iOS 11.0, tvOS 11.0, *) {
return perceptuallyCompare(
CIImage(cgImage: oldCgImage),
CIImage(cgImage: newCgImage),
pixelPrecision: precision,
perceptualPrecision: perceptualPrecision
)
} else {
let byteCountThreshold = Int((1 - precision) * Float(byteCount))
var differentByteCount = 0
// NB: We are purposely using a verbose 'while' loop instead of a 'for in' loop. When the
// compiler doesn't have optimizations enabled, like in test targets, a `while` loop is
// significantly faster than a `for` loop for iterating through the elements of a memory
// buffer. Details can be found in [SR-6983](https://github.com/apple/swift/issues/49531)
var index = 0
while index < byteCount {
defer { index += 1 }
if oldBytes[index] != newerBytes[index] {
differentByteCount += 1
}
}
if differentByteCount > byteCountThreshold {
let actualPrecision = 1 - Float(differentByteCount) / Float(byteCount)
return "Actual image precision \(actualPrecision) is less than required \(precision)"
}
}
return nil
}

private func context(for cgImage: CGImage, data: UnsafeMutableRawPointer? = nil) -> CGContext? {
Expand Down Expand Up @@ -262,9 +219,7 @@
}
}
}
let failingPixelPercent =
Float(failingPixelCount)
/ Float(deltaOutputImage.extent.width * deltaOutputImage.extent.height)
let failingPixelPercent = Float(failingPixelCount) / Float(deltaOutputImage.extent.width * deltaOutputImage.extent.height)
actualPixelPrecision = 1 - failingPixelPercent
}

Expand All @@ -273,9 +228,9 @@
// DeltaE is in a 0-100 scale, so we need to divide by 100 to transform it to a percentage.
let minimumPerceptualPrecision = 1 - min(maximumDeltaE / 100, 1)
return """
The percentage of pixels that match \(actualPixelPrecision) is less than required \(pixelPrecision)
The lowest perceptual color precision \(minimumPerceptualPrecision) is less than required \(perceptualPrecision)
"""
The percentage of pixels that match \(actualPixelPrecision) is less than required \(pixelPrecision)
The lowest perceptual color precision \(minimumPerceptualPrecision) is less than required \(perceptualPrecision)
"""
}

extension CIImage {
Expand All @@ -300,18 +255,15 @@
}

func renderSingleValue(in context: CIContext) -> Float? {
guard let buffer = render(in: context) else { return nil }
defer { buffer.free() }
return buffer.data.load(fromByteOffset: 0, as: Float.self)
guard let buffer = render(in: context) else { return nil }
defer { buffer.free() }
return buffer.data.load(fromByteOffset: 0, as: Float.self)
}

func render(in context: CIContext, format: CIFormat = CIFormat.Rh) -> vImage_Buffer? {
// Some hardware configurations (virtualized CPU renderers) do not support 32-bit float output formats,
// so use a compatible 16-bit float format and convert the output value to 32-bit floats.
guard
var buffer16 = try? vImage_Buffer(
width: Int(extent.width), height: Int(extent.height), bitsPerPixel: 16)
else { return nil }
guard var buffer16 = try? vImage_Buffer(width: Int(extent.width), height: Int(extent.height), bitsPerPixel: 16) else { return nil }
defer { buffer16.free() }
context.render(
self,
Expand All @@ -322,8 +274,7 @@
colorSpace: nil
)
guard
var buffer32 = try? vImage_Buffer(
width: Int(buffer16.width), height: Int(buffer16.height), bitsPerPixel: 32),
var buffer32 = try? vImage_Buffer(width: Int(buffer16.width), height: Int(buffer16.height), bitsPerPixel: 32),
vImageConvert_Planar16FtoPlanarF(&buffer16, &buffer32, 0) == kvImageNoError
else { return nil }
return buffer32
Expand Down
4 changes: 1 addition & 3 deletions Sources/SnapshotTesting/Snapshotting/UIView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,14 @@
/// - traits: A trait collection override.
public static func image(
drawHierarchyInKeyWindow: Bool = false,
precision: Float = 1,
perceptualPrecision: Float = 1,
size: CGSize? = nil,
traits: UITraitCollection = .init()
)
-> Snapshotting
{

return SimplySnapshotting.image(
precision: precision, perceptualPrecision: perceptualPrecision, scale: traits.displayScale
scale: traits.displayScale
).asyncPullback { view in
snapshotView(
config: .init(safeArea: .zero, size: size ?? view.frame.size, traits: .init()),
Expand Down
8 changes: 2 additions & 6 deletions Sources/SnapshotTesting/Snapshotting/UIViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,14 @@
/// - traits: A trait collection override.
public static func image(
on config: ViewImageConfig,
precision: Float = 1,
perceptualPrecision: Float = 1,
size: CGSize? = nil,
traits: UITraitCollection = .init()
)
-> Snapshotting
{

return SimplySnapshotting.image(
precision: precision, perceptualPrecision: perceptualPrecision, scale: traits.displayScale
scale: traits.displayScale
).asyncPullback { viewController in
snapshotView(
config: size.map { .init(safeArea: config.safeArea, size: $0, traits: config.traits) }
Expand Down Expand Up @@ -57,16 +55,14 @@
/// - traits: A trait collection override.
public static func image(
drawHierarchyInKeyWindow: Bool = false,
precision: Float = 1,
perceptualPrecision: Float = 1,
size: CGSize? = nil,
traits: UITraitCollection = .init()
)
-> Snapshotting
{

return SimplySnapshotting.image(
precision: precision, perceptualPrecision: perceptualPrecision, scale: traits.displayScale
scale: traits.displayScale
).asyncPullback { viewController in
snapshotView(
config: .init(safeArea: .zero, size: size, traits: traits),
Expand Down

0 comments on commit a49e295

Please sign in to comment.