fix: libvips serves stale pixels when a screenshot is rewritten within the same second - #254
Conversation
…n the same second libvips caches loader operations keyed on filename + mtime, and mtime has one-second resolution. Overwrite a path and re-read it within the same second and the loader hands back the PREVIOUS image. This gem does exactly that: the screenshoter writes `<name>.png`, `checkout_base_screenshot` writes `<name>.base.png` from VCS, and the comparison then reads both. The result is a comparison against an image that is no longer on disk -- a pass or a bogus diff the user cannot reproduce. `VipsDriver#from_file` is the single load site and every caller routes through it (`load_images`, `Screenshoter#take_screenshot`), so one keyword at that seam fixes all of them. `revalidate: true` is libvips 8.15+, hence the `Vips.at_least_libvips?` guard. Ported from #249, which fixes the same seam on the 2.1 branch. The vips cache flush in `test/system_test_case.rb` teardown (`cache_set_max(0)` then `1000`) was a workaround for this, sitting in the harness where it protected our own suite while the bug shipped to users. It is gone; the regression test in `test/unit/drivers/vips_driver_test.rb` guards the seam instead. Cost: none. Measured over 30 distinct-path reads per shape (the gem reads each path at most once per comparison, so the loader cache never helps either way): 80x60, 800x600, 1920x1080 and 1280x4000 all land within +/-0.9 ms and the sign of the delta flips between runs.
|
Warning Review limit reachedNext included review available in 40 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Reviewer's GuideEnsure VipsDriver always loads fresh image data instead of stale cached pixels when a screenshot file is overwritten and re-read within the same second, and remove a now-unnecessary test harness cache flush while adding a focused regression test for this behavior. Sequence diagram for fresh Vips image loading after overwritesequenceDiagram
participant Screenshoter
participant VipsDriver
participant Libvips
participant Filesystem
Screenshoter->>Filesystem: write screenshot path
Screenshoter->>VipsDriver: from_file(filename)
VipsDriver->>Libvips: Image.new_from_file(filename, **REVALIDATE)
Libvips->>Filesystem: read current pixels
Filesystem-->>Libvips: latest image data
Libvips-->>VipsDriver: fresh image
VipsDriver-->>Screenshoter: image for comparison
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Both master (#254) and this branch's first commit added the same constant to VipsDriver, far enough apart that git merged them without a conflict. Ruby does not warn on a frozen-value reassignment at load; standardrb's Lint/ConstantReassignment is what caught it.
The v2.0.0 section was written before #250, #253, #254, #255, #256, #261, #263, #264, #266 and #267 landed, and three of its claims had gone false: - "Known limitations: fork-based parallel tests produce no HTML report ... Fixed in 2.1" -- fixed in 2.0 by #266. Reproduced both sides here: 1.15.1 + `parallelize(workers: 2, threshold: 0)` writes NO report and prints no summary line; master writes one merged report and `4 verified, 4 changed, 0 new`. - "a suite whose only contact with the v1 API is `require \"capybara_screenshot_diff/minitest\"` + `include ...Assertions` still prints nothing" -- #263 made the require doors warn. That exact setup now prints the migration notice; verified in a scratch project. - "Two removals 2.0 cannot warn about ... `driver:` as a setting" -- #263 made both the setting writer and the per-screenshot key warn. Verified: `Capybara::Screenshot::Diff.driver = :vips` prints the removal line with a call site. And the silent-by-design constant list repeated the shape of the beta2 `defined?` mistake: it listed "Os, Region" inside a run of `Capybara::Screenshot::Diff::` names. Probed on master -- `defined?(Capybara::Screenshot::Diff::Os)` and `defined?(Capybara::Screenshot::Diff::Region)` are both nil. The real names are `Capybara::Screenshot::Os` and the top-level `Region`, neither of which existed under `::Diff` in 1.15.1 either. Fully qualified now, and `::Comparison` added to match docs/UPGRADING.md. New material, every claim checked against the code or a live run: - a "why upgrade" section for the four green-suite-testing-nothing bugs (#255, #256, #254, #266), plus the unfollowable CI message (#267) and the fail_if_new precedence change - before/after transcripts of the failure message (#264), taken from the same page rendered on 1.15.1 and on master - the summary line (#261), with the fact that it comes from the HTML reporter and needs its one-line require -- an omission that would have read as a missing feature - the #250 / #253 perf table, attributed to its harness, with columns labelled before/after rather than 1.x/2.0 - the libvips fix is stated as guarded on libvips 8.15+, so a reader on an older libvips knows the bug is still theirs Install snippets stay pinned to 2.0.0.beta3 on purpose: `~> 2.0` resolves to nothing on rubygems today. docs/RELEASE_PREP.md already carries a precise step to swap all five (its grep finds exactly those five), and gains one line so the record-modes placeholder in the entry cannot ship unfilled. `rake test:unit` 651 runs / 0 failures, `standardrb lib test` clean.
beta3 fixed the canonical entry points and shipped almost none of the behaviour. beta4 is the prerelease the 2.0.0 entry actually describes: the four green-suite bugs (#254 #255 #256 #258), the accept workflow (#259), the legible failure message (#264), and the deprecation warnings that make 2.1's removals visible (#246 #263). - `lib/snap_diff/version.rb` -> 2.0.0.beta4. Nothing else holds a version; the gemspec, the legacy version file and the mirror gemspec all read it. Verified with the release workflow's own guard command: `ruby -I lib -r capybara/screenshot/diff/version -e "puts Capybara::Screenshot::Diff::VERSION"` => 2.0.0.beta4 - CHANGELOG: a `[v2.0.0.beta4]` section written as the delta from beta3, plus the record-modes PLACEHOLDER filled from #259 now that it has shipped. `grep -n PLACEHOLDER CHANGELOG.md` returns nothing. - Install snippets moved beta3 -> beta4 in README, docs/UPGRADING.md and docs/migration-guide.md. They stay PINNED: `~> 2.0` resolves to nothing while only prereleases exist, so unpinning belongs to 2.0.0 final, not here. The notes name the #272 caveat explicitly. Removing `skip_area`'s implicit stabilization wait (10.012 s -> 0.009 s measured) means a selector not yet in the DOM now yields no mask, silently, where it previously resolved after the wait. #277's run-level tally shipped in the same beta as the replacement signal, and the notes say so rather than leaving it to be discovered. Gates: `rake test:unit` 720 runs / 2124 assertions / 0 failures / 0 skips under CI=true on 4.0.6, `standardrb lib test` clean over 161 files, and `gem build` produces capybara-screenshot-diff-2.0.0.beta4.gem (93 files, 13 docs, RELEASE_PREP correctly excluded).
The bug
libvips caches loader operations keyed on filename + mtime, and mtime has one-second resolution. Overwrite a path and re-read it within the same second, and the loader hands back the previous image.
This gem does exactly that: the screenshoter writes
<name>.png,checkout_base_screenshotwrites<name>.base.pngfrom VCS, and the comparison then reads both. The result is a comparison against an image that is no longer on disk — a pass, or a bogus diff, that the user cannot reproduce.Reproduced on unmodified
master(d5b7a9e), ruby-vips 2.3.0 / libvips 8.18.5:Not a red assertion — the driver returned b.png's pixels while a.png was on disk.
The fix
applied at the single load site,
VipsDriver#from_file. Every caller routes through it (load_images,Screenshoter#take_screenshot), so one keyword at that seam fixes all of them.revalidateis libvips 8.15+, hence the version guard;:revalidate("Don't use a cached result for this operation") andat_least_libvips?(major, minor)are both confirmed against the ruby-vips docs.Ported from #249, which fixes the same seam on the 2.1 branch.
Mutation check
1 failures—Expected 160.3706640625 to not be equal to 160.3706640625(b.png's average, the stale image)51 runs, 106 assertions, 0 failuresPerf
revalidate: truecosts nothing. The gem reads each path at most once per comparison, so the loader cache can never help it — the honest measurement gives every read a distinct path. 30 reads per shape, each forced through.avg:All four within ±0.9 ms, and the sign of the delta flips between runs. Matches #249's finding.
(A benchmark that re-reads one path N times appears to show
revalidatecosting up to +19 ms on 1920x1080 — but that baseline is serving stale cache hits, which is the bug itself, not a workload this gem has.)#250's byte-identical short-circuit runs before any decode, so it is untouched by this change.The test-harness workaround
test/system_test_case.rbflushed the vips cache in teardown (cache_set_max(0)then1000) — a workaround for this exact bug, sitting in the harness where it protected our own suite while the bug shipped to users. Removed here, as in #249.Evidence for the removal, all with
SCREENSHOT_DRIVER=vips:485 runs, 1 failure— only the new unit test485 runs, 1 failure— only the new unit test485 runs, 0 failuresWhich existing tests would have caught a regression if the workaround had stayed? None — and that is the point. With the workaround in place or removed, the mutated fix reds exactly one test: the new
#from_file re-reads a path that was overwritten within the same second. The system suite never caught this, because within one test the two paths (<name>.pngand<name>.base.png) are distinct and each is read once; the flush was guarding a cross-test rewrite of the same path that the suite does not deterministically hit. Removing it is safe, and the new unit test is now the only thing standing between this bug and users.Gates
rake test:unit— 588 runs, 0 failures (also green underSCREENSHOT_DRIVER=vips)rake test:canonical— 485 runs, 0 failures, 1 skip (also green underSCREENSHOT_DRIVER=vips)standardrb lib test— 151 files, no offensesUntouched:
CHANGELOG.md,README.md,docs/,version.rb.🤖 Generated with Claude Code
https://claude.ai/code/session_014BQJX6eWzBj2UTm5zQsjEs
Summary by Sourcery
Ensure Vips image loads revalidate rewritten screenshot files so comparisons always use the pixels currently on disk.
Bug Fixes:
Enhancements:
Tests: