From 3d220348fa54a97e4bf0ec0f85b3f1011a2cd024 Mon Sep 17 00:00:00 2001 From: Paul Keen <125715+pftg@users.noreply.github.com> Date: Sun, 23 Aug 2026 13:42:07 +0200 Subject: [PATCH 1/2] test: gate the canonical/legacy test split `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 (#236), three legacy-constant probes in a canonical file (#237), and a pre-existing umbrella guard #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. --- ...canonical_suite_has_no_legacy_refs_test.rb | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 test/unit/canonical_suite_has_no_legacy_refs_test.rb diff --git a/test/unit/canonical_suite_has_no_legacy_refs_test.rb b/test/unit/canonical_suite_has_no_legacy_refs_test.rb new file mode 100644 index 00000000..d87d10b6 --- /dev/null +++ b/test/unit/canonical_suite_has_no_legacy_refs_test.rb @@ -0,0 +1,163 @@ +# frozen_string_literal: true + +require "test_helper" + +# The TEST-TREE half of the split that core_tree_has_no_legacy_deps_test.rb +# guards for lib/. +# +# `rake test:canonical` is defined as "exactly what must still pass once +# test/legacy/ and the v1 trees are gone". A test that ASSERTS legacy +# behaviour from test/unit/ or test/integration/ is therefore a time bomb: +# it passes today and fails the day the deletion lands, long after its author +# has moved on. That has now happened three times in review -- a canonical +# surface table demanding a shim-only method, legacy-constant probes written +# into a canonical file, an umbrella guard that had to be relocated. Reviews +# caught all three; this catches the fourth. +# +# Scope: test/unit/ and test/integration/ -- the two trees `test:canonical` +# runs and the deletion keeps. test/legacy/ is deliberately NOT policed: its +# whole job is to exercise the legacy surface, and it is deleted with it. +# +# WHOLE-LINE comments are ignored, same as the twin gate: a comment +# explaining that a forwarder used to live under the old name is history, not +# a dependency. Everything else on a code line counts, strings included -- a +# subprocess script embedded in a heredoc runs, and a legacy require inside +# one dies with the trees just as loudly as one at the top of the file. +class CanonicalSuiteHasNoLegacyRefsTest < ActiveSupport::TestCase + TEST_ROOT = Pathname.new(__dir__).join("..").expand_path + + # This file cannot scan itself: a line-level allowlist has to quote the + # exact lines it blesses, and every such quote is itself a legacy + # reference. Scanning self would demand an allowlist entry for the + # allowlist, whose text is again an offence -- no fixed point exists. + SELF = Pathname.new(File.expand_path(__FILE__)) + + ALL_FILES = ( + Dir[TEST_ROOT.join("unit/**/*_test.rb")] + Dir[TEST_ROOT.join("integration/**/*_test.rb")] + ).map { |path| Pathname.new(path) }.sort.freeze + + CANONICAL_FILES = ALL_FILES.reject { |file| file == SELF }.freeze + + # A require of a doomed path: the v1 trees (`capybara/screenshot/...`, + # `capybara_screenshot_diff...`, the `capybara-screenshot-diff` gem-name + # entry) and the two core files that build the v1 surface and go with it. + # Plain `require "capybara"` / `"capybara/minitest"` is the base gem and + # must not match. + # + # Unanchored, unlike the lib-side twin: tests drive subprocesses, so a + # legacy require is as likely to sit inside a heredoc or a `-e` string as + # at the top of the file, and both really load it. The optional backslash + # covers the escaped-quote form those scripts use. + LEGACY_REQUIRE = %r{ + require(_relative)?\s+\\?["'](\.{1,2}/)* + (capybara(/screenshot|_screenshot_diff|-screenshot-diff)|snap_diff/(legacy_shims|deprecation)) + }x + + # A read or write of a v1 namespace constant. + LEGACY_CONSTANT = /(? [ + '["snap_diff.rb", %(require "snap_diff/legacy_shims"), nil],', + '%(require "capybara_screenshot_diff/minitest"),', + 'gate << "SnapDiff.start is still defined" if SnapDiff.respond_to?(:start)', + 'gate << "CapybaraScreenshotDiff is still defined" if defined?(CapybaraScreenshotDiff)', + 'assert_includes failure, "SnapDiff.start is still defined"' + ], + # The twin gate's own pattern literal. A detector has to spell what it + # detects; the line asserts nothing about legacy behaviour and is deleted + # with the trees it guards. + "unit/core_tree_has_no_legacy_deps_test.rb" => [ + 'LEGACY_CONSTANT = /(? Date: Sun, 23 Aug 2026 13:52:35 +0200 Subject: [PATCH 2/2] 2.1: delete the legacy namespace trees Removes the v1 compatibility surface: lib/capybara/, lib/capybara_screenshot_diff/, the two gem-name entry points, snap_diff/legacy_shims.rb, snap_diff/deprecation.rb and test/legacy/. 48 files, 2483 lines. 2.0 ships the compat layer intact -- working legacy names, per-constant deprecation warnings, one migration notice per process. 2.1 completes the move; docs/UPGRADING.md is the path. The two edits the deletion needs, exactly as deletion_3_0_test.rb had been asserting them for weeks: - lib/snap_diff.rb drops `require "snap_diff/legacy_shims"` - lib/snap_diff-capybara.rb repoints at "snap_diff/integrations/minitest" Fallout beyond those two: - test/test_helper.rb: SnapDiff::Deprecation.suppress_migration_notice! and the Warning guard that raised on "[snap_diff deprecation]" both go -- the channel that emitted those warnings no longer exists. - Rakefile: `test:canonical` was "everything except test/legacy", which is now `test`. Deleted rather than kept as a second name for one thing. `test:benchmark` deleted too: it required scripts/benchmark/find_region_benchmark, which is not in the repo, so the task has been raising LoadError. - scripts/generate_sample_report.rb (rake report:sample) and bin/console loaded v1 entry points; repointed at canonical names. Gates, per gate: - test/legacy/legacy_tree_is_alias_only_test.rb -- proved the v1 trees held no logic. Subject deleted; the test goes with it. - test/unit/core_tree_has_no_legacy_deps_test.rb -- KEPT, repurposed. A legacy require now fails loudly on its own, but a message or docstring naming Capybara::Screenshot.* does not: it survives the deletion and starts lying. The DELETED_IN_3_0 exclusion list is gone (both files it named are deleted). - test/unit/canonical_suite_has_no_legacy_refs_test.rb -- KEPT unchanged in mechanism; the allowlist swaps deletion_3_0_test.rb for its replacement and still holds two entries. - test/unit/deletion_3_0_test.rb -- SIMULATED the deletion (copy lib/, delete, probe). It is now real, and support_load_probe_test.rb runs the same entry-point and advertised-constant tables against the real lib/ on every run. Replaced by test/unit/legacy_surface_removed_test.rb, which keeps the one claim nothing else makes: the removed paths are not back under lib/ (everything there is packaged) and a fresh process defines none of the removed names. rake test: 468 runs, 0 failures, 1 skip. rake test:unit: 440/0. standardrb clean. --- Rakefile | 52 +-- bin/console | 2 +- lib/capybara-screenshot-diff.rb | 3 - lib/capybara/screenshot/diff.rb | 3 - .../screenshot/diff/annotation_service.rb | 5 - .../screenshot/diff/area_calculator.rb | 5 - .../screenshot/diff/browser_helpers.rb | 5 - lib/capybara/screenshot/diff/config_legacy.rb | 21 - lib/capybara/screenshot/diff/cucumber.rb | 7 - lib/capybara/screenshot/diff/difference.rb | 5 - lib/capybara/screenshot/diff/drivers.rb | 8 - .../screenshot/diff/drivers/base_driver.rb | 9 - .../diff/drivers/chunky_png_driver.rb | 7 - .../screenshot/diff/drivers/vips_driver.rb | 6 - lib/capybara/screenshot/diff/image_compare.rb | 16 - .../screenshot/diff/image_preprocessor.rb | 5 - lib/capybara/screenshot/diff/os.rb | 7 - lib/capybara/screenshot/diff/region.rb | 5 - .../screenshot/diff/reporters/default.rb | 8 - .../screenshot/diff/screenshot_matcher.rb | 5 - lib/capybara/screenshot/diff/screenshoter.rb | 5 - .../screenshot/diff/stable_screenshoter.rb | 5 - lib/capybara/screenshot/diff/utils.rb | 5 - lib/capybara/screenshot/diff/vcs.rb | 5 - lib/capybara/screenshot/diff/version.rb | 10 - lib/capybara_screenshot_diff.rb | 45 --- .../attempts_reporter.rb | 5 - lib/capybara_screenshot_diff/cucumber.rb | 8 - lib/capybara_screenshot_diff/dsl.rb | 12 - .../error_with_filtered_backtrace.rb | 5 - lib/capybara_screenshot_diff/minitest.rb | 14 - .../reporters/html.rb | 5 - lib/capybara_screenshot_diff/rspec.rb | 8 - .../screenshot_assertion.rb | 59 --- .../screenshot_namer.rb | 5 - lib/capybara_screenshot_diff/snap.rb | 5 - lib/capybara_screenshot_diff/snap_manager.rb | 5 - lib/capybara_screenshot_diff/static.rb | 13 - lib/snap_diff-capybara.rb | 5 +- lib/snap_diff.rb | 32 +- lib/snap_diff/deprecation.rb | 146 ------- lib/snap_diff/legacy_shims.rb | 363 ------------------ scripts/generate_sample_report.rb | 20 +- test/legacy/errors_alias_test.rb | 54 --- test/legacy/legacy_config_accessors_test.rb | 196 ---------- .../legacy_config_default_timing_test.rb | 58 --- test/legacy/legacy_entry_point_probe_test.rb | 286 -------------- test/legacy/legacy_forwarders_test.rb | 124 ------ .../legacy_namespace_deprecation_test.rb | 160 -------- test/legacy/legacy_tree_is_alias_only_test.rb | 162 -------- test/legacy/namespace_forwarding_test.rb | 153 -------- test/legacy/snap_diff_deprecation_test.rb | 236 ------------ test/test_helper.rb | 37 +- ...canonical_suite_has_no_legacy_refs_test.rb | 20 +- .../unit/core_tree_has_no_legacy_deps_test.rb | 54 ++- test/unit/deletion_3_0_test.rb | 196 ---------- test/unit/legacy_surface_removed_test.rb | 74 ++++ 57 files changed, 140 insertions(+), 2639 deletions(-) delete mode 100644 lib/capybara-screenshot-diff.rb delete mode 100644 lib/capybara/screenshot/diff.rb delete mode 100644 lib/capybara/screenshot/diff/annotation_service.rb delete mode 100644 lib/capybara/screenshot/diff/area_calculator.rb delete mode 100644 lib/capybara/screenshot/diff/browser_helpers.rb delete mode 100644 lib/capybara/screenshot/diff/config_legacy.rb delete mode 100644 lib/capybara/screenshot/diff/cucumber.rb delete mode 100644 lib/capybara/screenshot/diff/difference.rb delete mode 100644 lib/capybara/screenshot/diff/drivers.rb delete mode 100644 lib/capybara/screenshot/diff/drivers/base_driver.rb delete mode 100644 lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb delete mode 100644 lib/capybara/screenshot/diff/drivers/vips_driver.rb delete mode 100644 lib/capybara/screenshot/diff/image_compare.rb delete mode 100644 lib/capybara/screenshot/diff/image_preprocessor.rb delete mode 100644 lib/capybara/screenshot/diff/os.rb delete mode 100644 lib/capybara/screenshot/diff/region.rb delete mode 100644 lib/capybara/screenshot/diff/reporters/default.rb delete mode 100644 lib/capybara/screenshot/diff/screenshot_matcher.rb delete mode 100644 lib/capybara/screenshot/diff/screenshoter.rb delete mode 100644 lib/capybara/screenshot/diff/stable_screenshoter.rb delete mode 100644 lib/capybara/screenshot/diff/utils.rb delete mode 100644 lib/capybara/screenshot/diff/vcs.rb delete mode 100644 lib/capybara/screenshot/diff/version.rb delete mode 100644 lib/capybara_screenshot_diff.rb delete mode 100644 lib/capybara_screenshot_diff/attempts_reporter.rb delete mode 100644 lib/capybara_screenshot_diff/cucumber.rb delete mode 100644 lib/capybara_screenshot_diff/dsl.rb delete mode 100644 lib/capybara_screenshot_diff/error_with_filtered_backtrace.rb delete mode 100644 lib/capybara_screenshot_diff/minitest.rb delete mode 100644 lib/capybara_screenshot_diff/reporters/html.rb delete mode 100644 lib/capybara_screenshot_diff/rspec.rb delete mode 100644 lib/capybara_screenshot_diff/screenshot_assertion.rb delete mode 100644 lib/capybara_screenshot_diff/screenshot_namer.rb delete mode 100644 lib/capybara_screenshot_diff/snap.rb delete mode 100644 lib/capybara_screenshot_diff/snap_manager.rb delete mode 100644 lib/capybara_screenshot_diff/static.rb delete mode 100644 lib/snap_diff/deprecation.rb delete mode 100644 lib/snap_diff/legacy_shims.rb delete mode 100644 test/legacy/errors_alias_test.rb delete mode 100644 test/legacy/legacy_config_accessors_test.rb delete mode 100644 test/legacy/legacy_config_default_timing_test.rb delete mode 100644 test/legacy/legacy_entry_point_probe_test.rb delete mode 100644 test/legacy/legacy_forwarders_test.rb delete mode 100644 test/legacy/legacy_namespace_deprecation_test.rb delete mode 100644 test/legacy/legacy_tree_is_alias_only_test.rb delete mode 100644 test/legacy/namespace_forwarding_test.rb delete mode 100644 test/legacy/snap_diff_deprecation_test.rb delete mode 100644 test/unit/deletion_3_0_test.rb create mode 100644 test/unit/legacy_surface_removed_test.rb diff --git a/Rakefile b/Rakefile index c0c705ed..eadfd9c4 100644 --- a/Rakefile +++ b/Rakefile @@ -5,46 +5,24 @@ require "rake/testtask" task default: :test -# THE 3.0 SPLIT. +# `rake test:canonical` is gone with the thing it was defined against: it +# was "test/**/*_test.rb minus test/legacy/", i.e. exactly what had to keep +# passing once the v1 compatibility trees were deleted. 2.1 deleted them and +# test/legacy/ with them, so that task became a second name for `rake test`. # -# test/legacy/ holds every test whose SUBJECT is the v1 compatibility surface -# -- the old Capybara::Screenshot / CapybaraScreenshotDiff namespaces, their -# deprecation warnings, and the gates that keep lib/capybara* alias-only. -# Those tests guard the v1 contract for the whole 2.x line, so they stay and -# stay green; in 3.0 they are deleted by the same commit that deletes what -# they test: -# -# git rm -r lib/capybara* lib/capybara_screenshot_diff.rb \ -# lib/snap_diff/legacy_shims.rb lib/snap_diff/deprecation.rb \ -# test/legacy -# -# A directory rather than a list in this file: there is nothing to keep in -# sync, and the deletion is one `git rm -r`. -# -# `rake test` -- everything, today's gate. -# `rake test:canonical` -- exactly what must still pass once test/legacy and -# the v1 trees are gone. THE 3.0 GATE. -# `rake test:unit` -- unit-sized tests; test/legacy is unit-sized too -# (legacy/ marks lifetime, not kind), so it is in. -LEGACY_SURFACE_TESTS = "test/legacy/**/*_test.rb" - +# The split still has a live guard, on the other side: canonical tests must +# not grow legacy references BACK (test/unit/canonical_suite_has_no_legacy_ +# refs_test.rb, and its lib/ twin core_tree_has_no_legacy_deps_test.rb). Rake::TestTask.new(:test) do |t| t.libs << "test" t.libs << "lib" t.test_files = FileList["test/**/*_test.rb"] end -desc "Run every test that must survive the 3.0 deletion of the v1 surface" -Rake::TestTask.new("test:canonical") do |t| - t.libs << "test" - t.libs << "lib" - t.test_files = FileList["test/**/*_test.rb"].exclude(LEGACY_SURFACE_TESTS) -end - Rake::TestTask.new("test:unit") do |t| t.libs << "test" t.libs << "lib" - t.test_files = FileList["test/unit/**/*_test.rb", LEGACY_SURFACE_TESTS] + t.test_files = FileList["test/unit/**/*_test.rb"] end Rake::TestTask.new("test:integration") do |t| @@ -79,17 +57,3 @@ task "clobber" do puts "Cleanup tmp/" FileUtils.rm_rf(Dir["./tmp/*"]) end - -task "test:benchmark" do - require_relative "scripts/benchmark/find_region_benchmark" - benchmark = Capybara::Screenshot::Diff::Drivers::FindRegionBenchmark.new - - puts "For Medium Screen Size: 800x600" - benchmark.for_medium_size_screens - - puts "" - puts "*" * 100 - - puts "For Small Screen Size: 80x60" - benchmark.for_small_images -end diff --git a/bin/console b/bin/console index 0c77c72b..85227017 100755 --- a/bin/console +++ b/bin/console @@ -2,7 +2,7 @@ # frozen_string_literal: true require "bundler/setup" -require "capybara/screenshot/diff" +require "snap_diff" # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. diff --git a/lib/capybara-screenshot-diff.rb b/lib/capybara-screenshot-diff.rb deleted file mode 100644 index dff8d102..00000000 --- a/lib/capybara-screenshot-diff.rb +++ /dev/null @@ -1,3 +0,0 @@ -# frozen_string_literal: true - -require "capybara_screenshot_diff/minitest" diff --git a/lib/capybara/screenshot/diff.rb b/lib/capybara/screenshot/diff.rb deleted file mode 100644 index cf1931d3..00000000 --- a/lib/capybara/screenshot/diff.rb +++ /dev/null @@ -1,3 +0,0 @@ -# frozen_string_literal: true - -require "capybara-screenshot-diff" diff --git a/lib/capybara/screenshot/diff/annotation_service.rb b/lib/capybara/screenshot/diff/annotation_service.rb deleted file mode 100644 index e3c36727..00000000 --- a/lib/capybara/screenshot/diff/annotation_service.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -# Legacy-name forwarder: the old constant resolves via snap_diff/legacy_shims. -require "snap_diff/annotation_service" -require "snap_diff/legacy_shims" diff --git a/lib/capybara/screenshot/diff/area_calculator.rb b/lib/capybara/screenshot/diff/area_calculator.rb deleted file mode 100644 index b21ef790..00000000 --- a/lib/capybara/screenshot/diff/area_calculator.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -# Legacy-name forwarder: the old constant resolves via snap_diff/legacy_shims. -require "snap_diff/area_calculator" -require "snap_diff/legacy_shims" diff --git a/lib/capybara/screenshot/diff/browser_helpers.rb b/lib/capybara/screenshot/diff/browser_helpers.rb deleted file mode 100644 index d023d60d..00000000 --- a/lib/capybara/screenshot/diff/browser_helpers.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -# Legacy-name forwarder: the old constant resolves via snap_diff/legacy_shims. -require "snap_diff/browser_helpers" -require "snap_diff/legacy_shims" diff --git a/lib/capybara/screenshot/diff/config_legacy.rb b/lib/capybara/screenshot/diff/config_legacy.rb deleted file mode 100644 index 0da6e61a..00000000 --- a/lib/capybara/screenshot/diff/config_legacy.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -# Legacy Capybara::Screenshot / Capybara::Screenshot::Diff config surface. -# -# Nothing but requires is left here. The storage is SnapDiff::Config -# (ADR-008 step 1, the require leaf of the config graph); the derived values -# (active?, screenshot_area, default_options) live there too since step 7b; -# and the old accessor names, Diff.configure/.compare, SnapDiff.start and -# the AVAILABLE_DRIVERS alias are generated by snap_diff/legacy_shims -- the -# one file that holds the v1 surface as code, so that the canonical core -# needs nothing from this tree and 3.0 can delete both together. The v1 -# surface (Capybara::Screenshot.window_size = ..., Diff.configure { ... }, -# Diff.compare) keeps working unchanged: one storage, two views. -# -# Load order: requiring snap_diff/config first also eagerly evaluates the -# require-time defaults (ENV["CI"] for fail_if_new, Rails.root/pwd for -# root) at this same load moment, exactly when the old mattr_accessor -# default blocks used to run. Neither file requires back here, so the graph -# stays acyclic. -require "snap_diff/config" -require "snap_diff/legacy_shims" diff --git a/lib/capybara/screenshot/diff/cucumber.rb b/lib/capybara/screenshot/diff/cucumber.rb deleted file mode 100644 index 3ad15ef7..00000000 --- a/lib/capybara/screenshot/diff/cucumber.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -# Legacy (v1-documented) entry point: like every other legacy entry, it must -# load the legacy surface too -- requiring only the canonical integration -# left CapybaraScreenshotDiff half-present (module defined, but .verify / -# .reset / .reporters / ... gone) for v1 users of this path. -require "capybara_screenshot_diff/cucumber" diff --git a/lib/capybara/screenshot/diff/difference.rb b/lib/capybara/screenshot/diff/difference.rb deleted file mode 100644 index 7d9bf46d..00000000 --- a/lib/capybara/screenshot/diff/difference.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -# Legacy-name forwarder: the old constant resolves via snap_diff/legacy_shims. -require "snap_diff/comparison_result" -require "snap_diff/legacy_shims" diff --git a/lib/capybara/screenshot/diff/drivers.rb b/lib/capybara/screenshot/diff/drivers.rb deleted file mode 100644 index 6699ef79..00000000 --- a/lib/capybara/screenshot/diff/drivers.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -# Legacy-name forwarder: the old constant resolves via snap_diff/legacy_shims. -# The forwarded module is the same object, so Drivers.for and the -# Drivers::VipsDriver / Drivers::ChunkyPNGDriver constants keep resolving -# through the old name. -require "snap_diff/drivers" -require "snap_diff/legacy_shims" diff --git a/lib/capybara/screenshot/diff/drivers/base_driver.rb b/lib/capybara/screenshot/diff/drivers/base_driver.rb deleted file mode 100644 index 22e4f4a9..00000000 --- a/lib/capybara/screenshot/diff/drivers/base_driver.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -# BaseDriver was dissolved into the SnapDiff::Driver mixin (ADR-004 v2 -# step 4); since step 6 the old name resolves lazily via -# snap_diff/legacy_shims' const_missing, with a deprecation warning. -# Note it is now a module -- `class MyDriver < BaseDriver` becomes -# `include SnapDiff::Driver`. -require "snap_diff/driver" -require "snap_diff/legacy_shims" diff --git a/lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb b/lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb deleted file mode 100644 index 070acede..00000000 --- a/lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -# Legacy-name forwarder: the module alias in capybara/screenshot/diff/drivers.rb -# makes ChunkyPNGDriver reachable as -# Capybara::Screenshot::Diff::Drivers::ChunkyPNGDriver. -require "capybara/screenshot/diff/drivers" -require "snap_diff/drivers/chunky_png_driver" diff --git a/lib/capybara/screenshot/diff/drivers/vips_driver.rb b/lib/capybara/screenshot/diff/drivers/vips_driver.rb deleted file mode 100644 index fe8c11aa..00000000 --- a/lib/capybara/screenshot/diff/drivers/vips_driver.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -# Legacy-name forwarder: the module alias in capybara/screenshot/diff/drivers.rb -# makes VipsDriver reachable as Capybara::Screenshot::Diff::Drivers::VipsDriver. -require "capybara/screenshot/diff/drivers" -require "snap_diff/drivers/vips_driver" diff --git a/lib/capybara/screenshot/diff/image_compare.rb b/lib/capybara/screenshot/diff/image_compare.rb deleted file mode 100644 index 19863e3e..00000000 --- a/lib/capybara/screenshot/diff/image_compare.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -# Legacy-name forwarder for SnapDiff::Comparison (ex-ImageCompare). Requiring -# this path must keep providing everything the pre-move image_compare.rb did: -# snap_diff/comparison pulls in the ComparisonResult and Drivers units, and the -# shims keep ::Difference, ::Drivers, ::Comparison (the images struct) and -# LOADED_DRIVERS resolvable. -require "snap_diff/comparison" -require "snap_diff/legacy_shims" - -# Capybara::Screenshot::Diff::Comparison (the images-holder struct) is a -# documented user-facing name that adopters feature-detect with -# defined?/const_defined?, so it is assigned EAGERLY rather than shimmed -- -# const_defined? never triggers const_missing. That assignment now lives in -# snap_diff/legacy_shims (required above), with the rest of the v1 surface, -# so `require "snap_diff"` alone provides it too. diff --git a/lib/capybara/screenshot/diff/image_preprocessor.rb b/lib/capybara/screenshot/diff/image_preprocessor.rb deleted file mode 100644 index f97017d2..00000000 --- a/lib/capybara/screenshot/diff/image_preprocessor.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -# Legacy-name forwarder: the old constant resolves via snap_diff/legacy_shims. -require "snap_diff/image_preprocessor" -require "snap_diff/legacy_shims" diff --git a/lib/capybara/screenshot/diff/os.rb b/lib/capybara/screenshot/diff/os.rb deleted file mode 100644 index 376b6791..00000000 --- a/lib/capybara/screenshot/diff/os.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -# Legacy-name forwarder. `Capybara::Screenshot::Os` is an EAGER same-object -# alias assigned in snap_diff/legacy_shims -- the one file every entry point -# loads, canonical ones included, so a half-migrated app keeps the name. -require "snap_diff/os" -require "snap_diff/legacy_shims" diff --git a/lib/capybara/screenshot/diff/region.rb b/lib/capybara/screenshot/diff/region.rb deleted file mode 100644 index 91e1bb49..00000000 --- a/lib/capybara/screenshot/diff/region.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -# Legacy-name forwarder: snap_diff/region also defines the eager top-level -# `Region` alias. -require "snap_diff/region" diff --git a/lib/capybara/screenshot/diff/reporters/default.rb b/lib/capybara/screenshot/diff/reporters/default.rb deleted file mode 100644 index af50dbae..00000000 --- a/lib/capybara/screenshot/diff/reporters/default.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -# Legacy-name forwarder for SnapDiff::Reporters::Default. -# `Capybara::Screenshot::Diff::Reporters::Default` is an EAGER same-object -# alias assigned in snap_diff/legacy_shims -- the one file every entry point -# loads, canonical ones included, so a half-migrated app keeps the name. -require "snap_diff/reporters/default" -require "snap_diff/legacy_shims" diff --git a/lib/capybara/screenshot/diff/screenshot_matcher.rb b/lib/capybara/screenshot/diff/screenshot_matcher.rb deleted file mode 100644 index ad603917..00000000 --- a/lib/capybara/screenshot/diff/screenshot_matcher.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -# Legacy-name forwarder: the old constant resolves via snap_diff/legacy_shims. -require "snap_diff/screenshot_matcher" -require "snap_diff/legacy_shims" diff --git a/lib/capybara/screenshot/diff/screenshoter.rb b/lib/capybara/screenshot/diff/screenshoter.rb deleted file mode 100644 index 8c6a05e0..00000000 --- a/lib/capybara/screenshot/diff/screenshoter.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -# Legacy-name forwarder: the old constant resolves via snap_diff/legacy_shims. -require "snap_diff/screenshoter" -require "snap_diff/legacy_shims" diff --git a/lib/capybara/screenshot/diff/stable_screenshoter.rb b/lib/capybara/screenshot/diff/stable_screenshoter.rb deleted file mode 100644 index 03f4271a..00000000 --- a/lib/capybara/screenshot/diff/stable_screenshoter.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -# Legacy-name forwarder: the old constant resolves via snap_diff/legacy_shims. -require "snap_diff/stable_screenshoter" -require "snap_diff/legacy_shims" diff --git a/lib/capybara/screenshot/diff/utils.rb b/lib/capybara/screenshot/diff/utils.rb deleted file mode 100644 index 858a78eb..00000000 --- a/lib/capybara/screenshot/diff/utils.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -# Legacy-name forwarder: the old constant resolves via snap_diff/legacy_shims. -require "snap_diff/utils" -require "snap_diff/legacy_shims" diff --git a/lib/capybara/screenshot/diff/vcs.rb b/lib/capybara/screenshot/diff/vcs.rb deleted file mode 100644 index 5fcea3bf..00000000 --- a/lib/capybara/screenshot/diff/vcs.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -# Legacy-name forwarder: the old constant resolves via snap_diff/legacy_shims. -require "snap_diff/vcs" -require "snap_diff/legacy_shims" diff --git a/lib/capybara/screenshot/diff/version.rb b/lib/capybara/screenshot/diff/version.rb deleted file mode 100644 index 0d87948d..00000000 --- a/lib/capybara/screenshot/diff/version.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -# Capybara::Screenshot::Diff::VERSION is a documented name adopters read -# directly, so it is assigned EAGERLY rather than shimmed -- const_defined? -# never triggers const_missing. That assignment lives in -# snap_diff/legacy_shims (required below) with the rest of the v1 surface, -# because this file is no longer on any entry point's require path: the core -# reads SnapDiff::VERSION, and so does the gemspec. Assigning it here too -# would be a duplicate-constant warning, not a second safety net. -require "snap_diff/legacy_shims" diff --git a/lib/capybara_screenshot_diff.rb b/lib/capybara_screenshot_diff.rb deleted file mode 100644 index 7ec0d800..00000000 --- a/lib/capybara_screenshot_diff.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -require "capybara/dsl" -require "capybara/screenshot/diff/config_legacy" -require "capybara/screenshot/diff/version" -require "capybara/screenshot/diff/os" -require "capybara/screenshot/diff/browser_helpers" -require "capybara/screenshot/diff/utils" -require "capybara/screenshot/diff/image_compare" -require "capybara_screenshot_diff/snap_manager" -require "capybara_screenshot_diff/snap" -require "capybara/screenshot/diff/screenshoter" -require "capybara/screenshot/diff/stable_screenshoter" -require "capybara/screenshot/diff/vcs" -require "capybara/screenshot/diff/area_calculator" -require "capybara/screenshot/diff/image_preprocessor" -require "capybara/screenshot/diff/annotation_service" -require "capybara_screenshot_diff/screenshot_namer" -require "capybara_screenshot_diff/screenshot_assertion" -require "capybara_screenshot_diff/attempts_reporter" -require "capybara/screenshot/diff/screenshot_matcher" -require "capybara/screenshot/diff/reporters/default" - -require "capybara_screenshot_diff/error_with_filtered_backtrace" -require "snap_diff/errors" - -# RED_RGBA / ORANGE_RGBA moved to SnapDiff (snap_diff/annotation_service) so -# the bare "snap_diff" entry gets them too; the old names resolve via -# snap_diff/legacy_shims with a deprecation warning. -# -# The four error classes (CapybaraScreenshotDiffError, ExpectationNotMet, -# UnstableImage, WindowSizeMismatchError) used to be assigned here as EAGER -# same-object aliases. They still are eager -- just from -# snap_diff/legacy_shims, so a canonical-only require gets them too. -require "snap_diff/legacy_shims" - -require "capybara_screenshot_diff/dsl" - -# Eager, not autoload: several lib/snap_diff/* units above (Os, -# Screenshoter, ...) reopen `module SnapDiff` while loading, which cancels -# any registered `autoload :SnapDiff` before it ever fires (Ruby resolves -# the constant the first time anything reopens it, autoload or not) -- -# so SnapDiff.start/.compare/.config would silently never be defined -# without this. Safe eagerly: snap_diff.rb never requires this file back. -require "snap_diff" diff --git a/lib/capybara_screenshot_diff/attempts_reporter.rb b/lib/capybara_screenshot_diff/attempts_reporter.rb deleted file mode 100644 index 159b63aa..00000000 --- a/lib/capybara_screenshot_diff/attempts_reporter.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -# Legacy-name forwarder: the old constant resolves via snap_diff/legacy_shims. -require "snap_diff/attempts_reporter" -require "snap_diff/legacy_shims" diff --git a/lib/capybara_screenshot_diff/cucumber.rb b/lib/capybara_screenshot_diff/cucumber.rb deleted file mode 100644 index 523e5bad..00000000 --- a/lib/capybara_screenshot_diff/cucumber.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -# Legacy entry point: pre-move, this path transitively loaded the whole gem -# (via dsl -> umbrella). Preserve that contract for consumers who require -# only this file. -require "capybara_screenshot_diff" - -require "snap_diff/integrations/cucumber" diff --git a/lib/capybara_screenshot_diff/dsl.rb b/lib/capybara_screenshot_diff/dsl.rb deleted file mode 100644 index 33535a3e..00000000 --- a/lib/capybara_screenshot_diff/dsl.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -require "snap_diff/dsl" - -# Deliberately EAGER and silent (v2 step 6 exception): DSL is an advertised -# entry-point constant probed with Object.const_defined? by -# support_load_probe_test.rb, and const_defined? never triggers -# const_missing -- a lazy shim would break that contract. See -# snap_diff/legacy_shims.rb for the full exception list. -module CapybaraScreenshotDiff - DSL = SnapDiff::DSL -end diff --git a/lib/capybara_screenshot_diff/error_with_filtered_backtrace.rb b/lib/capybara_screenshot_diff/error_with_filtered_backtrace.rb deleted file mode 100644 index c4c32fea..00000000 --- a/lib/capybara_screenshot_diff/error_with_filtered_backtrace.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -# Legacy-name forwarder: the old constant resolves via snap_diff/legacy_shims. -require "snap_diff/error_with_filtered_backtrace" -require "snap_diff/legacy_shims" diff --git a/lib/capybara_screenshot_diff/minitest.rb b/lib/capybara_screenshot_diff/minitest.rb deleted file mode 100644 index 5c993cf4..00000000 --- a/lib/capybara_screenshot_diff/minitest.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -# Legacy entry point: pre-move, this path transitively loaded the whole gem -# (via dsl -> umbrella). Preserve that contract for consumers who require -# only this file. -require "capybara_screenshot_diff" - -require "snap_diff/integrations/minitest" - -module CapybaraScreenshotDiff - module Minitest - Assertions = SnapDiff::Minitest::Assertions - end -end diff --git a/lib/capybara_screenshot_diff/reporters/html.rb b/lib/capybara_screenshot_diff/reporters/html.rb deleted file mode 100644 index 84f06c87..00000000 --- a/lib/capybara_screenshot_diff/reporters/html.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -# Legacy-name forwarder: the old constant resolves via snap_diff/legacy_shims. -require "snap_diff/reporters/html" -require "snap_diff/legacy_shims" diff --git a/lib/capybara_screenshot_diff/rspec.rb b/lib/capybara_screenshot_diff/rspec.rb deleted file mode 100644 index d684fb96..00000000 --- a/lib/capybara_screenshot_diff/rspec.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -# Legacy entry point: pre-move, this path transitively loaded the whole gem -# (via dsl -> umbrella). Preserve that contract for consumers who require -# only this file. -require "capybara_screenshot_diff" - -require "snap_diff/integrations/rspec" diff --git a/lib/capybara_screenshot_diff/screenshot_assertion.rb b/lib/capybara_screenshot_diff/screenshot_assertion.rb deleted file mode 100644 index f11473b9..00000000 --- a/lib/capybara_screenshot_diff/screenshot_assertion.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -require "snap_diff/screenshot_assertion" -require "snap_diff/reporting" -# ScreenshotAssertion / AssertionRegistry forward lazily (with deprecation -# warnings) via snap_diff/legacy_shims' const_missing since v2 step 6. -require "snap_diff/legacy_shims" - -# Since ADR-008 step 6 every method here is a thin forwarder; the canonical -# homes are SnapDiff (session lifecycle: per-test, `SnapDiff.session` and -# friends) and SnapDiff::Reporting (reporter lifecycle: process-global, -# suite-long). Names, arities and object identities are unchanged -- this -# module stays as the compatibility surface for existing consumers. -module CapybaraScreenshotDiff - class << self - require "forwardable" - extend Forwardable - - # --- Session lifecycle (per-test) -> SnapDiff --- - - def registry - SnapDiff.session - end - - def_delegators :registry, :add_assertion, :assertions, :assertions_present?, - :failed_assertions, :record_new_screenshot, :new_screenshots, - :new_screenshots_present?, :screenshot_namer, :verify - - # Written out rather than def_delegators so the arities stay 0 (a - # Forwardable-generated method takes *args, **kwargs, &block). - def reset - SnapDiff.reset - end - - def pending_screenshots_message - SnapDiff.pending_screenshots_message - end - - # --- Reporter lifecycle (process-global, suite-long) -> SnapDiff::Reporting --- - - def reporters - SnapDiff::Reporting.reporters - end - - def reporters_mutex - SnapDiff::Reporting.mutex - end - - def finalize_reporters! - SnapDiff::Reporting.finalize! - end - - private - - def notify_reporters(assertions) - SnapDiff::Reporting.notify(assertions) - end - end -end diff --git a/lib/capybara_screenshot_diff/screenshot_namer.rb b/lib/capybara_screenshot_diff/screenshot_namer.rb deleted file mode 100644 index 6f6e53b8..00000000 --- a/lib/capybara_screenshot_diff/screenshot_namer.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -# Legacy-name forwarder: the old constant resolves via snap_diff/legacy_shims. -require "snap_diff/screenshot_namer" -require "snap_diff/legacy_shims" diff --git a/lib/capybara_screenshot_diff/snap.rb b/lib/capybara_screenshot_diff/snap.rb deleted file mode 100644 index 2ba5e5a8..00000000 --- a/lib/capybara_screenshot_diff/snap.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -# Legacy-name forwarder: the old constant resolves via snap_diff/legacy_shims. -require "snap_diff/snap" -require "snap_diff/legacy_shims" diff --git a/lib/capybara_screenshot_diff/snap_manager.rb b/lib/capybara_screenshot_diff/snap_manager.rb deleted file mode 100644 index 6446e9a0..00000000 --- a/lib/capybara_screenshot_diff/snap_manager.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -# Legacy-name forwarder: the old constant resolves via snap_diff/legacy_shims. -require "snap_diff/snap_manager" -require "snap_diff/legacy_shims" diff --git a/lib/capybara_screenshot_diff/static.rb b/lib/capybara_screenshot_diff/static.rb deleted file mode 100644 index 5d814184..00000000 --- a/lib/capybara_screenshot_diff/static.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -# Legacy entry point: loads the legacy surface (the umbrella) like its -# minitest/rspec/cucumber siblings, so consumers who require only this file -# still get CapybaraScreenshotDiff's session and reporter methods. -require "capybara_screenshot_diff" -require "snap_diff/static" - -module CapybaraScreenshotDiff - def self.serve(...) - SnapDiff.serve(...) - end -end diff --git a/lib/snap_diff-capybara.rb b/lib/snap_diff-capybara.rb index 2a35777b..c3a5a4db 100644 --- a/lib/snap_diff-capybara.rb +++ b/lib/snap_diff-capybara.rb @@ -3,6 +3,5 @@ # Bundler.require entry point for `gem "snap_diff-capybara"`: Bundler # requires the gem's own name, and its dash->slash fallback ("snap_diff/ # capybara") misses too, so without this file a Rails user gets a silent -# no-op and a confusing NameError later. Loads what the sibling -# capybara-screenshot-diff.rb loads. -require "capybara_screenshot_diff/minitest" +# no-op and a confusing NameError later. +require "snap_diff/integrations/minitest" diff --git a/lib/snap_diff.rb b/lib/snap_diff.rb index 1bc84e7b..36176cf7 100644 --- a/lib/snap_diff.rb +++ b/lib/snap_diff.rb @@ -27,27 +27,13 @@ def self.assert_single_gem!(loaded_specs = Gem.loaded_specs) end SnapDiff.assert_single_gem! -# This lean entry must never load the umbrella "capybara_screenshot_diff" -# -- snap_diff_test.rb's "bare require never loads the umbrella" guard -# enforces it -- so nothing required below may reach back here. None of -# these requires reaches into lib/capybara* at all, so the canonical entry -# point is exactly what 3.0 keeps. -# # "capybara/dsl" is needed directly (not just transitively) so # `Capybara.default_max_wait_time` in Config#default_options resolves even # when "snap_diff" is required standalone (SnapDiffTest's # "standalone-loadable in a fresh process" regression test). -# -# snap_diff/legacy_shims is deliberate and is the ONE line here that 3.0 -# drops: it carries the whole v1 surface (const_missing forwarders, the old -# mattr_accessors, SnapDiff.start), so a process that only ever requires -# "snap_diff" still resolves the old Capybara::Screenshot::Diff names -- -# with deprecation warnings -- exactly as it did when this file reached -# through the capybara/screenshot/diff/* forwarders to get them. require "capybara/dsl" require "snap_diff/config" require "snap_diff/comparison" -require "snap_diff/legacy_shims" require "snap_diff/version" # SnapDiff.session/.reset/.pending_screenshots_message are part of the # documented core surface (docs/snapdiff.md object map lists them with no @@ -55,12 +41,9 @@ def self.assert_single_gem!(loaded_specs = Gem.loaded_specs) # whichever integration happens to be loaded. require "snap_diff/screenshot_assertion" -# The canonical namespace for the gem. The old -# +Capybara::Screenshot::Diff+ constants are same-object const_missing shims -# (snap_diff/legacy_shims) that warn once per constant per process. +# The canonical namespace for the gem. module SnapDiff - # Compare two images on disk with the configured defaults. Canonical home; - # +Capybara::Screenshot::Diff.compare+ forwards here. + # Compare two images on disk with the configured defaults. # # Note the argument order swap: callers pass baseline first (reading # "compare baseline against current"), Comparison takes current first. @@ -68,14 +51,9 @@ def self.compare(baseline_path, current_path, **options) Comparison.new(current_path, baseline_path, config.default_options.merge(options)) end - # SnapDiff.start -- the v1-shaped two-holder config block -- is defined in - # snap_diff/legacy_shims (required above), because the holders it yields - # are the v1 surface and it cannot outlive them. - - # Forward-looking configuration: yields the single consolidated - # {SnapDiff::Config} object instead of the two old holders. Same - # underlying storage as +start+ / the old mattr_accessors -- this is a - # different *shape* of the same settings, not a second source of truth. + # The single configuration entry point (ADR-008): yields the one + # consolidated {SnapDiff::Config} object. The v1 two-holder block + # (+SnapDiff.start+) was removed with the legacy trees in 2.1. # # SnapDiff.configure do |config| # config.window_size = [1280, 1024] diff --git a/lib/snap_diff/deprecation.rb b/lib/snap_diff/deprecation.rb deleted file mode 100644 index f1b92a2c..00000000 --- a/lib/snap_diff/deprecation.rb +++ /dev/null @@ -1,146 +0,0 @@ -# frozen_string_literal: true - -module SnapDiff - # @api private - # - # Internal until the v2 namespace transition; not a public contract. - # - # Warn-once-per-subject deprecation engine for the legacy-namespace - # shims: snap_diff/legacy_shims routes every +const_missing+ hit on an - # old +Capybara::Screenshot::Diff+ / +CapybaraScreenshotDiff+ constant - # through {.warn}, so each deprecated name warns exactly once per - # process (ADR-004's v2 namespace transition). - module Deprecation - # Everything under lib/ is "the gem"; the first caller frame outside - # it is the user code that referenced the deprecated name (same - # filtering idea as BacktraceFilter in error_with_filtered_backtrace). - GEM_LIB_DIR = File.expand_path("..", __dir__) + File::SEPARATOR - # Emission channel: Kernel#warn, not a direct +$stderr.puts+. - # - # Kernel#warn delegates to +Warning.warn+ (Ruby >= 2.4), so anything - # that hooks +Warning.warn+ -- a test suite that raises on warnings, a - # custom log formatter, Ruby's own -W flag -- sees these messages the - # same way it sees every other Ruby warning. Writing straight to - # +$stderr+ would bypass that hook entirely and be invisible to any - # caller who has customized +Warning+ behavior. - MUTEX = Mutex.new - @seen = {} - @notified = false - @notice_suppressed = false - - # The ONE line a v1 user gets, whichever door they came through. Most of - # the v1 surface cannot warn per use -- the config accessors are plain - # delegators and the eager aliases never reach const_missing -- so - # without this a 2.x app is completely silent right up to the bare - # NameError it gets on 3.0. Deliberately generic and once per process: - # an actionable signal, not per-call stderr noise. - MIGRATION_NOTICE = - "[snap_diff deprecation] This process uses the v1 `Capybara::Screenshot*` / " \ - "`CapybaraScreenshotDiff*` API. It still works in 2.x and is REMOVED in 3.0 -- " \ - "see docs/UPGRADING.md for the SnapDiff replacements. Silence with " \ - "`SnapDiff.silence_deprecations = true` or SNAP_DIFF_SILENCE_DEPRECATIONS=1. " \ - "(shown once per process)" - - class << self - # Emit {MIGRATION_NOTICE}, exactly once per process. Called from every - # v1 entry that can be hooked: the const_missing shims (via {.warn}), - # the generated legacy config accessors, and `include - # Capybara::Screenshot[::Diff]`. - # @return [void] - def notice - return if @notified || @notice_suppressed || SnapDiff.silence_deprecations? - - first_time = MUTEX.synchronize { @notified ? false : (@notified = true) } - Kernel.warn(MIGRATION_NOTICE) if first_time - end - - # @api private - # - # Suppresses {MIGRATION_NOTICE} for the rest of the process, without - # touching the per-constant warnings. For hosts that ARE the v1 - # surface rather than users of it -- this gem's own test suite, which - # configures through `Capybara::Screenshot.*` by design and would - # otherwise print the notice on every run. Deliberately survives - # {reset!}, which exists to give a single test a clean slate. - # @return [void] - def suppress_migration_notice! - MUTEX.synchronize { @notice_suppressed = true } - end - - # Emit a deprecation warning for +subject+, exactly once per unique - # +subject+ per process -- preceded, the first time round, by - # {MIGRATION_NOTICE}. - # - # @param subject [String] the deprecated old-namespace name being - # referenced, e.g. "Capybara::Screenshot::Diff::ImageCompare" - # @param replacement [String] the new-namespace name to use instead - # @return [void] - def warn(subject, replacement) - return if SnapDiff.silence_deprecations? - - notice - - first_time = MUTEX.synchronize do - @seen.key?(subject) ? false : (@seen[subject] = true) - end - return unless first_time - - Kernel.warn(message_for(subject, replacement, caller_locations(1))) - end - - # @api private - # - # Clears the seen-set. For tests only -- lets each example assert - # "warns once" from a clean slate instead of leaking state across - # the suite. - # @return [void] - def reset! - MUTEX.synchronize do - @seen.clear - @notified = false - end - end - - private - - def message_for(subject, replacement, locations) - message = "[snap_diff deprecation] `#{subject}` is deprecated (constant); " \ - "use `#{replacement}` instead." - origin = origin_for(locations) - origin ? "#{message} (called from #{origin})" : message - end - - # First frame outside the gem's lib dir, formatted "file:line"; - # nil when every frame is internal (or paths are unavailable). - def origin_for(locations) - (locations || []).each do |location| - path = location.absolute_path || location.path - next if path.nil? || path.start_with?(GEM_LIB_DIR) - - return "#{path}:#{location.lineno}" - end - nil - 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 diff --git a/lib/snap_diff/legacy_shims.rb b/lib/snap_diff/legacy_shims.rb deleted file mode 100644 index 1f4a010d..00000000 --- a/lib/snap_diff/legacy_shims.rb +++ /dev/null @@ -1,363 +0,0 @@ -# frozen_string_literal: true - -require "snap_diff/comparison" -require "snap_diff/config" -require "snap_diff/deprecation" -require "snap_diff/drivers" -require "snap_diff/errors" -require "snap_diff/os" -require "snap_diff/reporters/default" -require "snap_diff/version" - -# THE v1 compatibility surface, in one file -- and the whole of it that is -# code. lib/capybara* is alias-only by contract -# (legacy_tree_is_alias_only_test.rb) and the canonical core names nothing -# from it (core_tree_has_no_legacy_deps_test.rb), so this file plus those -# trees is exactly what 3.0 deletes. -# -# Three things live here: -# 1. the const_missing forwarders for the pre-v2 namespaces (below); -# 2. CONFIG_MAPPING -- the old mattr_accessor surface, generated as thin -# delegators onto SnapDiff.config (which owns the storage); -# 3. the derived/config forwarders that used to sit in config_legacy.rb -# (Screenshot.active?, Diff.configure, SnapDiff.start, ...). -# -# 2 and 3 moved here so `require "snap_diff"` can keep offering the full v1 -# surface -- as it always has -- without the core requiring anything from -# lib/capybara/. -# -# const_missing-based forwarders for the pre-v2 namespaces. Every old-name -# lookup below resolves -- lazily -- to the exact -# same object as its SnapDiff:: replacement (identity pinned by -# test/unit/namespace_forwarding_test.rb) and emits a deprecation warning, -# once per constant per process, silenceable via -# SnapDiff.silence_deprecations or SNAP_DIFF_SILENCE_DEPRECATIONS=1. -# -# Deliberately eager-and-silent exceptions (plain constants assigned BELOW, -# never warn individually -- the once-per-process migration notice still -# fires for the paths that CAN be hooked; see Deprecation.notice): -# -# - Capybara::Screenshot::Os: an advertised entry-point constant probed with -# Object.const_defined? by support_load_probe_test.rb -- const_defined? -# never triggers const_missing, so a lazy shim would break that contract. -# - Capybara::Screenshot::Diff::VERSION, and ::Comparison (the images -# struct): documented user-facing names that adopters feature-detect with -# defined?/const_defined?. -# - Capybara::Screenshot::Diff::Reporters::Default (a documented subclassing -# extension point): same reasoning. -# - The CapybaraScreenshotDiff error classes: rescue-by-old-name and -# defined? feature detection must keep behaving exactly as before. -# - Drivers::ChunkyPNGDriver / Drivers::VipsDriver: real constants on the -# shared SnapDiff::Drivers module (the Drivers alias is same-object by -# contract), so const_missing can never fire for the leaf names; -# resolving them through the old path still warns for ...::Drivers. They -# are `autoload`ed there, so naming one loads it. -# - Diff::LOADED_DRIVERS: user code registers custom drivers by mutating -# this hash in place, so it must be the exact same object as the -# canonical SnapDiff::Drivers.loaded -- a lazy warn-once shim could not -# keep a mutable alias, and warning on a supported registration surface -# would be noise. Assigned eagerly below. -# - Diff::AVAILABLE_DRIVERS: stays a real constant, aliased BELOW from the -# canonical SnapDiff::Drivers::AVAILABLE_DRIVERS (same object; -# SnapDiff::Drivers.available is the canonical reader, and its constant is -# the stubbing point -- stubbing this alias only rebinds the alias). -# -# All of them are assigned HERE rather than in their own forwarder files -# under lib/capybara*. Those forwarders are loaded only by the LEGACY entry -# points, so a partially migrated app -- one that swapped its `require` line -# for a canonical `snap_diff*` one first, exactly as UPGRADING.md tells it -# to, and has not renamed its constants yet -- lost every one of them and -# died on `uninitialized constant Capybara::Screenshot::Os`. This file is -# required by every entry point, canonical and legacy, so it is the only -# place the eager exceptions can actually be eager. -# -# CapybaraScreenshotDiff::DSL and ::Minitest::Assertions are the two that -# CANNOT be eager here: snap_diff/dsl requires "snap_diff" (which requires -# this file), and snap_diff/integrations/minitest pulls in the minitest gem, -# which no canonical entry point should force on a process. They are mapped -# lazily below instead, and stay eager under the legacy entry points that -# load their forwarder files. - -# The v1 namespaces, predefined empty so CONFIG_MAPPING can name them at -# class-body eval time. Everything below reopens them. -module Capybara - module Screenshot - module Diff - end - end -end - -module SnapDiff - # @api private - module LegacyShims - # Installs a warn-then-forward const_missing on +namespace+. - # - # @param namespace [Module] the old namespace to hook - # @param old_prefix [String] how the old constant path reads to a human - # @param mapping [Hash{Symbol => String}] old leaf name => new full name - def self.install(namespace, old_prefix, mapping) - namespace.define_singleton_method(:const_missing) do |name| - target = mapping[name] - return super(name) unless target - - Deprecation.warn("#{old_prefix}::#{name}", target) - LegacyShims.resolve("#{old_prefix}::#{name}", target) - end - end - - # The handful of replacements whose file name does not follow the gem's - # own convention (SnapDiff::AreaCalculator -> snap_diff/area_calculator). - REQUIRE_PATHS = { - "SnapDiff::Minitest::Assertions" => "snap_diff/integrations/minitest" - }.freeze - - # Resolving an old name has to LOAD the replacement, not merely name it. - # The v1 entry points required the whole gem, so v1 code could say - # `Capybara::Screenshot::Diff::Utils` with nothing else required; the - # canonical entry points are lean, so the shim used to resolve its - # mapping and then die on a bare "uninitialized constant SnapDiff::Utils" - # -- an internal name the reader has no way to act on. - def self.resolve(old_name, target) - require_unit(target) unless Object.const_defined?(target) - Object.const_get(target) - rescue NameError - # Deliberately does NOT advise "reference #{target} directly": we just - # failed to load it, so that name does not exist either. - raise NameError, "`#{old_name}` maps to `#{target}`, which this process cannot load. " \ - "See docs/UPGRADING.md for the v1 -> SnapDiff name map." - end - - def self.require_unit(target) - require(REQUIRE_PATHS[target] || target - .gsub("::", "/") - .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2') - .gsub(/([a-z\d])([A-Z])/, '\1_\2') - .downcase) - rescue LoadError - # No file of its own -- the name lives inside another unit - # (SnapDiff::RED_RGBA, ::BacktraceFilter). The const_get above decides - # whether it is already loaded. - end - - # config attr name => [legacy module, legacy accessor name]. - # The keys are exactly SnapDiff::Config::SETTINGS; this hash only says - # which of the two legacy holders each one used to hang off, and under - # what name (only +screenshot_enabled+ differs -- see Config::SETTINGS). - CONFIG_MAPPING = { - # Capybara::Screenshot - add_driver_path: [Capybara::Screenshot, :add_driver_path], - add_os_path: [Capybara::Screenshot, :add_os_path], - blur_active_element: [Capybara::Screenshot, :blur_active_element], - screenshot_enabled: [Capybara::Screenshot, :enabled], - hide_caret: [Capybara::Screenshot, :hide_caret], - disable_animations: [Capybara::Screenshot, :disable_animations], - root: [Capybara::Screenshot, :root], - stability_time_limit: [Capybara::Screenshot, :stability_time_limit], - window_size: [Capybara::Screenshot, :window_size], - save_path: [Capybara::Screenshot, :save_path], - use_lfs: [Capybara::Screenshot, :use_lfs], - screenshot_format: [Capybara::Screenshot, :screenshot_format], - capybara_screenshot_options: [Capybara::Screenshot, :capybara_screenshot_options], - # Capybara::Screenshot::Diff - delayed: [Capybara::Screenshot::Diff, :delayed], - area_size_limit: [Capybara::Screenshot::Diff, :area_size_limit], - fail_if_new: [Capybara::Screenshot::Diff, :fail_if_new], - pending_if_new: [Capybara::Screenshot::Diff, :pending_if_new], - fail_on_difference: [Capybara::Screenshot::Diff, :fail_on_difference], - color_distance_limit: [Capybara::Screenshot::Diff, :color_distance_limit], - enabled: [Capybara::Screenshot::Diff, :enabled], - shift_distance_limit: [Capybara::Screenshot::Diff, :shift_distance_limit], - skip_area: [Capybara::Screenshot::Diff, :skip_area], - driver: [Capybara::Screenshot::Diff, :driver], - tolerance: [Capybara::Screenshot::Diff, :tolerance], - perceptual_threshold: [Capybara::Screenshot::Diff, :perceptual_threshold], - screenshoter: [Capybara::Screenshot::Diff, :screenshoter], - manager: [Capybara::Screenshot::Diff, :manager] - }.freeze - - # Installs the old mattr_accessor surface onto the legacy modules, - # delegating to the single storage in SnapDiff.config. mattr_accessor - # used to define both singleton and instance accessors (the instance - # ones are what `include Capybara::Screenshot::Diff` picks up), so both - # are installed. root keeps its historical asymmetry -- readable - # everywhere, writable only at module level (it was mattr_reader plus a - # custom module-level writer) -- with the Pathname coercion living in - # Config#root=. - def self.install_config_accessors - CONFIG_MAPPING.each do |name, (mod, mattr)| - [mod, mod.singleton_class].each do |target| - target.define_method(mattr) do - Deprecation.notice - SnapDiff.config.public_send(name) - end - next if name == :root && target == mod - - target.define_method(:"#{mattr}=") do |value| - Deprecation.notice - SnapDiff.config.public_send(:"#{name}=", value) - end - end - end - end - - # `include Capybara::Screenshot::Diff` is the third way into the v1 - # surface (it picks up the instance-level accessors installed above) and - # resolves no deprecated constant of its own, so it needs its own hook - # for the once-per-process notice. - def self.install_include_notice(mod) - mod.define_singleton_method(:included) do |base| - Deprecation.notice - super(base) - end - end - end -end - -SnapDiff::LegacyShims.install_config_accessors -SnapDiff::LegacyShims.install_include_notice(Capybara::Screenshot) -SnapDiff::LegacyShims.install_include_notice(Capybara::Screenshot::Diff) - -module SnapDiff - # v1-style configuration: yields the two legacy accessor holders - # (+Capybara::Screenshot+, +Capybara::Screenshot::Diff+) exactly as - # +Capybara::Screenshot::Diff.configure+ always has -- and, since ADR-008 - # step 7b, this is where that yield actually happens; Diff.configure - # forwards here. Both names stay identical in call shape. - # - # SnapDiff.start do |screenshot, diff| - # screenshot.window_size = [1280, 1024] - # diff.tolerance = 0.0005 - # end - # - # Defined in this file, not snap_diff.rb, because the two holders it - # yields ARE the v1 surface: it cannot outlive them. The consolidated - # shape, SnapDiff.configure, is the canonical one and lives in the core. - def self.start - yield Capybara::Screenshot, Capybara::Screenshot::Diff - end -end - -module Capybara - module Screenshot - # EAGER same-object alias (see header): the only place it can be eager - # for a canonical-only require, which is what a half-migrated app has. - Os = SnapDiff::Os - - # Derived config, ex config_legacy.rb: one-line forwarders onto the - # canonical implementations in SnapDiff::Config (ADR-008 step 7b). - class << self - def active? - SnapDiff.config.active? - end - - def screenshot_area - SnapDiff.config.screenshot_area - end - - def screenshot_area_abs - SnapDiff.config.screenshot_area_abs - end - end - - 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 - AVAILABLE_DRIVERS = SnapDiff::Drivers::AVAILABLE_DRIVERS - Comparison = SnapDiff::Comparison::Images - VERSION = SnapDiff::VERSION - - module Reporters - Default = SnapDiff::Reporters::Default - end - - # Configure screenshot and diff settings in one block. - # - # Capybara::Screenshot::Diff.configure do |screenshot, diff| - # screenshot.window_size = [1280, 1024] - # screenshot.stability_time_limit = 1 - # diff.driver = :vips - # diff.tolerance = 0.0005 - # end - # The bare `yield` (rather than an explicit &block) keeps this - # method's published arity byte-identical to what it always had. - def self.configure - SnapDiff.start { |screenshot, diff| yield screenshot, diff } - end - - def self.compare(baseline_path, current_path, **options) - SnapDiff.compare(baseline_path, current_path, **options) - end - - def self.default_options - SnapDiff::Deprecation.notice - SnapDiff.config.default_options - end - end - end -end - -module CapybaraScreenshotDiff - module Reporters - end - - # Predefined so the mapping below has a namespace to hang const_missing - # on; capybara_screenshot_diff/minitest reopens it with the eager alias. - module Minitest - end - - # EAGER same-object aliases (see header): rescue-by-old-name and - # defined?/const_defined? feature detection must behave as they always - # have, under canonical and legacy requires alike. - CapybaraScreenshotDiffError = SnapDiff::Error - ExpectationNotMet = SnapDiff::ExpectationNotMet - UnstableImage = SnapDiff::UnstableImage - WindowSizeMismatchError = SnapDiff::WindowSizeMismatchError -end - -SnapDiff::LegacyShims.install(Capybara::Screenshot, "Capybara::Screenshot", { - BrowserHelpers: "SnapDiff::BrowserHelpers", - Screenshoter: "SnapDiff::Screenshoter" -}.freeze) - -SnapDiff::LegacyShims.install(Capybara::Screenshot::Diff, "Capybara::Screenshot::Diff", { - Vcs: "SnapDiff::Vcs", - StableScreenshoter: "SnapDiff::StableScreenshoter", - ImagePreprocessor: "SnapDiff::ImagePreprocessor", - AreaCalculator: "SnapDiff::AreaCalculator", - AnnotationService: "SnapDiff::AnnotationService", - Utils: "SnapDiff::Utils", - ScreenshotMatcher: "SnapDiff::ScreenshotMatcher", - Drivers: "SnapDiff::Drivers", - ImageCompare: "SnapDiff::Comparison", - Difference: "SnapDiff::ComparisonResult" -}.freeze) - -SnapDiff::LegacyShims.install(CapybaraScreenshotDiff, "CapybaraScreenshotDiff", { - RED_RGBA: "SnapDiff::RED_RGBA", - ORANGE_RGBA: "SnapDiff::ORANGE_RGBA", - SnapManager: "SnapDiff::SnapManager", - Snap: "SnapDiff::Snap", - ScreenshotNamer: "SnapDiff::ScreenshotNamer", - AttemptsReporter: "SnapDiff::AttemptsReporter", - BacktraceFilter: "SnapDiff::BacktraceFilter", - ErrorWithFilteredBacktrace: "SnapDiff::ErrorWithFilteredBacktrace", - ScreenshotAssertion: "SnapDiff::ScreenshotAssertion", - AssertionRegistry: "SnapDiff::AssertionRegistry", - DSL: "SnapDiff::DSL" -}.freeze) - -SnapDiff::LegacyShims.install(CapybaraScreenshotDiff::Reporters, "CapybaraScreenshotDiff::Reporters", { - HTML: "SnapDiff::Reporters::HTML" -}.freeze) - -SnapDiff::LegacyShims.install(CapybaraScreenshotDiff::Minitest, "CapybaraScreenshotDiff::Minitest", { - Assertions: "SnapDiff::Minitest::Assertions" -}.freeze) - -# BaseDriver dissolved into the SnapDiff::Driver mixin; the Drivers alias is -# same-object, so the hook has to live on SnapDiff::Drivers itself. -# `class MyDriver < BaseDriver` becomes `include SnapDiff::Driver`. -SnapDiff::LegacyShims.install(SnapDiff::Drivers, "Capybara::Screenshot::Diff::Drivers", { - BaseDriver: "SnapDiff::Driver" -}.freeze) diff --git a/scripts/generate_sample_report.rb b/scripts/generate_sample_report.rb index 9b9245e1..87d20b50 100644 --- a/scripts/generate_sample_report.rb +++ b/scripts/generate_sample_report.rb @@ -4,14 +4,12 @@ # Uses absolute file:// paths so images load when opened directly in a browser. require "bundler/setup" -require "capybara_screenshot_diff" -require "capybara_screenshot_diff/minitest" -require "capybara/screenshot/diff" -require "capybara_screenshot_diff/reporters/html" +require "snap_diff" +require "snap_diff/reporters/html" -output_path = CapybaraScreenshotDiff::Reporters::HTML.default_output_path +output_path = SnapDiff::Reporters::HTML.default_output_path -# Build real comparisons using the gem's own ImageCompare. +# Build real comparisons using the gem's own Comparison. # Each pair gets a unique copy of the base image to avoid annotation file conflicts. pairs = [ {name: "islands-map", base: "a", new: "b"}, @@ -20,7 +18,7 @@ ] embed = ARGV.include?("--embed") || !!ENV["CI"] -reporter = CapybaraScreenshotDiff::Reporters::HTML.new(output_path: output_path, embed_images: embed) +reporter = SnapDiff::Reporters::HTML.new(output_path: output_path, embed_images: embed) fixtures = File.expand_path("../test/fixtures/images", __dir__) tmp_dir = File.expand_path("../tmp/sample_images", __dir__) FileUtils.mkdir_p(tmp_dir) @@ -32,17 +30,17 @@ FileUtils.cp("#{fixtures}/#{pair[:base]}.png", base_copy) FileUtils.cp("#{fixtures}/#{pair[:new]}.png", new_copy) - compare = Capybara::Screenshot::Diff::ImageCompare.new(new_copy, base_copy, driver: :vips) + compare = SnapDiff::Comparison.new(new_copy, base_copy, driver: :vips) compare.processed - CapybaraScreenshotDiff::ScreenshotAssertion.new(pair[:name]).tap { |a| a.compare = compare } + SnapDiff::ScreenshotAssertion.new(pair[:name]).tap { |a| a.compare = compare } end # Add passing assertions (identical images = no difference) passing = %w[dashboard settings profile users].map do |name| - compare = Capybara::Screenshot::Diff::ImageCompare.new("#{fixtures}/a.png", "#{fixtures}/a.png") + compare = SnapDiff::Comparison.new("#{fixtures}/a.png", "#{fixtures}/a.png") compare.processed - CapybaraScreenshotDiff::ScreenshotAssertion.new(name).tap { |a| a.compare = compare } + SnapDiff::ScreenshotAssertion.new(name).tap { |a| a.compare = compare } end reporter.record(assertions + passing) diff --git a/test/legacy/errors_alias_test.rb b/test/legacy/errors_alias_test.rb deleted file mode 100644 index 40eaed8c..00000000 --- a/test/legacy/errors_alias_test.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" -# The shared harness loads canonical entry points only, so a legacy-surface -# test pulls in the v1 entry itself -- the require goes with the file in 3.0. -require "capybara_screenshot_diff" - -# ADR-008 step 2: the error classes live in SnapDiff (snap_diff/errors); -# the old CapybaraScreenshotDiff names are EAGER same-object aliases -- -# deliberately not const_missing shims -- so adopter rescue clauses and -# defined?/const_defined? feature detection keep working unchanged. -# Note the absence of any deprecation-silencing here: eager aliases never -# warn, and the suite-wide guard in test_helper raises on unexpected -# warnings, so these tests double as proof the aliases stay warning-free. -# -# LEGACY SURFACE (test/legacy/, see the Rakefile): deleted with lib/capybara* -# in 3.0. The hierarchy assertions that outlive the aliases moved to -# test/unit/errors_test.rb. -class ErrorsAliasTest < ActiveSupport::TestCase - # old constant path => new constant path - MAPPING = { - "CapybaraScreenshotDiff::CapybaraScreenshotDiffError" => "SnapDiff::Error", - "CapybaraScreenshotDiff::ExpectationNotMet" => "SnapDiff::ExpectationNotMet", - "CapybaraScreenshotDiff::UnstableImage" => "SnapDiff::UnstableImage", - "CapybaraScreenshotDiff::WindowSizeMismatchError" => "SnapDiff::WindowSizeMismatchError" - }.freeze - - MAPPING.each do |old_name, new_name| - test "#{old_name} is the same object as #{new_name}" do - assert_same Object.const_get(new_name), Object.const_get(old_name), - "#{old_name} must alias the exact #{new_name} object" - end - - # The regression the eager choice prevents: const_defined? (and - # defined?) never trigger const_missing, so a lazy shim would make - # feature detection by the old name silently return false. - test "#{old_name} is visible to const_defined? without const_missing" do - mod, leaf = old_name.rpartition("::").values_at(0, 2) - assert Object.const_get(mod).const_defined?(leaf, false), - "#{leaf} must be an eagerly-defined constant on #{mod}" - assert defined?(CapybaraScreenshotDiff), "sanity: old namespace present" - end - end - - test "rescue by old name catches an error raised under the new name" do - caught = nil - begin - raise SnapDiff::ExpectationNotMet.new("probe", caller) - rescue CapybaraScreenshotDiff::ExpectationNotMet => e - caught = e - end - assert_equal "probe", caught.message - end -end diff --git a/test/legacy/legacy_config_accessors_test.rb b/test/legacy/legacy_config_accessors_test.rb deleted file mode 100644 index f52f6f13..00000000 --- a/test/legacy/legacy_config_accessors_test.rb +++ /dev/null @@ -1,196 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" -# The shared harness loads canonical entry points only, so a legacy-surface -# test pulls in the v1 entry itself -- the require goes with the file in 3.0. -require "capybara_screenshot_diff" - -# LEGACY SURFACE (test/legacy/, see the Rakefile). -# -# The v1 half of snap_diff_config_test.rb: SnapDiff::LegacyShims generates -# the old Capybara::Screenshot / Capybara::Screenshot::Diff mattr_accessors -# as a second VIEW of the one SnapDiff::Config storage. Everything here is -# about that view -- the mapping's completeness, and that a write through -# either surface is visible from the other. Verbatim from the canonical -# file, which keeps the Config-only half; both go on passing until 3.0 -# deletes legacy_shims.rb, this file, and the trees they serve. -class LegacyConfigAccessorsTest < ActiveSupport::TestCase - def config - SnapDiff.config - end - - # Reflection-based completeness check, reworked for the ADR-008 storage - # inversion (the old version derived settings from mattr class variables, - # which no longer exist). Two directions: - # - # (a) every singleton writer on the legacy modules is a mapped config - # setting -- a future `mattr_accessor :foo` (active_support's ext is - # one require away) or hand-rolled writer would create unmapped - # storage invisible to SnapDiff.config; - # (b) SnapDiff.config stores exactly one ivar per declared setting (that - # half stays in the canonical file: Config outlives the mapping). - NON_CONFIG_WRITERS = [].freeze # currently no non-config writer API on the legacy modules - - test "every legacy singleton writer is covered by SnapDiff::LegacyShims::CONFIG_MAPPING" do - covered = SnapDiff::LegacyShims::CONFIG_MAPPING.values - - [Capybara::Screenshot, Capybara::Screenshot::Diff].each do |mod| - writers = mod.singleton_class.public_instance_methods(false).grep(/=\z/) - NON_CONFIG_WRITERS - - assert_operator writers.size, :>, 0, "#{mod} lost all its config writers" - writers.each do |writer| - mattr = writer.to_s.delete_suffix("=").to_sym - - assert_includes covered, [mod, mattr], - "#{mod}.#{writer} is a config writer with no SnapDiff::Config mapping. " \ - "Add an entry to LegacyShims::CONFIG_MAPPING (rename the key if `#{mattr}` collides " \ - "with an existing one, as `enabled` does), or add it to NON_CONFIG_WRITERS " \ - "if it is deliberately not a config setting." - end - end - end - - # The two halves of the split are only safe while they agree: Config - # declares the settings and knows nothing about the legacy holders, - # LegacyShims says which holder each one is exposed on. A setting in one - # and not the other is either storage with no v1 accessor or a v1 - # accessor delegating to a setting that does not exist. - test "LegacyShims::CONFIG_MAPPING covers exactly Config::SETTINGS" do - assert_equal SnapDiff::Config::SETTINGS, SnapDiff::LegacyShims::CONFIG_MAPPING.keys - end - - test "every mapped setting is readable via config and equal to its mattr_accessor's value" do - SnapDiff::LegacyShims::CONFIG_MAPPING.each do |name, (mod, mattr)| - expected = mod.public_send(mattr) - actual = config.public_send(name) - - if expected.nil? - assert_nil actual, "config.#{name} should equal #{mod}.#{mattr}" - else - assert_equal expected, actual, "config.#{name} should equal #{mod}.#{mattr}" - end - end - end - - test "writing fail_if_new via the old mattr_accessor is visible via config, and back" do - original = Capybara::Screenshot::Diff.fail_if_new - - begin - Capybara::Screenshot::Diff.fail_if_new = true - assert_equal true, config.fail_if_new - - config.fail_if_new = false - assert_equal false, Capybara::Screenshot::Diff.fail_if_new - ensure - Capybara::Screenshot::Diff.fail_if_new = original - end - end - - test "writing window_size via the old mattr_accessor is visible via config, and back" do - original = Capybara::Screenshot.window_size - - begin - Capybara::Screenshot.window_size = [1280, 1024] - assert_equal [1280, 1024], config.window_size - - config.window_size = [800, 600] - assert_equal [800, 600], Capybara::Screenshot.window_size - ensure - Capybara::Screenshot.window_size = original - end - end - - # Capybara::Screenshot.enabled and Capybara::Screenshot::Diff.enabled are - # two independent settings that happen to share a bare name in their own - # modules (see Capybara::Screenshot.active?, which reads both). Config is - # flat, so it cannot expose two attributes both called `enabled` -- the - # Screenshot-side one is renamed `screenshot_enabled`. This test proves - # the rename didn't accidentally collapse them into one shared value. - test "screenshot_enabled and enabled stay independent settings under Config" do - original_screenshot = Capybara::Screenshot.enabled - original_diff = Capybara::Screenshot::Diff.enabled - - begin - config.screenshot_enabled = true - config.enabled = false - - assert_equal true, Capybara::Screenshot.enabled - assert_equal false, Capybara::Screenshot::Diff.enabled - assert_equal true, config.screenshot_enabled - assert_equal false, config.enabled - ensure - Capybara::Screenshot.enabled = original_screenshot - Capybara::Screenshot::Diff.enabled = original_diff - end - end - - # ADR-008 step 7b moved this precedence rule from - # Capybara::Screenshot.active? into Config#active?, and found it had no - # test at all: replacing the whole expression with a bare `enabled` kept - # all 529 unit tests green. The full truth table is pinned here, through - # both the canonical method and the legacy forwarder, so it cannot move - # again unnoticed. (The canonical file pins the Config#active? column on - # its own, so the rule survives this file's deletion.) - # - # The rule: the Screenshot-side flag wins whenever it was set to anything - # at all; only a nil there falls through to the Diff-side flag. - ACTIVE_TRUTH_TABLE = [ - [true, true, true], - [true, false, true], - [false, true, false], - [false, false, false], - [nil, true, true], - [nil, false, false] - ].freeze - - test "active? gives Screenshot.enabled precedence and only falls through on nil" do - original_screenshot = Capybara::Screenshot.enabled - original_diff = Capybara::Screenshot::Diff.enabled - - ACTIVE_TRUTH_TABLE.each do |screenshot_enabled, enabled, expected| - config.screenshot_enabled = screenshot_enabled - config.enabled = enabled - context = "screenshot_enabled=#{screenshot_enabled.inspect}, enabled=#{enabled.inspect}" - - assert_equal expected, !!config.active?, "Config#active? with #{context}" - assert_equal expected, !!Capybara::Screenshot.active?, "Capybara::Screenshot.active? with #{context}" - end - ensure - Capybara::Screenshot.enabled = original_screenshot - Capybara::Screenshot::Diff.enabled = original_diff - end - - test "writing root through config round-trips through the same Pathname coercion" do - original = Capybara::Screenshot.root - - begin - config.root = "/tmp" - - assert_equal Pathname("/tmp"), Capybara::Screenshot.root - assert_equal Pathname("/tmp"), config.root - ensure - Capybara::Screenshot.root = original - end - end - - test "SnapDiff.configure lets callers set values through the yielded config" do - original = Capybara::Screenshot::Diff.tolerance - - begin - SnapDiff.configure { |c| c.tolerance = 0.0321 } - assert_equal 0.0321, Capybara::Screenshot::Diff.tolerance - ensure - Capybara::Screenshot::Diff.tolerance = original - end - end - - test "SnapDiff.start (v1-style two-arg yield) and SnapDiff.configure (single Config yield) coexist" do - diff_yielded = [] - SnapDiff.start { |screenshot, diff| diff_yielded << [screenshot, diff] } - assert_equal [[Capybara::Screenshot, Capybara::Screenshot::Diff]], diff_yielded - - config_yielded = [] - SnapDiff.configure { |c| config_yielded << c } - assert_equal [config], config_yielded - end -end diff --git a/test/legacy/legacy_config_default_timing_test.rb b/test/legacy/legacy_config_default_timing_test.rb deleted file mode 100644 index 7640ae81..00000000 --- a/test/legacy/legacy_config_default_timing_test.rb +++ /dev/null @@ -1,58 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" -require "open3" -require "unit/config_default_timing_test" # single source of truth for the probe scripts - -# LEGACY SURFACE (test/legacy/, see the Rakefile). -# -# The v1 half of config_default_timing_test.rb. Two claims, both about the -# old entry points and the old accessor view, both deleted in 3.0: -# -# 1. every legacy entry point produces the SAME require-time defaults and -# the same freezing/liveness behaviour as the canonical ones -- proved by -# re-running the canonical file's probe scripts verbatim under a v1 -# PROBE_ENTRY, so the two files can never drift; -# 2. every mapped setting reads identically through SnapDiff.config and -# through its legacy mattr_accessor. Together with (1) that is exactly -# what the old `check_both` asserted: the value is right via config, and -# the two surfaces cannot fork. -class LegacyConfigDefaultTimingTest < ActiveSupport::TestCase - ENTRY_POINTS = %w[ - capybara_screenshot_diff - capybara_screenshot_diff/minitest - capybara/screenshot/diff - ].freeze - - def run_probe(script, env) - out, status = Open3.capture2e(env, RbConfig.ruby, "-Ilib", "-e", script) - - assert status.success?, "probe failed:\n#{out}" - end - - BOTH_SURFACES_SCRIPT = ConfigDefaultTimingTest::CHECK_HELPER + <<~'RUBY' - require ENV.fetch("PROBE_ENTRY") - - SnapDiff::LegacyShims::CONFIG_MAPPING.each do |name, (mod, mattr)| - check("SnapDiff.config.#{name} vs #{mod}.#{mattr}", mod.public_send(mattr), SnapDiff.config.public_send(name)) - end - RUBY - - ENTRY_POINTS.each do |entry| - test "#{entry}: defaults snapshot matches; ENV/pwd frozen at require, wait live" do - run_probe(ConfigDefaultTimingTest::SNAPSHOT_SCRIPT, {"PROBE_ENTRY" => entry, "CI" => nil}) - end - - test "#{entry}: CI=1 before require turns fail_if_new on; unset after require does not turn it off" do - run_probe(ConfigDefaultTimingTest::CI_SET_SCRIPT, {"PROBE_ENTRY" => entry, "CI" => "1"}) - end - - test "#{entry}: Rails.root defined before require wins; reassigning it after require is not seen" do - run_probe(ConfigDefaultTimingTest::RAILS_ROOT_SCRIPT, {"PROBE_ENTRY" => entry, "CI" => nil}) - end - - test "#{entry}: every mapped setting reads the same through config and its mattr_accessor" do - run_probe(BOTH_SURFACES_SCRIPT, {"PROBE_ENTRY" => entry, "CI" => nil}) - end - end -end diff --git a/test/legacy/legacy_entry_point_probe_test.rb b/test/legacy/legacy_entry_point_probe_test.rb deleted file mode 100644 index f9dc189d..00000000 --- a/test/legacy/legacy_entry_point_probe_test.rb +++ /dev/null @@ -1,286 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" -require "unit/support_load_probe_test" # single source of truth for the subprocess probe - -# LEGACY SURFACE (test/legacy/, see the Rakefile). -# -# The v1 half of support_load_probe_test.rb: what the OLD entry points must -# still provide. Names and constants restored verbatim -- every assertion -# here is about a name 3.0 deletes, so repointing them at SnapDiff would -# have quietly turned this file into a duplicate of the canonical one. -class LegacyEntryPointProbeTest < ActiveSupport::TestCase - # Alias-completeness probe (the f89cea2 bug class): each documented entry - # point must define its advertised constants when it is the ONLY require — - # the acyclic redesign once narrowed capybara_screenshot_diff/minitest so - # consumers lost CapybaraScreenshotDiff::DSL, and only one CI matrix leg - # noticed. capybara_screenshot_diff/cucumber is not probed: it calls - # World(...) at load, which only exists inside cucumber's runtime context. - # Documented user-facing constants that must stay EAGER (see - # snap_diff/legacy_shims.rb's exception list): const_defined? never - # triggers const_missing, so a lazy shim makes `defined?` feature - # detection in adopter code silently return nil. - EAGER_USER_FACING = %w[ - Capybara::Screenshot::Diff::Reporters::Default - Capybara::Screenshot::Diff::Comparison - ].freeze - - # The subset of EAGER_USER_FACING that must resolve under EVERY entry - # point, canonical ones included -- these are read directly (a version - # string, a struct), not just feature-detected, so a canonical-only app - # still hits them. The test above only covers the four legacy entries, - # which is how VERSION silently disappeared from six entry points when the - # core stopped requiring capybara/screenshot/diff/version.rb: nothing - # loaded the forwarder that assigned it, and const_missing does not fire - # for a constant legacy_shims deliberately leaves out of its map. - EAGER_EVERYWHERE = %w[ - Capybara::Screenshot::Diff::VERSION - Capybara::Screenshot::Diff::Comparison - ].freeze - - ENTRY_POINTS = { - "capybara_screenshot_diff" => %w[ - CapybaraScreenshotDiff::DSL Capybara::Screenshot::Os Capybara::Screenshot::Diff - ] + EAGER_USER_FACING, - "capybara_screenshot_diff/minitest" => %w[ - CapybaraScreenshotDiff::DSL CapybaraScreenshotDiff::Minitest::Assertions - Capybara::Screenshot::Os Capybara::Screenshot::Diff - ] + EAGER_USER_FACING, - "capybara_screenshot_diff/rspec" => %w[ - CapybaraScreenshotDiff::DSL Capybara::Screenshot::Os Capybara::Screenshot::Diff - ] + EAGER_USER_FACING, - "capybara-screenshot-diff" => %w[ - CapybaraScreenshotDiff::DSL CapybaraScreenshotDiff::Minitest::Assertions - Capybara::Screenshot::Os Capybara::Screenshot::Diff - ] + EAGER_USER_FACING - }.freeze - - test "every documented entry point defines its advertised constants standalone" do - failures = ENTRY_POINTS.filter_map do |entry, constants| - probe(entry, <<~RUBY) - require #{entry.inspect} - missing = #{constants.inspect}.reject { |c| Object.const_defined?(c) } - abort("missing: \#{missing.join(", ")}") unless missing.empty? - RUBY - end - - assert_empty failures, <<~MSG - Entry point(s) no longer provide their advertised constants standalone: - - #{failures.join("\n")} - MSG - end - - # The legacy entries had the mirror image of the beta3 canonical hole: - # some of them stopped loading the umbrella, so CapybaraScreenshotDiff.verify - # and friends vanished while `defined?(CapybaraScreenshotDiff)` still passed. - LEGACY_SESSION_SURFACE = %w[ - verify reset reporters finalize_reporters! assertions registry - pending_screenshots_message - ].freeze - - LEGACY_ENTRY_POINTS = %w[ - capybara-screenshot-diff - snap_diff-capybara - capybara_screenshot_diff - capybara_screenshot_diff/minitest - capybara_screenshot_diff/rspec - capybara_screenshot_diff/cucumber - capybara_screenshot_diff/static - capybara/screenshot/diff - capybara/screenshot/diff/cucumber - ].freeze - - test "every legacy entry point keeps the CapybaraScreenshotDiff session surface" do - failures = LEGACY_ENTRY_POINTS.filter_map do |entry| - probe(entry, <<~RUBY) - require #{entry.inspect} - missing = #{LEGACY_SESSION_SURFACE.inspect}.reject { |m| CapybaraScreenshotDiff.respond_to?(m) } - abort("missing: \#{missing.join(", ")}") unless missing.empty? - RUBY - end - - assert_empty failures, <<~MSG - Legacy entry point(s) leave CapybaraScreenshotDiff half-present - (the module answers `defined?` but not its own session methods): - - #{failures.join("\n")} - MSG - end - - # SnapDiff.start moved here out of the canonical CANONICAL_SURFACE gate: it - # is defined in legacy_shims.rb and yields the two v1 config holders, so a - # canonical gate demanding it fails the moment 3.0 deletes them. It is - # still a documented v1 method, so the per-entry-point availability claim - # the canonical gate used to make lives on here -- for the entries that - # actually keep it. (What it yields is pinned in legacy_forwarders_test.) - test "SnapDiff.start is available from every legacy entry point" do - failures = LEGACY_ENTRY_POINTS.filter_map do |entry| - probe(entry, <<~RUBY) - require #{entry.inspect} - abort("SnapDiff.start missing") unless SnapDiff.respond_to?(:start) - RUBY - end - - assert_empty failures, failures.join("\n") - end - - # Every documented entry point, canonical and legacy. capybara_screenshot_diff/dsl - # is listed only here: it is not in ENTRY_POINTS or LEGACY_ENTRY_POINTS, which - # is exactly why it was the legacy entry that lost VERSION unnoticed. - ALL_ENTRY_POINTS = ( - SupportLoadProbeTest::CANONICAL_ENTRY_POINTS.keys + LEGACY_ENTRY_POINTS + %w[capybara_screenshot_diff/dsl] - ).uniq.freeze - - test "the eager user-facing constants resolve under every entry point" do - failures = ALL_ENTRY_POINTS.filter_map do |entry| - probe(entry, <<~RUBY) - require #{entry.inspect} - missing = #{EAGER_EVERYWHERE.inspect}.reject { |c| Object.const_defined?(c) } - abort("missing: \#{missing.join(", ")}") unless missing.empty? - RUBY - end - - assert_empty failures, <<~MSG - Entry point(s) no longer resolve constants that are supposed to be eager - everywhere. `defined?` returns nil for these and const_missing does not - fire, so adopter feature detection fails silently: - - #{failures.join("\n")} - MSG - end - - # The canonical half of this claim lives in support_load_probe_test.rb; the - # v1 entry points get the same treatment here because the cycle they used - # to load through (drivers.rb <-> utils.rb) shouted at every user whose - # suite runs with warnings on -- which Rake::TestTask does by default. - test "no legacy entry point emits a circular require warning under -w" do - failures = LEGACY_ENTRY_POINTS.filter_map do |entry| - noise = SupportLoadProbeTest.verbose_load(entry).lines.grep(/circular require/) - "require \"#{entry}\" ->\n#{noise.join}" unless noise.empty? - end - - assert_empty failures, <<~MSG - Legacy entry point(s) load through a `require` cycle: - - #{failures.join("\n")} - MSG - end - - # Every legacy constant a consumer might touch, as of v1.12.0's surface. - # UPGRADING.md tells adopters to migrate their `require` line FIRST and - # rename constants afterwards, so this half-migrated state -- canonical - # require, v1 constants -- is a supported one, not an exotic edge case. - # It used to kill a whole suite at load on `Capybara::Screenshot::Os`. - LEGACY_CONSTANTS = %w[ - Capybara::Screenshot::Os - Capybara::Screenshot::BrowserHelpers - Capybara::Screenshot::Screenshoter - Capybara::Screenshot::Diff::Vcs - Capybara::Screenshot::Diff::StableScreenshoter - Capybara::Screenshot::Diff::ImagePreprocessor - Capybara::Screenshot::Diff::AreaCalculator - Capybara::Screenshot::Diff::AnnotationService - Capybara::Screenshot::Diff::Utils - Capybara::Screenshot::Diff::ScreenshotMatcher - Capybara::Screenshot::Diff::Drivers - Capybara::Screenshot::Diff::Drivers::BaseDriver - Capybara::Screenshot::Diff::Drivers::ChunkyPNGDriver - Capybara::Screenshot::Diff::ImageCompare - Capybara::Screenshot::Diff::Difference - Capybara::Screenshot::Diff::Comparison - Capybara::Screenshot::Diff::VERSION - Capybara::Screenshot::Diff::LOADED_DRIVERS - Capybara::Screenshot::Diff::AVAILABLE_DRIVERS - Capybara::Screenshot::Diff::Reporters::Default - Region - CapybaraScreenshotDiff::RED_RGBA - CapybaraScreenshotDiff::ORANGE_RGBA - CapybaraScreenshotDiff::SnapManager - CapybaraScreenshotDiff::Snap - CapybaraScreenshotDiff::ScreenshotNamer - CapybaraScreenshotDiff::AttemptsReporter - CapybaraScreenshotDiff::BacktraceFilter - CapybaraScreenshotDiff::ErrorWithFilteredBacktrace - CapybaraScreenshotDiff::ScreenshotAssertion - CapybaraScreenshotDiff::AssertionRegistry - CapybaraScreenshotDiff::CapybaraScreenshotDiffError - CapybaraScreenshotDiff::ExpectationNotMet - CapybaraScreenshotDiff::UnstableImage - CapybaraScreenshotDiff::WindowSizeMismatchError - CapybaraScreenshotDiff::DSL - CapybaraScreenshotDiff::Minitest::Assertions - CapybaraScreenshotDiff::Reporters::HTML - ].freeze - - test "every legacy constant resolves under a canonical-only require" do - failures = ALL_ENTRY_POINTS.filter_map do |entry| - probe(entry, <<~RUBY) - ENV["SNAP_DIFF_SILENCE_DEPRECATIONS"] = "1" - require #{entry.inspect} - # Resolvability only -- same-object identity is pinned separately by - # test/legacy/namespace_forwarding_test.rb. - broken = #{LEGACY_CONSTANTS.inspect}.filter_map do |name| - begin - Object.const_get(name) - nil - rescue NameError => e - "\#{name}: \#{e.message.lines.first.strip}" - end - end - abort(broken.join("\n")) unless broken.empty? - RUBY - end - - assert_empty failures, <<~MSG - Legacy constant(s) do not resolve under these entry points. A half- - migrated app -- canonical require, v1 constants, exactly what - UPGRADING.md walks users into -- dies on the first reference: - - #{failures.join("\n")} - MSG - end - - # Vips is optional, so its driver leaf only has to resolve where the - # library is actually installed. - test "the vips driver leaf resolves under a canonical-only require" do - skip "vips not available in this environment" unless SnapDiff::Drivers.available.include?(:vips) - - assert_nil SupportLoadProbeTest.probe("snap_diff", <<~RUBY) - ENV["SNAP_DIFF_SILENCE_DEPRECATIONS"] = "1" - require "snap_diff" - Object.const_get("Capybara::Screenshot::Diff::Drivers::VipsDriver") - RUBY - end - - # A legacy name whose replacement genuinely cannot load must say so in the - # user's vocabulary -- UPGRADING.md -- not leak a bare "uninitialized - # constant SnapDiff::Something" from gem internals. - test "an unloadable legacy constant points at the upgrade guide" do - out, status = Open3.capture2e( - RbConfig.ruby, "-Ilib", "-e", <<~RUBY, chdir: File.expand_path("../..", __dir__) - ENV["SNAP_DIFF_SILENCE_DEPRECATIONS"] = "1" - require "snap_diff" - mod = Module.new - SnapDiff::LegacyShims.install(mod, "Old::Prefix", {Gone: "SnapDiff::NoSuchThing"}) - begin - mod::Gone - rescue NameError => e - puts e.message - end - RUBY - ) - - assert status.success?, out - assert_includes out, "SnapDiff::NoSuchThing" - assert_includes out, "docs/UPGRADING.md" - refute_includes out, "Reference `SnapDiff::NoSuchThing` directly", - "must not advise referencing a name we just failed to load" - end - - private - - def probe(entry, script) - SupportLoadProbeTest.probe(entry, script) - end -end diff --git a/test/legacy/legacy_forwarders_test.rb b/test/legacy/legacy_forwarders_test.rb deleted file mode 100644 index b93ac092..00000000 --- a/test/legacy/legacy_forwarders_test.rb +++ /dev/null @@ -1,124 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" -require "open3" -# The shared harness loads canonical entry points only, so a legacy-surface -# test pulls in the v1 entry itself -- the require goes with the file in 3.0. -require "capybara_screenshot_diff" -require "capybara_screenshot_diff/static" - -# LEGACY SURFACE (test/legacy/, see the Rakefile). -# -# The identity claims that make the old CapybaraScreenshotDiff module a -# *view* of the canonical state rather than a second copy of it, plus the -# v1-shaped SnapDiff.start. Collected here from the canonical tests they -# used to sit in (registry_concurrency_test, reporters_mutex_test, -# snap_diff_test): a mechanical repoint would have turned each of them into -# `assert_same X, X`, which is how a real claim quietly becomes a tautology. -# Verbatim, so the v1 contract keeps exactly the coverage it had. -class LegacyForwardersTest < ActiveSupport::TestCase - setup do - # These resolve old-namespace names on purpose; the suite-wide guard in - # test_helper raises on unexpected shim warnings. - @original_silence = SnapDiff.silence_deprecations - SnapDiff.silence_deprecations = true - end - - teardown do - SnapDiff.silence_deprecations = @original_silence - end - - # ADR-008 step 6: SnapDiff.session is the canonical accessor and - # CapybaraScreenshotDiff.registry a forwarder over it -- they must hand - # back the *same* object, not two registries that happen to look alike. - test "SnapDiff.session and CapybaraScreenshotDiff.registry are the same object" do - assert_same SnapDiff.session, CapybaraScreenshotDiff.registry - - SnapDiff.session.record_new_screenshot("shared_object_probe") - assert_equal ["shared_object_probe"], CapybaraScreenshotDiff.new_screenshots - ensure - SnapDiff.session.reset - end - - # ADR-008 step 6: SnapDiff::Reporting.register is the canonical way in; - # CapybaraScreenshotDiff.reporters stays as the compat view of the same - # array, so a registration must be visible through both. - test "register appends to the array CapybaraScreenshotDiff.reporters exposes" do - original_reporters = CapybaraScreenshotDiff.reporters.dup - CapybaraScreenshotDiff.reporters.clear - reporter = Object.new - - assert_same reporter, SnapDiff::Reporting.register(reporter) - assert_same SnapDiff::Reporting.reporters, CapybaraScreenshotDiff.reporters - assert_includes CapybaraScreenshotDiff.reporters, reporter - ensure - CapybaraScreenshotDiff.reporters.clear - CapybaraScreenshotDiff.reporters.concat(original_reporters) - end - - test "CapybaraScreenshotDiff.reporters_mutex is the canonical Reporting mutex" do - assert_same SnapDiff::Reporting.mutex, CapybaraScreenshotDiff.reporters_mutex - end - - test "Capybara::Screenshot::Diff::ImageCompare aliases SnapDiff::Comparison" do - assert_same SnapDiff::Comparison, Capybara::Screenshot::Diff::ImageCompare - end - - test "CapybaraScreenshotDiff.serve forwards to SnapDiff.serve, custom root included" do - original_root = SnapDiff.config.root - - CapybaraScreenshotDiff.serve("test/fixtures", root: "/tmp") - - assert_equal Pathname("/tmp"), SnapDiff.config.root - ensure - Capybara.app = Rails.application - SnapDiff.config.root = original_root - end - - # Acyclicity contract (the #208 deadlock-class fix): the lean - # `require "snap_diff"` entry must NEVER pull the umbrella - # capybara_screenshot_diff.rb back in. The old autoload wiring had - # snap_diff <-> capybara_screenshot_diff requiring each other, which - # produced load-order deadlocks/partially-initialized constants; #208 - # broke the cycle, but until now only discipline guarded it -- a probe - # that reintroduced the cycle left the whole suite green. This asserts - # the contract as data: after a bare require, the umbrella file must be - # absent from $LOADED_FEATURES. - # - # Lives here rather than in snap_diff_test: its subject is the v1 - # umbrella, and once 3.0 deletes that file the grep below is empty by - # construction and the guard can never fail again. - test "bare require \"snap_diff\" never loads the umbrella capybara_screenshot_diff" do - script = <<~RUBY - require "snap_diff" - umbrella = $LOADED_FEATURES.grep(%r{/lib/capybara_screenshot_diff\\.rb\\z}) - abort("umbrella loaded via: \#{umbrella.join(", ")}") unless umbrella.empty? - RUBY - - out, status = Open3.capture2e(RbConfig.ruby, "-Ilib", "-e", script) - - assert status.success?, "expected bare `require \"snap_diff\"` to keep the umbrella unloaded, got:\n#{out}" - end - - test ".start yields the same objects Diff.configure yields" do - yielded = [] - Capybara::Screenshot::Diff.configure { |screenshot, diff| yielded << [screenshot, diff] } - - started = [] - SnapDiff.start { |screenshot, diff| started << [screenshot, diff] } - - assert_equal yielded, started - end - - test ".start applies a setting like Diff.configure does" do - original = SnapDiff.config.tolerance - - begin - SnapDiff.start { |_screenshot, diff| diff.tolerance = 0.0123 } - - assert_equal 0.0123, SnapDiff.config.tolerance - ensure - SnapDiff.config.tolerance = original - end - end -end diff --git a/test/legacy/legacy_namespace_deprecation_test.rb b/test/legacy/legacy_namespace_deprecation_test.rb deleted file mode 100644 index b8753695..00000000 --- a/test/legacy/legacy_namespace_deprecation_test.rb +++ /dev/null @@ -1,160 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" -require "open3" -require "snap_diff/deprecation" -require "legacy/namespace_forwarding_test" # single source of truth for the old->new MAPPING - -# ADR-004 v2 step 6: resolving an old-namespace constant emits a deprecation -# warning -- exactly once per constant per process, naming the SnapDiff -# replacement, silenceable through the pre-existing SnapDiff::Deprecation -# switches (SnapDiff.silence_deprecations / SNAP_DIFF_SILENCE_DEPRECATIONS). -# Same-object identity for every pair stays pinned by -# namespace_forwarding_test.rb; this file pins only the warning behavior. -# -# LEGACY SURFACE (test/legacy/, see the Rakefile): deleted with lib/capybara* -# and snap_diff/deprecation.rb in 3.0. -class LegacyNamespaceDeprecationTest < ActiveSupport::TestCase - # Documented exceptions that stay EAGER (real constants, never warn): - # - Os / DSL: advertised entry-point constants, probed with - # Object.const_defined? by support_load_probe_test.rb -- const_defined? - # never triggers const_missing, so a lazy shim would break that contract. - # - VERSION: the gemspec resolves Capybara::Screenshot::Diff::VERSION at - # build time; a lazy shim would make every `gem build` warn. - EAGER_SILENT = %w[ - Capybara::Screenshot::Os - CapybaraScreenshotDiff::DSL - Capybara::Screenshot::Diff::VERSION - ].freeze - - # Real constants on the shared SnapDiff::Drivers module (the Drivers alias - # is same-object by contract), so const_missing can never fire for the leaf - # name; resolving them through the old path still warns for ...::Drivers. - WARNED_VIA_PARENT = %w[ - Capybara::Screenshot::Diff::Drivers::ChunkyPNGDriver - Capybara::Screenshot::Diff::Drivers::VipsDriver - ].freeze - - def setup - @original_silence = SnapDiff.silence_deprecations - # silence_deprecations? also reads the env var live, so an inherited - # SNAP_DIFF_SILENCE_DEPRECATIONS would defeat the accessor below. - @original_silence_env = ENV.delete("SNAP_DIFF_SILENCE_DEPRECATIONS") - SnapDiff.silence_deprecations = false - SnapDiff::Deprecation.reset! - # These tests emit (and capture) the warnings on purpose; keep the - # suite-wide raise-on-deprecation guard from test_helper out of the way. - SnapDiffDeprecationGuard.expected = true - end - - def teardown - SnapDiffDeprecationGuard.expected = false - SnapDiff.silence_deprecations = @original_silence - ENV["SNAP_DIFF_SILENCE_DEPRECATIONS"] = @original_silence_env if @original_silence_env - SnapDiff::Deprecation.reset! - end - - def capture_warnings - _out, err = capture_io { yield } - err.lines.reject(&:empty?) - end - - NamespaceForwardingTest::MAPPING.each do |old_name, new_name| - next if EAGER_SILENT.include?(old_name) || WARNED_VIA_PARENT.include?(old_name) - - define_method(:"test_#{old_name}_warns_once_pointing_at_#{new_name}") do - lines = capture_warnings do - 2.times { Object.const_get(old_name) } - end - - own_lines = lines.grep(/`#{Regexp.escape(old_name)}` is deprecated/) - assert_equal 1, own_lines.size, - "expected exactly one deprecation warning for #{old_name} across two resolutions, " \ - "got #{own_lines.size} in:\n#{lines.join}" - assert_match(/#{Regexp.escape(new_name)}/, own_lines.first, - "warning for #{old_name} should name the replacement #{new_name}") - end - end - - test "eager compatibility constants stay defined and silent" do - EAGER_SILENT.each do |name| - assert Object.const_defined?(name), "#{name} must stay an eagerly-defined constant" - end - - lines = capture_warnings { EAGER_SILENT.each { |name| Object.const_get(name) } } - - assert_empty lines, "eager compatibility constants must not warn" - end - - test "silencing suppresses shim warnings" do - SnapDiff.silence_deprecations = true - - lines = capture_warnings { Object.const_get("Capybara::Screenshot::Diff::ImageCompare") } - - assert_empty lines - end - - test "unmapped old-namespace constants still raise NameError" do - error = assert_raises(NameError) { Capybara::Screenshot::Diff.const_get(:DoesNotExist) } - assert_match(/DoesNotExist/, error.message) - - assert_raises(NameError) { CapybaraScreenshotDiff.const_get(:DoesNotExist) } - assert_raises(NameError) { Capybara::Screenshot.const_get(:DoesNotExist) } - end - - test "old constants resolve to the same objects while warning" do - _out, _err = capture_io do - assert_same SnapDiff::Comparison, Object.const_get("Capybara::Screenshot::Diff::ImageCompare") - assert_same SnapDiff::SnapManager, Object.const_get("CapybaraScreenshotDiff::SnapManager") - end - end - - # --- subprocess probes: the gem's OWN code must never warn ------------- - - PROJECT_ROOT = File.expand_path("../..", __dir__) - FIXTURE_IMAGE = File.expand_path("../fixtures/images/a.png", __dir__) - - # Legacy entry points exercise load + a real comparison + the session - # lifecycle; the canonical snap_diff entry exercises load + comparison + - # config. Zero deprecation output allowed: internal code must reference - # SnapDiff names only. - ENTRY_POINT_PROBES = { - "capybara_screenshot_diff" => :legacy, - "capybara_screenshot_diff/minitest" => :legacy, - "capybara_screenshot_diff/rspec" => :legacy, - "capybara-screenshot-diff" => :legacy, - "snap_diff" => :canonical - }.freeze - - test "loading each entry point and running a trivial comparison emits no deprecation output" do - failures = ENTRY_POINT_PROBES.filter_map do |entry, kind| - body = "require #{entry.inspect}\n" - body << "raise \"compare failed\" unless SnapDiff.compare(#{FIXTURE_IMAGE.inspect}, #{FIXTURE_IMAGE.inspect}, driver: :chunky_png).quick_equal?\n" - if kind == :legacy - body << "CapybaraScreenshotDiff.assertions_present?\n" - body << "CapybaraScreenshotDiff.pending_screenshots_message\n" - body << "CapybaraScreenshotDiff.reset\n" - else - body << "SnapDiff.config\n" - end - - _out, err, status = Open3.capture3(RbConfig.ruby, "-Ilib", "-e", body, chdir: PROJECT_ROOT) - - if !status.success? - "require \"#{entry}\": probe failed:\n#{err}" - elsif err.include?("deprecation") - "require \"#{entry}\": internal use emitted deprecation output:\n#{err}" - end - end - - assert_empty failures, failures.join("\n\n") - end - - test "touching an old constant in a fresh unsilenced process does warn (control probe)" do - script = 'require "capybara_screenshot_diff"; Capybara::Screenshot::Diff::ImageCompare' - _out, err, status = Open3.capture3(RbConfig.ruby, "-Ilib", "-e", script, chdir: PROJECT_ROOT) - - assert status.success?, err - assert_match(/\[snap_diff deprecation\].*Capybara::Screenshot::Diff::ImageCompare.*SnapDiff::Comparison/, err) - end -end diff --git a/test/legacy/legacy_tree_is_alias_only_test.rb b/test/legacy/legacy_tree_is_alias_only_test.rb deleted file mode 100644 index 28e87030..00000000 --- a/test/legacy/legacy_tree_is_alias_only_test.rb +++ /dev/null @@ -1,162 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" - -# ADR-008 step 7: the mechanical gate that keeps 3.0 a `git rm`. -# -# lib/capybara/ and lib/capybara_screenshot_diff/ are the v1 compatibility -# surface. Every unit of behaviour has moved to lib/snap_diff/, so what is -# left must be nothing but requires, namespace reopening, constant aliases -# and one-line forwarders. If that stays true, dropping v1 support in 3.0 is -# a deletion; the moment real logic lands back in these trees it becomes a -# refactor. This test fails the second that happens, naming the file. -# -# LEGACY SURFACE (test/legacy/, see the Rakefile): deleted with the trees it -# scans in 3.0. Its mirror image, core_tree_has_no_legacy_deps_test.rb, -# guards what 3.0 KEEPS and so stays in test/unit/. -class LegacyTreeIsAliasOnlyTest < ActiveSupport::TestCase - LIB = Pathname.new(__dir__).join("../../lib").expand_path - - LEGACY_FILES = ( - Dir[LIB.join("capybara*.rb")] + - Dir[LIB.join("capybara/**/*.rb")] + - Dir[LIB.join("capybara_screenshot_diff/**/*.rb")] - ).map { |path| Pathname.new(path) }.sort.freeze - - # THE ALLOWLIST -- and as of ADR-008 step 7b it is EMPTY: not one file in - # the v1 trees holds logic any more. config_legacy.rb was the last entry; - # step 7b moved its derived config (.active? precedence, .screenshot_area - # path assembly, .default_options incl. the vips tolerance literal) into - # SnapDiff::Config, and the 3.0-readiness pass moved the remaining - # forwarders and the legacy accessor generator into - # snap_diff/legacy_shims.rb -- the one file that holds the v1 surface as - # code, and that 3.0 deletes together with these trees. There is not a - # single `def` left here. - # - # Keep it empty. Adding an entry back is a decision to keep behaviour on - # the v1 side of the 3.0 deletion, so it needs a written reason here AND - # an ADR-008 update -- never just to turn a red build green. - ALLOWED_WITH_CODE = {}.freeze - - # A `def` in these trees is only acceptable as a THREE-line forwarder -- - # signature, ONE delegating expression, `end` -- and this is that - # expression: a single method-call chain rooted at SnapDiff, passing its - # arguments straight through. - # - # SIMPLE_ARGS is where the strictness lives. Names, commas, `*`/`**`/`&` - # and keyword colons -- and nothing else. No parentheses, so a nested call - # cannot appear; no `.`, so neither can a bare receiver call; no `?`, `"` - # or `=`, so no conditional, literal or assignment. Two escapes this - # closes, both of which ran arbitrary code past the previous rule: - # - # 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 second also slipped past the semicolon check, because the walk below - # steps over a def's body line -- fixed there. - # - # No block form at all: nothing in these trees has a `def` left, and an - # unbounded `{ ... }` is exactly the hole above. A yield-through forwarder - # that genuinely needs one is a decision to re-open here, deliberately. - # `...` is Ruby's argument forwarding -- the purest forwarder there is - # (CapybaraScreenshotDiff.serve uses it), so it is spelled out rather than - # let in by loosening the character set. - SIMPLE_ARGS = /\.\.\.|[\w\s,:*&]*/ - FORWARDER_BODY = /\A - SnapDiff(::[A-Z]\w*)* # SnapDiff, SnapDiff::Reporting, ... - (\.[a-z_]\w*[?!]?)+ # .config.active?, .compare, ... - (\((?:#{SIMPLE_ARGS})\))? # at most one argument list, pass-through only - \z/x - - # Shapes that are pure compatibility plumbing rather than behaviour. - ALIAS_SHAPES = /\A( - require(_relative)?\s | - autoload\s | - (module|class)\s | - end\z | - private\z | - (extend|include)\s | - [A-Z]\w*\s*=\s | # constant alias: Foo = SnapDiff::Foo - def_delegators?\s - )/x - - test "every legacy file is aliases and forwarders only" do - refute_empty LEGACY_FILES, "legacy tree glob matched nothing -- the gate would pass vacuously" - - offenders = LEGACY_FILES.reject { |file| allowed?(file) }.flat_map { |file| offences(file) } - - assert_empty offenders, <<~MSG - Real logic found in the v1 compatibility trees. Move it to lib/snap_diff/ - (or, if it genuinely must stay, add the file to ALLOWED_WITH_CODE with a - written reason and update ADR-008): - - #{offenders.join("\n")} - MSG - end - - # The pinned method inventory that used to narrow config_legacy.rb's - # allowlist entry is gone with the entry itself (ADR-008 step 7b): with - # nothing allowlisted, the general rule above already checks every `def` - # in the tree, config_legacy.rb's included. - test "the allowlist is empty, so nothing is exempt from the general rule" do - assert_empty ALLOWED_WITH_CODE, - "an exemption came back -- see the comment on ALLOWED_WITH_CODE before keeping it" - end - - private - - def allowed?(file) - ALLOWED_WITH_CODE.key?(file.relative_path_from(LIB).to_s) - end - - # Walks the file's significant lines. A `def` is only acceptable when its - # whole body is one FORWARDER_BODY expression; anything else must match - # ALIAS_SHAPES. - def offences(file) - rel = file.relative_path_from(LIB) - lines = significant_lines(file) - - # A semicolon is how several statements -- or an entire - # `def x; body; end` -- hide inside one "line", which would then be - # judged as a single line. Never alias-shaped, whatever it says. - # - # Scanned over EVERY line up front, not inside the walk below: the walk - # steps past a def's body line without re-examining it, so a semicolon - # there went unseen. - found = lines.filter_map do |line| - "#{rel}: `#{line}` puts more than one statement on a line" if line.include?(";") - end - index = 0 - - while index < lines.length - line = lines[index] - - if line.start_with?("def ") - body, terminator = lines[index + 1], lines[index + 2] - unless FORWARDER_BODY.match?(body.to_s) && terminator == "end" - found << "#{rel}: `#{line}` is not a single-expression forwarder into SnapDiff" - end - index += 3 - else - found << "#{rel}: unexpected line `#{line}`" unless ALIAS_SHAPES.match?(line) - index += 1 - end - end - - found - end - - # Strips comments and blanks, and folds trailing-comma continuations back - # into one logical line (multi-line def_delegators lists, hash literals). - def significant_lines(file) - file.read.lines.map(&:strip) - .reject { |line| line.empty? || line.start_with?("#") } - .each_with_object([]) do |line, folded| - if folded.last&.end_with?(",") - folded[-1] = "#{folded.last} #{line}" - else - folded << line - end - end - end -end diff --git a/test/legacy/namespace_forwarding_test.rb b/test/legacy/namespace_forwarding_test.rb deleted file mode 100644 index 0fbe813a..00000000 --- a/test/legacy/namespace_forwarding_test.rb +++ /dev/null @@ -1,153 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" -# The shared harness loads canonical entry points only, so a legacy-surface -# test pulls in the v1 entry itself -- the require goes with the file in 3.0. -require "capybara_screenshot_diff" - -# Every old-namespace constant touched by the ADR-004 v2 file-tree move -# must forward to the exact same object as its SnapDiff:: replacement -- -# not a copy, not a lookalike, the same object. If a forwarder ever -# breaks (wrong target, deleted alias, typo), this fails loudly instead -# of surfacing as a mysterious downstream `NameError` or a comparison -# that always returns false. -# -# LEGACY SURFACE (test/legacy/, see the Rakefile): deleted with lib/capybara* -# in 3.0, when there is no old namespace left to forward. -class NamespaceForwardingTest < ActiveSupport::TestCase - # This file's whole purpose is resolving the old names, so silence the - # shims' deprecation warnings here (the suite-wide guard in test_helper - # raises on any unexpected one); the warning behavior itself is pinned by - # legacy_namespace_deprecation_test.rb. - setup do - @original_silence = SnapDiff.silence_deprecations - SnapDiff.silence_deprecations = true - end - - teardown do - SnapDiff.silence_deprecations = @original_silence - end - - # old constant path => new constant path - MAPPING = { - "Capybara::Screenshot::Os" => "SnapDiff::Os", - "Capybara::Screenshot::BrowserHelpers" => "SnapDiff::BrowserHelpers", - "Capybara::Screenshot::Diff::Vcs" => "SnapDiff::Vcs", - "Capybara::Screenshot::Diff::VERSION" => "SnapDiff::VERSION", - "Capybara::Screenshot::Screenshoter" => "SnapDiff::Screenshoter", - "Capybara::Screenshot::Diff::StableScreenshoter" => "SnapDiff::StableScreenshoter", - "Capybara::Screenshot::Diff::ImagePreprocessor" => "SnapDiff::ImagePreprocessor", - "Capybara::Screenshot::Diff::AreaCalculator" => "SnapDiff::AreaCalculator", - "Capybara::Screenshot::Diff::AnnotationService" => "SnapDiff::AnnotationService", - "Capybara::Screenshot::Diff::Utils" => "SnapDiff::Utils", - "Capybara::Screenshot::Diff::ScreenshotMatcher" => "SnapDiff::ScreenshotMatcher", - "CapybaraScreenshotDiff::DSL" => "SnapDiff::DSL", - "CapybaraScreenshotDiff::SnapManager" => "SnapDiff::SnapManager", - "CapybaraScreenshotDiff::Snap" => "SnapDiff::Snap", - "CapybaraScreenshotDiff::ScreenshotNamer" => "SnapDiff::ScreenshotNamer", - "CapybaraScreenshotDiff::AttemptsReporter" => "SnapDiff::AttemptsReporter", - "CapybaraScreenshotDiff::BacktraceFilter" => "SnapDiff::BacktraceFilter", - "CapybaraScreenshotDiff::ErrorWithFilteredBacktrace" => "SnapDiff::ErrorWithFilteredBacktrace", - "CapybaraScreenshotDiff::Reporters::HTML" => "SnapDiff::Reporters::HTML", - "CapybaraScreenshotDiff::ScreenshotAssertion" => "SnapDiff::ScreenshotAssertion", - "CapybaraScreenshotDiff::AssertionRegistry" => "SnapDiff::AssertionRegistry", - "Capybara::Screenshot::Diff::Drivers" => "SnapDiff::Drivers", - "Capybara::Screenshot::Diff::Drivers::BaseDriver" => "SnapDiff::Driver", - "Capybara::Screenshot::Diff::Drivers::ChunkyPNGDriver" => "SnapDiff::Drivers::ChunkyPNGDriver", - "Capybara::Screenshot::Diff::Drivers::VipsDriver" => "SnapDiff::Drivers::VipsDriver", - "Capybara::Screenshot::Diff::ImageCompare" => "SnapDiff::Comparison", - "Capybara::Screenshot::Diff::Difference" => "SnapDiff::ComparisonResult", - "CapybaraScreenshotDiff::RED_RGBA" => "SnapDiff::RED_RGBA", - "CapybaraScreenshotDiff::ORANGE_RGBA" => "SnapDiff::ORANGE_RGBA" - }.freeze - - # Explicit requires: a dedicated forwarder-identity test shouldn't rely - # on incidental transitive loads from other test files (or on rake's - # file-load order within a single process) to make every one of these - # constants resolvable. Most of these are already pulled in by - # test_helper's own "capybara_screenshot_diff/minitest" require; listed - # here anyway so this file passes standalone. - require "capybara/screenshot/diff/os" - require "capybara/screenshot/diff/browser_helpers" - require "capybara/screenshot/diff/vcs" - require "capybara/screenshot/diff/version" - require "capybara/screenshot/diff/screenshoter" - require "capybara/screenshot/diff/stable_screenshoter" - require "capybara/screenshot/diff/image_preprocessor" - require "capybara/screenshot/diff/area_calculator" - require "capybara/screenshot/diff/annotation_service" - require "capybara/screenshot/diff/utils" - require "capybara/screenshot/diff/screenshot_matcher" - require "capybara_screenshot_diff/dsl" - require "capybara_screenshot_diff/snap_manager" - require "capybara_screenshot_diff/snap" - require "capybara_screenshot_diff/screenshot_namer" - require "capybara_screenshot_diff/attempts_reporter" - require "capybara_screenshot_diff/error_with_filtered_backtrace" - require "capybara_screenshot_diff/reporters/html" - require "capybara_screenshot_diff/screenshot_assertion" - require "capybara/screenshot/diff/reporters/default" - require "capybara/screenshot/diff/drivers" - require "capybara/screenshot/diff/drivers/base_driver" - require "capybara/screenshot/diff/drivers/chunky_png_driver" - require "capybara/screenshot/diff/image_compare" - require "capybara/screenshot/diff/difference" - begin - require "capybara/screenshot/diff/drivers/vips_driver" - rescue LoadError, RuntimeError # vips_driver.rb re-raises missing-gem LoadError as RuntimeError - # vips-less runner: the VipsDriver pair reports as a skip below, - # mirroring test/unit/drivers/vips_driver_test.rb. - end - - MAPPING.each do |old_name, new_name| - define_method(:"test_#{old_name}_forwards_to_#{new_name}") do - skip "vips not available on this runner" if new_name.include?("Vips") && !defined?(SnapDiff::Drivers::VipsDriver) - - old_const = Object.const_get(old_name) - new_const = Object.const_get(new_name) - - assert_same new_const, old_const, - "expected #{old_name} to be the exact same object as #{new_name}, " \ - "got #{old_const.inspect} vs #{new_const.inspect}" - end - end - - test "MAPPING covers all 29 documented lazy forwarders" do - assert_equal 29, MAPPING.size - end - - # Documented user-facing constants (beta3 blocker): a subclassing - # extension point and the images struct. EAGER same-object aliases, not - # lazy shims, for the same reason as the error classes -- const_defined? - # and defined? never trigger const_missing, so a lazy shim makes feature - # detection by the old name silently report "absent", permanently. - { - "Capybara::Screenshot::Diff::Reporters::Default" => "SnapDiff::Reporters::Default", - "Capybara::Screenshot::Diff::Comparison" => "SnapDiff::Comparison::Images" - }.each do |old_name, new_name| - test "#{old_name} is an eager same-object alias of #{new_name}" do - mod, leaf = old_name.rpartition("::").values_at(0, 2) - - assert Object.const_get(mod).const_defined?(leaf, false), - "#{leaf} must be an eagerly-defined constant on #{mod}, not a const_missing shim" - assert_same Object.const_get(new_name), Object.const_get(old_name) - end - end - - # Driver registries (ADR-008 step 5b): not part of MAPPING because the - # old names are EAGER aliases (never warn) of the canonical - # SnapDiff::Drivers accessors -- LOADED_DRIVERS is mutated in place by - # user driver registration, so it must stay the exact same object. - test "driver registries are the same object under old and canonical names" do - assert_same SnapDiff::Drivers.loaded, Capybara::Screenshot::Diff::LOADED_DRIVERS - assert_same SnapDiff::Drivers.available, Capybara::Screenshot::Diff::AVAILABLE_DRIVERS - end - - test "driver registration through the legacy LOADED_DRIVERS constant is visible canonically" do - Capybara::Screenshot::Diff::LOADED_DRIVERS[:forwarding_probe] = :probe_driver - - assert_equal :probe_driver, SnapDiff::Drivers.loaded[:forwarding_probe] - ensure - SnapDiff::Drivers.loaded.delete(:forwarding_probe) - end -end diff --git a/test/legacy/snap_diff_deprecation_test.rb b/test/legacy/snap_diff_deprecation_test.rb deleted file mode 100644 index 9d2daccb..00000000 --- a/test/legacy/snap_diff_deprecation_test.rb +++ /dev/null @@ -1,236 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" -require "open3" -require "snap_diff/deprecation" - -# LEGACY SURFACE (test/legacy/, see the Rakefile): SnapDiff::Deprecation is -# the channel that announces the v1 shims, so snap_diff/deprecation.rb and -# this file are deleted together with lib/capybara* in 3.0. -class SnapDiffDeprecationTest < ActiveSupport::TestCase - def setup - SnapDiff::Deprecation.reset! - @original_silence = SnapDiff.silence_deprecations - @original_env = ENV["SNAP_DIFF_SILENCE_DEPRECATIONS"] - # These examples assert on the unsilenced default warning behavior, so - # tell the suite-wide raise-on-deprecation guard (test_helper) that the - # warnings emitted here -- including from spawned threads -- are expected. - SnapDiff.silence_deprecations = false - SnapDiffDeprecationGuard.expected = true - end - - def teardown - SnapDiffDeprecationGuard.expected = false - SnapDiff::Deprecation.reset! - SnapDiff.silence_deprecations = @original_silence - - if @original_env.nil? - ENV.delete("SNAP_DIFF_SILENCE_DEPRECATIONS") - else - ENV["SNAP_DIFF_SILENCE_DEPRECATIONS"] = @original_env - end - end - - # Emission channel under test: Kernel#warn delegates to Warning.warn - # (Ruby >= 2.4), whose default implementation writes to $stderr. Capturing - # $stderr via minitest's capture_io exercises that exact path -- the same - # one a caller who has customized Warning.warn (e.g. to raise on warnings, - # or RSpec/Rails deprecation collectors) would also observe -- without us - # having to monkey-patch Warning ourselves just to assert on it. - def capture_warnings - _out, err = capture_io { yield } - err.lines.reject(&:empty?) - end - - test "warns exactly once for repeated calls with the same subject" do - lines = capture_warnings do - 3.times { SnapDiff::Deprecation.warn("Old::Thing", "New::Thing") } - end - - assert_equal 1, lines.size - assert_match(/\[snap_diff deprecation\]/, lines.first) - assert_match(/Old::Thing/, lines.first) - assert_match(/New::Thing/, lines.first) - end - - # Actionable attribution: the first caller frame OUTSIDE the gem's lib - # dir is named, so users can find the deprecated reference. This test - # file plays the part of "user code" -- the warning must point here. - test "warning names the caller's file and line" do - lines = capture_warnings do - SnapDiff::Deprecation.warn("Old::Where", "New::Where") - end - - assert_match(/called from #{Regexp.escape(File.expand_path(__FILE__))}:\d+/, lines.first) - end - - test "warns separately for different subjects" do - lines = capture_warnings do - SnapDiff::Deprecation.warn("Old::A", "New::A") - SnapDiff::Deprecation.warn("Old::B", "New::B") - end - - assert_equal 2, lines.size - end - - test "silenced via SnapDiff.silence_deprecations accessor" do - SnapDiff.silence_deprecations = true - - lines = capture_warnings do - SnapDiff::Deprecation.warn("Old::Thing", "New::Thing") - end - - assert_empty lines - end - - test "not silenced when accessor is explicitly false" do - SnapDiff.silence_deprecations = false - - lines = capture_warnings do - SnapDiff::Deprecation.warn("Old::Thing", "New::Thing") - end - - assert_equal 1, lines.size - end - - test "silenced via SNAP_DIFF_SILENCE_DEPRECATIONS=1 env var" do - ENV["SNAP_DIFF_SILENCE_DEPRECATIONS"] = "1" - - lines = capture_warnings do - SnapDiff::Deprecation.warn("Old::Thing", "New::Thing") - end - - assert_empty lines - end - - test "silenced via SNAP_DIFF_SILENCE_DEPRECATIONS=true env var" do - ENV["SNAP_DIFF_SILENCE_DEPRECATIONS"] = "true" - - lines = capture_warnings do - SnapDiff::Deprecation.warn("Old::Thing", "New::Thing") - end - - assert_empty lines - end - - test "thread-safe: N threads warning about the same subject emit exactly once" do - lines = capture_warnings do - threads = Array.new(20) do - Thread.new { SnapDiff::Deprecation.warn("Old::Racy", "New::Racy") } - end - threads.each(&:join) - end - - assert_equal 1, lines.size - end - - test "reset! clears the seen-set so a subject warns again" do - capture_warnings { SnapDiff::Deprecation.warn("Old::Thing", "New::Thing") } - - SnapDiff::Deprecation.reset! - - lines = capture_warnings do - SnapDiff::Deprecation.warn("Old::Thing", "New::Thing") - end - - assert_equal 1, lines.size - end - - # --- the once-per-process migration notice --- - # - # Most of the v1 surface cannot warn per use: the legacy config accessors - # are plain delegators, and the eagerly-aliased constants (Os, the error - # classes, VERSION) never reach const_missing. Exercising all 14 legacy - # APIs a real setup file touches under -w produced ZERO warnings, so a 2.x - # app was completely silent right up to the bare NameError it would get on - # 3.0. {MIGRATION_NOTICE} is the one line that closes that gap; these - # probes run in subprocesses because "once per process" is the contract. - - NOTICE_MARKER = "shown once per process" - - # Every door into the v1 surface that CAN be hooked, each on its own so a - # regression in one is not masked by another still firing. (The eagerly - # aliased constants -- Os, the error classes, VERSION -- are deliberately - # absent: they never reach const_missing, which is exactly why the notice - # has to exist and why UPGRADING.md documents them as silent by design.) - LEGACY_DOORS = { - "config delegator (write)" => "Capybara::Screenshot.window_size = [80, 80]", - "config delegator (read)" => "Capybara::Screenshot::Diff.tolerance", - "const_missing constant" => "Capybara::Screenshot::Diff::ImageCompare", - "legacy include" => "Class.new { include Capybara::Screenshot::Diff }", - # Hand-written forwarder, not one of the generated delegators above, so - # it needs its own Deprecation.notice and its own row here. - "hand-written derived reader" => "Capybara::Screenshot::Diff.default_options" - }.freeze - - LEGACY_USE = LEGACY_DOORS.values.join("\n") - - LEGACY_DOORS.each do |door, code| - test "the migration notice fires for the #{door}, on its own" do - out = run_probe(<<~RUBY) - require "capybara_screenshot_diff" - 3.times { #{code} } - RUBY - - assert_equal 1, out.scan(NOTICE_MARKER).size, "expected exactly one migration notice, got:\n#{out}" - end - end - - test "the migration notice fires exactly once per process, however many legacy APIs are used" do - out = run_probe(<<~RUBY) - require "capybara_screenshot_diff" - 3.times do - #{LEGACY_USE} - end - RUBY - - assert_equal 1, out.scan(NOTICE_MARKER).size, "expected exactly one migration notice, got:\n#{out}" - assert_includes out, "docs/UPGRADING.md" - assert_includes out, "REMOVED in 3.0" - assert_includes out, "SNAP_DIFF_SILENCE_DEPRECATIONS" - end - - test "the migration notice does not fire for a purely canonical setup" do - out = run_probe(<<~RUBY) - require "snap_diff/integrations/minitest" - SnapDiff.configure { |c| c.window_size = [80, 80] } - SnapDiff.config.tolerance - SnapDiff::Comparison - SnapDiff::Os - RUBY - - assert_equal "", out.strip, "canonical-only usage must stay silent" - end - - test "the migration notice is silenced by the SnapDiff.silence_deprecations accessor" do - out = run_probe(<<~RUBY) - require "capybara_screenshot_diff" - SnapDiff.silence_deprecations = true - #{LEGACY_USE} - RUBY - - assert_equal "", out.strip - end - - test "the migration notice is silenced by SNAP_DIFF_SILENCE_DEPRECATIONS" do - out = run_probe(<<~RUBY, "SNAP_DIFF_SILENCE_DEPRECATIONS" => "1") - require "capybara_screenshot_diff" - #{LEGACY_USE} - RUBY - - assert_equal "", out.strip - end - - private - - # Runs +script+ in a fresh process with only lib/ on the load path and - # returns whatever it wrote to stderr, minus warnings from other gems. - def run_probe(script, env = {}) - project_root = File.expand_path("../..", __dir__) - _out, err, status = Open3.capture3( - env, RbConfig.ruby, "-Ilib", "-e", script, chdir: project_root - ) - assert_predicate status, :success?, err - err.lines.grep(/\[snap_diff/).join - end -end diff --git a/test/test_helper.rb b/test/test_helper.rb index 5eccadb6..68a17e1b 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -24,34 +24,10 @@ require "snap_diff/integrations/minitest" -# This suite IS the v1 surface, not a consumer of it: it configures through -# `Capybara::Screenshot.*` below and exercises the legacy entry points on -# purpose, so the once-per-process migration notice would fire on every run. -# Mark it shown rather than silencing deprecations wholesale -- the -# per-constant warnings must still reach the guard below and raise. -SnapDiff::Deprecation.suppress_migration_notice! - -# v2 step 8: the suite exercises only canonical SnapDiff:: names, so any -# legacy-shim deprecation warning during a test run is a bug in the -# referencing test -- fail loud at the resolution site instead of letting -# it scroll by on stderr. Tests that exercise the legacy surface on purpose -# opt out per test: namespace_forwarding_test silences deprecations in its -# own setup; the deprecation-machinery tests set `expected = true` around -# the warnings they capture and assert on. A plain module flag (not a -# thread-local) so warnings emitted from threads a test spawns are covered -# too; tests run serially, so there is no cross-test race. -module SnapDiffDeprecationGuard - singleton_class.attr_accessor :expected - - def warn(message, ...) - if message.to_s.include?("[snap_diff deprecation]") && !SnapDiffDeprecationGuard.expected - raise "old-namespace constant resolved inside the test suite: #{message}" - end - - super - end -end -Warning.extend(SnapDiffDeprecationGuard) +# The deprecation channel (snap_diff/deprecation.rb) and the shim layer it +# announced were deleted in 2.1, so there is nothing left to silence and no +# warning left to guard against: the only names this suite can reference are +# canonical ones. require "support/stub_test_methods" require "support/setup_capybara_drivers" @@ -80,9 +56,8 @@ class ActiveSupport::TestCase # Snapshot ALL global config state before each test, restore after. # Prevents one test from poisoning another via leaked config changes. - # Since ADR-008 step 1 the single storage is the SnapDiff.config instance - # (the legacy Capybara::Screenshot / ::Diff accessors delegate to it), so - # snapshotting its instance variables covers both surfaces. + # Since ADR-008 step 1 the single storage is the SnapDiff.config instance, + # so snapshotting its instance variables covers every setting. setup do config = SnapDiff.config @_global_snapshots = config.instance_variables.map { |iv| diff --git a/test/unit/canonical_suite_has_no_legacy_refs_test.rb b/test/unit/canonical_suite_has_no_legacy_refs_test.rb index d87d10b6..629036fc 100644 --- a/test/unit/canonical_suite_has_no_legacy_refs_test.rb +++ b/test/unit/canonical_suite_has_no_legacy_refs_test.rb @@ -76,16 +76,16 @@ class CanonicalSuiteHasNoLegacyRefsTest < ActiveSupport::TestCase # entry is a signal that canonical tests are still entangled with the # legacy surface -- it needs a decision, not a green build. ALLOWED = { - # Simulates the deletion for real (copies lib/, removes the trees, loads - # every canonical entry point). It names the deletion set and the edits - # by construction, and asserts its own gate line can reject an intact - # tree -- it cannot do that without spelling the doomed names. - "unit/deletion_3_0_test.rb" => [ - '["snap_diff.rb", %(require "snap_diff/legacy_shims"), nil],', - '%(require "capybara_screenshot_diff/minitest"),', - 'gate << "SnapDiff.start is still defined" if SnapDiff.respond_to?(:start)', - 'gate << "CapybaraScreenshotDiff is still defined" if defined?(CapybaraScreenshotDiff)', - 'assert_includes failure, "SnapDiff.start is still defined"' + # Asserts the removed surface is ABSENT -- from lib/ on disk and from a + # fresh process. It names the removed paths and constants by + # construction: there is no way to check a name is gone without writing + # it down. + "unit/legacy_surface_removed_test.rb" => [ + 'back << "Capybara::Screenshot" if defined?(Capybara::Screenshot)', + 'back << "CapybaraScreenshotDiff" if defined?(CapybaraScreenshotDiff)', + 'back << "SnapDiff::Deprecation" if defined?(SnapDiff::Deprecation)', + 'back << "SnapDiff.start" if SnapDiff.respond_to?(:start)', + 'back << "SnapDiff.silence_deprecations" if SnapDiff.respond_to?(:silence_deprecations)' ], # The twin gate's own pattern literal. A detector has to spell what it # detects; the line asserts nothing about legacy behaviour and is deleted diff --git a/test/unit/core_tree_has_no_legacy_deps_test.rb b/test/unit/core_tree_has_no_legacy_deps_test.rb index 04e89527..11363e04 100644 --- a/test/unit/core_tree_has_no_legacy_deps_test.rb +++ b/test/unit/core_tree_has_no_legacy_deps_test.rb @@ -2,20 +2,20 @@ require "test_helper" -# The REVERSE of legacy_tree_is_alias_only_test.rb, and the other half of -# what makes 3.0 a `git rm`. +# Wrote the 2.1 deletion; still earns its keep after it. # -# That test proves the v1 trees hold no logic. This one proves the canonical -# core does not reach BACK into them -- which is the half that actually -# breaks the gem if it is wrong: as long as any file under lib/snap_diff/ -# requires a `capybara/...` path or reads a `Capybara::Screenshot.*` / -# `CapybaraScreenshotDiff::*` constant, deleting lib/capybara* leaves a core -# that no longer loads. +# Before: this gate and legacy_tree_is_alias_only_test.rb were the two halves +# of what made the removal a plain `git rm` -- one proved the v1 trees held +# no logic, this one proved the canonical core never reached BACK into them. +# The alias-only half died with test/legacy/. # -# Scope: everything the 3.0 deletion KEEPS. Files that are themselves part of -# the deletion set (DELETED_IN_3_0 below) live under lib/snap_diff/ only -# because the generator for the v1 surface has to be code, and the v1 trees -# have to stay alias-only -- they are legacy by design and go with it. +# After: the trees are gone, so a `require "capybara/screenshot/..."` fails +# loudly on its own. What does NOT fail loudly is the rest -- a user-facing +# message telling someone to set `Capybara::Screenshot.tolerance`, a +# docstring pointing at a forwarder, a rescue naming an old constant. Those +# survive the deletion and start lying the day it lands. This gate is what +# keeps the removed names from creeping back into the code that shipped +# without them. # # WHOLE-LINE comments are ignored: "ex +SnapDiff.config.active?+" on its # own line is history, not a dependency. Everything else on a code line @@ -27,17 +27,12 @@ class CoreTreeHasNoLegacyDepsTest < ActiveSupport::TestCase LIB = Pathname.new(__dir__).join("../../lib").expand_path - # Deleted alongside lib/capybara* in 3.0: these files exist to BUILD the - # v1 compatibility surface (const_missing shims, the legacy config - # accessor generator, the deprecation channel that announces both). - DELETED_IN_3_0 = %w[ - snap_diff/legacy_shims.rb - snap_diff/deprecation.rb - ].freeze - + # No exclusions: snap_diff/legacy_shims.rb and snap_diff/deprecation.rb + # were the two files under here that existed to BUILD the v1 surface, and + # 2.1 deleted them with it. Everything left is canonical by definition. CORE_FILES = ( [LIB.join("snap_diff.rb")] + Dir[LIB.join("snap_diff/**/*.rb")].map { |p| Pathname.new(p) } - ).sort.reject { |file| DELETED_IN_3_0.include?(file.relative_path_from(LIB).to_s) }.freeze + ).sort.freeze # A require of anything in the v1 trees: `capybara/screenshot/...`, # `capybara_screenshot_diff...`, `capybara-screenshot-diff`. Plain @@ -61,10 +56,10 @@ class CoreTreeHasNoLegacyDepsTest < ActiveSupport::TestCase # the second. # # EMPTY. It was seeded with all 64 core->legacy edges that existed the day - # the gate was written, and every one of them is gone. Keep it empty: an - # entry is a decision to keep a core->legacy edge across the 3.0 deletion, - # so it needs a written reason here AND an ADR-008 update -- never just a - # red build turned green. + # the gate was written, and every one of them is gone -- along with the + # trees they pointed at. An entry now is a decision to name a REMOVED api + # in shipped code, so it needs a written reason here AND an ADR-008 + # update -- never just a red build turned green. ALLOWED = {}.freeze test "no core file requires or references the v1 namespaces" do @@ -73,10 +68,11 @@ class CoreTreeHasNoLegacyDepsTest < ActiveSupport::TestCase offenders = CORE_FILES.flat_map { |file| offences(file) } assert_empty offenders, <<~MSG - The canonical core still depends on the v1 compatibility trees. Repoint - these at their snap_diff/* equivalents (`SnapDiff.config.*`, - `SnapDiff::Drivers.*`, `require "snap_diff/..."`) -- until they are gone, - `git rm lib/capybara*` breaks the gem: + The canonical core names the v1 compatibility surface, which 2.1 + removed. Repoint these at their snap_diff/* equivalents + (`SnapDiff.config.*`, `SnapDiff::Drivers.*`, `require "snap_diff/..."`) -- + a require here does not resolve at all, and a message or docstring + here sends users to an API that is gone: #{offenders.join("\n")} MSG diff --git a/test/unit/deletion_3_0_test.rb b/test/unit/deletion_3_0_test.rb deleted file mode 100644 index 31cfdb8c..00000000 --- a/test/unit/deletion_3_0_test.rb +++ /dev/null @@ -1,196 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" -require "open3" -require "tmpdir" -require "fileutils" -require "unit/support_load_probe_test" # single source of truth for the canonical entry-point tables - -# THE 3.0 DELETION, ACTUALLY RUN. -# -# legacy_tree_is_alias_only_test.rb and core_tree_has_no_legacy_deps_test.rb -# are STATIC proxies for one claim: `git rm` the v1 surface and the gem still -# loads. This test stops proxying. It copies lib/ to a tmpdir, performs the -# deletion, applies the edits the deletion needs, and requires every canonical -# entry point in a fresh subprocess. -# -# THE GATE LINE (see GATE_SCRIPT) is why this is evidence rather than -# decoration. An earlier lane's "green" run turned out to have measured the -# INTACT tree: BUNDLE_GEMFILE pointed at the gemspec, which unshifts the real -# lib/ onto $LOAD_PATH ahead of any -I. A run that cannot tell the deleted -# tree from the intact one proves nothing, so before asserting anything about -# the surface every probe HARD-ASSERTS that the deletion is in effect -- the -# deleted names are gone AND every snap_diff file that loaded came from the -# tmpdir. The subprocess is isolated from bundler as well (see #probe), but -# that isolation is precisely the thing that silently stopped working last -# time -- the gate line is what notices when it does. -class Deletion30Test < ActiveSupport::TestCase - PROJECT_ROOT = Pathname.new(File.expand_path("../..", __dir__)) - - # The 3.0 `git rm`, verbatim from the Rakefile's header comment (minus - # test/legacy, which this test does not load). - DELETED = %w[ - capybara - capybara_screenshot_diff - capybara-screenshot-diff.rb - capybara_screenshot_diff.rb - snap_diff/legacy_shims.rb - snap_diff/deprecation.rb - ].freeze - - # The edits the deletion needs, as [file, exact line to remove or replace, - # replacement or nil]. Exact-match on purpose: if one of these lines is - # reworded, the edit must go red here rather than silently not applying and - # leaving the probe to fail somewhere confusing. - EDITS = [ - # The one line in the canonical entry point that 3.0 drops. - ["snap_diff.rb", %(require "snap_diff/legacy_shims"), nil], - # The new gem name's Bundler entry point is KEPT, repointed off the v1 - # umbrella. It matches neither gate's file glob, so this is the only - # thing that checks its post-3.0 shape at all. - ["snap_diff-capybara.rb", - %(require "capybara_screenshot_diff/minitest"), - %(require "snap_diff/integrations/minitest")] - ].freeze - - ENTRY_POINTS = SupportLoadProbeTest::CANONICAL_ENTRY_POINTS - - # Runs FIRST in every probe, before a single surface assertion. Proves the - # process is looking at the deleted tree and nothing else. - GATE_SCRIPT = <<~'RUBY' - tree = ENV.fetch("DELETED_TREE") - gate = [] - - # Defined in legacy_shims.rb; its presence means the deletion did not take. - gate << "SnapDiff.start is still defined" if SnapDiff.respond_to?(:start) - gate << "CapybaraScreenshotDiff is still defined" if defined?(CapybaraScreenshotDiff) - - deleted = $LOADED_FEATURES.grep(%r{/snap_diff/(legacy_shims|deprecation)\.rb\z}) - gate << "deleted files loaded: #{deleted.join(", ")}" unless deleted.empty? - - v1 = $LOADED_FEATURES.grep(%r{/lib/capybara(-screenshot-diff|_screenshot_diff|/screenshot)}) - gate << "v1 tree loaded: #{v1.join(", ")}" unless v1.empty? - - # The BUNDLE_GEMFILE trap: files resolving from the INTACT lib/ while the - # tmpdir sits unused on the load path. - strays = $LOADED_FEATURES.grep(/snap_diff/).reject { |f| f.start_with?(tree) } - gate << "loaded from outside the deleted tree: #{strays.join(", ")}" unless strays.empty? - - unless gate.empty? - abort("GATE: this process is NOT running the deleted tree, so nothing below is evidence:\n- " + gate.join("\n- ")) - end - RUBY - - test "every canonical entry point loads and keeps its surface after the 3.0 deletion" do - in_deleted_tree do |tree| - failures = ENTRY_POINTS.filter_map do |entry, methods| - probe(tree, <<~RUBY) - require #{entry.inspect} - #{GATE_SCRIPT} - missing = #{methods.inspect}.reject { |m| SnapDiff.respond_to?(m) } - missing << "VERSION" unless defined?(SnapDiff::VERSION) - abort("missing: \#{missing.join(", ")}") unless missing.empty? - RUBY - end - - assert_empty failures, <<~MSG - `git rm` of the v1 surface breaks canonical entry point(s) -- 3.0 is a - refactor, not a deletion, until these load: - - #{failures.join("\n")} - MSG - end - end - - test "every canonical entry point defines its advertised constants after the 3.0 deletion" do - in_deleted_tree do |tree| - failures = SupportLoadProbeTest::CANONICAL_ADVERTISED_CONSTANTS.filter_map do |entry, constants| - probe(tree, <<~RUBY) - require #{entry.inspect} - #{GATE_SCRIPT} - missing = #{constants.inspect}.reject { |c| Object.const_defined?(c) } - abort("missing: \#{missing.join(", ")}") unless missing.empty? - RUBY - end - - assert_empty failures, <<~MSG - Entry point(s) lose their advertised constants once the v1 surface is deleted: - - #{failures.join("\n")} - MSG - end - end - - # The gate line has to be able to FAIL, or it is a comment with an `if` - # around it. Same probe, run against an UNTOUCHED copy of lib/: every - # surface assertion would pass there, so only the gate can reject it. - test "the gate line rejects an intact tree" do - Dir.mktmpdir("snapdiff_intact") do |dir| - tree = copy_lib_to(dir) - - failure = probe(tree, <<~RUBY) - require "snap_diff" - #{GATE_SCRIPT} - RUBY - - assert failure, "the gate line passed on an INTACT tree -- it cannot distinguish the deletion" - assert_includes failure, "SnapDiff.start is still defined" - end - end - - private - - # $LOADED_FEATURES holds resolved real paths, and Dir.mktmpdir hands back - # the symlinked /var form on macOS -- so the gate's "outside the tree" - # check compared /private/var/... against /var/... and rejected the very - # tree it had just built. Resolve once, here. - def copy_lib_to(dir) - FileUtils.cp_r(PROJECT_ROOT.join("lib").to_s, File.join(dir, "lib")) - File.realpath(File.join(dir, "lib")) - end - - # Yields the path to a lib/ with the 3.0 deletion applied. - def in_deleted_tree - Dir.mktmpdir("snapdiff_deleted") do |dir| - tree = copy_lib_to(dir) - - DELETED.each do |path| - target = File.join(tree, path) - assert File.exist?(target), "3.0 deletion set names #{path}, which does not exist" - FileUtils.rm_rf(target) - end - - EDITS.each do |file, line, replacement| - target = Pathname.new(File.join(tree, file)) - source = target.read - - assert_includes source, line, "3.0 edit for #{file} no longer matches the file" - target.write(source.sub(line + "\n", replacement ? replacement + "\n" : "")) - end - - yield tree - end - end - - # A fresh process with ONLY +tree+ on the load path. - # - # `chdir: tree` is the load-bearing half, and NOT a detail. Scrubbing - # RUBYOPT/BUNDLE_GEMFILE is not sufficient on its own: with the cwd still - # inside the project, RubyGems auto-discovers gems.rb, puts - # `-rbundler/setup` BACK into RUBYOPT, and the gemspec unshifts the real - # lib/ ahead of the -I dir -- measured, this exact scrub with cwd at the - # project root loads 24 files from the intact tree. Running from the - # tmpdir means there is no gems.rb to find. Both defenses are here because - # the gate line inside the script is the only one that says so out loud - # when they stop working. - def probe(tree, script) - preamble = <<~RUBY - $LOAD_PATH.unshift(#{tree.inspect}) - #{SupportLoadProbeTest::CUCUMBER_RUNTIME_STUB} - RUBY - env = {"DELETED_TREE" => tree, "RUBYOPT" => nil, "BUNDLE_GEMFILE" => nil, "RUBYLIB" => nil} - out, status = Open3.capture2e(env, RbConfig.ruby, "-e", preamble + script, chdir: tree) - - "#{script.lines.first.strip} -> #{out}" unless status.success? - end -end diff --git a/test/unit/legacy_surface_removed_test.rb b/test/unit/legacy_surface_removed_test.rb new file mode 100644 index 00000000..c34d91c5 --- /dev/null +++ b/test/unit/legacy_surface_removed_test.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require "test_helper" +require "open3" + +# What is left of deletion_3_0_test.rb once the deletion is real. +# +# That test SIMULATED the removal -- copy lib/ to a tmpdir, delete the v1 +# trees, apply the two edits, probe every canonical entry point in a fresh +# process -- because the trees were still there. They are not. Every +# entry-point claim it made is now measured against the real lib/ by +# support_load_probe_test.rb on every run, so the simulation harness (and +# its gate line, which existed to prove the simulation was looking at the +# deleted tree) went with the trees. +# +# One claim survives it, and nothing else covers it: ABSENCE. lib/**/*.rb is +# packaged wholesale, so a v1 file brought back by a bad rebase or an +# incomplete `git rm` ships to users; and a constant redefined under an old +# name makes the gem quietly support a surface documented as removed. +class LegacySurfaceRemovedTest < ActiveSupport::TestCase + PROJECT_ROOT = File.expand_path("../..", __dir__) + LIB = Pathname.new(PROJECT_ROOT).join("lib") + + # The 2.1 `git rm`, minus test/legacy (deleted with the same commit; a + # test tree is not packaged and cannot come back unnoticed). + REMOVED_PATHS = %w[ + capybara + capybara_screenshot_diff + capybara-screenshot-diff.rb + capybara_screenshot_diff.rb + snap_diff/legacy_shims.rb + snap_diff/deprecation.rb + ].freeze + + test "no removed file is back under lib/" do + resurrected = REMOVED_PATHS.select { |path| LIB.join(path).exist? } + + assert_empty resurrected, <<~MSG + The v1 compatibility trees were removed in 2.1, but lib/ carries these + again. Everything under lib/ is packaged, so whatever is here ships: + + #{resurrected.join("\n")} + MSG + end + + # A subprocess because test_helper.rb loads the whole gem plus its + # support files, any of which could define one of these names and mask a + # gem that no longer does. + test "a fresh process loading the gem defines none of the removed names" do + script = <<~'RUBY' + require "snap_diff" + require "snap_diff/integrations/minitest" + + back = [] + back << "Capybara::Screenshot" if defined?(Capybara::Screenshot) + back << "CapybaraScreenshotDiff" if defined?(CapybaraScreenshotDiff) + back << "SnapDiff::Deprecation" if defined?(SnapDiff::Deprecation) + back << "SnapDiff.start" if SnapDiff.respond_to?(:start) + back << "SnapDiff.silence_deprecations" if SnapDiff.respond_to?(:silence_deprecations) + + abort("still defined: #{back.join(", ")}") unless back.empty? + RUBY + + out, status = Open3.capture2e(RbConfig.ruby, "-Ilib", "-e", script, chdir: PROJECT_ROOT) + + assert status.success?, <<~MSG + The removed v1 surface is reachable again from a plain require. 2.1 + dropped it deliberately (ADR-008: SnapDiff.configure is the single + config entry point) and docs/UPGRADING.md tells users it is gone: + + #{out} + MSG + end +end