Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion docs/UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,8 @@ This means you can migrate your codebase incrementally **now**, before opting in

### Deprecation Warnings

v2.0 emits two different things, and it is worth knowing which is which.
v2.0 emits three different things, and it is worth knowing which is which. The first two are
about the old namespaces; the third is about the driver features 2.1 removes.

#### 1. The migration notice — one line per process

Expand Down Expand Up @@ -199,6 +200,39 @@ ErrorWithFilteredBacktrace, ScreenshotAssertion, AssertionRegistry}`;
canonical `snap_diff*` require**. Under the v1 entry points — what an unmigrated app actually
uses — they are eagerly defined and silent, like everything in the next section.

#### 3. Removal warnings — the driver half, removed in 2.1

The warnings above are about *names*. These are about *features*: 2.1 makes **libvips the only
image backend** and deletes the rest of the driver machinery. 2.0 still supports all of it and
warns once per process per subject, through the same channel and the same silencing switches.

| You will see it when you… | Removed in 2.1 | Do this instead |
|---|---|---|
| select the ChunkyPNG driver — `driver: :chunky_png`, `SnapDiff.config.driver = :chunky_png`, or the legacy `Capybara::Screenshot::Diff.driver =` | the `:chunky_png` driver | add `gem "ruby-vips"` (plus the libvips system package) and drop the option |
| run on `driver: :auto` **without `ruby-vips` installed** | the `:auto` fallback to ChunkyPNG | same — install libvips + `ruby-vips`. This is the case worth reading twice: nothing in your setup says `chunky_png`, so the warning is the only sign that 2.1 will break this process |
| set `shift_distance_limit` — globally or per screenshot | `shift_distance_limit` (ChunkyPNG-only) | `median_filter_window_size`, `tolerance`, or `color_distance_limit` — see [Configuration](configuration.md#allowed-shift-distance) |
| read `SnapDiff::Drivers.loaded` (the custom-driver registry) | the registry | nothing — custom drivers are removed, see below |
| read `SnapDiff::Drivers.available` | driver detection | require `ruby-vips` instead of branching on a detected list |
| `include SnapDiff::Driver` in your own driver class | the driver mixin | nothing — see below |

```
[snap_diff deprecation] `driver: :auto` selected chunky_png because libvips is not available in this process. The chunky_png driver is REMOVED in 2.1, when libvips (the `ruby-vips` gem) becomes required -- install it now, or this setup stops comparing on 2.1. See docs/drivers.md. Silence with `SnapDiff.silence_deprecations = true` or SNAP_DIFF_SILENCE_DEPRECATIONS=1. (shown once per process) (called from /app/test/test_helper.rb:12)
```

**Custom drivers have no migration path.** The whole abstraction goes: the `SnapDiff::Driver`
mixin, the `SnapDiff::Drivers.loaded` registry, `SnapDiff::Drivers.available` /
`SnapDiff::Utils.detect_available_drivers`, and selecting a driver by name. Nothing replaces
them, and this guide is not going to pretend otherwise — if you maintain a third-party driver,
say so on [#166](https://github.com/snap-diff/snap_diff-capybara/issues/166) before 2.1 ships.

Three spots on the same chopping block stay silent: the legacy
`Capybara::Screenshot::Diff::LOADED_DRIVERS` / `::AVAILABLE_DRIVERS` aliases are plain constants
with nothing to hook (use `SnapDiff::Drivers.loaded` / `.available` to hear the warning);
`SnapDiff::Drivers.for` is not warned on at all — the gem itself calls it for every comparison,
so warning there would fire on setups that are not affected by anything on this list; and
detection (`SnapDiff::Drivers.detect_available` / `SnapDiff::Utils.detect_available_drivers`)
runs at load, before any user code.

#### Silent by design

Some legacy names never warn individually, and that is deliberate — the migration notice above is
Expand Down
11 changes: 10 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ Just `require 'snap_diff/integrations/minitest'` (legacy: `capybara_screenshot_d
| Setting | When to use |
|---------|-------------|
| `perceptual_threshold` | Anti-aliasing false positives across OS/browser versions |
| `shift_distance_limit` | Content shifts by a few pixels (ChunkyPNG only) |
| `shift_distance_limit` | Content shifts by a few pixels (ChunkyPNG only — **removed in 2.1**) |
| `area_size_limit` | Allow small diff regions below a pixel count |
| `color_distance_limit` | Fine-tune raw RGB channel tolerance |
| `median_filter_window_size` | Smooth noise before comparison (VIPS only) |
Expand Down Expand Up @@ -313,6 +313,15 @@ Capybara::Screenshot::Diff.color_distance_limit = 42

### Allowed shift distance

> **Removed in 2.1.** `shift_distance_limit` is implemented only by the ChunkyPNG driver,
> and 2.1 removes that driver — libvips becomes the only backend. Setting it anywhere
> (`SnapDiff.config.shift_distance_limit =`, the legacy
> `Capybara::Screenshot::Diff.shift_distance_limit =`, or `screenshot 'index',
> shift_distance_limit: 2`) warns once per process in 2.0. There is no vips equivalent:
> use `median_filter_window_size` (the faster answer to the same problem — see
> [Drivers](drivers.md#median-filter-size-vips-only)), `tolerance`, or
> `color_distance_limit`.

Sometimes you want to allow small movements in the images. For example, jquery-tablesorter
renders the same table slightly differently sometimes. You can set set the shift distance
threshold for the comparison using the `shift_distance_limit` option to the `screenshot`
Expand Down
39 changes: 39 additions & 0 deletions docs/drivers.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,41 @@
> [Custom drivers](snapdiff.md#custom-drivers) for the `SnapDiff::Driver` mixin and how
> registration in `SnapDiff::Drivers.loaded` works.

## Removed in 2.1: everything on this page except VIPS

2.1 makes **libvips the only backend**. 2.0 is the transitional release — all of the
following still works, and warns once per process naming 2.1. Silence the warnings with
`SnapDiff.silence_deprecations = true` or `SNAP_DIFF_SILENCE_DEPRECATIONS=1`.

| Removed in 2.1 | What to do in 2.0 |
|---|---|
| the `:chunky_png` driver | add `gem "ruby-vips"` to your Gemfile and drop `driver: :chunky_png` |
| `driver: :auto` (and the `:auto` default) | with one backend there is nothing to choose; install `ruby-vips` and the default just works |
| `shift_distance_limit` | ChunkyPNG-only. Use `median_filter_window_size`, `tolerance` or `color_distance_limit` — see [Configuration](configuration.md#allowed-shift-distance) |
| `SnapDiff::Driver` (the custom-driver mixin) | nothing — see below |
| `SnapDiff::Drivers.loaded` (the registry) | nothing — see below |
| `SnapDiff::Drivers.available` (driver detection) | require `ruby-vips` instead of branching on a detected list |

Three related names on the same chopping block stay **silent**, and deliberately so:
`SnapDiff::Drivers.for` (the gem calls it for every comparison — warning there would fire on
setups that nothing in this table affects), `SnapDiff::Drivers.detect_available` /
`SnapDiff::Utils.detect_available_drivers` (run at load, before any user code), and the legacy
`Capybara::Screenshot::Diff::LOADED_DRIVERS` / `::AVAILABLE_DRIVERS` constant aliases (plain
constants, nothing to hook). Reach the same values through `.loaded` / `.available` and you
will hear about them.

**libvips becomes a hard requirement.** Install it with your system package manager
(`brew install vips`, `apt-get install libvips`) and add `gem "ruby-vips"`. A 2.1 process
without it cannot compare images at all.

**Custom drivers: there is no migration path.** The driver abstraction is removed whole —
the `SnapDiff::Driver` mixin, the `SnapDiff::Drivers.loaded` registry, and driver
selection by name. Third-party drivers stop working in 2.1 and nothing replaces them
(the decision was made deliberately: no measurable demand, and one backend is what keeps
the comparison engine honest). If you maintain one, say so on
[the issue tracker](https://github.com/snap-diff/snap_diff-capybara/issues) before 2.1
ships — that is the only thing that can change this.

## Perceptual color comparison (VIPS only)

By default, color differences are measured using raw RGB channel distance. This can produce
Expand Down Expand Up @@ -52,6 +87,10 @@ There are several options to setup active driver: `:auto`, `:chunky_png` and `:v
* `:auto` - will try to load `:vips` if there is gem `ruby-vips`, in other cases will load `:chunky_png`
* `:chunky_png` and `:vips` will load correspondent driver

> **2.1 keeps only `:vips`.** `:auto` and `:chunky_png` are removed; each warns once per
> process in 2.0. If `:auto` is quietly running you on ChunkyPNG today (no `ruby-vips`
> installed), the warning says so — that is the setup 2.1 breaks.

## Enable VIPS image processing

[Vips](https://www.rubydoc.info/gems/ruby-vips/Vips/Image) driver provides a faster comparison,
Expand Down
12 changes: 10 additions & 2 deletions docs/snapdiff.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,8 @@ integration require; a few objects need their own require, noted below.
| `SnapDiff::ExpectationNotMet` | A screenshot did not match its baseline |
| `SnapDiff::UnstableImage` | No stable capture within `stability_time_limit` / `wait` |
| `SnapDiff::WindowSizeMismatchError` | Browser window is not the configured `window_size` |
| `SnapDiff::Driver` | Mixin with the shared driver defaults (`require "snap_diff/driver"`) |
| `SnapDiff::Drivers` | Driver factory and registry — `.for`, `.loaded`, `.available` |
| `SnapDiff::Driver` | Mixin with the shared driver defaults (`require "snap_diff/driver"`) — **removed in 2.1** |
| `SnapDiff::Drivers` | Driver factory and registry — `.for`, `.loaded`, `.available` — **removed in 2.1** |
| `SnapDiff::Reporting` | Process-global reporter lifecycle (`require "snap_diff/reporting"`) |
| `SnapDiff::Reporters::HTML` | The interactive HTML report (`require "snap_diff/reporters/html"`) |
| `SnapDiff::Reporters::Default` | Builds the annotated diff images and the failure message |
Expand Down Expand Up @@ -264,6 +264,14 @@ SnapDiff::Reporting.finalize!

## Custom drivers

> **Removed in 2.1 — no replacement.** libvips becomes the only backend, and the driver
> abstraction goes with the choice: the `SnapDiff::Driver` mixin, the
> `SnapDiff::Drivers.loaded` registry, `SnapDiff::Drivers.available`, and selecting a driver
> by name. In 2.0 all of it still works and warns once per process (silence with
> `SnapDiff.silence_deprecations = true` or `SNAP_DIFF_SILENCE_DEPRECATIONS=1`). Nothing
> here migrates to a 2.1 shape — there is no 2.1 shape. If you maintain a driver, say so on
> [#166](https://github.com/snap-diff/snap_diff-capybara/issues/166) before 2.1 ships.

A driver is a plain object that does the image work. Include `SnapDiff::Driver` for the shared
defaults, then implement the operations the comparison engine calls:

Expand Down
8 changes: 8 additions & 0 deletions lib/snap_diff/comparison.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
require "snap_diff/comparison_result"
require "snap_diff/drivers"
require "snap_diff/image_preprocessor"
require "snap_diff/removal"
require "snap_diff/reporters/default"

module SnapDiff
Expand Down Expand Up @@ -53,6 +54,13 @@ def initialize(image_path, base_image_path, options = {})
ensure_files_exist!

@driver_options = options.freeze
# The per-comparison half of the shift_distance_limit removal (the
# global half is Config#shift_distance_limit=). Presence is not enough:
# config.default_options carries the key on EVERY comparison, nil for
# everyone who never set it.
if options[:shift_distance_limit]
Removal.warn_once(:shift_distance_limit, Removal::SHIFT_DISTANCE_LIMIT_REMOVED)
end
@driver = Drivers.for(@driver_options)
@without_tolerable_options = (driver_options.keys & TOLERABLE_OPTIONS).empty?
end
Expand Down
18 changes: 16 additions & 2 deletions lib/snap_diff/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
# Referenced by Config#initialize (screenshoter/manager defaults), which
# runs at the eager Config.new at the bottom of this file, so they must be
# real, already-loaded classes first. Neither requires back here.
require "snap_diff/removal"
require "snap_diff/screenshoter"
require "snap_diff/snap_manager"

Expand Down Expand Up @@ -78,8 +79,11 @@ class Config
manager
].freeze

attr_accessor(*(SETTINGS - [:root]))
attr_reader :root
# shift_distance_limit is excluded from the generated writers and hand
# written below (it announces its 2.1 removal); generating it here too
# would print Ruby's "method redefined" warning on every load.
attr_accessor(*(SETTINGS - %i[root shift_distance_limit]))
attr_reader :root, :shift_distance_limit

def initialize
# Every setting gets its ivar up front (nil-defaulted ones included)
Expand Down Expand Up @@ -112,6 +116,16 @@ def root=(path)
@root = Pathname(path).expand_path
end

# Overrides the generated accessor above to announce the 2.1 removal
# (chunky_png-only, and chunky_png goes too). The writer, not the reader:
# the reader runs on every comparison through #default_options, including
# for the overwhelming majority who never set this. #initialize seeds the
# ivar directly, so booting the gem stays silent.
def shift_distance_limit=(value)
Removal.warn_once(:shift_distance_limit, Removal::SHIFT_DISTANCE_LIMIT_REMOVED) unless value.nil?
@shift_distance_limit = value
end

# --- Derived config (ADR-008 step 7b) -------------------------------
# Read-only values computed from the storage above. They used to live
# on the legacy modules; those now one-line forward here.
Expand Down
26 changes: 6 additions & 20 deletions lib/snap_diff/deprecation.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# frozen_string_literal: true

# SnapDiff.silence_deprecations? -- the one switch that silences BOTH halves
# of the story -- lives in snap_diff/removal.rb, not here: the other half
# (the driver features 2.1 removes) is announced from core files that outlive
# this one, and they cannot depend on a file the same deletion removes.
require "snap_diff/removal"

module SnapDiff
# @api private
#
Expand Down Expand Up @@ -123,24 +129,4 @@ def origin_for(locations)
end
end
end

class << self
# @api private
attr_accessor :silence_deprecations

# @api private
#
# @return [Boolean] true if deprecation warnings should be suppressed,
# either via the {silence_deprecations} accessor or the
# SNAP_DIFF_SILENCE_DEPRECATIONS env var (truthy = "1"/"true").
def silence_deprecations?
!!silence_deprecations || truthy_env?(ENV["SNAP_DIFF_SILENCE_DEPRECATIONS"])
end

private

def truthy_env?(value)
%w[1 true].include?(value.to_s.downcase)
end
end
end
18 changes: 18 additions & 0 deletions lib/snap_diff/driver.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require "snap_diff/removal"

module SnapDiff
# Shared default behavior for image-processing drivers.
#
Expand All @@ -10,6 +12,22 @@ module SnapDiff
module Driver
PNG_EXTENSION = ".png"

# Including this mixin is what makes a custom driver a driver, so it is
# where a custom-driver author can be told that 2.1 removes the whole
# abstraction. Scoped to drivers that are NOT the gem's own two: both
# bundled drivers include it themselves, and warning there would fire on
# every plain vips setup -- about code the user does not own.
def self.included(base)
return if base.name.to_s.start_with?("SnapDiff::")

Removal.warn_once(
:driver_mixin,
"`include SnapDiff::Driver` (in #{base.name || base.inspect}) is REMOVED in 2.1: the " \
"driver abstraction goes away and libvips becomes the only backend, so custom drivers " \
"stop working. There is no replacement -- see docs/drivers.md."
)
end

def same_dimension?(comparison)
dimension(comparison.base_image) == dimension(comparison.new_image)
end
Expand Down
43 changes: 38 additions & 5 deletions lib/snap_diff/drivers.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require "snap_diff/removal"

module SnapDiff
# utils.rb requires THIS file at its top. Requiring it back at load time
# made Ruby shout "circular require considered harmful" under $VERBOSE --
Expand All @@ -19,14 +21,34 @@ def self.for(driver_options = {})
Utils.find_driver_class_for(driver_option).new
end

# @api private
#
# The registry itself, unannounced: driver name => driver class, filled
# lazily by Utils.find_driver_class_for and mutated in place. The gem's
# own reads go through HERE rather than through .loaded, so the removal
# warning below stays a signal about USER code -- a gem that warns at
# itself teaches people to ignore its warnings.
def self.registry
@registry ||= {}
end

# Canonical driver-class cache (ADR-008 step 5b, ex
# Capybara::Screenshot::Diff::LOADED_DRIVERS): driver name => driver
# class, filled lazily by Utils.find_driver_class_for. Mutated in
# place -- including by user registration through the legacy constant,
# which legacy_shims pins as an EAGER same-object alias of this hash
# (a lazy copy would silently drop such registrations).
# class. Mutated in place -- including by user registration through the
# legacy constant, which legacy_shims pins as an EAGER same-object alias
# of this hash (a lazy copy would silently drop such registrations).
#
# THE documented custom-driver registration point (docs/snapdiff.md), so
# a custom-driver author has to hear that 2.1 takes it away.
def self.loaded
@loaded ||= {}
Removal.warn_once(
:drivers_loaded,
"`SnapDiff::Drivers.loaded` is REMOVED in 2.1 together with the rest of the driver " \
"abstraction (`SnapDiff::Driver`, `SnapDiff::Drivers.available`, `driver: :auto`): " \
"libvips becomes the only backend and custom drivers are no longer supported. " \
"See docs/drivers.md."
)
registry
end

# Which image drivers this process can actually load, in preference
Expand Down Expand Up @@ -76,7 +98,18 @@ def self.detect_available
# than caching, because the constant is the published stubbing point
# (image_compare_test stubs it to [] to exercise the no-drivers error
# path).
#
# Detection only exists because there is a choice of backend to detect;
# 2.1 removes the choice, so it warns. The gem's own callers read
# AVAILABLE_DRIVERS directly -- same value, same stubbing point, no
# warning at itself.
def self.available
Removal.warn_once(
:drivers_available,
"`SnapDiff::Drivers.available` is REMOVED in 2.1: with libvips the only backend there " \
"is nothing left to detect. Require the `ruby-vips` gem instead of branching on this " \
"list. See docs/drivers.md."
)
AVAILABLE_DRIVERS
end
end
Expand Down
5 changes: 4 additions & 1 deletion lib/snap_diff/legacy_shims.rb
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,10 @@ def screenshot_area_abs
module Diff
# EAGER same-object aliases of canonical values (see header for why
# each one is eager rather than a warn-once const_missing shim).
LOADED_DRIVERS = SnapDiff::Drivers.loaded
# .registry, not .loaded: this alias is assigned at load time by the
# gem itself, and .loaded announces its own 2.1 removal. Same object
# either way -- which is the whole point of the alias.
LOADED_DRIVERS = SnapDiff::Drivers.registry
AVAILABLE_DRIVERS = SnapDiff::Drivers::AVAILABLE_DRIVERS
Comparison = SnapDiff::Comparison::Images
VERSION = SnapDiff::VERSION
Expand Down
Loading
Loading