canon 0.3.26 → 0.3.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,9 @@ require "canon" unless defined?(Canon)
4
4
  require "canon/comparison"
5
5
  require "canon/diff_formatter"
6
6
  require "canon/config"
7
+ require "canon/rebaseliner"
8
+ require "canon/pretty_printer/xml"
9
+ require "canon/pretty_printer/html"
7
10
 
8
11
  begin
9
12
  require "rspec/expectations"
@@ -88,60 +91,135 @@ module Canon
88
91
  end
89
92
 
90
93
  def matches?(target)
94
+ # Capture caller_locations only when rebaseliner is enabled so that
95
+ # passing assertions don't pay for stack walking.
96
+ @rebaseliner_caller = caller_locations(1, 12) if Canon::Rebaseliner.enabled?
97
+ equivalent = compute_equivalent(target)
98
+ return true if equivalent
99
+
100
+ attempt_rebaseline
101
+ end
102
+
103
+ # Skip the rebaseliner path for `.not_to`. RSpec invokes
104
+ # `does_not_match?` if defined, instead of negating `matches?`.
105
+ def does_not_match?(target)
106
+ !compute_equivalent(target)
107
+ end
108
+
109
+ def failure_message
110
+ "expected #{format_name} to be equivalent\n\n#{diff_output}"
111
+ end
112
+
113
+ def failure_message_when_negated
114
+ "expected #{format_name} not to be equivalent"
115
+ end
116
+
117
+ def expected
118
+ @expected
119
+ end
120
+
121
+ def actual
122
+ @target
123
+ end
124
+
125
+ def diffable # rubocop:disable Naming/PredicateMethod
126
+ false
127
+ end
128
+
129
+ private
130
+
131
+ # Run the actual comparison and set @target / @comparison_result.
132
+ # Returns the boolean equivalence result.
133
+ def compute_equivalent(target)
91
134
  @target = target
92
135
 
93
- # Build comparison options from config and matcher params
94
136
  opts = build_comparison_options
95
-
96
- # Add format hint if explicitly provided
97
137
  opts[:format] = @format if @format
98
138
 
99
- # Delegate to Canon::Comparison.equivalent? - the SINGLE source of truth
100
- # Comparison handles format detection, HTML parsing, and all business logic
101
139
  @comparison_result = Canon::Comparison.equivalent?(
102
140
  @expected,
103
141
  @target,
104
142
  opts,
105
143
  )
106
144
 
107
- # When verbose: true, result is a ComparisonResult object
108
- # Use the equivalent? method to check for normative differences
109
145
  case @comparison_result
110
146
  when Canon::Comparison::ComparisonResult
111
147
  @comparison_result.equivalent?
112
148
  when Hash
113
- # Legacy format - Hash with :differences array and :preprocessed strings
114
149
  @comparison_result[:differences].empty?
115
150
  when Array
116
- # Legacy format - XML/JSON/YAML returns []
117
151
  @comparison_result.empty?
118
152
  else
119
- # Boolean result
120
153
  @comparison_result
121
154
  end
122
155
  end
123
156
 
124
- def failure_message
125
- "expected #{format_name} to be equivalent\n\n#{diff_output}"
126
- end
157
+ # When the rebaseliner env var is on and the comparison failed, try to
158
+ # rewrite the heredoc that backs the expected value. Returns `true` to
159
+ # the matcher (so the assertion is treated as passing) when a rewrite
160
+ # succeeded; otherwise `false` so the normal failure path runs.
161
+ def attempt_rebaseline
162
+ return false unless Canon::Rebaseliner.enabled?
163
+ return false unless @rebaseliner_caller
127
164
 
128
- def failure_message_when_negated
129
- "expected #{format_name} not to be equivalent"
130
- end
165
+ frame = first_user_frame(@rebaseliner_caller)
166
+ return false unless frame
131
167
 
132
- def expected
133
- @expected
168
+ prettyprinted = pretty_print_actual
169
+ return false unless prettyprinted
170
+
171
+ status = Canon::Rebaseliner.rewrite!(
172
+ spec_path: frame.absolute_path || frame.path,
173
+ line: frame.lineno,
174
+ prettyprinted_actual: prettyprinted,
175
+ )
176
+ status == :rewritten
177
+ rescue StandardError => e
178
+ Canon::Rebaseliner::Logger.log(
179
+ :error,
180
+ spec_path: frame&.path.to_s,
181
+ line: frame&.lineno.to_i,
182
+ detail: "#{e.class}: #{e.message}",
183
+ )
184
+ false
134
185
  end
135
186
 
136
- def actual
137
- @target
187
+ # Drop frames inside RSpec internals and canon itself; the first
188
+ # remaining is the user's spec file.
189
+ def first_user_frame(locations)
190
+ locations.find do |loc|
191
+ path = loc.absolute_path || loc.path
192
+ next false unless path
193
+ next false if %r{/gems/rspec-(expectations|core|mocks|support)-}.match?(path)
194
+ next false if %r{/lib/rspec/(expectations|core|mocks|support)/}.match?(path)
195
+ next false if path.include?("/canon/lib/canon/")
196
+ next false if path.end_with?("/lib/canon/rspec_matchers.rb")
197
+
198
+ true
199
+ end
138
200
  end
139
201
 
140
- def diffable # rubocop:disable Naming/PredicateMethod
141
- false
202
+ # Format the actual value with the appropriate pretty-printer for the
203
+ # heredoc rewrite. Returns nil when no prettyprinter is wired (e.g.
204
+ # JSON/YAML in v1).
205
+ def pretty_print_actual
206
+ fmt = @format || detect_format
207
+ case fmt
208
+ when :xml
209
+ Canon::PrettyPrinter::Xml.new.format(@target.to_s)
210
+ when :html, :html4, :html5
211
+ # fixture_ready: true emits actually-indented XHTML-shaped
212
+ # output suitable for direct paste into a heredoc.
213
+ Canon::PrettyPrinter::Html.new(fixture_ready: true)
214
+ .format(@target.to_s)
215
+ end
142
216
  end
143
217
 
144
- private
218
+ def detect_format
219
+ Canon::Comparison::FormatDetector.detect(@expected)
220
+ rescue StandardError
221
+ nil
222
+ end
145
223
 
146
224
  def format_name
147
225
  # Use explicitly provided format if available
data/lib/canon/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Canon
4
- VERSION = "0.3.26"
4
+ VERSION = "0.3.28"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: canon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.26
4
+ version: 0.3.28
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-09-07 00:00:00.000000000 Z
11
+ date: 2026-09-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: diff-lcs
@@ -187,6 +187,7 @@ files:
187
187
  - docs/features/match-options/index.adoc
188
188
  - docs/features/match-options/pretty-printed-fixtures.adoc
189
189
  - docs/features/performance.adoc
190
+ - docs/features/regenerate-expected.adoc
190
191
  - docs/getting-started/index.adoc
191
192
  - docs/getting-started/quick-start.adoc
192
193
  - docs/guides/choosing-configuration.adoc
@@ -342,6 +343,13 @@ files:
342
343
  - lib/canon/pretty_printer/json.rb
343
344
  - lib/canon/pretty_printer/xml.rb
344
345
  - lib/canon/pretty_printer/xml_normalized.rb
346
+ - lib/canon/rebaseliner.rb
347
+ - lib/canon/rebaseliner/atomic_writer.rb
348
+ - lib/canon/rebaseliner/call_site_resolver.rb
349
+ - lib/canon/rebaseliner/heredoc_locator.rb
350
+ - lib/canon/rebaseliner/heredoc_rewriter.rb
351
+ - lib/canon/rebaseliner/heredoc_target.rb
352
+ - lib/canon/rebaseliner/logger.rb
345
353
  - lib/canon/rspec_matchers.rb
346
354
  - lib/canon/tree_diff.rb
347
355
  - lib/canon/tree_diff/adapters.rb