canon 0.1.8 → 0.1.10
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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +83 -22
- data/docs/Gemfile +1 -0
- data/docs/_config.yml +90 -1
- data/docs/advanced/diff-classification.adoc +196 -24
- data/docs/features/match-options/index.adoc +239 -1
- data/lib/canon/comparison/format_detector.rb +2 -1
- data/lib/canon/comparison/html_comparator.rb +19 -8
- data/lib/canon/comparison/html_compare_profile.rb +8 -2
- data/lib/canon/comparison/markup_comparator.rb +109 -2
- data/lib/canon/comparison/match_options/base_resolver.rb +7 -0
- data/lib/canon/comparison/whitespace_sensitivity.rb +208 -0
- data/lib/canon/comparison/xml_comparator/child_comparison.rb +15 -7
- data/lib/canon/comparison/xml_comparator/diff_node_builder.rb +108 -0
- data/lib/canon/comparison/xml_comparator/node_parser.rb +10 -5
- data/lib/canon/comparison/xml_comparator/node_type_comparator.rb +14 -7
- data/lib/canon/comparison/xml_comparator.rb +240 -23
- data/lib/canon/comparison/xml_node_comparison.rb +25 -3
- data/lib/canon/diff/diff_classifier.rb +119 -5
- data/lib/canon/diff/formatting_detector.rb +1 -1
- data/lib/canon/diff/xml_serialization_formatter.rb +153 -0
- data/lib/canon/rspec_matchers.rb +37 -8
- data/lib/canon/version.rb +1 -1
- data/lib/canon/xml/data_model.rb +24 -13
- metadata +4 -78
- data/docs/plans/2025-01-17-html-parser-selection-fix.adoc +0 -250
- data/false_positive_analysis.txt +0 -0
- data/file1.html +0 -1
- data/file2.html +0 -1
- data/old-docs/ADVANCED_TOPICS.adoc +0 -20
- data/old-docs/BASIC_USAGE.adoc +0 -16
- data/old-docs/CHARACTER_VISUALIZATION.adoc +0 -567
- data/old-docs/CLI.adoc +0 -497
- data/old-docs/CUSTOMIZING_BEHAVIOR.adoc +0 -19
- data/old-docs/DIFF_ARCHITECTURE.adoc +0 -435
- data/old-docs/DIFF_FORMATTING.adoc +0 -540
- data/old-docs/DIFF_PARAMETERS.adoc +0 -261
- data/old-docs/DOM_DIFF.adoc +0 -1017
- data/old-docs/ENV_CONFIG.adoc +0 -876
- data/old-docs/FORMATS.adoc +0 -867
- data/old-docs/INPUT_VALIDATION.adoc +0 -477
- data/old-docs/MATCHER_BEHAVIOR.adoc +0 -90
- data/old-docs/MATCH_ARCHITECTURE.adoc +0 -463
- data/old-docs/MATCH_OPTIONS.adoc +0 -912
- data/old-docs/MODES.adoc +0 -432
- data/old-docs/NORMATIVE_INFORMATIVE_DIFFS.adoc +0 -219
- data/old-docs/OPTIONS.adoc +0 -1387
- data/old-docs/PREPROCESSING.adoc +0 -491
- data/old-docs/README.old.adoc +0 -2831
- data/old-docs/RSPEC.adoc +0 -814
- data/old-docs/RUBY_API.adoc +0 -485
- data/old-docs/SEMANTIC_DIFF_REPORT.adoc +0 -646
- data/old-docs/SEMANTIC_TREE_DIFF.adoc +0 -765
- data/old-docs/STRING_COMPARE.adoc +0 -345
- data/old-docs/TMP.adoc +0 -3384
- data/old-docs/TREE_DIFF.adoc +0 -1080
- data/old-docs/UNDERSTANDING_CANON.adoc +0 -17
- data/old-docs/VERBOSE.adoc +0 -482
- data/old-docs/VISUALIZATION_MAP.adoc +0 -625
- data/old-docs/WHITESPACE_TREATMENT.adoc +0 -1155
- data/scripts/analyze_current_state.rb +0 -85
- data/scripts/analyze_false_positives.rb +0 -114
- data/scripts/analyze_remaining_failures.rb +0 -105
- data/scripts/compare_current_failures.rb +0 -95
- data/scripts/compare_dom_tree_diff.rb +0 -158
- data/scripts/compare_failures.rb +0 -151
- data/scripts/debug_attribute_extraction.rb +0 -66
- data/scripts/debug_blocks_839.rb +0 -115
- data/scripts/debug_meta_matching.rb +0 -52
- data/scripts/debug_p_matching.rb +0 -192
- data/scripts/debug_signature_matching.rb +0 -118
- data/scripts/debug_sourcecode_124.rb +0 -32
- data/scripts/debug_whitespace_sensitive.rb +0 -192
- data/scripts/extract_false_positives.rb +0 -138
- data/scripts/find_actual_false_positives.rb +0 -125
- data/scripts/investigate_all_false_positives.rb +0 -161
- data/scripts/investigate_batch1.rb +0 -127
- data/scripts/investigate_classification.rb +0 -150
- data/scripts/investigate_classification_detailed.rb +0 -190
- data/scripts/investigate_common_failures.rb +0 -342
- data/scripts/investigate_false_negative.rb +0 -80
- data/scripts/investigate_false_positive.rb +0 -83
- data/scripts/investigate_false_positives.rb +0 -227
- data/scripts/investigate_false_positives_batch.rb +0 -163
- data/scripts/investigate_mixed_content.rb +0 -125
- data/scripts/investigate_remaining_16.rb +0 -214
- data/scripts/run_single_test.rb +0 -29
- data/scripts/test_all_false_positives.rb +0 -95
- data/scripts/test_attribute_details.rb +0 -61
- data/scripts/test_both_algorithms.rb +0 -49
- data/scripts/test_both_simple.rb +0 -49
- data/scripts/test_enhanced_semantic_output.rb +0 -125
- data/scripts/test_readme_examples.rb +0 -131
- data/scripts/test_semantic_tree_diff.rb +0 -99
- data/scripts/test_semantic_ux_improvements.rb +0 -135
- data/scripts/test_single_false_positive.rb +0 -119
- data/scripts/test_size_limits.rb +0 -99
- data/test_html_1.html +0 -21
- data/test_html_2.html +0 -21
- data/test_nokogiri.rb +0 -33
- data/test_normalize.rb +0 -45
data/old-docs/RSPEC.adoc
DELETED
|
@@ -1,814 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
layout: default
|
|
3
|
-
title: RSpec Matchers
|
|
4
|
-
nav_order: 12
|
|
5
|
-
parent: Basic Usage
|
|
6
|
-
---
|
|
7
|
-
= Canon RSpec matchers
|
|
8
|
-
:toc:
|
|
9
|
-
:toclevels: 3
|
|
10
|
-
|
|
11
|
-
== Scope
|
|
12
|
-
|
|
13
|
-
This document describes how to use Canon's RSpec matchers for testing. Canon
|
|
14
|
-
provides semantic comparison matchers that focus on content rather than
|
|
15
|
-
formatting.
|
|
16
|
-
|
|
17
|
-
For Ruby API usage, see link:RUBY_API[Ruby API documentation].
|
|
18
|
-
|
|
19
|
-
For command-line usage, see link:CLI[CLI documentation].
|
|
20
|
-
|
|
21
|
-
== General
|
|
22
|
-
|
|
23
|
-
Canon's RSpec matchers use canonical (c14n) mode for comparison, ensuring that
|
|
24
|
-
formatting differences don't affect test results. Tests focus on semantic
|
|
25
|
-
equivalence rather than exact string matching.
|
|
26
|
-
|
|
27
|
-
== Installation
|
|
28
|
-
|
|
29
|
-
Add to your `spec_helper.rb` or `spec/support/canon.rb`:
|
|
30
|
-
|
|
31
|
-
[source,ruby]
|
|
32
|
-
----
|
|
33
|
-
require 'canon/rspec_matchers'
|
|
34
|
-
----
|
|
35
|
-
|
|
36
|
-
== Basic usage
|
|
37
|
-
|
|
38
|
-
=== Unified matcher
|
|
39
|
-
|
|
40
|
-
The `be_serialization_equivalent_to` matcher works with all formats:
|
|
41
|
-
|
|
42
|
-
.Unified matcher syntax
|
|
43
|
-
[example]
|
|
44
|
-
====
|
|
45
|
-
[source,ruby]
|
|
46
|
-
----
|
|
47
|
-
require 'canon/rspec_matchers'
|
|
48
|
-
|
|
49
|
-
RSpec.describe 'Serialization' do
|
|
50
|
-
it 'compares XML' do
|
|
51
|
-
xml1 = '<root><a>1</a><b>2</b></root>'
|
|
52
|
-
xml2 = '<root> <b>2</b> <a>1</a> </root>'
|
|
53
|
-
expect(xml1).to be_serialization_equivalent_to(xml2, format: :xml)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
it 'compares HTML' do
|
|
57
|
-
html1 = '<div><p>Hello</p></div>'
|
|
58
|
-
html2 = '<div> <p> Hello </p> </div>'
|
|
59
|
-
expect(html1).to be_serialization_equivalent_to(html2, format: :html)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
it 'compares JSON' do
|
|
63
|
-
json1 = '{"a":1,"b":2}'
|
|
64
|
-
json2 = '{"b":2,"a":1}'
|
|
65
|
-
expect(json1).to be_serialization_equivalent_to(json2, format: :json)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
it 'compares YAML' do
|
|
69
|
-
yaml1 = "a: 1\nb: 2"
|
|
70
|
-
yaml2 = "b: 2\na: 1"
|
|
71
|
-
expect(yaml1).to be_serialization_equivalent_to(yaml2, format: :yaml)
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
----
|
|
75
|
-
====
|
|
76
|
-
|
|
77
|
-
=== Format-specific matchers
|
|
78
|
-
|
|
79
|
-
Canon provides dedicated matchers for each format:
|
|
80
|
-
|
|
81
|
-
.Format-specific matchers
|
|
82
|
-
[example]
|
|
83
|
-
====
|
|
84
|
-
[source,ruby]
|
|
85
|
-
----
|
|
86
|
-
RSpec.describe 'Format-specific matchers' do
|
|
87
|
-
it 'uses XML matcher' do
|
|
88
|
-
expect(actual_xml).to be_xml_equivalent_to(expected_xml)
|
|
89
|
-
# or legacy alias
|
|
90
|
-
expect(actual_xml).to be_analogous_with(expected_xml)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
it 'uses HTML matcher' do
|
|
94
|
-
expect(actual_html).to be_html_equivalent_to(expected_html)
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
it 'uses JSON matcher' do
|
|
98
|
-
expect(actual_json).to be_json_equivalent_to(expected_json)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
it 'uses YAML matcher' do
|
|
102
|
-
expect(actual_yaml).to be_yaml_equivalent_to(expected_yaml)
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
----
|
|
106
|
-
====
|
|
107
|
-
|
|
108
|
-
== Global configuration
|
|
109
|
-
|
|
110
|
-
Configure matchers globally in your `spec_helper.rb`:
|
|
111
|
-
|
|
112
|
-
=== Syntax
|
|
113
|
-
|
|
114
|
-
[source,ruby]
|
|
115
|
-
----
|
|
116
|
-
Canon::RSpecMatchers.configure do |config|
|
|
117
|
-
config.<format>.match.profile = :profile_name
|
|
118
|
-
config.<format>.match.options = { ... }
|
|
119
|
-
config.<format>.diff.mode = :mode_name
|
|
120
|
-
config.<format>.diff.use_color = true/false
|
|
121
|
-
config.<format>.diff.context_lines = n
|
|
122
|
-
config.<format>.diff.grouping_lines = n
|
|
123
|
-
end
|
|
124
|
-
----
|
|
125
|
-
|
|
126
|
-
Where `<format>` is one of: `xml`, `html`, `json`, `yaml`
|
|
127
|
-
|
|
128
|
-
=== Match configuration
|
|
129
|
-
|
|
130
|
-
.Match profiles
|
|
131
|
-
[example]
|
|
132
|
-
====
|
|
133
|
-
[source,ruby]
|
|
134
|
-
----
|
|
135
|
-
# spec_helper.rb
|
|
136
|
-
Canon::RSpecMatchers.configure do |config|
|
|
137
|
-
# Use spec_friendly profile for XML
|
|
138
|
-
config.xml.match.profile = :spec_friendly
|
|
139
|
-
|
|
140
|
-
# Use rendered profile for HTML
|
|
141
|
-
config.html.match.profile = :rendered
|
|
142
|
-
|
|
143
|
-
# Use strict profile for JSON
|
|
144
|
-
config.json.match.profile = :strict
|
|
145
|
-
end
|
|
146
|
-
----
|
|
147
|
-
|
|
148
|
-
Available profiles: `:strict`, `:rendered`, `:spec_friendly`, `:content_only`
|
|
149
|
-
|
|
150
|
-
See link:MATCH_OPTIONS[Match options] for profile details.
|
|
151
|
-
====
|
|
152
|
-
|
|
153
|
-
.Match dimension options
|
|
154
|
-
[example]
|
|
155
|
-
====
|
|
156
|
-
[source,ruby]
|
|
157
|
-
----
|
|
158
|
-
# spec_helper.rb
|
|
159
|
-
Canon::RSpecMatchers.configure do |config|
|
|
160
|
-
config.xml.match.options = {
|
|
161
|
-
text_content: :normalize,
|
|
162
|
-
structural_whitespace: :ignore,
|
|
163
|
-
attribute_order: :ignore,
|
|
164
|
-
comments: :ignore
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
config.html.match.options = {
|
|
168
|
-
text_content: :normalize,
|
|
169
|
-
structural_whitespace: :ignore
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
config.json.match.options = {
|
|
173
|
-
key_order: :ignore
|
|
174
|
-
}
|
|
175
|
-
end
|
|
176
|
-
----
|
|
177
|
-
|
|
178
|
-
See link:MATCH_OPTIONS[Match options] for dimension reference.
|
|
179
|
-
====
|
|
180
|
-
|
|
181
|
-
=== Diff configuration
|
|
182
|
-
|
|
183
|
-
.Diff mode and colors
|
|
184
|
-
[example]
|
|
185
|
-
====
|
|
186
|
-
[source,ruby]
|
|
187
|
-
----
|
|
188
|
-
# spec_helper.rb
|
|
189
|
-
Canon::RSpecMatchers.configure do |config|
|
|
190
|
-
# XML with by-line mode
|
|
191
|
-
config.xml.diff.mode = :by_line
|
|
192
|
-
config.xml.diff.use_color = true
|
|
193
|
-
|
|
194
|
-
# HTML with by-line mode
|
|
195
|
-
config.html.diff.mode = :by_line
|
|
196
|
-
|
|
197
|
-
# JSON with by-object mode (default)
|
|
198
|
-
config.json.diff.mode = :by_object
|
|
199
|
-
config.json.diff.use_color = true
|
|
200
|
-
end
|
|
201
|
-
----
|
|
202
|
-
|
|
203
|
-
See link:MODES[Diff modes] for mode details.
|
|
204
|
-
====
|
|
205
|
-
|
|
206
|
-
.Diff formatting
|
|
207
|
-
[example]
|
|
208
|
-
====
|
|
209
|
-
[source,ruby]
|
|
210
|
-
----
|
|
211
|
-
# spec_helper.rb
|
|
212
|
-
Canon::RSpecMatchers.configure do |config|
|
|
213
|
-
# Show 5 lines of context
|
|
214
|
-
config.xml.diff.context_lines = 5
|
|
215
|
-
|
|
216
|
-
# Group changes within 10 lines
|
|
217
|
-
config.xml.diff.grouping_lines = 10
|
|
218
|
-
|
|
219
|
-
# Disable colors for CI environments
|
|
220
|
-
config.xml.diff.use_color = !ENV['CI']
|
|
221
|
-
end
|
|
222
|
-
----
|
|
223
|
-
|
|
224
|
-
See link:DIFF_FORMATTING[Diff formatting] for options.
|
|
225
|
-
====
|
|
226
|
-
|
|
227
|
-
=== Complete configuration example
|
|
228
|
-
|
|
229
|
-
.Full configuration
|
|
230
|
-
[example]
|
|
231
|
-
====
|
|
232
|
-
[source,ruby]
|
|
233
|
-
----
|
|
234
|
-
# spec_helper.rb
|
|
235
|
-
require 'canon/rspec_matchers'
|
|
236
|
-
|
|
237
|
-
Canon::RSpecMatchers.configure do |config|
|
|
238
|
-
# XML configuration
|
|
239
|
-
config.xml.match.profile = :spec_friendly
|
|
240
|
-
config.xml.match.options = {
|
|
241
|
-
text_content: :normalize,
|
|
242
|
-
structural_whitespace: :ignore,
|
|
243
|
-
comments: :ignore
|
|
244
|
-
}
|
|
245
|
-
config.xml.diff.mode = :by_line
|
|
246
|
-
config.xml.diff.use_color = true
|
|
247
|
-
config.xml.diff.context_lines = 3
|
|
248
|
-
|
|
249
|
-
# HTML configuration
|
|
250
|
-
config.html.match.profile = :rendered
|
|
251
|
-
config.html.diff.mode = :by_line
|
|
252
|
-
config.html.diff.grouping_lines = 2
|
|
253
|
-
|
|
254
|
-
# JSON configuration
|
|
255
|
-
config.json.match.profile = :spec_friendly
|
|
256
|
-
config.json.match.options = {
|
|
257
|
-
key_order: :ignore
|
|
258
|
-
}
|
|
259
|
-
config.json.diff.mode = :by_object
|
|
260
|
-
config.json.diff.context_lines = 5
|
|
261
|
-
|
|
262
|
-
# YAML configuration
|
|
263
|
-
config.yaml.match.options = {
|
|
264
|
-
key_order: :ignore
|
|
265
|
-
}
|
|
266
|
-
end
|
|
267
|
-
----
|
|
268
|
-
====
|
|
269
|
-
|
|
270
|
-
== Per-test configuration
|
|
271
|
-
|
|
272
|
-
Override global configuration on a per-test basis using matcher options.
|
|
273
|
-
|
|
274
|
-
=== Using match profiles
|
|
275
|
-
|
|
276
|
-
.Override with profile
|
|
277
|
-
[example]
|
|
278
|
-
====
|
|
279
|
-
[source,ruby]
|
|
280
|
-
----
|
|
281
|
-
RSpec.describe 'Override global config' do
|
|
282
|
-
it 'uses strict matching for this test' do
|
|
283
|
-
expect(actual_xml).to be_xml_equivalent_to(expected_xml)
|
|
284
|
-
.with_profile(:strict)
|
|
285
|
-
end
|
|
286
|
-
|
|
287
|
-
it 'uses rendered profile for HTML' do
|
|
288
|
-
expect(actual_html).to be_html_equivalent_to(expected_html)
|
|
289
|
-
.with_profile(:rendered)
|
|
290
|
-
end
|
|
291
|
-
end
|
|
292
|
-
----
|
|
293
|
-
====
|
|
294
|
-
|
|
295
|
-
=== Using match options
|
|
296
|
-
|
|
297
|
-
.Override with specific dimensions
|
|
298
|
-
[example]
|
|
299
|
-
====
|
|
300
|
-
[source,ruby]
|
|
301
|
-
----
|
|
302
|
-
RSpec.describe 'Override specific dimensions' do
|
|
303
|
-
it 'requires strict whitespace for this test' do
|
|
304
|
-
expect(actual_xml).to be_xml_equivalent_to(expected_xml)
|
|
305
|
-
.with_options(
|
|
306
|
-
structural_whitespace: :strict,
|
|
307
|
-
text_content: :strict
|
|
308
|
-
)
|
|
309
|
-
end
|
|
310
|
-
|
|
311
|
-
it 'ignores attribute order' do
|
|
312
|
-
expect(actual_xml).to be_xml_equivalent_to(expected_xml)
|
|
313
|
-
.with_options(attribute_order: :ignore)
|
|
314
|
-
end
|
|
315
|
-
end
|
|
316
|
-
----
|
|
317
|
-
====
|
|
318
|
-
|
|
319
|
-
=== Combining profile and options
|
|
320
|
-
|
|
321
|
-
.Profile with overrides
|
|
322
|
-
[example]
|
|
323
|
-
====
|
|
324
|
-
[source,ruby]
|
|
325
|
-
----
|
|
326
|
-
RSpec.describe 'Combine profile and options' do
|
|
327
|
-
it 'uses spec_friendly but checks comments' do
|
|
328
|
-
expect(actual_xml).to be_xml_equivalent_to(expected_xml)
|
|
329
|
-
.with_profile(:spec_friendly)
|
|
330
|
-
.with_options(comments: :strict)
|
|
331
|
-
end
|
|
332
|
-
|
|
333
|
-
it 'uses rendered but requires strict text' do
|
|
334
|
-
expect(actual_html).to be_html_equivalent_to(expected_html)
|
|
335
|
-
.with_profile(:rendered)
|
|
336
|
-
.with_options(text_content: :strict)
|
|
337
|
-
end
|
|
338
|
-
end
|
|
339
|
-
----
|
|
340
|
-
====
|
|
341
|
-
|
|
342
|
-
[source,ruby]
|
|
343
|
-
----
|
|
344
|
-
RSpec.describe 'Verbose diff output' do
|
|
345
|
-
it 'shows detailed XML diff on failure' do
|
|
346
|
-
expect(actual_xml).to be_xml_equivalent_to(expected_xml, verbose: true)
|
|
347
|
-
end
|
|
348
|
-
|
|
349
|
-
it 'shows detailed HTML diff on failure' do
|
|
350
|
-
expect(actual_html).to be_html_equivalent_to(expected_html, verbose: true)
|
|
351
|
-
end
|
|
352
|
-
|
|
353
|
-
it 'shows detailed JSON diff on failure' do
|
|
354
|
-
expect(actual_json).to be_json_equivalent_to(expected_json, verbose: true)
|
|
355
|
-
end
|
|
356
|
-
|
|
357
|
-
# Combine with profile and options
|
|
358
|
-
it 'shows verbose diff with custom options' do
|
|
359
|
-
expect(actual_xml).to be_xml_equivalent_to(expected_xml, verbose: true)
|
|
360
|
-
.with_profile(:spec_friendly)
|
|
361
|
-
.with_options(comments: :strict)
|
|
362
|
-
end
|
|
363
|
-
end
|
|
364
|
-
----
|
|
365
|
-
|
|
366
|
-
== Common patterns
|
|
367
|
-
== Verbose output
|
|
368
|
-
|
|
369
|
-
Use `verbose: true` to show detailed diff output on test failures.
|
|
370
|
-
|
|
371
|
-
.Verbose diff examples
|
|
372
|
-
[example]
|
|
373
|
-
[source,ruby]
|
|
374
|
-
----
|
|
375
|
-
RSpec.describe 'Verbose diff output' do
|
|
376
|
-
it 'shows detailed XML diff on failure' do
|
|
377
|
-
expect(actual_xml).to be_xml_equivalent_to(expected_xml, verbose: true)
|
|
378
|
-
end
|
|
379
|
-
|
|
380
|
-
it 'shows detailed HTML diff on failure' do
|
|
381
|
-
expect(actual_html).to be_html_equivalent_to(expected_html, verbose: true)
|
|
382
|
-
end
|
|
383
|
-
|
|
384
|
-
it 'shows detailed JSON diff on failure' do
|
|
385
|
-
expect(actual_json).to be_json_equivalent_to(expected_json, verbose: true)
|
|
386
|
-
end
|
|
387
|
-
|
|
388
|
-
# Combine with profile and options
|
|
389
|
-
it 'shows verbose diff with custom options' do
|
|
390
|
-
expect(actual_xml).to be_xml_equivalent_to(expected_xml, verbose: true)
|
|
391
|
-
.with_profile(:spec_friendly)
|
|
392
|
-
.with_options(comments: :strict)
|
|
393
|
-
end
|
|
394
|
-
end
|
|
395
|
-
----
|
|
396
|
-
|
|
397
|
-
== Diff algorithms
|
|
398
|
-
|
|
399
|
-
Canon supports two diff algorithms that can be selected based on your needs:
|
|
400
|
-
|
|
401
|
-
* **`:dom`** (default): Positional, DOM-based matching
|
|
402
|
-
* **`:semantic`**: Tree-based matching with operation detection
|
|
403
|
-
|
|
404
|
-
=== DOM algorithm
|
|
405
|
-
|
|
406
|
-
The DOM algorithm performs positional matching and is the default behavior:
|
|
407
|
-
|
|
408
|
-
.DOM algorithm usage
|
|
409
|
-
[example]
|
|
410
|
-
[source,ruby]
|
|
411
|
-
----
|
|
412
|
-
RSpec.describe 'DOM algorithm' do
|
|
413
|
-
it 'uses positional matching by default' do
|
|
414
|
-
expect(actual_xml).to be_xml_equivalent_to(expected_xml)
|
|
415
|
-
end
|
|
416
|
-
|
|
417
|
-
it 'explicitly specifies DOM algorithm' do
|
|
418
|
-
expect(actual_xml).to be_xml_equivalent_to(expected_xml,
|
|
419
|
-
diff_algorithm: :dom,
|
|
420
|
-
verbose: true
|
|
421
|
-
)
|
|
422
|
-
end
|
|
423
|
-
end
|
|
424
|
-
----
|
|
425
|
-
|
|
426
|
-
=== Semantic algorithm
|
|
427
|
-
|
|
428
|
-
The semantic algorithm uses tree-based matching and detects operations like
|
|
429
|
-
insert, delete, update, move, merge, and split:
|
|
430
|
-
|
|
431
|
-
.Semantic algorithm usage
|
|
432
|
-
[example]
|
|
433
|
-
[source,ruby]
|
|
434
|
-
----
|
|
435
|
-
RSpec.describe 'Semantic algorithm' do
|
|
436
|
-
it 'detects semantic operations' do
|
|
437
|
-
xml1 = '<root><a>1</a><b>2</b></root>'
|
|
438
|
-
xml2 = '<root><b>2</b><a>1</a><c>3</c></root>'
|
|
439
|
-
|
|
440
|
-
expect(xml1).to be_xml_equivalent_to(xml2,
|
|
441
|
-
diff_algorithm: :semantic,
|
|
442
|
-
verbose: true
|
|
443
|
-
)
|
|
444
|
-
end
|
|
445
|
-
|
|
446
|
-
it 'identifies element moves' do
|
|
447
|
-
html1 = '<div><p>First</p><p>Second</p></div>'
|
|
448
|
-
html2 = '<div><p>Second</p><p>First</p></div>'
|
|
449
|
-
|
|
450
|
-
expect(html1).to be_html_equivalent_to(html2,
|
|
451
|
-
diff_algorithm: :semantic,
|
|
452
|
-
verbose: true
|
|
453
|
-
)
|
|
454
|
-
end
|
|
455
|
-
|
|
456
|
-
it 'detects insertions and deletions' do
|
|
457
|
-
json1 = '{"users": [{"id": 1}, {"id": 2}]}'
|
|
458
|
-
json2 = '{"users": [{"id": 1}, {"id": 2}, {"id": 3}]}'
|
|
459
|
-
|
|
460
|
-
expect(json1).to be_json_equivalent_to(json2,
|
|
461
|
-
diff_algorithm: :semantic,
|
|
462
|
-
verbose: true
|
|
463
|
-
)
|
|
464
|
-
end
|
|
465
|
-
end
|
|
466
|
-
----
|
|
467
|
-
|
|
468
|
-
=== Global algorithm configuration
|
|
469
|
-
|
|
470
|
-
Configure the diff algorithm globally:
|
|
471
|
-
|
|
472
|
-
.Global diff algorithm configuration
|
|
473
|
-
[example]
|
|
474
|
-
[source,ruby]
|
|
475
|
-
----
|
|
476
|
-
# spec_helper.rb
|
|
477
|
-
Canon::RSpecMatchers.configure do |config|
|
|
478
|
-
# Use semantic algorithm for XML by default
|
|
479
|
-
config.xml.diff.algorithm = :semantic
|
|
480
|
-
|
|
481
|
-
# Use DOM algorithm for HTML (default)
|
|
482
|
-
config.html.diff.algorithm = :dom
|
|
483
|
-
|
|
484
|
-
# Use semantic algorithm for JSON
|
|
485
|
-
config.json.diff.algorithm = :semantic
|
|
486
|
-
end
|
|
487
|
-
----
|
|
488
|
-
|
|
489
|
-
=== Per-test algorithm override
|
|
490
|
-
|
|
491
|
-
Override the global algorithm configuration for specific tests:
|
|
492
|
-
|
|
493
|
-
.Per-test algorithm override
|
|
494
|
-
[example]
|
|
495
|
-
[source,ruby]
|
|
496
|
-
----
|
|
497
|
-
RSpec.describe 'Algorithm override' do
|
|
498
|
-
# Global config uses :semantic
|
|
499
|
-
# But this test uses :dom
|
|
500
|
-
it 'uses DOM algorithm for this specific test' do
|
|
501
|
-
expect(actual_xml).to be_xml_equivalent_to(expected_xml,
|
|
502
|
-
diff_algorithm: :dom,
|
|
503
|
-
verbose: true
|
|
504
|
-
)
|
|
505
|
-
end
|
|
506
|
-
|
|
507
|
-
# Global config uses :dom
|
|
508
|
-
# But this test uses :semantic
|
|
509
|
-
it 'uses semantic algorithm to detect operations' do
|
|
510
|
-
expect(actual_html).to be_html_equivalent_to(expected_html,
|
|
511
|
-
diff_algorithm: :semantic,
|
|
512
|
-
verbose: true
|
|
513
|
-
)
|
|
514
|
-
end
|
|
515
|
-
end
|
|
516
|
-
----
|
|
517
|
-
|
|
518
|
-
=== Combining algorithm with other options
|
|
519
|
-
|
|
520
|
-
The diff algorithm can be combined with match profiles and options:
|
|
521
|
-
|
|
522
|
-
.Algorithm with profiles and options
|
|
523
|
-
[example]
|
|
524
|
-
[source,ruby]
|
|
525
|
-
----
|
|
526
|
-
RSpec.describe 'Combined configuration' do
|
|
527
|
-
it 'uses semantic algorithm with spec_friendly profile' do
|
|
528
|
-
expect(actual_xml).to be_xml_equivalent_to(expected_xml,
|
|
529
|
-
diff_algorithm: :semantic,
|
|
530
|
-
verbose: true
|
|
531
|
-
)
|
|
532
|
-
.with_profile(:spec_friendly)
|
|
533
|
-
end
|
|
534
|
-
|
|
535
|
-
it 'uses semantic algorithm with custom match options' do
|
|
536
|
-
expect(actual_xml).to be_xml_equivalent_to(expected_xml,
|
|
537
|
-
diff_algorithm: :semantic,
|
|
538
|
-
verbose: true
|
|
539
|
-
)
|
|
540
|
-
.with_options(
|
|
541
|
-
structural_whitespace: :ignore,
|
|
542
|
-
comments: :ignore
|
|
543
|
-
)
|
|
544
|
-
end
|
|
545
|
-
|
|
546
|
-
it 'uses DOM algorithm with strict matching' do
|
|
547
|
-
expect(actual_html).to be_html_equivalent_to(expected_html,
|
|
548
|
-
diff_algorithm: :dom,
|
|
549
|
-
verbose: true
|
|
550
|
-
)
|
|
551
|
-
.with_profile(:strict)
|
|
552
|
-
end
|
|
553
|
-
end
|
|
554
|
-
----
|
|
555
|
-
|
|
556
|
-
== Common patterns
|
|
557
|
-
====
|
|
558
|
-
[source,ruby]
|
|
559
|
-
----
|
|
560
|
-
RSpec.describe 'Verbose diff output' do
|
|
561
|
-
it 'shows detailed XML diff on failure' do
|
|
562
|
-
expect(actual_xml).to be_xml_equivalent_to(expected_xml, verbose: true)
|
|
563
|
-
end
|
|
564
|
-
|
|
565
|
-
it 'shows detailed HTML diff on failure' do
|
|
566
|
-
expect(actual_html).to be_html_equivalent_to(expected_html, verbose: true)
|
|
567
|
-
end
|
|
568
|
-
|
|
569
|
-
it 'shows detailed JSON diff on failure' do
|
|
570
|
-
expect(actual_json).to be_json_equivalent_to(expected_json, verbose: true)
|
|
571
|
-
end
|
|
572
|
-
|
|
573
|
-
# Combine with profile and options
|
|
574
|
-
it 'shows verbose diff with custom options' do
|
|
575
|
-
expect(actual_xml).to be_xml_equivalent_to(expected_xml, verbose: true)
|
|
576
|
-
.with_profile(:spec_friendly)
|
|
577
|
-
.with_options(comments: :strict)
|
|
578
|
-
end
|
|
579
|
-
end
|
|
580
|
-
----
|
|
581
|
-
====
|
|
582
|
-
|
|
583
|
-
== Common patterns
|
|
584
|
-
|
|
585
|
-
=== Testing XML generation
|
|
586
|
-
|
|
587
|
-
.XML generation tests
|
|
588
|
-
[example]
|
|
589
|
-
====
|
|
590
|
-
[source,ruby]
|
|
591
|
-
----
|
|
592
|
-
RSpec.describe 'XML generation' do
|
|
593
|
-
let(:expected_xml) do
|
|
594
|
-
<<~XML
|
|
595
|
-
<document>
|
|
596
|
-
<title>Test Document</title>
|
|
597
|
-
<content>
|
|
598
|
-
<p>Paragraph content</p>
|
|
599
|
-
</content>
|
|
600
|
-
</document>
|
|
601
|
-
XML
|
|
602
|
-
end
|
|
603
|
-
|
|
604
|
-
it 'generates correct XML structure' do
|
|
605
|
-
actual_xml = generate_xml(title: 'Test Document',
|
|
606
|
-
content: '<p>Paragraph content</p>')
|
|
607
|
-
|
|
608
|
-
expect(actual_xml).to be_xml_equivalent_to(expected_xml)
|
|
609
|
-
end
|
|
610
|
-
|
|
611
|
-
it 'handles attributes correctly' do
|
|
612
|
-
actual = generate_element(id: '123', class: 'active')
|
|
613
|
-
expected = '<element class="active" id="123"/>'
|
|
614
|
-
|
|
615
|
-
# Attribute order doesn't matter with default config
|
|
616
|
-
expect(actual).to be_xml_equivalent_to(expected)
|
|
617
|
-
end
|
|
618
|
-
end
|
|
619
|
-
----
|
|
620
|
-
====
|
|
621
|
-
|
|
622
|
-
=== Testing HTML output
|
|
623
|
-
|
|
624
|
-
.HTML output tests
|
|
625
|
-
[example]
|
|
626
|
-
====
|
|
627
|
-
[source,ruby]
|
|
628
|
-
----
|
|
629
|
-
RSpec.describe 'HTML generation' do
|
|
630
|
-
let(:expected_html) do
|
|
631
|
-
<<~HTML
|
|
632
|
-
<!DOCTYPE html>
|
|
633
|
-
<html>
|
|
634
|
-
<head>
|
|
635
|
-
<title>Test Page</title>
|
|
636
|
-
</head>
|
|
637
|
-
<body>
|
|
638
|
-
<h1>Welcome</h1>
|
|
639
|
-
<p>Content here</p>
|
|
640
|
-
</body>
|
|
641
|
-
</html>
|
|
642
|
-
HTML
|
|
643
|
-
end
|
|
644
|
-
|
|
645
|
-
it 'generates correct HTML' do
|
|
646
|
-
actual_html = generate_page(
|
|
647
|
-
title: 'Test Page',
|
|
648
|
-
heading: 'Welcome',
|
|
649
|
-
content: 'Content here'
|
|
650
|
-
)
|
|
651
|
-
|
|
652
|
-
expect(actual_html).to be_html_equivalent_to(expected_html)
|
|
653
|
-
end
|
|
654
|
-
|
|
655
|
-
# Formatting differences are ignored
|
|
656
|
-
it 'ignores whitespace differences' do
|
|
657
|
-
compact = '<div><p>Text</p></div>'
|
|
658
|
-
pretty = <<~HTML
|
|
659
|
-
<div>
|
|
660
|
-
<p>Text</p>
|
|
661
|
-
</div>
|
|
662
|
-
HTML
|
|
663
|
-
|
|
664
|
-
expect(compact).to be_html_equivalent_to(pretty)
|
|
665
|
-
end
|
|
666
|
-
end
|
|
667
|
-
----
|
|
668
|
-
====
|
|
669
|
-
|
|
670
|
-
=== Testing JSON APIs
|
|
671
|
-
|
|
672
|
-
.JSON API tests
|
|
673
|
-
[example]
|
|
674
|
-
====
|
|
675
|
-
[source,ruby]
|
|
676
|
-
----
|
|
677
|
-
RSpec.describe 'API responses' do
|
|
678
|
-
let(:expected_response) do
|
|
679
|
-
{
|
|
680
|
-
"user" => {
|
|
681
|
-
"id" => 123,
|
|
682
|
-
"name" => "John Doe",
|
|
683
|
-
"email" => "john@example.com"
|
|
684
|
-
},
|
|
685
|
-
"status" => "success"
|
|
686
|
-
}.to_json
|
|
687
|
-
end
|
|
688
|
-
|
|
689
|
-
it 'returns correct user data' do
|
|
690
|
-
response = api_client.get_user(123)
|
|
691
|
-
|
|
692
|
-
expect(response).to be_json_equivalent_to(expected_response)
|
|
693
|
-
end
|
|
694
|
-
|
|
695
|
-
# Key order doesn't matter with default config
|
|
696
|
-
it 'ignores key ordering' do
|
|
697
|
-
actual = '{"b":2,"a":1}'
|
|
698
|
-
expected = '{"a":1,"b":2}'
|
|
699
|
-
|
|
700
|
-
expect(actual).to be_json_equivalent_to(expected)
|
|
701
|
-
end
|
|
702
|
-
end
|
|
703
|
-
----
|
|
704
|
-
====
|
|
705
|
-
|
|
706
|
-
=== Testing configuration files
|
|
707
|
-
|
|
708
|
-
.Configuration file tests
|
|
709
|
-
[example]
|
|
710
|
-
====
|
|
711
|
-
[source,ruby]
|
|
712
|
-
----
|
|
713
|
-
RSpec.describe 'Configuration files' do
|
|
714
|
-
it 'generates correct YAML config' do
|
|
715
|
-
config = generate_config(
|
|
716
|
-
database: { host: 'localhost', port: 5432 },
|
|
717
|
-
logging: { level: 'info' }
|
|
718
|
-
)
|
|
719
|
-
|
|
720
|
-
expected = <<~YAML
|
|
721
|
-
database:
|
|
722
|
-
host: localhost
|
|
723
|
-
port: 5432
|
|
724
|
-
logging:
|
|
725
|
-
level: info
|
|
726
|
-
YAML
|
|
727
|
-
|
|
728
|
-
expect(config).to be_yaml_equivalent_to(expected)
|
|
729
|
-
end
|
|
730
|
-
|
|
731
|
-
it 'handles nested structures' do
|
|
732
|
-
actual_yaml = to_yaml(deeply: { nested: { structure: 'value' } })
|
|
733
|
-
expected_yaml = "deeply:\n nested:\n structure: value"
|
|
734
|
-
|
|
735
|
-
expect(actual_yaml).to be_yaml_equivalent_to(expected_yaml)
|
|
736
|
-
end
|
|
737
|
-
end
|
|
738
|
-
----
|
|
739
|
-
====
|
|
740
|
-
|
|
741
|
-
== Troubleshooting
|
|
742
|
-
|
|
743
|
-
=== Debugging test failures
|
|
744
|
-
|
|
745
|
-
When a test fails, Canon shows a detailed diff. Use verbose mode for maximum
|
|
746
|
-
detail:
|
|
747
|
-
|
|
748
|
-
.Debugging example
|
|
749
|
-
[example]
|
|
750
|
-
====
|
|
751
|
-
[source,ruby]
|
|
752
|
-
----
|
|
753
|
-
it 'shows exactly what differs' do
|
|
754
|
-
expect(actual).to be_xml_equivalent_to(expected, verbose: true)
|
|
755
|
-
.with_profile(:strict) # Use strict to see all differences
|
|
756
|
-
end
|
|
757
|
-
----
|
|
758
|
-
|
|
759
|
-
The diff output will show:
|
|
760
|
-
|
|
761
|
-
* Line numbers
|
|
762
|
-
* Color-coded changes (red/green)
|
|
763
|
-
* Whitespace visualization
|
|
764
|
-
* Non-ASCII character warnings
|
|
765
|
-
====
|
|
766
|
-
|
|
767
|
-
=== Temporarily disabling global config
|
|
768
|
-
|
|
769
|
-
.Override global config
|
|
770
|
-
[example]
|
|
771
|
-
====
|
|
772
|
-
[source,ruby]
|
|
773
|
-
----
|
|
774
|
-
it 'uses different config for this test' do
|
|
775
|
-
# Global config uses spec_friendly, but we want strict here
|
|
776
|
-
expect(actual).to be_xml_equivalent_to(expected)
|
|
777
|
-
.with_profile(:strict)
|
|
778
|
-
.with_options(
|
|
779
|
-
text_content: :strict,
|
|
780
|
-
structural_whitespace: :strict
|
|
781
|
-
)
|
|
782
|
-
end
|
|
783
|
-
----
|
|
784
|
-
====
|
|
785
|
-
|
|
786
|
-
=== Checking what's being compared
|
|
787
|
-
|
|
788
|
-
.Debug preprocessed content
|
|
789
|
-
[example]
|
|
790
|
-
====
|
|
791
|
-
[source,ruby]
|
|
792
|
-
----
|
|
793
|
-
# In your test, temporarily add:
|
|
794
|
-
result = Canon::Comparison.equivalent?(actual, expected, verbose: true)
|
|
795
|
-
|
|
796
|
-
# Inspect preprocessed content
|
|
797
|
-
puts "Preprocessed actual:"
|
|
798
|
-
puts result[:preprocessed][0]
|
|
799
|
-
|
|
800
|
-
puts "Preprocessed expected:"
|
|
801
|
-
puts result[:preprocessed][1]
|
|
802
|
-
|
|
803
|
-
puts "Differences:"
|
|
804
|
-
pp result[:differences]
|
|
805
|
-
----
|
|
806
|
-
====
|
|
807
|
-
|
|
808
|
-
== See also
|
|
809
|
-
|
|
810
|
-
* link:RUBY_API[Ruby API documentation]
|
|
811
|
-
* link:CLI[Command-line interface]
|
|
812
|
-
* link:MATCH_OPTIONS[Match options reference]
|
|
813
|
-
* link:MODES[Diff modes]
|
|
814
|
-
* link:DIFF_FORMATTING[Diff formatting options]
|