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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions test/unit/capture/viewport_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,33 @@ class ViewportTest < ActiveSupport::TestCase
Viewport.prepare!([800, 600])
end
assert_includes error.message, "[800, 600]"
assert_includes error.message, "Actual: unknown"
end
end
end

# The selenium arm of the ternary was never taken -- the test above
# stubs selenium? to false, and nothing else calls prepare! -- so the
# whole `session.driver.browser.manage.window.size` chain (the only
# reason the message is ever useful) was unexecuted. Under selenium the
# message must name the size the browser ACTUALLY has.
test "prepare! reports the real window size when the driver is selenium" do
window = Struct.new(:size).new("(width: 1024, height: 768)")
manage = Struct.new(:window).new(window)
browser = Struct.new(:manage).new(manage)
driver = Struct.new(:browser).new(browser)
session = Struct.new(:driver).new(driver)

BrowserHelpers.stub(:window_size_is_wrong?, true) do
BrowserHelpers.stub(:selenium?, true) do
BrowserHelpers.stub(:session, session) do
error = assert_raises(SnapDiff::WindowSizeMismatchError) do
Viewport.prepare!([800, 600])
end

assert_includes error.message, "Expected: [800, 600]"
assert_includes error.message, "Actual: (width: 1024, height: 768)"
end
end
end
end
Expand Down
196 changes: 196 additions & 0 deletions test/unit/deletion_3_0_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
# 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
27 changes: 27 additions & 0 deletions test/unit/reporters/default_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,33 @@ class DefaultReporterTest < ActiveSupport::TestCase
assert_not reporter.heatmap_diff_path.exist?, "heatmap diff should be cleaned"
end

# The test above calls clean_tmp_files directly, so #generate's OWN call to
# it on the equal path had no coverage: deleting that line left the suite
# green while every passing comparison leaked the previous run's diff
# artifacts, which the next run then reports as stale output.
test "#generate removes the previous run's diff artifacts when the images are equal" do
driver = SnapDiff::Drivers::VipsDriver.new
reporter = SnapDiff::Reporters::Default.new(driver.find_difference_region(build_comparison_for(driver, "a.png", "b.png")))
reporter.generate # a.png vs b.png differ: writes the artifacts

assert_predicate reporter.annotated_image_path, :exist?
assert_predicate reporter.heatmap_diff_path, :exist?

# Equal images, but the SAME artifact paths as the comparison above, so
# what gets cleaned is exactly what got written.
equal_image = driver.from_file(TEST_IMAGES_DIR.join("a.png"))
equal_difference = driver.find_difference_region(
SnapDiff::Comparison::Images.new(equal_image, equal_image, {}, driver, @_tmpdir / "a.png", @_tmpdir / "b.png")
)
assert_predicate equal_difference, :equal?

assert_nil SnapDiff::Reporters::Default.new(equal_difference).generate

assert_not reporter.annotated_image_path.exist?, "diff should be cleaned on the equal path"
assert_not reporter.annotated_base_image_path.exist?, "base diff should be cleaned on the equal path"
assert_not reporter.heatmap_diff_path.exist?, "heatmap diff should be cleaned on the equal path"
end

test "failure message reports metrics without leaking image objects" do
driver = SnapDiff::Drivers::VipsDriver.new
comparison = build_comparison_for(driver, "a.png", "b.png")
Expand Down
63 changes: 63 additions & 0 deletions test/unit/snap_diff_config_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,69 @@ def config
config.enabled = original_diff
end

# The two path-segment flags were only ever exercised TOGETHER (both true
# in test/system_test_case.rb and the rspec fixtures, both nil everywhere
# else), so either `if` could be deleted or swapped for the other flag and
# the whole suite stayed green. All four combinations, with the segment
# sources stubbed so the assertion names the expected path literally
# rather than recomputing it.
SCREENSHOT_AREA_COMBINATIONS = [
[nil, nil, "doc/screenshots"],
[true, nil, "doc/screenshots/fake_os"],
[nil, true, "doc/screenshots/fake_driver"],
[true, true, "doc/screenshots/fake_os/fake_driver"]
].freeze

test "screenshot_area appends the os and driver segments independently" do
original_os, original_driver, original_save_path =
config.add_os_path, config.add_driver_path, config.save_path

SnapDiff::Os.stub(:name, "fake_os") do
Capybara.stub(:current_driver, :fake_driver) do
config.save_path = "doc/screenshots"

SCREENSHOT_AREA_COMBINATIONS.each do |add_os_path, add_driver_path, expected|
config.add_os_path = add_os_path
config.add_driver_path = add_driver_path

assert_equal expected, config.screenshot_area,
"screenshot_area with add_os_path=#{add_os_path.inspect}, " \
"add_driver_path=#{add_driver_path.inspect}"
end
end
end
ensure
config.add_os_path = original_os
config.add_driver_path = original_driver
config.save_path = original_save_path
end

# The vips tolerance floor in Config#default_options is the one literal in
# there that is not a stored setting, and deleting it left the full suite
# green (config.rb's then-arm had a hit count of 0): nothing ever asked for
# default_options with driver == :vips and no explicit tolerance.
test "default_options floors tolerance at 0.001 for vips and only for vips" do
original_driver, original_tolerance = config.driver, config.tolerance

begin
config.tolerance = nil

config.driver = :vips
assert_equal 0.001, config.default_options[:tolerance]

config.driver = :chunky_png
assert_nil config.default_options[:tolerance]

# An explicit tolerance still wins over the floor.
config.tolerance = 0.5
config.driver = :vips
assert_equal 0.5, config.default_options[:tolerance]
ensure
config.driver = original_driver
config.tolerance = original_tolerance
end
end

test "writing root through config round-trips through the same Pathname coercion" do
original = config.root

Expand Down
18 changes: 18 additions & 0 deletions test/unit/snap_diff_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,24 @@ class SnapDiffTest < ActiveSupport::TestCase
assert_equal 0.02, result.driver_options[:tolerance]
end

# The test above only pins that an EXPLICIT option round-trips, so
# dropping `config.default_options.merge` from .compare entirely left the
# full suite green -- callers who configure once and then call .compare
# with no options silently lost every configured default.
test ".compare with no options still carries the configured defaults" do
original = SnapDiff.config.tolerance

begin
SnapDiff.config.tolerance = 0.0123

result = SnapDiff.compare(TEST_IMAGES_DIR / "a.png", TEST_IMAGES_DIR / "b.png")

assert_equal 0.0123, result.driver_options[:tolerance]
ensure
SnapDiff.config.tolerance = original
end
end

# Regression test for a load-order bug: `require "snap_diff"` standalone
# (nothing else preloaded) used to raise
# `NameError: uninitialized constant ...ImageCompare::Drivers` because
Expand Down
Loading