capybara-screenshot-diff 1.15.0 → 2.0.0.alpha1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +59 -0
- data/docs/RELEASE_PREP.md +6 -6
- data/docs/thread_safety.md +15 -0
- data/lib/capybara/screenshot/diff/annotation_service.rb +5 -77
- data/lib/capybara/screenshot/diff/area_calculator.rb +5 -54
- data/lib/capybara/screenshot/diff/browser_helpers.rb +5 -119
- data/lib/capybara/screenshot/diff/config_legacy.rb +113 -0
- data/lib/capybara/screenshot/diff/cucumber.rb +1 -1
- data/lib/capybara/screenshot/diff/difference.rb +6 -102
- data/lib/capybara/screenshot/diff/drivers/base_driver.rb +7 -39
- data/lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb +5 -299
- data/lib/capybara/screenshot/diff/drivers/vips_driver.rb +5 -169
- data/lib/capybara/screenshot/diff/drivers.rb +7 -14
- data/lib/capybara/screenshot/diff/image_compare.rb +10 -228
- data/lib/capybara/screenshot/diff/image_preprocessor.rb +5 -70
- data/lib/capybara/screenshot/diff/os.rb +8 -13
- data/lib/capybara/screenshot/diff/reporters/default.rb +5 -3
- data/lib/capybara/screenshot/diff/screenshot_matcher.rb +5 -129
- data/lib/capybara/screenshot/diff/screenshoter.rb +5 -127
- data/lib/capybara/screenshot/diff/stable_screenshoter.rb +5 -104
- data/lib/capybara/screenshot/diff/utils.rb +5 -40
- data/lib/capybara/screenshot/diff/vcs.rb +5 -37
- data/lib/capybara/screenshot/diff/version.rb +7 -1
- data/lib/capybara_screenshot_diff/attempts_reporter.rb +5 -47
- data/lib/capybara_screenshot_diff/cucumber.rb +5 -17
- data/lib/capybara_screenshot_diff/dsl.rb +7 -129
- data/lib/capybara_screenshot_diff/error_with_filtered_backtrace.rb +6 -30
- data/lib/capybara_screenshot_diff/minitest.rb +6 -56
- data/lib/capybara_screenshot_diff/reporters/html.rb +5 -135
- data/lib/capybara_screenshot_diff/rspec.rb +5 -63
- data/lib/capybara_screenshot_diff/screenshot_assertion.rb +39 -157
- data/lib/capybara_screenshot_diff/screenshot_namer.rb +5 -79
- data/lib/capybara_screenshot_diff/snap.rb +5 -64
- data/lib/capybara_screenshot_diff/snap_manager.rb +5 -82
- data/lib/capybara_screenshot_diff/static.rb +3 -5
- data/lib/capybara_screenshot_diff.rb +29 -100
- data/lib/snap_diff/annotation_service.rb +84 -0
- data/lib/snap_diff/area_calculator.rb +56 -0
- data/lib/snap_diff/attempts_reporter.rb +51 -0
- data/lib/snap_diff/browser_helpers.rb +121 -0
- data/lib/snap_diff/capture/viewport.rb +40 -0
- data/lib/snap_diff/comparison.rb +238 -0
- data/lib/snap_diff/comparison_result.rb +104 -0
- data/lib/snap_diff/config.rb +6 -2
- data/lib/snap_diff/deprecation.rb +84 -0
- data/lib/snap_diff/driver.rb +37 -0
- data/lib/snap_diff/drivers/chunky_png_driver.rb +298 -0
- data/lib/snap_diff/drivers/vips_driver.rb +171 -0
- data/lib/snap_diff/drivers.rb +14 -0
- data/lib/snap_diff/dsl.rb +143 -0
- data/lib/snap_diff/error_with_filtered_backtrace.rb +32 -0
- data/lib/snap_diff/image_preprocessor.rb +68 -0
- data/lib/snap_diff/integrations/cucumber.rb +22 -0
- data/lib/snap_diff/integrations/minitest.rb +70 -0
- data/lib/snap_diff/integrations/rspec.rb +68 -0
- data/lib/snap_diff/legacy_shims.rb +99 -0
- data/lib/snap_diff/os.rb +17 -0
- data/lib/snap_diff/reporters/html.rb +143 -0
- data/lib/snap_diff/reporting.rb +53 -0
- data/lib/snap_diff/screenshot_assertion.rb +143 -0
- data/lib/snap_diff/screenshot_matcher.rb +113 -0
- data/lib/snap_diff/screenshot_namer.rb +81 -0
- data/lib/snap_diff/screenshoter.rb +129 -0
- data/lib/snap_diff/snap.rb +66 -0
- data/lib/snap_diff/snap_manager.rb +125 -0
- data/lib/snap_diff/stable_screenshoter.rb +103 -0
- data/lib/snap_diff/static.rb +11 -0
- data/lib/snap_diff/utils.rb +38 -0
- data/lib/snap_diff/vcs.rb +35 -0
- data/lib/snap_diff/version.rb +5 -0
- data/lib/snap_diff.rb +22 -6
- metadata +36 -2
- /data/lib/{capybara_screenshot_diff → snap_diff}/reporters/templates/report.html.erb +0 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
require "snap_diff/comparison_result"
|
|
7
|
+
require "snap_diff/drivers"
|
|
8
|
+
require "snap_diff/image_preprocessor"
|
|
9
|
+
require "capybara/screenshot/diff/reporters/default"
|
|
10
|
+
|
|
11
|
+
# The internal images-holder struct and the driver cache keep their legacy
|
|
12
|
+
# Capybara::Screenshot::Diff homes for now: SnapDiff::Comparison is the
|
|
13
|
+
# comparison class below (ex-ImageCompare), so the struct cannot take the
|
|
14
|
+
# same name. Its SnapDiff home arrives only when it is folded into
|
|
15
|
+
# Comparison as a nested value (v2 design section 2) -- renaming it here
|
|
16
|
+
# would exceed step 5's two approved renames.
|
|
17
|
+
module Capybara
|
|
18
|
+
module Screenshot
|
|
19
|
+
module Diff
|
|
20
|
+
LOADED_DRIVERS = {}
|
|
21
|
+
|
|
22
|
+
# Holds the two images (and their paths/options/driver) being compared.
|
|
23
|
+
class Comparison < Struct.new(:new_image, :base_image, :options, :driver, :new_image_path, :base_image_path)
|
|
24
|
+
def skip_area
|
|
25
|
+
options[:skip_area]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
module SnapDiff
|
|
33
|
+
# Handles comparison of two images with a focus on performance and accuracy.
|
|
34
|
+
#
|
|
35
|
+
# This class implements a multi-layered optimization strategy for image comparison:
|
|
36
|
+
#
|
|
37
|
+
# 1. Early File-based Checks (Fastest):
|
|
38
|
+
# - Verifies both images exist (raises ArgumentError if not)
|
|
39
|
+
# - Compares file sizes (different sizes → different images)
|
|
40
|
+
# - Performs byte-by-byte comparison for identical files (exact match)
|
|
41
|
+
#
|
|
42
|
+
# 2. Quick Comparison (Fast):
|
|
43
|
+
# - Compares image dimensions (different dimensions → different images)
|
|
44
|
+
# - Performs pixel-by-pixel comparison if dimensions match
|
|
45
|
+
#
|
|
46
|
+
# 3. Detailed Analysis (Slower):
|
|
47
|
+
# - Only performed if quick comparison finds differences
|
|
48
|
+
# - Handles anti-aliasing, color tolerance, and shift detection
|
|
49
|
+
# - Respects skip_area and other comparison parameters
|
|
50
|
+
#
|
|
51
|
+
# This layered approach ensures optimal performance by:
|
|
52
|
+
# - Using the fastest possible method for early rejection
|
|
53
|
+
# - Only performing expensive operations when absolutely necessary
|
|
54
|
+
# - Maintaining high accuracy for complex comparisons
|
|
55
|
+
class Comparison
|
|
56
|
+
TOLERABLE_OPTIONS = [:tolerance, :color_distance_limit, :shift_distance_limit, :area_size_limit].freeze
|
|
57
|
+
|
|
58
|
+
attr_reader :driver, :driver_options
|
|
59
|
+
attr_reader :image_path, :base_image_path
|
|
60
|
+
attr_reader :difference, :error_message
|
|
61
|
+
|
|
62
|
+
def initialize(image_path, base_image_path, options = {})
|
|
63
|
+
@image_path = Pathname.new(image_path)
|
|
64
|
+
@base_image_path = Pathname.new(base_image_path)
|
|
65
|
+
|
|
66
|
+
ensure_files_exist!
|
|
67
|
+
|
|
68
|
+
@driver_options = options.freeze
|
|
69
|
+
@driver = Drivers.for(@driver_options)
|
|
70
|
+
@without_tolerable_options = (driver_options.keys & TOLERABLE_OPTIONS).empty?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Performs a quick comparison of two image files.
|
|
74
|
+
#
|
|
75
|
+
# This method is optimized for speed and will return as soon as a difference is found.
|
|
76
|
+
# It's used for fast rejection before performing more expensive comparisons.
|
|
77
|
+
#
|
|
78
|
+
# @return [Boolean]
|
|
79
|
+
# - `true` if images are exactly identical (byte-for-byte match)
|
|
80
|
+
# - `false` if images are different or if a quick difference is detected
|
|
81
|
+
#
|
|
82
|
+
# @note This method will raise ArgumentError if either image file is missing.
|
|
83
|
+
def quick_equal?
|
|
84
|
+
if base_image_path.size == image_path.size
|
|
85
|
+
return true if files_identical?(base_image_path, image_path)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
result, difference = find_difference(quick_mode: true)
|
|
89
|
+
self.difference = difference
|
|
90
|
+
result
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def ensure_files_exist!
|
|
94
|
+
raise ArgumentError, "There is no original (base) screenshot located at #{@base_image_path}" unless @base_image_path.exist?
|
|
95
|
+
raise ArgumentError, "There is no new screenshot located at #{@image_path}" unless @image_path.exist?
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Determines if the images are different according to the comparison rules.
|
|
99
|
+
#
|
|
100
|
+
# This method performs a full comparison if not already done, including any
|
|
101
|
+
# configured tolerances for color differences and shift distances.
|
|
102
|
+
#
|
|
103
|
+
# @return [Boolean]
|
|
104
|
+
# - `true` if the images are different beyond configured tolerances
|
|
105
|
+
# - `false` if the images are considered identical
|
|
106
|
+
#
|
|
107
|
+
# @see #processed
|
|
108
|
+
# @see #analyze_difference
|
|
109
|
+
def different?
|
|
110
|
+
processed.difference.different?
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def dimensions_changed?
|
|
114
|
+
difference.failed_by&.[](:different_dimensions)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def reporter
|
|
118
|
+
@reporter ||= build_reporter
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def processed?
|
|
122
|
+
!!difference
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def processed
|
|
126
|
+
self.difference = find_difference(quick_mode: false) unless processed?
|
|
127
|
+
@error_message ||= reporter.generate
|
|
128
|
+
self
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def without_tolerable_options?
|
|
134
|
+
@without_tolerable_options
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def load_images_and_build_comparison(base_path, new_path, options)
|
|
138
|
+
base_img, new_img = driver.load_images(base_path, new_path)
|
|
139
|
+
Capybara::Screenshot::Diff::Comparison.new(new_img, base_img, options, driver, new_path, base_path)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def image_preprocessor
|
|
143
|
+
@image_preprocessor ||= ImagePreprocessor.new(driver, driver_options)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def find_difference(quick_mode: false)
|
|
147
|
+
# Validate images exist
|
|
148
|
+
return build_null_difference("missing_image") unless images_exist?
|
|
149
|
+
|
|
150
|
+
# Create comparison with preprocessed images
|
|
151
|
+
comparison = load_comparison(base_image_path, image_path, driver_options)
|
|
152
|
+
|
|
153
|
+
analyze_difference(comparison, quick_mode: quick_mode)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Analyzes the comparison and determines if images are different.
|
|
157
|
+
#
|
|
158
|
+
# @param comparison [Capybara::Screenshot::Diff::Comparison] The comparison object containing images to analyze.
|
|
159
|
+
# @param quick_mode [Boolean] When true, performs minimal checks and returns early.
|
|
160
|
+
# In quick mode, returns [is_equal, difference] where:
|
|
161
|
+
# - is_equal is true if images are considered equal
|
|
162
|
+
# - difference is a ComparisonResult object or nil
|
|
163
|
+
# When false, returns a ComparisonResult object directly.
|
|
164
|
+
# @return [Array, ComparisonResult] Result format depends on quick_mode parameter.
|
|
165
|
+
def analyze_difference(comparison, quick_mode: true)
|
|
166
|
+
# Handle dimension differences
|
|
167
|
+
unless driver.same_dimension?(comparison)
|
|
168
|
+
result = ComparisonResult.build_null(comparison, comparison.base_image_path, comparison.new_image_path, {different_dimensions: true})
|
|
169
|
+
return quick_mode ? [false, result] : result
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Handle identical pixels
|
|
173
|
+
if driver.same_pixels?(comparison)
|
|
174
|
+
result = ComparisonResult.build_null(comparison, comparison.base_image_path, comparison.new_image_path)
|
|
175
|
+
return quick_mode ? [true, result] : result
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Handle early return for non-tolerable options
|
|
179
|
+
if quick_mode && without_tolerable_options?
|
|
180
|
+
return [false, nil]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Process difference region
|
|
184
|
+
region = driver.find_difference_region(comparison)
|
|
185
|
+
|
|
186
|
+
# Only create a proper difference object if we've completed the comparison
|
|
187
|
+
quick_mode ? [!region.different?, region] : region
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def difference=(new_difference)
|
|
191
|
+
@error_message = nil
|
|
192
|
+
@reporter = nil
|
|
193
|
+
@difference = new_difference
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def build_reporter
|
|
197
|
+
current_difference = difference || build_null_difference
|
|
198
|
+
Capybara::Screenshot::Diff::Reporters::Default.new(current_difference)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Loads and preprocesses images for detailed comparison.
|
|
202
|
+
#
|
|
203
|
+
# This method is responsible for:
|
|
204
|
+
# 1. Loading both images using the configured driver
|
|
205
|
+
# 2. Applying any necessary preprocessing (cropping, normalization)
|
|
206
|
+
# 3. Creating a Capybara::Screenshot::Diff::Comparison object that holds the image data
|
|
207
|
+
#
|
|
208
|
+
# @param base_path [String,Pathname] Path to the baseline/reference image
|
|
209
|
+
# @param new_path [String,Pathname] Path to the new/candidate image
|
|
210
|
+
# @param options [Hash] Comparison options including:
|
|
211
|
+
# - :crop [Array<Integer>] Optional crop area [x, y, width, height]
|
|
212
|
+
# - :skip_area [Array<Array>] Areas to exclude from comparison
|
|
213
|
+
# - :tolerance [Numeric] Color tolerance threshold
|
|
214
|
+
# @return [Capybara::Screenshot::Diff::Comparison] Prepared comparison object ready for analysis
|
|
215
|
+
# @raise [ArgumentError] If image files are invalid or unreadable
|
|
216
|
+
def load_comparison(base_path, new_path, options)
|
|
217
|
+
comparison = load_images_and_build_comparison(base_path, new_path, options)
|
|
218
|
+
image_preprocessor.process_comparison(comparison)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def build_null_difference(failed_by = nil)
|
|
222
|
+
comparison = Capybara::Screenshot::Diff::Comparison.new(nil, nil, driver_options, driver, image_path, base_image_path).freeze
|
|
223
|
+
ComparisonResult.build_null(comparison, base_image_path, image_path, failed_by)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Check if both images exist
|
|
227
|
+
def images_exist?
|
|
228
|
+
base_image_path.exist? && image_path.exist?
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Check if files are identical by content
|
|
232
|
+
def files_identical?(file1, file2)
|
|
233
|
+
FileUtils.compare_file(file1, file2)
|
|
234
|
+
rescue SystemCallError, IOError
|
|
235
|
+
false
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module SnapDiff
|
|
6
|
+
# Represents the result of comparing two images
|
|
7
|
+
#
|
|
8
|
+
# This value object encapsulates the result of an image comparison operation.
|
|
9
|
+
# It follows the Single Responsibility Principle by focusing solely on representing
|
|
10
|
+
# the difference state, including:
|
|
11
|
+
# - Whether images are different or equal
|
|
12
|
+
# - Why they differ (dimensions, pixels, etc.)
|
|
13
|
+
# - The specific region of difference
|
|
14
|
+
# - Whether differences are tolerable based on configured thresholds
|
|
15
|
+
#
|
|
16
|
+
# As part of the layered comparison architecture, this class represents the final
|
|
17
|
+
# output of the comparison process, containing all data needed for reporting.
|
|
18
|
+
class ComparisonResult < Struct.new(:region, :meta, :comparison, :failed_by, :base_image_path, :image_path, keyword_init: nil)
|
|
19
|
+
def self.build_null(comparison, base_image_path, new_image_path, failed_by = nil)
|
|
20
|
+
ComparisonResult.new(
|
|
21
|
+
nil,
|
|
22
|
+
{difference_level: nil, max_color_distance: 0},
|
|
23
|
+
comparison,
|
|
24
|
+
failed_by,
|
|
25
|
+
base_image_path,
|
|
26
|
+
new_image_path
|
|
27
|
+
).freeze
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def different?
|
|
31
|
+
failed? || !(blank? || tolerable?)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def equal?
|
|
35
|
+
!different?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def failed?
|
|
39
|
+
!!failed_by
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def options
|
|
43
|
+
comparison.options
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def tolerance
|
|
47
|
+
options[:tolerance]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def skip_area
|
|
51
|
+
comparison.skip_area
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def area_size_limit
|
|
55
|
+
options[:area_size_limit]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def blank?
|
|
59
|
+
region.nil? || region_area_size.zero?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def region_area_size
|
|
63
|
+
@region_area_size ||= region&.size || 0
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def ratio
|
|
67
|
+
meta[:difference_level]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def to_h
|
|
71
|
+
{area_size: region_area_size, region: coordinates}.merge!(meta)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def coordinates
|
|
75
|
+
region&.to_edge_coordinates
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# One-line debugging summary with the difference metrics.
|
|
79
|
+
# (Error messages use #to_h — see Reporters::Default#build_error_message.)
|
|
80
|
+
def inspect
|
|
81
|
+
"#<#{self.class.name} different=#{different?} failed_by=#{failed_by.inspect} " \
|
|
82
|
+
"area_size=#{region_area_size} region=#{coordinates.inspect} " \
|
|
83
|
+
"difference_level=#{ratio.inspect} " \
|
|
84
|
+
"base=#{original_image_path} new=#{new_image_path}>"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def tolerable?
|
|
88
|
+
!!((area_size_limit && area_size_limit >= region_area_size) || (tolerance && tolerance >= ratio))
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Path accessors for backward compatibility
|
|
92
|
+
def new_image_path
|
|
93
|
+
image_path || comparison&.new_image_path
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def original_image_path
|
|
97
|
+
base_image_path || comparison&.base_image_path
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def diff_mask
|
|
101
|
+
meta[:diff_mask]
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
data/lib/snap_diff/config.rb
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# The MAPPING below references the legacy modules, so they must be loaded
|
|
4
|
-
|
|
3
|
+
# The MAPPING below references the legacy modules, so they must be loaded
|
|
4
|
+
# first. Requires config_legacy directly (a leaf, see its own header
|
|
5
|
+
# comment), not the capybara_screenshot_diff umbrella -- config.rb is
|
|
6
|
+
# required by both snap_diff.rb and (indirectly) capybara_screenshot_diff.rb,
|
|
7
|
+
# and neither of those may lead back here.
|
|
8
|
+
require "capybara/screenshot/diff/config_legacy"
|
|
5
9
|
|
|
6
10
|
module SnapDiff
|
|
7
11
|
# Flat, additive consolidation of every existing
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SnapDiff
|
|
4
|
+
# @api private
|
|
5
|
+
#
|
|
6
|
+
# Internal until the v2 namespace transition; not a public contract.
|
|
7
|
+
#
|
|
8
|
+
# Warn-once-per-subject deprecation helper. Dormant: nothing in the
|
|
9
|
+
# current codebase calls this yet -- it exists so the gated v2 shim layer
|
|
10
|
+
# (ADR-004's +const_missing+-based legacy constant shim and the
|
|
11
|
+
# mattr_accessor-to-Config method shims) has pre-tested warning
|
|
12
|
+
# machinery to call into once it lands.
|
|
13
|
+
module Deprecation
|
|
14
|
+
# Emission channel: Kernel#warn, not a direct +$stderr.puts+.
|
|
15
|
+
#
|
|
16
|
+
# Kernel#warn delegates to +Warning.warn+ (Ruby >= 2.4), so anything
|
|
17
|
+
# that hooks +Warning.warn+ -- a test suite that raises on warnings, a
|
|
18
|
+
# custom log formatter, Ruby's own -W flag -- sees these messages the
|
|
19
|
+
# same way it sees every other Ruby warning. Writing straight to
|
|
20
|
+
# +$stderr+ would bypass that hook entirely and be invisible to any
|
|
21
|
+
# caller who has customized +Warning+ behavior.
|
|
22
|
+
MUTEX = Mutex.new
|
|
23
|
+
@seen = {}
|
|
24
|
+
|
|
25
|
+
class << self
|
|
26
|
+
# Emit a deprecation warning for +subject+, exactly once per unique
|
|
27
|
+
# +subject+ per process.
|
|
28
|
+
#
|
|
29
|
+
# @param subject [String] the deprecated old-namespace name being
|
|
30
|
+
# referenced, e.g. "Capybara::Screenshot::Diff::ImageCompare"
|
|
31
|
+
# @param replacement [String] the new-namespace name to use instead
|
|
32
|
+
# @param category [Symbol] what kind of thing moved, for the human
|
|
33
|
+
# reading the message (e.g. :constant, :config)
|
|
34
|
+
# @return [void]
|
|
35
|
+
def warn(subject, replacement, category:)
|
|
36
|
+
return if SnapDiff.silence_deprecations?
|
|
37
|
+
|
|
38
|
+
first_time = MUTEX.synchronize do
|
|
39
|
+
@seen.key?(subject) ? false : (@seen[subject] = true)
|
|
40
|
+
end
|
|
41
|
+
return unless first_time
|
|
42
|
+
|
|
43
|
+
Kernel.warn(message_for(subject, replacement, category))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @api private
|
|
47
|
+
#
|
|
48
|
+
# Clears the seen-set. For tests only -- lets each example assert
|
|
49
|
+
# "warns once" from a clean slate instead of leaking state across
|
|
50
|
+
# the suite.
|
|
51
|
+
# @return [void]
|
|
52
|
+
def reset!
|
|
53
|
+
MUTEX.synchronize { @seen.clear }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def message_for(subject, replacement, category)
|
|
59
|
+
"[snap_diff deprecation] `#{subject}` is deprecated (#{category}); " \
|
|
60
|
+
"use `#{replacement}` instead."
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
class << self
|
|
66
|
+
# @api private
|
|
67
|
+
attr_accessor :silence_deprecations
|
|
68
|
+
|
|
69
|
+
# @api private
|
|
70
|
+
#
|
|
71
|
+
# @return [Boolean] true if deprecation warnings should be suppressed,
|
|
72
|
+
# either via the {silence_deprecations} accessor or the
|
|
73
|
+
# SNAP_DIFF_SILENCE_DEPRECATIONS env var (truthy = "1"/"true").
|
|
74
|
+
def silence_deprecations?
|
|
75
|
+
!!silence_deprecations || truthy_env?(ENV["SNAP_DIFF_SILENCE_DEPRECATIONS"])
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def truthy_env?(value)
|
|
81
|
+
%w[1 true].include?(value.to_s.downcase)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SnapDiff
|
|
4
|
+
# Shared default behavior for image-processing drivers.
|
|
5
|
+
#
|
|
6
|
+
# Replaces the old +Capybara::Screenshot::Diff::Drivers::BaseDriver+
|
|
7
|
+
# superclass (ADR-004 v2 step 4): concrete drivers +include Driver+
|
|
8
|
+
# instead of inheriting. Method names are intentionally unchanged from
|
|
9
|
+
# v1 — see dissent #4 in the v2 architecture design.
|
|
10
|
+
module Driver
|
|
11
|
+
PNG_EXTENSION = ".png"
|
|
12
|
+
|
|
13
|
+
def same_dimension?(comparison)
|
|
14
|
+
dimension(comparison.base_image) == dimension(comparison.new_image)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def height_for(image)
|
|
18
|
+
image.height
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def width_for(image)
|
|
22
|
+
image.width
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def image_area_size(image)
|
|
26
|
+
width_for(image) * height_for(image)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def dimension(image)
|
|
30
|
+
[width_for(image), height_for(image)]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def supports?(feature)
|
|
34
|
+
respond_to?(feature)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|