2.1: delete the legacy namespace trees and the driver abstraction - #8
Closed
pftg wants to merge 370 commits into
Closed
2.1: delete the legacy namespace trees and the driver abstraction#8pftg wants to merge 370 commits into
pftg wants to merge 370 commits into
Conversation
* moved raising error per tests * refact: adds rspec and cucumber test integration * upgrades minitest * refact: adds depreacations * chore: adds deprecation warnings * refactoring: cleanup and fixes * refact: renames validate_screenshots to verify to mock rspec/mock * refact: drops unsed code * refact: drops unsed code * build: fix tests with updates (snap-diff#117) * build: optimize time-limit per jruby (snap-diff#118) * build: increase time limits per jruby * refact: move to separate action to setup ruby * build: setup jruby config to make it pass * adds more jruby options (snap-diff#120) * refact: cleanup test workflow * removed redundant names per steps * doc: adds more info about usage of the new API
* build: adds release action * build: bump to 1.9.0
…ult (snap-diff#125) Co-authored-by: Alan Savage <3028205+asavageiv@users.noreply.github.com>
* refact: cleanup redundant code and make explicit names * refact: extracted snap manager * refact: cleanup * refact: removes usage of the filesystem to access to the screenshots * chore: cleanup lint * wip * cleanup vcs * chore: cleanup vcs * chore: cleanup vcs * refact: use snapshots for screenshoter * test: cleanup tests to use snap manager * chore: fixes lint * test: cleanup tests to use snap manager * refact: removes scrn_path * refact: adds usage of snapshot for take screenshot methods * refcat: snap tracks attempts version * cleanup * chore: cleanup * chore: cleanup * chore: cleanup * refact: move Snap to separate file * refact: extracted annotation * fix: cleanup
* feat: do not expect to lazy loading images Closes: snap-diff#127 * bug: allows to pass wait and override timeout for stable screenshoter Closes: snap-diff#131 * chore: remove redundant operation * chore: reduce requirement of the Rails * chore: remove redundant chunky_png requirement * chore: updates system font
* Relax rails version constraints * Update version.rb
* chore: cleanup * build: generate screenshots on fails * WIP: build: generate screenshots on fails * WIP: build: generate screenshots on fails * WIP: build: generate screenshots on fails * build: makes build works * build: update debug artifcats * build: update test images for CI * Update .gitignore to exclude report image files Added patterns to ignore .webp files alongside .png for reports. This ensures consistency in ignoring generated report files across formats. Helps maintain a cleaner working directory.
…p 1 guard) (snap-diff#223) Subprocess probes that pin, for each documented entry point (capybara_screenshot_diff, capybara_screenshot_diff/minitest, snap_diff, capybara/screenshot/diff): - every SnapDiff::Config::MAPPING setting reads identically through both surfaces (SnapDiff.config.x and the legacy mattr_accessor) and matches the expected default - fail_if_new's ENV["CI"] read happens at require time and is frozen: mutating ENV after the require (even before the first read) is not seen - root's Rails.root / pwd fallback is likewise evaluated at require time and frozen against post-require Rails.root reassignment or chdir - Diff.default_options[:wait] is the opposite: it reads Capybara.default_max_wait_time live, at call time These are green on current master by design: they pin CURRENT behavior so the coming ADR-005 storage inversion (SnapDiff::Config becoming the single store, config_legacy flipped to delegating writers) goes red if it shifts when any default is evaluated or what it evaluates to. Gate-checked: temporarily hoisting the fail_if_new ENV read from class-body eval to a lazy read-time default turned 8 of the 12 probes red; reverting restored green.
… step 1) (snap-diff#224) * refactor: SnapDiff::Config becomes the single config storage (ADR-008 step 1) Inverts config storage ownership: SnapDiff::Config now holds every setting as instance state on the eager SnapDiff.config singleton, and the legacy Capybara::Screenshot / Capybara::Screenshot::Diff accessors are thin delegators generated from Config::MAPPING (singleton + instance methods, mirroring the old mattr_accessor surface; root keeps its reader-only instance asymmetry and its Pathname-coercing writer, now in Config#root=). Require topology: snap_diff/config is the new leaf (it predefines the empty legacy module skeleton, same technique as legacy_shims.rb, so MAPPING's module references resolve); config_legacy requires it, and the old config.rb -> config_legacy edge is gone. Graph stays acyclic. Default timing (pinned by config_default_timing_test.rb, all 12 green): fail_if_new (ENV["CI"]) and root (Rails.root/pwd) evaluate in Config#initialize at the eager Config.new at require time of the leaf -- the same load moment the mattr default blocks ran at. default_options[:wait] stays a live method-body read of Capybara.default_max_wait_time. * test: per-test config isolation snapshots SnapDiff.config ivars The global-state snapshot/restore in test_helper walked the legacy modules' class_variables, which no longer exist after the storage inversion -- it would have silently become a no-op. Snapshot the single storage (SnapDiff.config's instance variables) instead; one storage means this covers both surfaces. * fix: Config#initialize creates every mapped ivar up front Nil-defaulted settings had no ivar until first write, so test_helper's per-test ivar snapshot missed them and teardown could not restore them -- a legacy write to e.g. tolerance mid-test leaked into the next test. Pre-set all MAPPING keys to nil before the explicit defaults so the full ivar set always exists. Also drops the now-unused active_support attribute_accessors require in snap_manager (nothing in lib uses mattr_* since the storage inversion). * test: rework MAPPING completeness guard for the inverted storage The old reflection test derived settings from mattr class variables, which no longer exist -- it passed vacuously. Replace with two directions: every legacy singleton writer must be mapped (a future mattr_accessor or hand-rolled writer on the legacy modules would create unmapped storage), and SnapDiff.config must store exactly one ivar per MAPPING key (red repro for the ivar-initialization fix; also guards the test_helper snapshot completeness).
…p-diff#225) SnapDiff::Error / ExpectationNotMet / UnstableImage / WindowSizeMismatchError now live in lib/snap_diff/errors.rb; the old CapybaraScreenshotDiff names are eager same-object aliases (not const_missing shims, so rescue and defined?/const_defined? feature detection behave exactly as before). All gem-internal raise/rescue sites and doc comments flipped to the SnapDiff names. Guard tests pin alias identity, const_defined? visibility, rescue-old-catches-new, and the (unchanged) error hierarchy.
* refactor: move region.rb into lib/snap_diff (pure rename, ADR-008 step 3) Pure git mv so rename detection tracks history; the namespace wrap, forwarder, and reference flips land in the follow-up commit. * refactor: Region gets its SnapDiff home (ADR-008 step 3) - lib/snap_diff/region.rb: wrap in module SnapDiff; keep an EAGER same-object top-level alias (Region = SnapDiff::Region) because user configs build skip_area entries with Region.new and feature-detect via defined?(Region), which a lazy const_missing shim would break (same rationale as ADR-008 step 2's eager error aliases). - old path becomes a one-line forwarder requiring snap_diff/region. - browser_helpers.rb / area_calculator.rb: drop the 'Region has not moved yet' TODO headers, require snap_diff/region. Call sites stay bare Region per house style (like ComparisonResult): inside the SnapDiff nesting they now resolve to SnapDiff::Region. - guards: same-object identity, defined?(Region) truthiness, and an is_a? probe pushing a user-built top-level Region through AreaCalculator's skip_area partition.
…napDiff homes (ADR-008 steps 4+5) (snap-diff#227) * refactor: move reporters/default.rb to its snap_diff home (mv only) Pure git mv so the rename survives with history; the rewrap to SnapDiff::Reporters::Default and the old-path forwarder land in the next commit. * refactor: default reporter becomes SnapDiff::Reporters::Default (ADR-008 step 4) The last old-namespace CLASS dependency in the core: comparison.rb now requires and constructs SnapDiff::Reporters::Default. The old constant Capybara::Screenshot::Diff::Reporters::Default joins the legacy_shims mapping (lazy, warn-once, same-object -- pinned by the MAPPING-driven forwarding and deprecation tests, now 30 pairs). The old file path stays requirable as a forwarder. Test edits are canonical-name migrations only (per the snap-diff#221 policy -- the raise-on-deprecation guard would trip on the old name otherwise): default_test.rb and image_compare_test.rb reference SnapDiff::Reporters::Default; no expectations changed. * refactor: images-holder struct becomes SnapDiff::Comparison::Images (ADR-008 step 5a) Resolves the Comparison name collision: the images-holder struct that lived at Capybara::Screenshot::Diff::Comparison (blocked from a SnapDiff home because SnapDiff::Comparison is the comparator class) is now nested inside the comparator as SnapDiff::Comparison::Images. The old constant joins the legacy_shims mapping (lazy, warn-once, same-object -- 31 pairs now). comparison.rb constructs Images directly; no old-namespace class reference remains in the compare path. Test edits are canonical-name migrations only (per the snap-diff#221 policy; the raise-on-deprecation guard would otherwise trip): the struct constructions in driver_contract_tests, image_preprocessor_test, annotation_service_test and default_test now spell the canonical name. No expectations changed. * refactor: driver registry gets its SnapDiff home (ADR-008 step 5b) SnapDiff::Drivers.loaded is now the canonical driver-class cache (ex Capybara::Screenshot::Diff::LOADED_DRIVERS, which comparison.rb no longer defines); the old constant stays as an EAGER same-object alias in legacy_shims because user code registers custom drivers by mutating the hash in place -- a lazy copy would silently drop registrations, and warning on a supported surface would be noise. SnapDiff::Drivers.available is the canonical reader for the detected drivers list; the value itself stays on the eager Capybara::Screenshot::Diff::AVAILABLE_DRIVERS constant (config_legacy detection moment unchanged, and image_compare_test pins that constant as the stub point for the no-drivers error path). Utils' find_driver_class_for now reads both registries through the canonical accessors, removing the last old-namespace reads from the core compare path. Guard tests pin same-object identity and legacy-write -> canonical-read visibility.
snap-diff#228) ADR-008 step 6, the last alias-safe move of the accepted ADR. Session lifecycle: the per-test AssertionRegistry accessor moves to SnapDiff.session, with SnapDiff.reset and SnapDiff.pending_screenshots_message alongside it in snap_diff/screenshot_assertion.rb (the file that already owns the registry class). CapybaraScreenshotDiff keeps its entire surface as thin forwarders -- singleton_methods + arities dumped before and after, diff is empty (16 public + 1 private, unchanged). Thread.current is fiber-local; that is left exactly as it was (issue snap-diff#217) -- this relocates the canonical accessor, it does not change the semantics. Reporting completion: SnapDiff::Reporting.register(reporter) is the canonical way in, with the append under the existing mutex (issue snap-diff#217 item 2); .reporters stays public and mutable for compatibility. The integrations (Minitest, RSpec, Cucumber) and the HTML reporter's auto-registration now call SnapDiff.session / SnapDiff.reset / SnapDiff.pending_screenshots_message / SnapDiff::Reporting.finalize! / SnapDiff::Reporting.register directly instead of routing through CapybaraScreenshotDiff, and require the snap_diff files they actually use. AssertionRegistry#verify no longer reaches back through CapybaraScreenshotDiff for its own assertions -- same objects, one less round trip through the compat surface. Guards: SnapDiff.session and CapybaraScreenshotDiff.registry are the same object; register lands in the array .reporters exposes; 32 concurrent register calls retain all 32; requiring the HTML reporter auto-registers exactly one.
…s-only (ADR-008 step 7) (snap-diff#229) Part A -- kill `anchor:`. Viewport.prepare! accepted an `anchor:` kwarg no caller ever passed non-nil (both ScreenshotMatcher call sites hardcoded `anchor: nil`), plus a test defending its acceptance. Adding an optional kwarg later is 100% non-breaking, so reserving it now bought nothing and shipped a test guarding dead code. v3's scroll-preservation work designs the real contract. Part B -- the CI gate. New unit test asserts every .rb file under lib/capybara/ and lib/capybara_screenshot_diff/ is nothing but requires, namespace reopening, constant aliases and one-line forwarders into SnapDiff. That is what keeps 3.0 a `git rm` instead of a refactor. One allowlisted file (config_legacy.rb, with a written reason), narrowed by pinning its method inventory so new logic there still reds.
…p 7b) (snap-diff#230) config_legacy.rb was the last file in the v1 trees holding real logic. Step 1 moved config STORAGE to SnapDiff::Config but left the DERIVED values behind, so deleting lib/capybara/ at 3.0 would have lost behaviour rather than being a `git rm`. Moved into SnapDiff::Config as instance methods: - #active? (was Capybara::Screenshot.active?) - #screenshot_area (was Capybara::Screenshot.screenshot_area) - #screenshot_area_abs - #default_options (was Diff.default_options, incl. the vips tolerance 0.001 literal) Inverted so the canonical names hold the bodies: - SnapDiff.compare now builds the Comparison; Diff.compare forwards. - SnapDiff.start now does the two-arg yield; Diff.configure forwards. The Config::MAPPING accessor generator moved to snap_diff/config.rb alongside the storage it delegates to -- same reason legacy_shims.rb generates the legacy constants from the snap_diff side: the generator is code, and the v1 trees must stay alias-only. Net effect: the alias-only gate's ALLOWED_WITH_CODE allowlist is now EMPTY, and config_legacy.rb is checked by the general rule like every other legacy file. The pinned method inventory that narrowed its exemption goes with the exemption. default_options[:wait] stays a call-time read of Capybara.default_max_wait_time -- freezing it into Config#initialize reds all four snap-diff#223 timing guards. Found while moving: the active? precedence rule had no test. Replacing the whole expression with a bare `enabled` kept all 529 unit tests green. Its truth table is now pinned through both the canonical method and the legacy forwarder. AVAILABLE_DRIVERS stays in config_legacy.rb (it is a bare constant assignment, so alias-shaped and never a gate blocker). Moving the storage to SnapDiff::Drivers was tried and reverted: eagerly it needs Utils, which requires Drivers back; lazily it breaks image_compare_test's published stubbing point, because Utils reads Drivers.available and would no longer see a stubbed legacy constant. Legacy public surface verified byte-identical: singleton_methods + parameters for Capybara::Screenshot and Capybara::Screenshot::Diff are unchanged. On the SnapDiff side, .compare gains an explicit signature (was a `(...)` pass-through) and .start loses its &block capture (now a bare yield) -- both a consequence of the bodies landing there, and both call-compatible. rake test:unit 530 runs / 0 failures; rake test 563 runs / 0 failures.
…008 step 8) (snap-diff#231) Every doc page taught legacy names only. `SnapDiff::Minitest::Assertions` and `snap_diff/integrations/{minitest,rspec,cucumber}` worked but appeared in zero docs, and docs/configuration.md contained zero occurrences of "SnapDiff" — so a reader who bought the "SnapDiff is canonical" pitch was routed straight back through legacy requires and includes. - NEW docs/snapdiff.md: the SnapDiff-native page. Quick start for all three integrations with the require paths that actually work, configuration via SnapDiff.configure/.config, the canonical object map, standalone SnapDiff.compare, custom reporters via SnapDiff::Reporting.register, custom drivers via `include SnapDiff::Driver` + the real SnapDiff::Drivers.loaded registration mechanism, and the per-test lifecycle for other frameworks. - docs/configuration.md: SnapDiff.configure shown first as canonical, legacy two-holder block kept alongside, with the one-storage/two-views note. - Cross-links from README (2.0 note + docs list) and docs/UPGRADING.md. - docs/architecture.md: corrected what steps 1-7b made stale — config storage ownership is now SnapDiff::Config (config_legacy installs delegators, not the other way round), errors/Region/reporter-registry/session homes, the drivers registry, and the assertion lifecycle names. - Sweep: short canonical-equivalent notes added to framework-setup.md, drivers.md, reporters.md and ci-integration.md. Those pages keep teaching the still-supported legacy surface. reporters.md also gained the missing `summary` method — Reporting.finalize! calls it unconditionally. Docs only; no lib/ or test/ changes.
…ckers) (snap-diff#232) Every `snap_diff/*` entry a user is told to require now routes through snap_diff.rb, so the docs' own quick start stops handing out a fragment: - snap_diff/dsl requires "snap_diff" (acyclic: snap_diff.rb never requires dsl back), which fixes SnapDiff.configure/.start/.compare and ::VERSION under snap_diff/dsl, snap_diff/integrations/*, snap_diff/static -- and makes the dual-install guard fire on all of them. - snap_diff.rb owns the rest of its documented core: version and the session lifecycle (.session/.reset/.pending_screenshots_message). - Legacy entries load the legacy surface again: capybara/screenshot/diff/ cucumber and capybara_screenshot_diff/static required only the canonical half, leaving CapybaraScreenshotDiff half-present (module defined, .verify/.reset/.reporters/... gone). - Reporters::Default and Diff::Comparison become EAGER same-object aliases instead of lazy const_missing shims, so defined?/const_defined? feature detection works again (same rationale as the error aliases). - SnapDiff::Error is the catch-all docs/snapdiff.md claims: WindowSizeMismatchError and DualInstallError now inherit it. - lib/snap_diff-capybara.rb: `Bundler.require` on the snap_diff-capybara gem was a silent no-op. - Reporting.notify's failure warning matches finalize!'s brand and format. Guards: subprocess probes per canonical and per legacy entry point (surface, dual-install guard, session surface), a discovered-not-listed error-hierarchy test, and eager-identity tests for the two constants.
…ss) (snap-diff#233) Dead code (no caller in lib, test or docs): - ScreenshotAssertion.assert_image_not_changed - ScreenshotNamer#full_name_with_path / #current_group_directory, and the @screenshot_area duplication of Config#screenshot_area they were the only users of - three pure pass-throughs on Reporters::Default (save_annotation_for, annotate_difference, annotate_skip_areas) plus #save - VipsDriver.difference_area - Deprecation.warn's category: kwarg (one value in the tree) Structural: - AssertionRegistry#verify drops the guard verify_screenshots! already applies and stops computing failed_assertions.first outside the branch that reads it - DSL inlines two empty private hops (build_screenshot_assertion, screenshot_namer) Narrative: collapsed the 20 boilerplate ADR-step forwarder headers under lib/capybara* to one line each and stripped migration chronology from snap_diff.rb, dsl.rb, capybara_screenshot_diff.rb, legacy_shims.rb, image_compare.rb, capture/viewport.rb, screenshoter.rb and snap_manager.rb, keeping the live constraints. rake test:unit 532 -> 522, rake test 565 -> 555, both 0 failures.
…cklog) (snap-diff#234) Three independent beta2-review backlog items. 1. Vips::Image leaked into failure messages. ComparisonResult#to_h merged the whole meta hash, including the vips-only :diff_mask image object, so a failing assertion printed "diff_mask":"#<Vips::Image:0x...>". #to_h now excludes it; the object stays reachable via #diff_mask. 2. test/integration/report_screenshot_test.rb skipped all five tests unless RECORD_SCREENSHOTS -- the mode that records baselines rather than verifying them. Baselines existed only for macos/cuprite, so on the only CI platform (linux) there was nothing to compare against. Deleted, along with the orphan baselines. The HTML reporter keeps 18 unit tests, and `rake report:sample` already produces a sample report for eyeballing. 3. The gemspec shipped gems.rb, Rakefile and itself while omitting README.md, leaving a dead ../README.md link in the packaged docs. Replaced the deny-list regex with an allow-list: lib/, docs/, README, LICENSE, CHANGELOG.
…ess) (snap-diff#235) * test: reverse gate -- no core file may depend on the v1 trees legacy_tree_is_alias_only_test.rb proves the v1 trees hold no logic. Nothing proved the mirror image, and it was false: 64 core->legacy edges (6 backward requires, AVAILABLE_DRIVERS, ~55 config reads through the legacy view) meant `git rm lib/capybara*` would break the gem. This gate reports every one as file:line and starts with them all allowlisted, so it lands green and the following commits can only shrink it. Red on master with an empty allowlist; mutation-checked by adding a Capybara::Screenshot::Diff reference to region.rb (gate named it). Comments are ignored (history, not dependency); strings are not. * refactor: driver detection lives on SnapDiff::Drivers SnapDiff::Drivers.available is documented canonical API, but the list it read was defined only in config_legacy.rb, so ruby -Ilib -e 'require "snap_diff/drivers"; SnapDiff::Drivers.available' raised NameError: uninitialized constant Capybara::Screenshot::Diff. It now answers. Detection moved to Drivers.detect_available and runs at drivers.rb load (same load moment in every entry-point path); SnapDiff::Utils .detect_available_drivers one-lines into it, so the documented Utils name and its tests are unchanged. config_legacy keeps AVAILABLE_DRIVERS as an eager same-object alias -- test_helper still reads it at boot, and namespace_forwarding_test still pins assert_same. The stubbing point moves with the value: image_compare_test now stubs SnapDiff::Drivers::AVAILABLE_DRIVERS. Stubbing the legacy alias would only rebind the alias, which is the whole point of cutting the edge. utils.rb requires drivers and drivers now needs Utils at call time, so drivers.rb requires utils at the bottom -- either file can be required first. * refactor: core config reads go to SnapDiff.config, not the legacy view Twenty-nine reads across twelve core files went through Capybara::Screenshot / Capybara::Screenshot::Diff -- the legacy VIEW of a storage that has lived in SnapDiff::Config since snap-diff#230. Same storage, canonical name; behaviour is unchanged and the legacy delegators stay for users. Two follow-ons: - browser_helpers dropped its respond_to?(:window_size) guard: that existed because the mattr_accessor might not be installed yet, and Config always has the attribute. - the fail_if_new error message now tells users 'SnapDiff.config.fail_if_new = false' -- it named an accessor that 3.0 deletes. Tests that STUBBED the legacy accessors now stub SnapDiff.config: a delegator write still reaches the storage, but a stubbed delegator method does not, so those stubs were silently no-ops against the new reads. The legacy accessors keep their own coverage in snap_diff_config_test.rb (every MAPPING writer) and config_default_timing_test.rb. * refactor: core requires point at snap_diff/*, not the v1 forwarders dsl.rb, reporters/html.rb and screenshot_matcher.rb pulled their dependencies through capybara/screenshot/diff/* and capybara_screenshot_diff/* forwarders -- files whose only content is a require of the snap_diff/* unit the core actually wanted. Point at the units directly. snap_diff.rb's own two backward requires are the last ones left and go with the v1-surface consolidation. * refactor: one file holds the v1 surface, and the core names none of it Three legacy edges were left in the canonical core: - lib/snap_diff/config.rb DEFINED the Capybara::Screenshot::Diff module skeleton so Config::MAPPING could name it, and generated the legacy mattr_accessors. After a 3.0 `git rm` that would have survived: the core would still define a phantom v1 namespace, so adopters' `defined?(Capybara::Screenshot::Diff)` checks would keep passing. - lib/snap_diff.rb required config_legacy.rb and image_compare.rb -- the only reason being that those forwarders happened to install the v1 surface for bare `require "snap_diff"` processes. - SnapDiff.start yields the two legacy holders, so it cannot outlive them. All three move to lib/snap_diff/legacy_shims.rb, which now holds the whole v1 surface as code: const_missing forwarders, CONFIG_MAPPING and its generator, the derived forwarders that were in config_legacy.rb (Screenshot.active?, Diff.configure/.compare/.default_options), SnapDiff.start, and the eager AVAILABLE_DRIVERS / Comparison aliases. config_legacy.rb and image_compare.rb are now requires only. Config keeps the setting list as Config::SETTINGS and names nothing legacy; a new test pins CONFIG_MAPPING.keys == SETTINGS so the split cannot drift into storage with no accessor (or the reverse). Behaviour is unchanged, including the awkward part: bare `require "snap_diff"` still answers Capybara::Screenshot.active?, Diff.default_options, Diff::AVAILABLE_DRIVERS and Diff::Comparison, exactly as it did when it reached through the v1 forwarders (verified before/after in a fresh process). The reverse gate's allowlist is now EMPTY. * test: alias-only gate demands a real single-expression forwarder The rule was `body.include?("SnapDiff")` plus a following `end`, which accepted anything that merely mentioned the canonical namespace: def x SnapDiff.config.a ? b : c # a conditional -- real behaviour end def x; SnapDiff.config.a; File.write(...); end # two statements Both are now rejected: a semicolon is never alias-shaped (it is how several statements, or a whole def, hide inside one "line"), and a forwarder body must match the whole of FORWARDER_BODY -- one method-call chain rooted at SnapDiff, at most one argument list and one block. Mutation-checked with exactly those two shapes plus a genuine forwarder, which still passes. * build: gemspec resolves the version from snap_diff/version The gemspec required capybara/screenshot/diff/version and read Capybara::Screenshot::Diff::VERSION -- a build-time dependency on a file 3.0 deletes, so `gem build` would have failed the moment it did. Same value (the legacy constant is an alias of SnapDiff::VERSION), one fewer 3.0 blocker. The legacy constant stays for adopters who read it. Found by the 3.0 dry run; fixed here because it is one line and in scope. * fix: legacy VERSION vanished from six entry points Capybara::Screenshot::Diff::VERSION raised NameError -- and defined? returned nil -- under "snap_diff", snap_diff/dsl, both integrations, snap_diff/static and, worst, the LEGACY capybara_screenshot_diff/dsl. Cause: the constant was assigned by capybara/screenshot/diff/version.rb, and the 3.0-readiness pass stopped the core requiring that forwarder. legacy_shims deliberately omits VERSION from its const_missing map -- it is one of the documented eager exceptions -- so nothing filled the gap and adopters' `defined?` feature detection silently went false. Assign it in legacy_shims next to the other eager aliases: that file is required by every entry point, canonical and legacy, so it is the only place the eager exceptions can actually be eager. version.rb drops the assignment (a second one is a duplicate-constant warning, not a safety net) and becomes a require. The suite could not have caught it: EAGER_USER_FACING was only probed for the four legacy entry points, and VERSION was not in the list at all. New EAGER_EVERYWHERE probe covers all 15 entry points including capybara_screenshot_diff/dsl, which is in none of the existing lists -- exactly why it was the legacy entry that lost VERSION unnoticed. Red without the alias, naming all six. Also corrects two now-false claims in the legacy_shims header: it said AVAILABLE_DRIVERS was aliased in config_legacy.rb (it is aliased in this file) and that VERSION/Comparison were defined by their own forwarder files (nothing requires those anymore). * test: reverse gate catches require_relative escapes LEGACY_REQUIRE anchored straight on the opening quote, so require_relative "../capybara/screenshot/diff/version" passed the gate while genuinely loading the v1 file (confirmed via $LOADED_FEATURES). Every core file sits one directory below lib/, so that is a one-line escape, not a hypothetical. Allow an optional (\.{1,2}/)* -- mutation-checked with exactly that line. Two smaller fixes while here: the header claimed comments are ignored when only WHOLE-LINE ones are (trailing notes on a live code line are scanned, which is the behaviour worth keeping -- the header now says so), and the stale-allowlist check now reports a deleted allowlisted file instead of raising Errno::ENOENT on it. * test: forwarder rule takes simple pass-through args only The tightened rule rejected ternaries and semicolons but left `(.*)` and `{ .* }` unbounded, so both of these still passed as "forwarders": SnapDiff.config.x(File.exist?("/etc/passwd") ? raise("boom") : ENV.fetch("HOME")) SnapDiff.config.tap { |c| File.write("/tmp/pwned", c.inspect); exit 1 } The block case also dodged the semicolon check, because the walk steps over a def's body line without re-examining it -- that scan now runs over every line up front. An argument list is now names, commas, splats and keyword colons, or Ruby's `...` forwarding. No parens, so no nested call; no `.`, so no bare receiver call; no `?`/quote/`=`, so no conditional, literal or assignment. The block form is gone entirely: nothing in these trees has a def left, and an unbounded block is exactly the hole above. Mutation-checked with both shapes above (rejected, the block twice) plus three genuine forwarders that must not false-positive: a bare chain, an argument pass-through, and CapybaraScreenshotDiff.serve(...) -- which the first draft of this rule did reject. * docs: flag the two beta moves that fail silently Both are read-identical and only break on write, so nothing warns: - stubbing Capybara::Screenshot::Diff::AVAILABLE_DRIVERS now only rebinds an alias -- the gem reads SnapDiff::Drivers::AVAILABLE_DRIVERS, so a downstream test stubbing it to [] stops exercising the no-drivers path and passes for the wrong reason; - SnapDiff::Config::MAPPING is gone mid-beta, split into Config::SETTINGS and the @api-private LegacyShims::CONFIG_MAPPING.
…ap-diff#236) * test: move the v1-surface tests into test/legacy/ and add rake test:canonical The five tests whose SUBJECT is the v1 compatibility surface now live in test/legacy/, so the 3.0 deletion is one more path on the same git rm: git rm -r lib/capybara* ... test/legacy A directory rather than a list in the Rakefile: nothing to keep in sync. - rake test unchanged, runs everything (today's gate) - rake test:canonical NEW, everything except test/legacy (the 3.0 gate) - rake test:unit test/unit + test/legacy, so the release gate keeps its coverage (legacy/ marks lifetime, not kind) errors_alias_test.rb was mixed: the four CapybaraScreenshotDiff::* alias pairs are v1 surface, the hierarchy assertions outlive them. Split rather than moved whole -- test/unit/errors_test.rb keeps the two canonical tests verbatim, so no assertion is lost at 3.0. 530 runs, 1519 assertions, 0 failures (unchanged). * test: point the whole canonical suite at SnapDiff names The suite still spoke v1 everywhere, so it would have broken on the 3.0 deletion even though the gem no longer does. Mechanical, no behaviour and no assertion values changed: - harness: test_helper + system_test_case load snap_diff/integrations/* and configure through SnapDiff.config; the support stubs (DSLStub, ScreenshoterStub, TestDoubles, DriverCoverage, NonMinitest) stop reopening gem namespaces and become plain top-level modules - 33 test files were defined inside module Capybara::Screenshot(::Diff) / CapybaraScreenshotDiff -- de-nested to top-level classes, so no bare constant resolves into a namespace 3.0 deletes - 270 legacy constant/accessor/session call sites repointed (CapybaraScreenshotDiff.registry -> SnapDiff.session, .reporters -> SnapDiff::Reporting.reporters, Capybara::Screenshot.root -> SnapDiff.config.root, ...) and 25 legacy require paths - legacy-surface tests now require the v1 entry point themselves, since the shared harness no longer loads it Three claims would have become tautologies under a blind repoint (assert_same SnapDiff.session, SnapDiff.session and friends): they were forwarder-identity claims about the v1 view. Preserved verbatim in the new test/legacy/legacy_forwarders_test.rb together with SnapDiff.start, which yields the two v1 holders and cannot outlive them. rake test:unit 534 runs, 1526 assertions, 0F/0E (was 530/1519) rake test 562 runs, 1571 assertions, 0F/0E/1S (was 558/1564) +4 runs: legacy_forwarders_test keeps the v1 claim where the canonical file also kept its own version (register-appends, reporters_mutex, serve). * test: split the mixed config/entry-point files along the same line Four files asserted the canonical behaviour AND the v1 view of it in one place, so a receiver repoint turned real claims into tautologies. Each is now two files; the v1 half is verbatim, and the canonical half stands on its own after 3.0: - snap_diff_config_test -> + test/legacy/legacy_config_accessors_test (CONFIG_MAPPING completeness, the mattr_accessor round trips, active? through the legacy forwarder, SnapDiff.start) - config_default_timing_test -> + test/legacy/legacy_config_default_timing_test canonical keeps snap_diff + snap_diff/integrations/minitest and reads SnapDiff.config only; the legacy file re-runs the SAME probe scripts under the v1 entries and adds the both-surfaces-agree loop, which is exactly what check_both asserted -- one source of truth, no drift - support_load_probe_test -> + test/legacy/legacy_entry_point_probe_test (advertised v1 constants, the CapybaraScreenshotDiff session surface, EAGER_USER_FACING / EAGER_EVERYWHERE under their OLD names) - errors_alias_test (earlier commit) -> test/unit/errors_test snap_diff-capybara joins CANONICAL_ENTRY_POINTS: 3.0 keeps that entry point (repointed at snap_diff/integrations/minitest), and it was covered only as a legacy entry, so it would have lost all coverage. Also repointed the last legacy call sites the sweep left: the rspec fixtures stubbed Capybara::Screenshot::Diff.pending_if_new, which the core stopped reading in snap-diff#235 -- a silent no-op stub, now SnapDiff.config. rake test:unit 544 runs, 1544 assertions, 0F/0E rake test 572 runs, 1589 assertions, 0F/0E/1S * test: port the canonical claims that only a legacy-surface test was pinning Audit of every assertion moving into test/legacy/, asking: if this file vanished at 3.0, would any CANONICAL behaviour become untested? Two hits, both now duplicated (not moved) into a canonical test -- the v1 originals stay put, they still guard the v1 contract for all of 2.x: - namespace_forwarding_test was the only place proving SnapDiff::Drivers .loaded is ONE hash mutated in place (it asserted the v1 LOADED_DRIVERS constant is that same object, and that registering through it shows up canonically). Utils.find_driver_class_for caches through .loaded, so a copy-returning refactor would break user driver registration silently. -> drivers_test ".loaded is a single hash mutated in place" mutation: `.loaded.dup[...] = ...` -> red, Expected :probe_driver, got nil - the entry-point probe was the only place asserting an entry point defines its advertised CONSTANTS when it is the ONLY require (the f89cea2 bug class) -- but only for the v1 names. -> support_load_probe_test "every canonical entry point defines its advertised constants standalone", same claim over snap_diff/dsl, /integrations/minitest, /integrations/rspec, snap_diff-capybara. Entry-specific, because bare snap_diff carries neither DSL nor reporters by design. mutation: a bogus constant in the list -> red, naming it Also: attempts_reporter_test now requires snap_diff/attempts_reporter -- stable_screenshoter pulls it in lazily and the v1 umbrella was what loaded it eagerly, so it was the one canonical test the deletion actually broke. Judged legacy-only and safe to lose at 3.0: const_missing/eager alias semantics, deprecation warn-once + silencing, CONFIG_MAPPING completeness, the alias-only scan of lib/capybara*, and the CapybaraScreenshotDiff session/reporter forwarders -- every one is about a name 3.0 deletes, and its canonical counterpart is pinned in test/unit/. rake test 574 runs, 1593 assertions, 0F/0E/1S * fix: the canonical gate demanded SnapDiff.start, which 3.0 deletes Caught by independent review: `rake test:canonical` in the deleted tree is 1F, not the 0F I published. CANONICAL_SURFACE listed `start`, applied to all 7 canonical entry points. SnapDiff.start is defined only in lib/snap_diff/legacy_shims.rb:169 and yields the two v1 config holders, so it cannot outlive them (snap-diff#235 decided this). A canonical gate demanding a method 3.0 deletes is a gate that goes red the day the deletion lands -- and I widened it in e789c8a by adding snap_diff-capybara. My own PR body filed .start under "safe to lose at 3.0". mutation (start put back, deleted tree): require "snap_diff" -> missing: start require "snap_diff/dsl" -> missing: start require "snap_diff/integrations/minitest" -> missing: start ... all 7 entry points .start keeps full coverage on the legacy side: legacy_forwarders_test pins what it yields and that it applies a setting, and a new per-entry-point probe in legacy_entry_point_probe_test pins the availability claim the canonical gate used to make -- for the entries that actually keep it. Also, per review: - "bare require never loads the umbrella" moves to legacy_forwarders_test. Its subject is lib/capybara_screenshot_diff.rb; once 3.0 deletes that file the $LOADED_FEATURES grep is empty by construction and the guard can never fail again. (-1 canonical run: 458 -> 457.) - backtrace_filter_test built synthetic paths under lib/capybara_screenshot_diff/. Pure string inputs to a prefix matcher, so no assertion changes -- but one of them named the real file the filter defaults to, which 3.0 deletes. rake test:unit 547 runs, 1550 assertions, 0F/0E/0S rake test 575 runs, 1595 assertions, 0F/0E/1S rake test:canonical 457 runs, 1288 assertions, 0F/0E/1S ... and 457/1288/0F/0E/1S in the deleted tree, identical.
…nap-diff#238) Six gaps a coverage+mutation audit measured, each closed with a test that was proven to go red against the mutation it is meant to catch. The headline is test/unit/deletion_3_0_test.rb: the alias-only gate and the reverse gate are static PROXIES for "git rm the v1 surface and the gem still loads". This runs it -- copies lib/ to a tmpdir, applies the deletion set and the two edits it needs, and requires every canonical entry point in a fresh subprocess. Every probe hard-asserts the deletion is in effect BEFORE it asserts anything about the surface, because a run that cannot tell the deleted tree from the intact one is not evidence. Also: the vips tolerance floor, SnapDiff.compare's default_options merge, Reporters::Default#generate's equal-path cleanup, Viewport.prepare!'s selenium arm, and all four screenshot_area path-segment combinations. No lib/ changes.
…nt deprecation strategy (snap-diff#237) * fix: circular require warning, partial-migration constant crash, silent deprecation strategy Three defects found by running the gem against a real consumer project (jetthoughts.github.io, upgraded in Docker against its committed baselines). 1. drivers.rb <-> utils.rb formed a require cycle. Harmless functionally, but Ruby shouts "circular require considered harmful" with an 18-frame backtrace whenever $VERBOSE is on -- which Rake::TestTask sets by default, i.e. every standard Rails/Minitest suite. Deferred the require into Drivers.for, the only method that needs Utils. 2. A canonical require left most of the v1 constant surface unresolvable, including Capybara::Screenshot::Os, which killed a real suite before a test ran. Two causes, both fixed at the class level: the eager aliases lived in lib/capybara* forwarders that only legacy entry points load (moved to legacy_shims, which every entry point loads), and the const_missing shims named their replacement without loading it (they now require the target unit, and raise a NameError naming the SnapDiff name and UPGRADING.md if it truly cannot load). 3. Exercising all 14 legacy APIs a real setup file touches produced zero warnings: Deprecation.warn was reachable only through const_missing, so config delegators and eager aliases were structurally silent. Added one migration notice per process, fired from every hookable door, and corrected the docs that claimed every legacy name warns. * fix: gate the driver-leaf autoloads on what this process can actually load Review follow-up. The unconditional `autoload :VipsDriver` made `const_defined?(:VipsDriver)` true on a box without ruby-vips, where `const_get` then raises. Neither driver gem is a runtime dependency and the documented v1 pattern is `Diff.driver = :vips if defined?(...Drivers::VipsDriver)`, so that branch started being taken and then dying. v1.12.0 loaded vips_driver.rb only from find_driver_class_for, so `defined?` was nil there. Both leaves are now declared only when their gem is present. Also: `autoload :Utils` instead of a deferred require inside Drivers.for. Removing the bottom-of-file require narrowed the surface -- `require "snap_diff/drivers"` stopped defining SnapDiff::Utils, which drivers_test caught. Autoload breaks the cycle without narrowing anything. And `Capybara::Screenshot::Diff.default_options`, a documented v1 read, is hand-written rather than a generated delegator, so it fired no migration notice. One line, plus its own row in the per-door guard table.
`rake test:canonical` is defined as "exactly what must still pass once test/legacy/ and the v1 trees are gone". Three times in one day a test asserting LEGACY behaviour was written into test/unit/, i.e. into that suite: a canonical surface table demanding the shim-only SnapDiff.start (snap-diff#236), three legacy-constant probes in a canonical file (snap-diff#237), and a pre-existing umbrella guard snap-diff#236 had to relocate. Each would have failed the day the deletion landed, long after its author moved on. Reviews caught all three; the fourth would ship. The test-tree twin of core_tree_has_no_legacy_deps_test.rb: no file under test/unit/ or test/integration/ may require a doomed path, name a v1 namespace constant, or use a shim-only name (SnapDiff.start, .silence_deprecations, SnapDiff::Deprecation, suppress_migration_notice!). test/legacy/ is deliberately not policed -- exercising the legacy surface is its job. Same conventions as the twin: file:line: reason -- `code`, whole-line comments ignored, a vacuity guard, and a sub-test that fails on stale allowlist entries. The allowlist holds two entries, both gates rather than tests of behaviour (deletion_3_0_test.rb, which names the deletion set by construction, and the twin gate's own pattern literal). A third entry means canonical tests are still entangled and needs a decision, not a green build. This file cannot scan itself: a line-level allowlist has to quote the lines it blesses, and every quote is itself an offence -- no fixed point exists.
The stray detector matched a bare /snap_diff/ substring against $LOADED_FEATURES. CI checks this repo out at .../snap_diff-capybara/, so every gem under vendor/bundle matched and the gate aborted on nokogiri. It passed locally only because the dev checkout is named capybara-screenshot-diff. Anchored on /lib/snap_diff(/|.rb).
* ci: exercise the vips driver on JRuby
The JRuby cells install libvips and ruby-vips already -- the setup action
installs libvips unconditionally and gems.rb declares ruby-vips with no
platform guard -- so `Drivers.detect_available` has always reported
`vips, chunky_png` there, and the vips-gated unit tests have always run.
What never ran on JRuby was the integration path: `bin/rake test` leaves
SCREENSHOT_DRIVER unset, and test/system_test_case.rb defaults that to
chunky_png. So capture -> compare -> annotate, plus the vips cache flush
in that file's teardown, were MRI-only.
Point the JRuby cells at vips. Non-JRuby cells get chunky_png spelled
out, which is the default they already had.
Verified locally on JRuby 10.0.6.0 + libvips 8.18.5:
test/unit/drivers/vips_driver_test.rb
50 runs, 104 assertions, 0 failures, 0 errors, 0 skips
test/integration (cuprite, SCREENSHOT_DRIVER=vips)
28 runs, 45 assertions, 0 failures, 0 errors, 1 skips
Identical to the MRI 4.0.6 control on the same machine (28 runs, 45
assertions, 0 failures, 0 errors, 1 skips).
* ci: give the JRuby cells a budget that fits the suite
Separate from the vips switch and a fix for a failure already on master:
every JRuby cell of run 32638878557 (master b1d01af, chunky_png, no
change from this branch) burns all three attempts on
`Timeout of 420000ms hit` and is killed at the 20-minute job cap. The
cells have stopped gating anything.
The suite outgrew the budget -- it is ~600 tests now. Measured on JRuby
10.0.6.0 locally, same machine, same suite, only SCREENSHOT_DRIVER
differing:
vips 598 runs, 1683 assertions, 0 failures, 0 errors -- 390.4s
chunky_png 598 runs, 1683 assertions, 0 failures, 0 errors -- 434.4s
vips is the faster of the two, so this is not driver cost. Raise the
per-attempt budget to 15 minutes and the job cap to 25 so one attempt
plus setup fits with headroom. A passing cell still costs ~8 minutes; a
failing one costs 25 instead of 20, but produces a result rather than a
cancellation.
The retry budget was larger than the job cap on both engines, so the last attempt was always killed partway and the cell reported `cancelled`. Measured on master run 32643567648 (post-snap-diff#243), the 5 JRuby cells: jruby-10.0 rails81 suite 713s, no hang, 1 attempt -> 12m02 pass jruby-10.0 rails71 suite 545s + 6m hang -> 15m t/o; attempt 2 clean at 543s -> 24m20 pass jruby-10.0 rails80 attempt 1 hung -> 15m t/o; attempt 2 killed at the 25m cap -> cancelled jruby-10.0 rails72 same -> cancelled jruby-head rails81 same -> cancelled 3 of 5 JRuby cells gate nothing. The cause is arithmetic, not the driver: max_attempts 3 x timeout_minutes 15 = 45 min against timeout-minutes 25. Attempt 3 could never start, and attempt 2 had only 25 - 15 - 0.7 = 9.3 min to finish a run that measures 9-12 min -- so whether a cell survived a hang came down to which gemfile it drew. MRI has the same shape, smaller: 3x3 = 9 against a cap of 8. - JRuby job cap 25 -> 31, so 1 + 15 + 15 fits. - max_attempts 3 -> 2 on both engines, since 3 was never reachable. A doubly-hung JRuby cell now costs 31 min instead of 25, but today's 25 min buys no verdict at all. Coverage, drivers and the full-ci/cron opt-in model are unchanged. Also adds the concurrency group Lint never had, so superseded PR pushes stop running the linter to completion.
…action (removed in 2.1) (snap-diff#246) 2.0 is the transitional release: the contract is published before it is enforced. The legacy-namespace half of that promise already warns. The driver half -- everything 2.1 removes so that libvips becomes the only backend -- warned about nothing. A user on `driver: :auto` without libvips had no way to learn that 2.1 stops comparing for them. Six warnings, each once per process, each silenceable through the existing switches (`SnapDiff.silence_deprecations`, SNAP_DIFF_SILENCE_DEPRECATIONS): - chunky_png SELECTED, from Utils.find_driver_class_for -- the one funnel every selection surface ends up in (`driver:` per comparison, SnapDiff.config.driver, the legacy Diff.driver=). - `:auto` FALLING BACK to chunky_png because libvips is absent. Same funnel, its own message and key: these users never asked for chunky_png. - shift_distance_limit SET, from Config#shift_distance_limit= and from Comparison#initialize (non-nil only -- default_options carries the key on every comparison). One shared key, so it warns once whichever fires. - Drivers.loaded, the documented custom-driver registration point. - Drivers.available, i.e. driver detection. - `include SnapDiff::Driver` by a class that is not one of the gem's own. NOT warned on, and documented as silent: Drivers.for (the gem calls it for every comparison, so warning there would fire on vips-only setups that nothing here affects), detection itself (runs at load, before user code), and the eager LOADED_DRIVERS / AVAILABLE_DRIVERS constant aliases (nothing to hook). Each entry names the warning-capable equivalent instead. The machinery is SnapDiff::Removal -- same channel, same discipline and the same silencing switches as SnapDiff::Deprecation, in its own file: the legacy shims and their deprecation channel are part of what the deletion removes, while these call sites (utils, config, comparison, drivers) are core files that outlive them and cannot depend on a doomed file. SnapDiff.silence_deprecations moved there for the same reason -- it is the one switch that silences both halves -- which is why the canonical-suite gate no longer lists it as shim-only surface. Two internal seams keep the gem from warning at itself: Drivers.registry (the unannounced registry the gem reads) and reading the AVAILABLE_DRIVERS constant directly instead of .available. test_helper suppresses the channel the same way it already suppresses the migration notice: this suite runs its whole matrix on chunky_png by design. Evidence: 13 subprocess probes ("once per process" cannot be measured inside one long-lived suite), mutation-tested -- dropping the dedup goes 9 red, dropping the silence check 2 red, routing an internal read back through .loaded 5 red, unscoping the mixin hook 5 red, warning on the presence of shift_distance_limit rather than a value 4 red. rake test:unit 585/0, rake test 613/0/1, rake test:canonical 482/0/1, standardrb clean.
2.0 is the transitional release; 2.1 deletes the legacy namespace trees. The migration notice, its pinning test, the reverse gate's constant and the deletion test's name all still said 3.0 -- text that ships to users in 2.0. The notice test now asserts the whole clause rather than the version alone, so a wrong deprecation WINDOW fails it too, not just a wrong removal version.
…nap-diff#248) JRuby joins every live Ruby thread at interpreter teardown (Ruby.tearDown -> ThreadService.teardown -> Thread#join) where MRI just kills them. Puma's reactor thread parks in a native KQueue.poll/epoll_wait inside nio4r that no interrupt can wake, so a Capybara-booted Puma keeps a JRuby process alive forever after an otherwise green run (snap-diff#244). The process that actually hangs is usually not the suite's own: it is one of the Open3.capture2e subprocesses run by the RSpec fixtures, which load support/setup_capybara, boot Puma and Chrome, finish their example and never exit -- the parent then blocks in capture2e with no timeout, which is the reported "stalls mid-run" shape. Capybara's stock :puma block builds its Puma::Server into a block-local and joins it, so nothing keeps a handle and Capybara::Server has no #stop. Register an equivalent block that keeps the handle and stop the server for real once the framework is done. Puma::Server#stop closes the reactor's input queue and wakes the selector, which is the only thing that gets that thread out of the native poll. Ferrum is not implicated: its threads park on Queue#pop, which JRuby interrupts cleanly, so no browser quit is needed.
Deletes everything 2.0 announced as removed in 2.1, in one release: - The v1 namespace trees (lib/capybara/, lib/capybara_screenshot_diff/, the two gem-name entry points, legacy_shims.rb, deprecation.rb, test/legacy/). SnapDiff.configure is the single config entry point; SnapDiff.start yielded the two v1 holders and could not outlive them. - The chunky_png driver and shift_distance_limit (chunky-only, no libvips equivalent). - The whole driver abstraction: the SnapDiff::Driver mixin, the SnapDiff::Drivers registry (.loaded/.available/.for/.registry/ .detect_available), AVAILABLE_DRIVERS, Utils.detect_available_drivers, and :auto selection. Comparison and Screenshoter construct Drivers::VipsDriver directly. - The `driver:` config setting. With one backend it cannot select anything, and accept-and-ignore would let a config claim a backend choice that does not exist. - snap_diff/removal.rb, which existed only to warn about the above. SnapDiff.silence_deprecations goes with it: no channel is left. ruby-vips becomes a real gemspec runtime dependency (>= 2.0, < 3), so a missing binding is a resolver error rather than a runtime one. Fixes a latent bug the deletion exposed: libvips caches loader operations on filename + mtime, and mtime has one-second resolution, so rewriting a screenshot path and re-reading it within the same second served the PREVIOUS image. It was masked because a Comparison built without an explicit driver defaulted to chunky_png, which always re-read the file. VipsDriver#from_file now passes `revalidate: true`; the cache-flush workarounds in system_test_case.rb and vips_driver_test.rb are gone. Gates: legacy_deletion_test (simulated a deletion that has happened) and legacy_tree_is_alias_only_test (its subject is gone) are replaced by removed_surface_test, which asserts absence -- no removed path back under lib/, no removed name defined in a fresh process -- behind a gate line that proves it measured this repo's lib/. The two reverse gates survive, repurposed to catch removed names in strings and docstrings. test:canonical was "everything except test/legacy/", which is now exactly test; the tasks converged and the second name is gone. `rake test` is THE gate. test:benchmark is deleted (it required a script not in this repo).
Member
Author
|
Opened against the wrong remote by mistake; the upstream is snap-diff/snap_diff-capybara. Reopening there. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft. Nothing ships until the 2.1 plan is approved. No version bump, no CHANGELOG entry, no release.
Supersedes snap-diff#240, which was opened before snap-diff#242/snap-diff#245/snap-diff#246/snap-diff#247/snap-diff#248 landed and before the scope grew to include the driver abstraction. Redone from current
origin/master(1cd89c9) rather than rebased — see Rebase or redo.2.0 is the transitional release: both APIs work and everything that dies warns. 2.1 is the single cleanup release. There is no 3.0.
Migrating from 2.0 to 2.1
Measured, not imagined: a real consumer (a Jekyll/Rails site with committed visual baselines, running its suite in Docker) was upgraded end to end against this deletion. 17 lines, two files, zero blockers — 38 runs, 0 failures, 55 screenshots compared, byte-identical before and after. Every v1 API had a canonical equivalent.
That validation covered the namespace half. The driver half is new in this PR and is the part most likely to touch your config — read Drivers and image processing even if you never used the v1 names.
1. Namespaces
Requires
require "capybara_screenshot_diff/minitest"require "snap_diff/integrations/minitest"require "capybara_screenshot_diff/rspec"require "snap_diff/integrations/rspec"require "capybara_screenshot_diff/cucumber"require "snap_diff/integrations/cucumber"require "capybara_screenshot_diff/reporters/html"require "snap_diff/reporters/html"require "capybara/screenshot/diff"require "snap_diff"gem "capybara-screenshot-diff"(Bundler auto-require)gem "snap_diff-capybara"Configuration
Both v1 holders collapse into one object.
SnapDiff.configureis the single config entry point (ADR-008).Capybara::Screenshot.<setting> = …SnapDiff.config.<setting> = …Capybara::Screenshot::Diff.<setting> = …SnapDiff.config.<setting> = …Capybara::Screenshot::Diff.configure { |screenshot, diff| … }SnapDiff.configure { |config| … }SnapDiff.start { |screenshot, diff| … }SnapDiff.configure { |config| … }SnapDiff.startyielded the two v1 holders, so it could not outlive them — removed, not renamed. Same forSnapDiff.silence_deprecationsandSNAP_DIFF_SILENCE_DEPRECATIONS: with no deprecations left to emit there is nothing to silence.Constants and includes
Capybara::Screenshot::Os.nameSnapDiff::Os.nameCapybara::Screenshot::Diff::ImageCompareSnapDiff::ComparisonCapybaraScreenshotDiff::Reporters::HTMLSnapDiff::Reporters::HTMLCapybaraScreenshotDiff::ScreenshotAssertionSnapDiff::ScreenshotAssertioninclude CapybaraScreenshotDiff::DSLinclude CapybaraScreenshotDiff::Minitest::Assertionsinclude SnapDiff::Minitest::Assertions— the two collapse into oneCapybara::Screenshot::Osis the one to grep for. In the real upgrade it was the only hard crash: on 2.0 it raisesNameErrorfrom the shim internals once the require line has been migrated but the constant has not — a partially-migrated setup looks fine (config setters keep working) untilOsaborts the whole suite before a single test runs. On 2.1 it is simply gone.2. Drivers and image processing
libvips is now the only backend, and
ruby-vipsis a gemspec runtime dependency (>= 2.0, < 3). You no longer add it yourself; Bundler installs it. libvips itself is still a system package (brew install vips,apt-get install libvips), and without it the gem cannot compare images at all.Making it a real dependency is the point: before this, neither driver was declared, so a box without either got a runtime error deep in a test run. Now it is a resolver error at
bundle install.gem "ruby-vips"in your GemfileSnapDiff.config.driver = :vips/= :auto/= :chunky_pngscreenshot "index", driver: :vipsshift_distance_limit(anywhere)SnapDiff::Drivers.availableto branch on what is installedSnapDiff::Drivers.loaded[:mine] = MyDriverinclude SnapDiff::Driverin your own driverWhat breaks loudly vs. quietly
Worth knowing before you upgrade:
SnapDiff.config.driver = :vipsraisesNoMethodError: undefined method 'driver='at config time, before any test runs. Loud, greppable, one line to delete.screenshot "index", driver: :vipsis silently ignored — per-screenshot options are a free-form hash, so an unknown key is inert. It does not change behaviour (there is one backend), but nothing tells you the line is dead. Grep for it.shift_distance_limitbehaves the same way:NoMethodErroron the config object, silently ignored per screenshot.shift_distance_limithas no replacementIt was implemented only by the chunky_png driver, and libvips has no shift-distance comparison. Use one of:
median_filter_window_sizetolerancecolor_distance_limitNumbers in failure messages change
If you assert on comparison output, note that libvips reports differently from chunky_png:
area_sizeandregioncome out as floats, and there is nomax_color_distance. For thea/ctest fixtures:This only surfaced now because a
Comparisonbuilt without an explicitdriver:defaulted to chunky_png (Drivers.fordidfetch(:driver, :chunky_png)). Anyone going through the normal DSL was already on libvips viadriver: :auto.Custom drivers: there is no migration path
The abstraction is removed whole — the
SnapDiff::Drivermixin, theSnapDiff::Driversregistry (.loaded,.available,.for,.registry,.detect_available),AVAILABLE_DRIVERS,Utils.detect_available_drivers, and:autoselection. A third-party driver stops working on 2.1 and nothing replaces it. This is a deliberate call, not an oversight: one backend is what keeps every option meaning one thing. If you maintain one, say so on the issue tracker — that is the only thing that can reopen it.SnapDiff::Drivers::VipsDriversurvives, andSnapDiff::Driverssurvives as its namespace — not as a registry.The
driver:setting: removed, and whyThe open question was whether
driver:survives as an accept-and-ignore knob. Removed. Reasoning:driver: :chunky_pngwould keep comparing on libvips, i.e. actively lie about which backend runs.NoMethodError: undefined method 'driver='fires at config time, before a single test, naming the exact line. An ignored setting fails nowhere and is discovered when someone wonders why tuning it does nothing.:vipsand:auto-with-libvips-present already selected what 2.1 does unconditionally; the real-world validation consumer never set it at all. The two values that did change behaviour (:chunky_png,:autofalling back to chunky) are precisely the ones 2.0 warns about.The honest caveat is in What breaks loudly vs. quietly: the per-screenshot
driver:form is silently ignored rather than raising, because per-screenshot options are a free-form hash. Adding validation there would be a new feature in a deletion release; it is documented instead.A bug the deletion exposed (and fixes)
libvips served stale images. libvips caches loader operations keyed on filename + mtime, and mtime has one-second resolution — so rewriting a screenshot path and re-reading it within the same second handed back the previous image. This gem does exactly that: the screenshoter writes
<name>.png,checkout_base_screenshotwrites<name>.base.pngfrom VCS, and the comparison then reads both.It was latent because comparisons built without an explicit driver defaulted to chunky_png, which always re-reads. With libvips as the only backend it became the only behaviour — and it turned identical images into "different" and vice versa. Reproduced directly:
Fix:
VipsDriver#from_filepassesrevalidate: true(libvips 8.15+, gated onVips.at_least_libvips?(8, 15)). Two cache-flush workarounds that had been papering over this —Vips.cache_set_max(0); Vips.cache_set_max(1000)intest/system_test_case.rbandvips_driver_test.rbteardowns — are deleted. A regression test pins it, and goes red whenrevalidateis removed.What this deletes
lib/capybara/,lib/capybara_screenshot_diff/,lib/capybara-screenshot-diff.rb,lib/capybara_screenshot_diff.rb,lib/snap_diff/legacy_shims.rb,lib/snap_diff/deprecation.rb,lib/snap_diff/removal.rb,lib/snap_diff/driver.rb,lib/snap_diff/drivers.rb,lib/snap_diff/drivers/chunky_png_driver.rb,lib/snap_diff/utils.rb,test/legacy/, and six driver/deprecation test files.59 files deleted (3 751 lines). Whole diff: 101 files, +680 / −4 494 — a net 3 814 lines removed.
lib/test/docs/+ READMElib/is down to 34 packaged files.Fallout beyond the deletions
test/test_helper.rb—SnapDiff::Deprecation.suppress_migration_notice!,SnapDiff::Removal.suppress!and theWarningguard that raised on any[snap_diff deprecation]line all go; the channel that emitted them no longer exists. TheDriverCoveragebanner/abort goes too: it guarded against a silent fallback to chunky_png, and a missing libvips is now arequire "vips"LoadError at boot.Rakefile—test:canonicalwas "everything excepttest/legacy/", which is now exactlytest; the two converged and the second name is deleted rather than kept as an alias for one thing.rake testis THE gate.test:benchmarkis deleted: it requiredscripts/benchmark/find_region_benchmark, which is not in this repo, so it raisedLoadErroron every invocation, and its body named a v1 constant..github/workflows/test.yml— thescreenshot-driver: [vips, chunky_png]matrix axis is gone (half the cells, same coverage), as isSCREENSHOT_DRIVEReverywhere. The job idmatrix-screenshot-driveris renamedmatrix-capybara-driver.scripts/generate_sample_report.rb(rake report:sample) andbin/consoleloaded v1 entry points; repointed and both verified running.gems.rb—chunky_png,oily_pngand the explicitruby-vipsline removed (the last is now a gemspec runtime dep).ImagePreprocessor— thedriver.supports?(:filter_image_with_median)probe and its warning fallback are deleted. They existed because chunky_png did not implement median filtering; libvips does, so the branch was unreachable.supports?went with the mixin.Gate dispositions
test/legacy/legacy_tree_is_alias_only_test.rbtest/unit/legacy_deletion_test.rblib/,rm, probe in a subprocess) because the trees were still there. They are not, andsupport_load_probe_test.rbalready runs the same entry-point and advertised-constant tables against the reallib/on every runtest/unit/removed_surface_test.rbtest/unit/core_tree_has_no_legacy_deps_test.rbrequireinlib/now fails by itself, but a docstring or user-facing message namingCapybara::Screenshot.*survives the deletion and starts lying. Its exclusion list is empty (both files were deleted, not exempted) and it gained aREMOVED_SURFACEpattern for the driver halftest/unit/canonical_suite_has_no_legacy_refs_test.rbSnapDiff::Removal,SnapDiff::Driver, the registry methods andsilence_deprecationstest/unit/support_load_probe_test.rblib/Gate line discipline
removed_surface_test.rbruns a subprocess, so it carries a gate line that runs before any absence assertion. The trap here is the inverse of the one that bit us twice: the subject is the reallib/, so the risk is measuring an installed copy of the gem, or measuring nothing at all — "the constant is gone" and "nothing was ever loaded" look identical. The gate asserts snap_diff files were loaded, that every one came from this repo'slib/(anchored on the library path, not a bare/snap_diff/substring — CI checks out into a directory of that name), and two positive controls. Itchdirs to a tmpdir and scrubsRUBYOPT/BUNDLE_GEMFILE/RUBYLIB, because with the cwd inside the project RubyGems re-adds-rbundler/setupand the gemspec unshiftslib/ahead of any-I. A dedicated test proves the gate line rejects a process that loaded nothing.Mutation checks (every gate broken, watched to fail, restored)
touch lib/snap_diff/removal.rbExpected ["snap_diff/removal.rb"] to be emptydef self.starttosnap_diff.rbstill defined: SnapDiff.startshift_distance_limitin alib/stringcore_tree_has_no_legacy_depsconfig.rb:98: names a surface 2.1 removedrevalidate:fromfrom_filevips served the cached b.png after the path was overwrittenWhat remains of driver-contract coverage
test/support/driver_contract_tests.rbis kept, with one includer instead of two. It stops being a shared contract and becomes the pin on the interfaceComparison,ImagePreprocessor,ScreenshoterandAnnotationServiceall call — signatures, arity, andload_imagesslot order, all of which would break those callers silently if they drifted. Behavioural option coverage (tolerance,color_distance_limit,skip_area, resize, missing-file errors) is still meaningful against one driver.What is honestly lost: the value of running identical expectations against two implementations, which is what a contract test is normally for. And
supports?(feature)is gone — it existed soImagePreprocessorcould ask whether a driver implemented median filtering, a question with one answer now.filter_image_with_medianwas added to the interface list to compensate, since nothing else asserts it exists.Deleted outright:
chunky_png_driver_test.rb,drivers_test.rb(registry/detection),drivers/utils_test.rb(detection),driver_coverage_test.rb+test/support/driver_coverage.rb,removed_in_2_1_deprecation_test.rb(its six warnings are what this release makes true).Suites
Baseline on
origin/master(1cd89c9) was613 runs, 0 failures, 1 skipforrake test,585fortest:unitand482/0/1fortest:canonical. The drop istest/legacy/(9 files), the deletion simulation, and the driver/deprecation tests leaving, minus the new gate arriving. The one skip isBrowserScreenshotTest#test_animated_example, anoptional_test— the same skip as baseline.Also verified:
gem buildsucceeds and packages 34lib/files withcapybaraandruby-vipsas runtime dependencies;rake report:sampleandbin/consoleboth run against the repointed entry points.Rebase or redo
Redone from scratch. snap-diff#240's diff touched files that snap-diff#246/snap-diff#247/snap-diff#248 subsequently rewrote —
removal.rbdid not exist when it was opened,deletion_3_0_test.rbwas renamed and its constants with it, the Rakefile header was rewritten — so nearly every file it touched would have conflicted. The scope also roughly doubled (drivers, chunky_png,shift_distance_limit, the gemspec dependency). Starting from currentorigin/masterwas the smaller job.Deliberately not done here: the v1 docs sweep
The driver half of the docs is updated:
docs/drivers.mdis rewritten as "Image Processing" (libvips only),docs/configuration.mdloses theshift_distance_limitsection and the ChunkyPNG comparison columns,docs/snapdiff.md's custom-driver section becomes the no-migration-path note,docs/architecture.md's driver layer and file layout are corrected, and the README's install/requirements blocks are updated.The v1 namespace half is left out to keep this diff reviewable — same call snap-diff#240 made. Still describing the old surface as present, by match count:
docs/UPGRADING.md·docs/migration-guide.md·docs/framework-setup.md·docs/architecture.md(lines 5, 219, 293) ·docs/configuration.md(the legacy two-holder config block) ·docs/organization.md·docs/thread_safety.md·docs/reporters.md·CONTRIBUTING.md. Not all are wrong — a migration guide must name the old API — but every one needs a read.For the reviewer
Removing a public API in a MINOR departs from semver. It does. The mitigation is that it is announced rather than surprising: 2.0's release notes,
docs/UPGRADING.mdand the runtime migration notice all say "removed in 2.1" explicitly, so the contract is published before it is enforced. On 2.0 a v1 user gets working legacy names, a per-constant warning carrying their ownfile:line, and one migration notice per process pointing at the guide — and since snap-diff#246, six more warnings covering exactly the driver half this PR removes.