-
Notifications
You must be signed in to change notification settings - Fork 18
fix: skip_area wait, an invisible summary line, and a phantom assertion (#272, #269, #270) #275
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0f3a8f6
4d06d05
57f7ac3
90a78c3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,30 @@ The report includes a sidebar with thumbnails, side-by-side comparison with diff | |
|
|
||
| **Note:** The report is not generated when all screenshots match. | ||
|
|
||
| ## The end-of-run summary | ||
|
|
||
| Every run ends with what it actually did, whether or not you require a reporter: | ||
|
|
||
| ``` | ||
| [snap_diff] 12 verified, 1 changed, 2 new (not verified). | ||
| ``` | ||
|
Comment on lines
+27
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Add a language identifier to both output fences. Lines 27 and 43 violate markdownlint rule MD040. Use Also applies to: 43-45 🧰 Tools🪛 markdownlint-cli2 (0.23.2)[warning] 27-27: Fenced code blocks should have a language specified (MD040, fenced-code-language) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
|
|
||
| - **verified** — a committed baseline existed and was compared | ||
| - **changed** — of those, the ones that differed | ||
| - **new** — captured but *not* compared, for want of a committed baseline: neither a pass nor a | ||
| failure. Commit the files to turn them into baselines. | ||
|
|
||
| `0 verified` is printed as `NOTHING WAS VERIFIED`, because it is the only signal for the failures | ||
| no per-assertion rule can see: a `rake test` that ran zero system tests, or an inherited `GIT_DIR` | ||
| sending every baseline lookup to the wrong repository. Both leave a green suite that compared | ||
| nothing. | ||
|
|
||
| Requiring `snap_diff/reporters/html` adds one more line, naming the file it wrote: | ||
|
|
||
| ``` | ||
| [snap_diff] Report: doc/screenshots/snap_diff_report.html | ||
| ``` | ||
|
|
||
| ## Parallel test runs | ||
|
|
||
| `finalize` — the hook that writes the report — runs from the framework's end-of-suite hook. Whether | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,10 +19,25 @@ module Reporting | |
| @mutex = Mutex.new | ||
| @missing_baselines = Set.new | ||
| @rerecorded_baselines = Set.new | ||
| @verified = 0 | ||
| @changed = 0 | ||
|
|
||
| class << self | ||
| attr_reader :reporters, :mutex | ||
|
|
||
| # How many screenshots were compared to a committed baseline, and how | ||
| # many of those differed. | ||
| # | ||
| # These counters live HERE, not in a reporter (issue #269). Counting | ||
| # is core honesty; writing an HTML file is a feature. The summary | ||
| # exists to catch the failure modes no per-assertion rule can see -- a | ||
| # run where zero system tests executed, or where an inherited GIT_DIR | ||
| # redirected every baseline lookup -- and `0 verified` is the only | ||
| # signal for either. It shipped inside Reporters::HTML, the gem's one | ||
| # and only `register` call site, so the documented Rails setup (which | ||
| # requires just the Minitest integration) printed nothing at all. | ||
| attr_reader :verified, :changed | ||
|
|
||
| # Remembers a screenshot that had no COMMITTED baseline and was | ||
| # therefore never compared. | ||
| # | ||
|
|
@@ -43,9 +58,16 @@ def missing_baselines_count | |
| end | ||
|
|
||
| # @api private | ||
| # Per-test isolation for this gem's own suite. | ||
| def reset_missing_baselines! | ||
| @mutex.synchronize { @missing_baselines.clear } | ||
| # Per-test isolation for this gem's own suite: everything {finalize!} | ||
| # reports, cleared in one call. One surface rather than one reset per | ||
| # tally, so a tally added later cannot be forgotten at the call site. | ||
| def reset_run_totals! | ||
| @mutex.synchronize do | ||
| @missing_baselines.clear | ||
| @rerecorded_baselines.clear | ||
| @verified = 0 | ||
| @changed = 0 | ||
| end | ||
| end | ||
|
|
||
| # Remembers a screenshot re-recorded by `record: :all` -- captured as | ||
|
|
@@ -58,11 +80,6 @@ def record_rerecorded_baseline(name) | |
| @mutex.synchronize { !!@rerecorded_baselines.add?(name) } | ||
| end | ||
|
|
||
| # @api private | ||
| def reset_rerecorded_baselines! | ||
| @mutex.synchronize { @rerecorded_baselines.clear } | ||
| end | ||
|
|
||
| # Registers a reporter for the rest of the process. The canonical way | ||
| # in: the append happens under the mutex, so concurrent registrations | ||
| # cannot lose one (issue #217 item 2). `reporters` stays public and | ||
|
|
@@ -80,6 +97,18 @@ def register(reporter) | |
| def notify(assertions) | ||
| return if assertions.nil? || assertions.empty? | ||
|
|
||
| # Warned about and skipped, never raised: `notify` runs inside every | ||
| # test's teardown (SnapDiff.reset), and a raise here would abort the | ||
| # reset before it clears the registry -- leaking one test's | ||
| # assertions into the next. A tally must not be able to take a | ||
| # user's suite down. Same contract the reporter loop below applies, | ||
| # and just as loud: unconditional, not DEBUG-gated. | ||
| begin | ||
| count(assertions) | ||
| rescue => e | ||
| warn "[snap_diff] Could not tally the run (#{e.class}: #{e.message})" | ||
| end | ||
|
|
||
| reporters_snapshot = @mutex.synchronize { @reporters.dup } | ||
| return if reporters_snapshot.empty? | ||
|
|
||
|
|
@@ -90,10 +119,70 @@ def notify(assertions) | |
| end | ||
| end | ||
|
|
||
| # End-of-suite hook: finalizes each reporter and prints its summary. | ||
| # A raising reporter is warned about and skipped; the rest are still | ||
| # finalized. | ||
| # Tallies a finished test's assertions. An assertion with no | ||
| # `compare` never reached a baseline, so it is neither verified nor | ||
| # changed -- it is counted, if at all, by {record_missing_baseline}. | ||
| def count(assertions) | ||
| verified = 0 | ||
| changed = 0 | ||
|
|
||
| assertions.each do |assertion| | ||
| compare = assertion.compare | ||
| next unless compare | ||
|
|
||
| verified += 1 | ||
| changed += 1 if compare.difference&.different? | ||
| end | ||
|
|
||
| @mutex.synchronize do | ||
| @verified += verified | ||
| @changed += changed | ||
| end | ||
| end | ||
|
Comment on lines
+122
to
+141
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Keep tallying after one malformed assertion. If Rescue per assertion inside the loop. Keep tallying the remaining assertions. 🤖 Prompt for AI Agents |
||
|
|
||
| # The last line of the run, and the only place it says what it | ||
| # actually did: | ||
| # | ||
| # verified -- a committed baseline existed and was compared | ||
| # changed -- of those, the ones that differed | ||
| # new -- captured but NOT compared, for want of a committed | ||
| # baseline: neither a pass nor a failure | ||
| # | ||
| # Printed on every run, passing or failing, reporter or no reporter, | ||
| # and never nil. "N screenshots compared" counted only what it | ||
| # compared, so it was silent about exactly the screenshots it did not | ||
| # -- and silent altogether when it compared nothing, which is the one | ||
| # case worth shouting about. | ||
| def counts_summary | ||
| verified, changed, new_count, rerecorded = @mutex.synchronize { | ||
| [@verified, @changed, @missing_baselines.size, @rerecorded_baselines.size] | ||
| } | ||
| line = "[snap_diff] #{verified} verified, #{changed} changed, #{new_count} new (not verified)." | ||
|
|
||
| # `record: :all` (#274) accepts the rendering as the new baseline | ||
| # without comparing, so those are neither verified nor changed -- | ||
| # and not "new" either, which is a different fact. Only shown when | ||
| # it happened; the names are on their own line below. | ||
| line += " #{rerecorded} re-recorded (not verified)." if rerecorded.positive? | ||
|
|
||
| # The shout is for an UNEXPLAINED zero -- a suite that ran no system | ||
| # tests, a GIT_DIR pointed at the wrong repository. Re-recording | ||
| # explains it, and the user asked for it: shouting there is a false | ||
| # alarm, and false alarms are how the real one stops being read. | ||
| if verified.zero? && rerecorded.zero? | ||
| return "#{line} NOTHING WAS VERIFIED -- no screenshot was compared to a committed baseline." | ||
| end | ||
|
|
||
| line | ||
| end | ||
|
|
||
| # End-of-suite hook: prints the counts, then finalizes each reporter | ||
| # and prints its summary. A raising reporter is warned about and | ||
| # skipped; the rest are still finalized -- and the counts line is | ||
| # already out, so no reporter can take it down with it. | ||
| def finalize! | ||
| $stdout.puts counts_summary | ||
|
|
||
| @mutex.synchronize { @reporters.dup }.each do |reporter| | ||
| reporter.finalize | ||
| if (msg = reporter.summary) | ||
|
|
@@ -160,6 +249,8 @@ def dump_parallel_fragment | |
| payload = { | ||
| "missing_baselines" => @mutex.synchronize { @missing_baselines.to_a }, | ||
| "rerecorded_baselines" => @mutex.synchronize { @rerecorded_baselines.to_a }, | ||
| "verified" => @verified, | ||
| "changed" => @changed, | ||
| "reporters" => @mutex.synchronize { @reporters.dup } | ||
| .map { |reporter| reporter.dump_state if reporter.respond_to?(:dump_state) } | ||
| } | ||
|
|
@@ -182,10 +273,19 @@ def merge_parallel_fragments! | |
| Dir[File.join(parallel_fragments_dir, "*.json")].sort.each do |fragment| | ||
| payload = JSON.parse(File.read(fragment)) | ||
|
|
||
| @mutex.synchronize { payload["missing_baselines"].each { |name| @missing_baselines << name } } | ||
| # `to_a` on a fresh install predates this key: a fragment written | ||
| # by an older worker has no "rerecorded_baselines" at all. | ||
| @mutex.synchronize { payload.fetch("rerecorded_baselines", []).each { |name| @rerecorded_baselines << name } } | ||
| # Only "missing_baselines" is read without a default: it is the | ||
| # one key every version of this fragment has ever written. Every | ||
| # key added since is `fetch`ed with one, because the fragments | ||
| # directory is keyed by pid under the system temp dir -- a | ||
| # recycled pid can hand this merge a fragment left behind by an | ||
| # older version of the gem, and a partial payload must not take | ||
| # the run down. | ||
| @mutex.synchronize do | ||
| payload["missing_baselines"].each { |name| @missing_baselines << name } | ||
| payload.fetch("rerecorded_baselines", []).each { |name| @rerecorded_baselines << name } | ||
| @verified += payload.fetch("verified", 0) | ||
| @changed += payload.fetch("changed", 0) | ||
| end | ||
|
|
||
| reporters_snapshot = @mutex.synchronize { @reporters.dup } | ||
| payload["reporters"].each_with_index do |state, index| | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: snap-diff/snap_diff-capybara
Length of output: 33773
🏁 Script executed:
Repository: snap-diff/snap_diff-capybara
Length of output: 50384
🏁 Script executed:
Repository: snap-diff/snap_diff-capybara
Length of output: 46370
🌐 Web query:
ActiveSupport::Testing::TestsWithoutAssertions Rails source missing assertions test_case.rb💡 Result:
ActiveSupport::Testing::TestsWithoutAssertions is a module in Rails designed to warn developers when a test case runs without performing any assertions [1][2]. It is located at activesupport/lib/active_support/testing/tests_without_assertions.rb and is included in ActiveSupport::TestCase via the prepend method [3][2][4]. The module works by overriding the after_teardown method [1][2]. After calling super, it checks the number of assertions performed; if the count is zero and the test did not end in an error or skip state, it issues a warning that includes the test name and its file location [1][2]. If you are looking for this in your codebase and feel it is missing, verify your Rails version. It was introduced to help identify broken or incomplete tests [1]. You can confirm its presence by checking if the file exists in your ActiveSupport directory or by inspecting the ActiveSupport::TestCase class ancestry in your environment [3][5][6]. If you are using a very recent version of Rails, ensure you have not accidentally disabled this functionality through custom configuration or third-party test suite overrides [2].
Citations:
🏁 Script executed:
Repository: snap-diff/snap_diff-capybara
Length of output: 311
Scope the missing-assertions warning to Rails/ActiveSupport.
Plain Minitest does not emit this warning.
ActiveSupport::Testing::TestsWithoutAssertionsemits it forActiveSupport::TestCase. Update the sentence accordingly.🤖 Prompt for AI Agents