ast-merge 1.0.0
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 +7 -0
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +46 -0
- data/CITATION.cff +20 -0
- data/CODE_OF_CONDUCT.md +134 -0
- data/CONTRIBUTING.md +227 -0
- data/FUNDING.md +74 -0
- data/LICENSE.txt +21 -0
- data/README.md +852 -0
- data/REEK +0 -0
- data/RUBOCOP.md +71 -0
- data/SECURITY.md +21 -0
- data/lib/ast/merge/ast_node.rb +87 -0
- data/lib/ast/merge/comment/block.rb +195 -0
- data/lib/ast/merge/comment/empty.rb +78 -0
- data/lib/ast/merge/comment/line.rb +138 -0
- data/lib/ast/merge/comment/parser.rb +278 -0
- data/lib/ast/merge/comment/style.rb +282 -0
- data/lib/ast/merge/comment.rb +36 -0
- data/lib/ast/merge/conflict_resolver_base.rb +399 -0
- data/lib/ast/merge/debug_logger.rb +271 -0
- data/lib/ast/merge/fenced_code_block_detector.rb +211 -0
- data/lib/ast/merge/file_analyzable.rb +307 -0
- data/lib/ast/merge/freezable.rb +82 -0
- data/lib/ast/merge/freeze_node_base.rb +434 -0
- data/lib/ast/merge/match_refiner_base.rb +312 -0
- data/lib/ast/merge/match_score_base.rb +135 -0
- data/lib/ast/merge/merge_result_base.rb +169 -0
- data/lib/ast/merge/merger_config.rb +258 -0
- data/lib/ast/merge/node_typing.rb +373 -0
- data/lib/ast/merge/region.rb +124 -0
- data/lib/ast/merge/region_detector_base.rb +114 -0
- data/lib/ast/merge/region_mergeable.rb +364 -0
- data/lib/ast/merge/rspec/shared_examples/conflict_resolver_base.rb +416 -0
- data/lib/ast/merge/rspec/shared_examples/debug_logger.rb +174 -0
- data/lib/ast/merge/rspec/shared_examples/file_analyzable.rb +193 -0
- data/lib/ast/merge/rspec/shared_examples/freeze_node_base.rb +219 -0
- data/lib/ast/merge/rspec/shared_examples/merge_result_base.rb +106 -0
- data/lib/ast/merge/rspec/shared_examples/merger_config.rb +202 -0
- data/lib/ast/merge/rspec/shared_examples/reproducible_merge.rb +115 -0
- data/lib/ast/merge/rspec/shared_examples.rb +26 -0
- data/lib/ast/merge/rspec.rb +4 -0
- data/lib/ast/merge/section_typing.rb +303 -0
- data/lib/ast/merge/smart_merger_base.rb +417 -0
- data/lib/ast/merge/text/conflict_resolver.rb +161 -0
- data/lib/ast/merge/text/file_analysis.rb +168 -0
- data/lib/ast/merge/text/line_node.rb +142 -0
- data/lib/ast/merge/text/merge_result.rb +42 -0
- data/lib/ast/merge/text/section.rb +93 -0
- data/lib/ast/merge/text/section_splitter.rb +397 -0
- data/lib/ast/merge/text/smart_merger.rb +141 -0
- data/lib/ast/merge/text/word_node.rb +86 -0
- data/lib/ast/merge/text.rb +35 -0
- data/lib/ast/merge/toml_frontmatter_detector.rb +88 -0
- data/lib/ast/merge/version.rb +12 -0
- data/lib/ast/merge/yaml_frontmatter_detector.rb +108 -0
- data/lib/ast/merge.rb +165 -0
- data/lib/ast-merge.rb +4 -0
- data/sig/ast/merge.rbs +195 -0
- data.tar.gz.sig +0 -0
- metadata +326 -0
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Shared examples for validating ConflictResolverBase integration
|
|
4
|
+
#
|
|
5
|
+
# Usage in your spec:
|
|
6
|
+
# require "ast/merge/rspec/shared_examples/conflict_resolver_base"
|
|
7
|
+
#
|
|
8
|
+
# RSpec.describe MyMerge::ConflictResolver do
|
|
9
|
+
# it_behaves_like "Ast::Merge::ConflictResolverBase" do
|
|
10
|
+
# let(:conflict_resolver_class) { MyMerge::ConflictResolver }
|
|
11
|
+
# let(:strategy) { :node } # or :batch or :boundary
|
|
12
|
+
# # Factory to create a conflict resolver instance
|
|
13
|
+
# let(:build_conflict_resolver) do
|
|
14
|
+
# ->(preference:, template_analysis:, dest_analysis:, **opts) {
|
|
15
|
+
# conflict_resolver_class.new(
|
|
16
|
+
# preference: preference,
|
|
17
|
+
# template_analysis: template_analysis,
|
|
18
|
+
# dest_analysis: dest_analysis,
|
|
19
|
+
# **opts
|
|
20
|
+
# )
|
|
21
|
+
# }
|
|
22
|
+
# end
|
|
23
|
+
# # Factory to create mock analysis objects
|
|
24
|
+
# let(:build_mock_analysis) { -> { double("Analysis") } }
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# @note The extending class should inherit from or behave like Ast::Merge::ConflictResolverBase
|
|
29
|
+
|
|
30
|
+
RSpec.shared_examples("Ast::Merge::ConflictResolverBase") do
|
|
31
|
+
# Required let blocks:
|
|
32
|
+
# - conflict_resolver_class: The class under test
|
|
33
|
+
# - strategy: The resolution strategy (:node, :batch, or :boundary)
|
|
34
|
+
# - build_conflict_resolver: Lambda that creates a conflict resolver instance
|
|
35
|
+
# - build_mock_analysis: Lambda that creates mock analysis objects
|
|
36
|
+
|
|
37
|
+
describe "decision constants" do
|
|
38
|
+
it "has DECISION_DESTINATION" do
|
|
39
|
+
expect(conflict_resolver_class::DECISION_DESTINATION).to(eq(:destination))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "has DECISION_TEMPLATE" do
|
|
43
|
+
expect(conflict_resolver_class::DECISION_TEMPLATE).to(eq(:template))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "has DECISION_ADDED" do
|
|
47
|
+
expect(conflict_resolver_class::DECISION_ADDED).to(eq(:added))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "has DECISION_FROZEN" do
|
|
51
|
+
expect(conflict_resolver_class::DECISION_FROZEN).to(eq(:frozen))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "has DECISION_IDENTICAL" do
|
|
55
|
+
expect(conflict_resolver_class::DECISION_IDENTICAL).to(eq(:identical))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it "has DECISION_KEPT_DEST" do
|
|
59
|
+
expect(conflict_resolver_class::DECISION_KEPT_DEST).to(eq(:kept_destination))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "has DECISION_KEPT_TEMPLATE" do
|
|
63
|
+
expect(conflict_resolver_class::DECISION_KEPT_TEMPLATE).to(eq(:kept_template))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "has DECISION_APPENDED" do
|
|
67
|
+
expect(conflict_resolver_class::DECISION_APPENDED).to(eq(:appended))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it "has DECISION_FREEZE_BLOCK" do
|
|
71
|
+
expect(conflict_resolver_class::DECISION_FREEZE_BLOCK).to(eq(:freeze_block))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it "has DECISION_RECURSIVE" do
|
|
75
|
+
expect(conflict_resolver_class::DECISION_RECURSIVE).to(eq(:recursive))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it "has DECISION_REPLACED" do
|
|
79
|
+
expect(conflict_resolver_class::DECISION_REPLACED).to(eq(:replaced))
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
describe "initialization" do
|
|
84
|
+
let(:template_analysis) { build_mock_analysis.call }
|
|
85
|
+
let(:dest_analysis) { build_mock_analysis.call }
|
|
86
|
+
|
|
87
|
+
context "with :destination preference" do
|
|
88
|
+
let(:resolver) do
|
|
89
|
+
build_conflict_resolver.call(
|
|
90
|
+
preference: :destination,
|
|
91
|
+
template_analysis: template_analysis,
|
|
92
|
+
dest_analysis: dest_analysis,
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it "sets preference to :destination" do
|
|
97
|
+
expect(resolver.preference).to(eq(:destination))
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it "sets strategy correctly" do
|
|
101
|
+
expect(resolver.strategy).to(eq(strategy))
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it "stores template_analysis" do
|
|
105
|
+
expect(resolver.template_analysis).to(eq(template_analysis))
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "stores dest_analysis" do
|
|
109
|
+
expect(resolver.dest_analysis).to(eq(dest_analysis))
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
context "with :template preference" do
|
|
114
|
+
let(:resolver) do
|
|
115
|
+
build_conflict_resolver.call(
|
|
116
|
+
preference: :template,
|
|
117
|
+
template_analysis: template_analysis,
|
|
118
|
+
dest_analysis: dest_analysis,
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it "sets preference to :template" do
|
|
123
|
+
expect(resolver.preference).to(eq(:template))
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
describe "attr_readers" do
|
|
129
|
+
let(:template_analysis) { build_mock_analysis.call }
|
|
130
|
+
let(:dest_analysis) { build_mock_analysis.call }
|
|
131
|
+
let(:resolver) do
|
|
132
|
+
build_conflict_resolver.call(
|
|
133
|
+
preference: :destination,
|
|
134
|
+
template_analysis: template_analysis,
|
|
135
|
+
dest_analysis: dest_analysis,
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it "has #strategy reader" do
|
|
140
|
+
expect(resolver).to(respond_to(:strategy))
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it "has #preference reader" do
|
|
144
|
+
expect(resolver).to(respond_to(:preference))
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it "has #template_analysis reader" do
|
|
148
|
+
expect(resolver).to(respond_to(:template_analysis))
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it "has #dest_analysis reader" do
|
|
152
|
+
expect(resolver).to(respond_to(:dest_analysis))
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it "has #add_template_only_nodes reader" do
|
|
156
|
+
expect(resolver).to(respond_to(:add_template_only_nodes))
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
describe "#resolve" do
|
|
161
|
+
let(:template_analysis) { build_mock_analysis.call }
|
|
162
|
+
let(:dest_analysis) { build_mock_analysis.call }
|
|
163
|
+
let(:resolver) do
|
|
164
|
+
build_conflict_resolver.call(
|
|
165
|
+
preference: :destination,
|
|
166
|
+
template_analysis: template_analysis,
|
|
167
|
+
dest_analysis: dest_analysis,
|
|
168
|
+
)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
it "responds to #resolve" do
|
|
172
|
+
expect(resolver).to(respond_to(:resolve))
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
describe "#freeze_node?" do
|
|
177
|
+
let(:template_analysis) { build_mock_analysis.call }
|
|
178
|
+
let(:dest_analysis) { build_mock_analysis.call }
|
|
179
|
+
let(:resolver) do
|
|
180
|
+
build_conflict_resolver.call(
|
|
181
|
+
preference: :destination,
|
|
182
|
+
template_analysis: template_analysis,
|
|
183
|
+
dest_analysis: dest_analysis,
|
|
184
|
+
)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
it "returns false for nodes without freeze_node? method" do
|
|
188
|
+
node = double("Node")
|
|
189
|
+
expect(resolver.freeze_node?(node)).to(be(false))
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
it "returns true for nodes that respond to freeze_node? and return true" do
|
|
193
|
+
node = double("FreezeNode", freeze_node?: true)
|
|
194
|
+
expect(resolver.freeze_node?(node)).to(be(true))
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
it "returns false for nodes that respond to freeze_node? and return false" do
|
|
198
|
+
node = double("RegularNode", freeze_node?: false)
|
|
199
|
+
expect(resolver.freeze_node?(node)).to(be(false))
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
RSpec.shared_examples("Ast::Merge::ConflictResolverBase validation") do
|
|
205
|
+
# Tests for invalid initialization arguments
|
|
206
|
+
# These test the base class validation directly
|
|
207
|
+
|
|
208
|
+
let(:template_analysis) { build_mock_analysis.call }
|
|
209
|
+
let(:dest_analysis) { build_mock_analysis.call }
|
|
210
|
+
|
|
211
|
+
describe "argument validation" do
|
|
212
|
+
it "raises ArgumentError for invalid strategy" do
|
|
213
|
+
expect do
|
|
214
|
+
Ast::Merge::ConflictResolverBase.new(
|
|
215
|
+
strategy: :invalid,
|
|
216
|
+
preference: :destination,
|
|
217
|
+
template_analysis: template_analysis,
|
|
218
|
+
dest_analysis: dest_analysis,
|
|
219
|
+
)
|
|
220
|
+
end.to(raise_error(ArgumentError, /Invalid strategy/))
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
it "raises ArgumentError for invalid preference" do
|
|
224
|
+
expect do
|
|
225
|
+
Ast::Merge::ConflictResolverBase.new(
|
|
226
|
+
strategy: :node,
|
|
227
|
+
preference: :invalid,
|
|
228
|
+
template_analysis: template_analysis,
|
|
229
|
+
dest_analysis: dest_analysis,
|
|
230
|
+
)
|
|
231
|
+
end.to(raise_error(ArgumentError, /Invalid preference/))
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
it "accepts :node strategy" do
|
|
235
|
+
expect do
|
|
236
|
+
Ast::Merge::ConflictResolverBase.new(
|
|
237
|
+
strategy: :node,
|
|
238
|
+
preference: :destination,
|
|
239
|
+
template_analysis: template_analysis,
|
|
240
|
+
dest_analysis: dest_analysis,
|
|
241
|
+
)
|
|
242
|
+
end.not_to(raise_error)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
it "accepts :batch strategy" do
|
|
246
|
+
expect do
|
|
247
|
+
Ast::Merge::ConflictResolverBase.new(
|
|
248
|
+
strategy: :batch,
|
|
249
|
+
preference: :destination,
|
|
250
|
+
template_analysis: template_analysis,
|
|
251
|
+
dest_analysis: dest_analysis,
|
|
252
|
+
)
|
|
253
|
+
end.not_to(raise_error)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
it "accepts :boundary strategy" do
|
|
257
|
+
expect do
|
|
258
|
+
Ast::Merge::ConflictResolverBase.new(
|
|
259
|
+
strategy: :boundary,
|
|
260
|
+
preference: :destination,
|
|
261
|
+
template_analysis: template_analysis,
|
|
262
|
+
dest_analysis: dest_analysis,
|
|
263
|
+
)
|
|
264
|
+
end.not_to(raise_error)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
it "accepts :destination preference" do
|
|
268
|
+
expect do
|
|
269
|
+
Ast::Merge::ConflictResolverBase.new(
|
|
270
|
+
strategy: :node,
|
|
271
|
+
preference: :destination,
|
|
272
|
+
template_analysis: template_analysis,
|
|
273
|
+
dest_analysis: dest_analysis,
|
|
274
|
+
)
|
|
275
|
+
end.not_to(raise_error)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
it "accepts :template preference" do
|
|
279
|
+
expect do
|
|
280
|
+
Ast::Merge::ConflictResolverBase.new(
|
|
281
|
+
strategy: :node,
|
|
282
|
+
preference: :template,
|
|
283
|
+
template_analysis: template_analysis,
|
|
284
|
+
dest_analysis: dest_analysis,
|
|
285
|
+
)
|
|
286
|
+
end.not_to(raise_error)
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
RSpec.shared_examples("Ast::Merge::ConflictResolverBase node strategy") do
|
|
292
|
+
# Additional examples specific to :node strategy resolvers
|
|
293
|
+
# Use when strategy is :node
|
|
294
|
+
|
|
295
|
+
let(:template_analysis) { build_mock_analysis.call }
|
|
296
|
+
let(:dest_analysis) { build_mock_analysis.call }
|
|
297
|
+
let(:resolver) do
|
|
298
|
+
build_conflict_resolver.call(
|
|
299
|
+
preference: :destination,
|
|
300
|
+
template_analysis: template_analysis,
|
|
301
|
+
dest_analysis: dest_analysis,
|
|
302
|
+
)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
describe "node strategy" do
|
|
306
|
+
it "has :node strategy" do
|
|
307
|
+
expect(resolver.strategy).to(eq(:node))
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
it "delegates resolve to resolve_node_pair" do
|
|
311
|
+
expect(resolver).to(respond_to(:resolve))
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
RSpec.shared_examples("Ast::Merge::ConflictResolverBase batch strategy") do
|
|
317
|
+
# Additional examples specific to :batch strategy resolvers
|
|
318
|
+
# Use when strategy is :batch
|
|
319
|
+
|
|
320
|
+
let(:template_analysis) { build_mock_analysis.call }
|
|
321
|
+
let(:dest_analysis) { build_mock_analysis.call }
|
|
322
|
+
let(:resolver) do
|
|
323
|
+
build_conflict_resolver.call(
|
|
324
|
+
preference: :destination,
|
|
325
|
+
template_analysis: template_analysis,
|
|
326
|
+
dest_analysis: dest_analysis,
|
|
327
|
+
)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
describe "batch strategy" do
|
|
331
|
+
it "has :batch strategy" do
|
|
332
|
+
expect(resolver.strategy).to(eq(:batch))
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
it "delegates resolve to resolve_batch" do
|
|
336
|
+
expect(resolver).to(respond_to(:resolve))
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
describe "#add_template_only_nodes" do
|
|
341
|
+
context "with default value" do
|
|
342
|
+
it "defaults to false" do
|
|
343
|
+
expect(resolver.add_template_only_nodes).to(be(false))
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
context "with add_template_only_nodes: true" do
|
|
348
|
+
let(:resolver_with_add) do
|
|
349
|
+
build_conflict_resolver.call(
|
|
350
|
+
preference: :destination,
|
|
351
|
+
template_analysis: template_analysis,
|
|
352
|
+
dest_analysis: dest_analysis,
|
|
353
|
+
add_template_only_nodes: true,
|
|
354
|
+
)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
it "returns true when set" do
|
|
358
|
+
expect(resolver_with_add.add_template_only_nodes).to(be(true))
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
RSpec.shared_examples("Ast::Merge::ConflictResolverBase boundary strategy") do
|
|
365
|
+
# Additional examples specific to :boundary strategy resolvers
|
|
366
|
+
# Use when strategy is :boundary
|
|
367
|
+
#
|
|
368
|
+
# Boundary strategy is used for ASTs where content is processed
|
|
369
|
+
# in sections/ranges rather than individual nodes or all at once.
|
|
370
|
+
# This is typical for languages like Ruby where file alignment
|
|
371
|
+
# identifies boundaries (sections with differences) between
|
|
372
|
+
# template and destination files.
|
|
373
|
+
|
|
374
|
+
let(:template_analysis) { build_mock_analysis.call }
|
|
375
|
+
let(:dest_analysis) { build_mock_analysis.call }
|
|
376
|
+
let(:resolver) do
|
|
377
|
+
build_conflict_resolver.call(
|
|
378
|
+
preference: :destination,
|
|
379
|
+
template_analysis: template_analysis,
|
|
380
|
+
dest_analysis: dest_analysis,
|
|
381
|
+
)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
describe "boundary strategy" do
|
|
385
|
+
it "has :boundary strategy" do
|
|
386
|
+
expect(resolver.strategy).to(eq(:boundary))
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
it "delegates resolve to resolve_boundary" do
|
|
390
|
+
expect(resolver).to(respond_to(:resolve))
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
describe "#add_template_only_nodes" do
|
|
395
|
+
context "with default value" do
|
|
396
|
+
it "defaults to false" do
|
|
397
|
+
expect(resolver.add_template_only_nodes).to(be(false))
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
context "with add_template_only_nodes: true" do
|
|
402
|
+
let(:resolver_with_add) do
|
|
403
|
+
build_conflict_resolver.call(
|
|
404
|
+
preference: :destination,
|
|
405
|
+
template_analysis: template_analysis,
|
|
406
|
+
dest_analysis: dest_analysis,
|
|
407
|
+
add_template_only_nodes: true,
|
|
408
|
+
)
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
it "returns true when set" do
|
|
412
|
+
expect(resolver_with_add.add_template_only_nodes).to(be(true))
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
end
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "silent_stream"
|
|
4
|
+
require "rspec/stubbed_env"
|
|
5
|
+
|
|
6
|
+
# Shared examples for validating DebugLogger integration
|
|
7
|
+
#
|
|
8
|
+
# Usage in your spec:
|
|
9
|
+
# require "ast/merge/rspec/shared_examples/debug_logger"
|
|
10
|
+
#
|
|
11
|
+
# RSpec.describe MyMerge::DebugLogger do
|
|
12
|
+
# it_behaves_like "Ast::Merge::DebugLogger" do
|
|
13
|
+
# let(:described_logger) { MyMerge::DebugLogger }
|
|
14
|
+
# let(:env_var_name) { "MY_MERGE_DEBUG" }
|
|
15
|
+
# let(:log_prefix) { "[MyMerge]" }
|
|
16
|
+
# end
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# @note The extending module must configure:
|
|
20
|
+
# - self.env_var_name = "YOUR_ENV_VAR"
|
|
21
|
+
# - self.log_prefix = "[YourPrefix]"
|
|
22
|
+
|
|
23
|
+
RSpec.shared_examples("Ast::Merge::DebugLogger") do
|
|
24
|
+
include SilentStream
|
|
25
|
+
|
|
26
|
+
include_context "with stubbed env"
|
|
27
|
+
|
|
28
|
+
# Required let blocks that must be provided by including spec:
|
|
29
|
+
# - described_logger: The module that extends Ast::Merge::DebugLogger
|
|
30
|
+
# - env_var_name: Expected environment variable name
|
|
31
|
+
# - log_prefix: Expected log prefix
|
|
32
|
+
|
|
33
|
+
describe "module configuration" do
|
|
34
|
+
it "has env_var_name configured" do
|
|
35
|
+
expect(described_logger.env_var_name).to(eq(env_var_name))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "has log_prefix configured" do
|
|
39
|
+
expect(described_logger.log_prefix).to(eq(log_prefix))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "has BENCHMARK_AVAILABLE constant from base" do
|
|
43
|
+
expect(described_logger::BENCHMARK_AVAILABLE).to(eq(Ast::Merge::DebugLogger::BENCHMARK_AVAILABLE))
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
describe "core methods from Ast::Merge::DebugLogger" do
|
|
48
|
+
it "responds to #enabled?" do
|
|
49
|
+
expect(described_logger).to(respond_to(:enabled?))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "responds to #debug" do
|
|
53
|
+
expect(described_logger).to(respond_to(:debug))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "responds to #info" do
|
|
57
|
+
expect(described_logger).to(respond_to(:info))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "responds to #warning" do
|
|
61
|
+
expect(described_logger).to(respond_to(:warning))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "responds to #time" do
|
|
65
|
+
expect(described_logger).to(respond_to(:time))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "responds to #log_node" do
|
|
69
|
+
expect(described_logger).to(respond_to(:log_node))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it "responds to #extract_node_info" do
|
|
73
|
+
expect(described_logger).to(respond_to(:extract_node_info))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "responds to #safe_type_name" do
|
|
77
|
+
expect(described_logger).to(respond_to(:safe_type_name))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "responds to #extract_lines" do
|
|
81
|
+
expect(described_logger).to(respond_to(:extract_lines))
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
describe "when debug is enabled" do
|
|
86
|
+
before do
|
|
87
|
+
stub_env(env_var_name => "1")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "#enabled? returns true" do
|
|
91
|
+
expect(described_logger.enabled?).to(be(true))
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "#debug outputs message with configured prefix" do
|
|
95
|
+
expect { described_logger.debug("test message") }
|
|
96
|
+
.to(output(/#{Regexp.escape(log_prefix)} test message/).to_stderr)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it "#debug includes context hash when provided" do
|
|
100
|
+
expect { described_logger.debug("test", key: "value") }
|
|
101
|
+
.to(output(/key.*value/).to_stderr)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it "#info outputs with INFO label" do
|
|
105
|
+
expect { described_logger.info("info message") }
|
|
106
|
+
.to(output(/#{Regexp.escape(log_prefix)} INFO\] info message/).to_stderr)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it "#time logs start and completion with timing" do
|
|
110
|
+
output = capture(:stderr) { described_logger.time("test operation") { 42 } }
|
|
111
|
+
expect(output).to(include("Starting: test operation"))
|
|
112
|
+
expect(output).to(include("Completed: test operation"))
|
|
113
|
+
expect(output).to(match(/real_ms/))
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it "#time returns the block result" do
|
|
117
|
+
result = described_logger.time("operation") { 42 }
|
|
118
|
+
expect(result).to(eq(42))
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
describe "when debug is disabled" do
|
|
123
|
+
before do
|
|
124
|
+
stub_env(env_var_name => nil)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it "#enabled? returns false" do
|
|
128
|
+
expect(described_logger.enabled?).to(be(false))
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "#debug does not output anything" do
|
|
132
|
+
expect { described_logger.debug("test") }.not_to(output.to_stderr)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it "#info does not output anything" do
|
|
136
|
+
expect { described_logger.info("test") }.not_to(output.to_stderr)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it "#time still executes block and returns result without logging" do
|
|
140
|
+
output = capture(:stderr) { @result = described_logger.time("operation") { 42 } }
|
|
141
|
+
expect(@result).to(eq(42))
|
|
142
|
+
expect(output).to(be_empty)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
describe "#warning (always outputs)" do
|
|
147
|
+
before do
|
|
148
|
+
stub_env(env_var_name => nil)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it "outputs warning even when debug is disabled" do
|
|
152
|
+
expect { described_logger.warning("warning message") }
|
|
153
|
+
.to(output(/#{Regexp.escape(log_prefix)} WARNING\] warning message/).to_stderr)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
describe "#extract_node_info" do
|
|
158
|
+
it "extracts type name from object" do
|
|
159
|
+
node = Object.new
|
|
160
|
+
info = described_logger.extract_node_info(node)
|
|
161
|
+
expect(info[:type]).to(eq("Object"))
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
describe "#safe_type_name" do
|
|
166
|
+
it "returns class name for standard objects" do
|
|
167
|
+
expect(described_logger.safe_type_name("test")).to(eq("String"))
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
it "returns short name without module prefix" do
|
|
171
|
+
expect(described_logger.safe_type_name([])).to(eq("Array"))
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|